Learn about our RFC process, Open RFC meetings & more.Join in the discussion! »

postcss-rtlcss

1.6.1 • Public • Published

PostCSS RTLCSS

PostCSS plugin to build Cascading Style Sheets (CSS) with Left-To-Right (LTR) and Right-To-Left (RTL) rules using RTLCSS

Build Status   Coverage Status

Demo

https://elchininet.github.io/postcss-rtlcss/

Install

npm

nmp install postcss-rtlcss --save-dev

yarn

yarn add postcss-rtlcss -D

Basic usage

Usage with commonJS

const postcss = require('postcss');
const { postcssRTLCSS } = require('postcss-rtlcss');
 
const options = { ... available options ... };
const result = postcss([
    postcssRTLCSS(options)
]).process(cssInput);
 
const rtlCSS = result.css;

Usage with ES6 modules

import postcss from 'postcss';
import { postcssRTLCSS } from 'postcss-rtlcss';
 
const options = { ... available options ... };
const result = postcss([
    postcssRTLCSS(options)
]).process(cssInput);
 
const rtlCSS = result.css;

Usage in Webpack with postcss-loader

rules: [
    {
        test: /\.css$/,
        use: [
            { loader: 'style-loader' },
            { loader: 'css-loader' },
            {
                loader: 'postcss-loader',
                options: {
                    ident: 'postcss',
                    plugins: () => [ require('postcss-rtlcss')(options) ]
                }
            }
        ]
    }
]

Examples

Input

.test1.test2 {
    background-color: #FFF;
    background-position: 10px 20px;
    border-radius: 0 2px 0 8px;
    color: #666;
    padding-right: 20px;
    text-align: left;
    transform: translate(-50%50%);
    width: 100%;
}
 
.test3 {
    direction: ltr;
    margin: 1px 2px 3px;
    padding: 10px 20px;
    text-align: center;
}

Output using the combined mode (default)

This is the recommended method, it will generate more CSS code but each direction will have their specific CSS declarations and there is not need to override properties.

.test1.test2 {
    background-color: #FFF;
    background-position: 10px 20px;
    color: #666;
    width: 100%;
}
 
[dir="ltr"] .test1[dir="ltr"] .test2 {
    border-radius: 0 2px 0 8px;
    padding-right: 20px;
    text-align: left;
    transform: translate(-50%50%);
}
 
[dir="rtl"] .test1[dir="rtl"] .test2 {
    border-radius: 2px 0 8px 0;
    padding-left: 20px;
    text-align: right;
    transform: translate(50%50%);
}
 
.test3 {
    margin: 1px 2px 3px;
    padding: 10px 20px;
    text-align: center;
}
 
[dir="ltr"] .test3 {
    direction: ltr;
}
 
[dir="rtl"] .test3 {
    direction: rtl;
}

Output using the override mode

This is the alternative method, it will generate less code because it lets the main rule intact and generates a shorter specific rule to override the properties that are affected by the direction of the text.

.test1.test2 {
    background-color: #FFF;
    background-position: 10px 20px;
    border-radius: 0 2px 0 8px;
    color: #666;
    padding-right: 20px;
    text-align: left;
    transform: translate(-50%50%);
    width: 100%;
}
 
[dir="rtl"] .test1[dir="rtl"] .test2 {
    border-radius: 2px 0 8px 0;
    padding-right: unset;
    padding-left: 20px;
    text-align: right;
    transform: translate(50%50%);
}
 
.test3 {
    direction: ltr;
    margin: 1px 2px 3px;
    padding: 10px 20px;
    text-align: center;
}
 
[dir="rtl"] .test3 {
    direction: rtl;
}

But this method has a disadvantage:

Disadvantage of the override method

Use this method carefully. It can override a property that is coming from another class if multiple classes are used at the same time. Take a look at the next HTML and CSS codes:

<div class="test1 test2">
    This is an example
</div>    
.test1 {
    background: #666;
    color: #FFF;
    padding: 20px;
}
 
.test2 {
    padding-right: 10px;
}

Using the combined method, the generated code will be the next one:

.test1 {
    background: #666;
    color: #FFF;
    padding: 20px;
}
 
[dir="ltr"] .test2 {
    padding-right: 10px;
}
 
[dir="rtl"] .test2 {
    padding-left: 10px;
}

So, the div will have a padding of 20px 10px 20px 20px in LTR and 20px 20px 20px 10px in RTL.

However, using the override method the generated code will be the next one:

.test1 {
    background: #666;
    color: #FFF;
    padding: 20px;
}
 
.test2 {
    padding-right: 10px;
}
 
[dir="rtl"] .test2 {
    padding-right: unset;
    padding-left: 10px;
}

Now the div has a padding of 20px 10px 20px 20px in LTR and 20px 0 20px 10px in RTL, because the override of the class test2 doesn't take into account that this class could be used with test1 having the same properties. The solution, in this case, is to provide the property that has been inherited:

.test1 {
    background: #666;
    color: #FFF;
    padding: 20px;
}
 
.test2 {
    padding-left: 20px;
    padding-right: 10px;
}

So, the generated code will be:

.test1 {
    background: #666;
    color: #FFF;
    padding: 20px;
}
 
.test2 {
    padding-left: 20px;
    padding-right: 10px;
}
 
[dir="rtl"] .test2 {
    padding-right: 20px;
    padding-left: 10px;
}

Options

All the options are optional, and a default value will be used, if any of them is omitted or de type or format of them is wrong

Option Type Default Description
mode Mode (string) Mode.combined Mode of generating the final CSS rules
ltrPrefix string or string[] [dir="ltr"] Prefix to use in the left-to-right CSS rules
rtlPrefix string or string[] [dir="rtl"] Prefix to use in the right-to-left CSS rules
bothPrefix string or string[] [dir] Prefix to use for styles in both directions when the specificity of the ltr or rtl styles will override them
safeBothPrefix boolean false Add the bothPrefix to those declarations that can be flipped to avoid them being overridden by specificity
source Source (string) Source.ltr The direction from which the final CSS will be generated
processUrls boolean false Change the strings using the string map also in URLs
processKeyFrames boolean false Flip keyframe animations
useCalc boolean false Flips background-position, background-position-x and transform-origin properties if they are expressed in length units using calc
stringMap PluginStringMap[] Check below An array of strings maps that will be used to make the replacements of the URLs and rules selectors names
autoRename Autorename (string) Autorename.disabled Flip or not the selectors names of the rules without directional properties using the stringMap
greedy boolean false When autoRename is enabled and greedy is true, the strings replacements will not take into account word boundaries

mode

Expand

The mode option has been explained in the Output using the combined mode and Output using the override mode sections. To avoid using magic strings, the package exposes an object with these values, but it is possible to using strings values:

import postcss from 'postcss';
import { postcssRTLCSS, Mode } from 'postcss-trlcss';
 
const input = '... css code ...';
const optionsCombined = { mode: Mode.combined }; // This is the default value
const optionsOverride = { mode: Mode.override };
 
const outputCombined = postcss([
    postcssRTLCSS(optionsCombined)
]).process(input);
 
const outputOverride = postcss([
    postcssRTLCSS(optionsOverride)
]).process(input);


ltrPrefix and rtlPrefix

Expand

These two options manage the prefix strings for each direction. They can be strings or arrays of strings:

input
.test1.test2 {
    left: 10px;
}
 
.test3,
.test4 {
    text-align: left;
}
Using strings
const options = {
    ltrPrefix: '.ltr',
    rtlPrefix: '.rtl'
};
output
.ltr .test1.ltr .test2 {
    left: 10px;
}
 
.rtl .test1.rtl .test2 {
    right: 10px;
}
 
.ltr .test3,
.ltr .test4 {
    text-align: left;
}
 
.rtl .test3,
.rtl .test4 {
    text-align: right;
}
Using arrays of strings
const options = {
    ltrPrefix: ['[dir="ltr"]', '.ltr'],
    rtlPrefix: ['[dir="rtl"]', '.rtl']
};
output
[dir="ltr"] .test1.ltr .test1[dir="ltr"] .test2.ltr .test2 {
    left: 10px;
}
 
[dir="rtl"] .test1.rtl .test1[dir="rtl"] .test2.rtl .test2 {
    right: 10px;
}
 
[dir="ltr"] .test3,
.ltr .test3,
[dir="ltr"] .test4,
.ltr .test4 {
    text-align: left;
}
 
[dir="rtl"] .test3,
.rtl .test3,
[dir="rtl"] .test4,
.rtl .test4 {
    text-align: right;
}


bothPrefix

Expand

This prefix will be used in some specific cases in which a ltr or rtl rule will override declarations located in the main rule due to specificity. Consider the next example using the option processUrls as true:

.test1 {
    background: url('icons/ltr/arrow.png');
    background-size: 10px 20px;
    width: 10px;
}

The generated CSS would be:

.test1 {
    background-size: 10px 20px;
    width: 10px;
}
 
[dir="ltr"] .test1 {
    background: url('icons/ltr/arrow.png');
}
 
[dir="rtl"] .test1 {
    background: url('icons/rtl/arrow.png');
}

In the previous case, the background-size property has been overridden by the background one. Even if we change the order of the rules, the last ones have a higher specificity, so they will rule over the first one.

To solve this, another rule will be created at the end using the bothPrefix parameter:

.test1 {
    width: 10px;
}
 
[dir="ltr"] .test1 {
    background: url('icons/ltr/arrow.png');
}
 
[dir="rtl"] .test1 {
    background: url('icons/rtl/arrow.png');
}
 
[dir] {
    background-size: 10px 20px;
}

And no matter the direction, the background-size property is respected.


safeBothPrefix

Expand

This option will add the boxPrefix option to those declarations that can be flipped, no matter if they are not overridden in the same rule. This avoids them being overridden by specificity of other flipped declarations contained in other rules. For example, let's consider that we have a div element with the next rules:

<div class="test1 test2">
    This is an example
</div> 
.test1 {
    color: #FFF;
    padding: 4px 10px 4px 20px;
    width: 100%;
}
 
.test2 {
    padding: 0;
}

The expecting result is that the padding of the element becomes 0 as it has been reset by test2. With safeBothPrefix in false, the generated CSS will be:

.test1 {
    color: #FFF;
    width: 100%;
}
 
[dir="ltr"] .test1 {
    padding: 4px 10px 4px 20px;
}
 
[dir="rtl"] .test1 {
    padding: 4px 20px 4px 10px;
}
 
.test2 {
    padding: 0;
}

The result is that the padding properties of test1 have more specificity than the same property in tes2, so it is not reset if both rules are applied at the same time. Let's check the result if safeBothPrefix is true:

.test1 {
    color: #FFF;
    width: 100%;
}
 
[dir="ltr"] .test1 {
    padding: 4px 10px 4px 20px;
}
 
[dir="rtl"] .test1 {
    padding: 4px 20px 4px 10px;
}
 
[dir] .test2 {
    padding: 0;
}

As test2 has the same level of specificity as test1, now the result is that the padding is reset if both rules are used at the same time.


source

Expand

This option manages if the conversion will be from LTR to RTL or vice versa.

input
.test1.test2 {
    left: 10px;
}
Using Source.ltr in combined mode
import { Mode, Source } from 'postcss-rtlcss';
 
const options = {
    mode: Mode.combined,
    source: Source.ltr // This is the default value
};
output
[dir="ltr"] .test1[dir="ltr"] .test2 {
    left: 10px;
}
 
[dir="rtl"] .test1[dir="rtl"] .test2 {
    right: 10px;
}
Using Source.rtl in override mode
import { Mode, Source } from 'postcss-rtlcss';
 
const options = {
    mode: Mode.override,
    source: Source.rtl
};
output
.test1.test2 {
    left: 10px;
}
 
[dir="ltr"] .test1[dir="ltr"] .test2 {
    left: unset;
    right: 10px;
}


processUrls

Expand

This options manages if the strings of the URLs should be flipped taken into account the string map:

input
.test1.test2 {
    background-image: url("./folder/subfolder/icons/ltr/chevron-left.png");
    left: 10px;
}
processUrls false
const options = { processUrls: false }; // This is the default value
output
.test1.test2 {
    background-image: url("./folder/subfolder/icons/ltr/chevron-left.png");
}
 
[dir="ltr"] .test1[dir="ltr"] .test2 {
    left: 10px;
}
 
[dir="rtl"] .test1[dir="rtl"] .test2 {
    right: 10px;
}
processUrls true
const options = { processUrls: true };
output
[dir="ltr"] .test1[dir="ltr"] .test2 {
    background-image: url("./folder/subfolder/icons/ltr/chevron-left.png");
    left: 10px;
}
 
[dir="rtl"] .test1[dir="rtl"] .test2 {
    background-image: url("./folder/subfolder/icons/rtl/chevron-right.png");
    right: 10px;
}


processKeyFrames

Expand

This option manages if the @keyframes animation rules should be flipped:

input
.test1 {
    animation: 5s flip 1s ease-in-out;
    color: #FFF;
}
 
@keyframes flip {
    from {
        transform: translateX(100px);
    }
    to {
        transform: translateX(0);
    }
}
processKeyFrames false
const options = { processKeyFrames: false }; // This is the default value
output
.test1 {
    animation: 5s flip 1s ease-in-out;
    color: #FFF;
}
 
@keyframes flip {
    from {
        transform: translateX(100px);
    }
    to {
        transform: translateX(0);
    }
}
processKeyFrames true
const options = { processKeyFrames: true };
output
.test1 {
    color: #FFF;
}
 
[dir="ltr"] .test1 {
    animation: 5s flip-ltr 1s ease-in-out;
}
 
[dir="rtl"] .test1 {
    animation: 5s flip-rtl 1s ease-in-out;
}
 
@keyframes flip-ltr {
    from {
        transform: translateX(100px);
    }
    to {
        transform: translateX(0);
    }
}
 
@keyframes flip-rtl {
    from {
        transform: translateX(-100px);
    }
    to {
        transform: translateX(0);
    }
}


useCalc

Expand

This options, when it is enabled, flips background-position, background-position-x and transform-origin properties if they are expressed in length units using calc:

input
.test {
    background-image: url("./folder/subfolder/icons/ltr/chevron-left.png");
    background-position-x: 5px;
    left: 10px;
    transform-origin: 10px 20px;
    transform: scale(0.50.5);
}
useCalc false
const options = { useCalc: false }; // This is the default value
output
.test {
    background-image: url("./folder/subfolder/icons/ltr/chevron-left.png");
    background-position-x: 5px;
    transform-origin: 10px 20px;
    transform: scale(0.50.5);
}
 
[dir="ltr"] .test {
    left: 10px;
}
 
[dir="rtl"] .test {
    right: 10px;
}
useCalc true
const options = { useCalc: true };
output
.test {
    background-image: url("./folder/subfolder/icons/ltr/chevron-left.png");
    transform: scale(0.50.5);
}
 
[dir="ltr"] .test {
    background-position-x: 5px;
    left: 10px;
    transform-origin: 10px 20px;
}
 
[dir="rtl"] .test {
    background-position-x: calc(100% - 5px);
    right: 10px;
    transform-origin: calc(100% - 10px) 20px;
}


stringMap

Expand

An array of strings maps that will be used to make the replacements of the URLs and rules selectors names. The name parameter is optional, but if you want to override any of the default string maps, just add your own using the same name.

// This is the default string map object
const options = {
    stringMap: [
        {
            name: 'left-right',
            search : ['left', 'Left', 'LEFT'],
            replace : ['right', 'Right', 'RIGHT']
        },
        {
            name: 'ltr-rtl',
            search  : ['ltr', 'Ltr', 'LTR'],
            replace : ['rtl', 'Rtl', 'RTL'],
        }
    ]
};


autoRename

Expand

Flip or not the selectors names of the rules without directional properties using the stringMap.

input
.test1-ltr {
    color: #FFF;
}
 
.test2-left::before {
    content: "\f007";
}
 
.test2-right::before {
    content: "\f010";
}
Using Autorename.flexible
import { Autorename } from 'postcss-rtlcss';
 
const options = {
    autoRename: Autorename.flexible
};
output
.test1-rtl {
    color: #FFF;
}
 
.test2-right::before {
    content: "\f007";
}
 
.test2-left::before {
    content: "\f010";
}
Using Autorename.strict
import { Autorename } from 'postcss-rtlcss';
 
const options = {
    autoRename: Autorename.strict
};
output
/* This selector will not be flipped because it doesn't have a counterpart */
.test1-ltr {
    color: #FFF;
}
 
.test2-right::before {
    content: "\f007";
}
 
.test2-left::before {
    content: "\f010";
}


greedy

Expand

When autoRename is enabled and greedy is true, the strings replacements will not take into account word boundaries.

input
.test1-ltr {
    color: #FFF;
}
 
.test2ltr {
    width: 100%;
}
greedy false
import { Autorename } from 'postcss-rtlcss';
 
const options = {
    autoRename: Autorename.flexible,
    greedy: false // This is the default value
};
output
.test1-rtl {
    color: #FFF;
}
 
.test2ltr {
    width: 100%;
}
greedy true
import { Autorename } from 'postcss-rtlcss';
 
const options = {
    autoRename: Autorename.flexible,
    greedy: true
};
output
.test1-rtl {
    color: #FFF;
}
 
.test2rtl {
    width: 100%;
}


Control Directives

Control directives are placed between rules or declarations. They can target a single node or a set of nodes.

Directive Description
/*rtl:ignore*/ Ignores processing of the following rule or declaration
/*rtl:begin:ignore*/ Starts an ignoring block
/*rtl:end:ignore*/ Ends an ignoring block
/*rtl:rename*/ This directive forces renaming in the next rule or declaration no mattering the value of the properties processUrls or autoRename
/*rtl:begin:rename*/ Starts a renaming block
/*rtl:end:rename*/ Ends a renaming block
/*rtl:raw:{CSS}*/ Parses the CSS parameter and inserts it in its place. Depending on the source parameter the parsed CSS will be treated as rtl or ltr

/*rtl:ignore*/

Expand

This directive ignores processing of the following rule or declaration. In the next block the whole declaration will be ignored:

input
/*rtl:ignore*/
.test1.test2 {
    text-align: left;
    left: 10px;
}
output
.test1.test2 {
    text-align: left;
    left: 10px;
}

In the next block only the left property will be ignored:

input
.test3.test4 {
    text-align: left;
    /*rtl:ignore*/
    left: 10px;
}
output
.test3.test4 {
    left: 10px;
}
 
[dir="ltr"] .test3[dir="ltr"] .test4 {
    text-align: left;
}
 
[dir="rtl"] .test3[dir="rtl"] .test4 {
    text-align: right;
}


/*rtl:begin:ignore*/ and /*rtl:end:ignore*/

Expand

These directives should be used together, they will provide the beginning and the end for ignoring rules or declarations.

Note: The directives inserted between these blocks will be ignored and maintained in the final output.

Ignoring multiple rules:

input
/*rtl:begin:ignore*/
.test1.test2 {
    left: 10px;
    text-align: left;
}
 
.test3 {
    padding: 1px 2px 3px 4px;
}
/*rtl:end:ignore*/
output
.test1.test2 {
    left: 10px;
    text-align: left;
}
 
.test3 {
    padding: 1px 2px 3px 4px;
}

Ignoring multiple declarations:

input
.test1.test2 {
    left: 10px;
    /*rtl:begin:ignore*/
    margin-left: 4em;
    padding: 1px 2px 3px 4px;
    /*rtl:end:ignore*/
    text-align: left;
}
output
.test1.test2 {
    margin-left: 4em;
    padding: 1px 2px 3px 4px;
}
 
[dir="ltr"] .test1[dir="ltr"] .test2 {
    left: 10px;
    text-align: left;
}
 
[dir="rtl"] .test1[dir="rtl"] .test2 {
    right: 10px;
    text-align: right;
}


/*rtl:rename*/

Expand

This directive forces renaming in the next rule or declaration no mattering the value of the properties processUrls or autoRename:

input
/*rtl:rename*/
.test-left {
    width: 100%;
}
 
.test {
    /*rtl:rename*/
    background-image: url("/icons/icon-left.png");
}
output
.test-right {
    width: 100%
}
 
[dir="ltr"] .test {
    background-image: url("/icons/icon-left.png");
}
 
[dir="rtl"] .test {
    background-image: url("/icons/icon-right.png");
}


/*rtl:begin:rename*/ and /*rtl:end:rename*/

Expand

These directives should be used together, they will provide the beginning and the end for renaming rules or declarations.

input
/*rtl:begin:rename*/
.icon-left {
    content: "\\f40";
}
 
.icon-right {
    content: "\\f56";
}
/*rtl:end:rename*/
 
.test {
    /*rtl:begin:rename*/
    background-image: url("/images/background-left.png");
    cursor: url("/images/cursor-ltr.png");
    /*rtl:end:rename*/
}
output
.icon-right {
    content: "\\f40";
}
 
.icon-left {
    content: "\\f56";
}
 
[dir="ltr"] .test {
    background-image: url("/images/background-left.png");
    cursor: url("/images/cursor-ltr.png");
}
 
[dir="rtl"] .test {
    background-image: url("/images/background-right.png");
    cursor: url("/images/cursor-rtl.png");
}


/*rtl:raw:{CSS}*/

Expand

Parses the CSS parameter and inserts it in its place. Depending on the source parameter the parsed CSS will be treated as rtl or ltr:

input
.test1 {
    color: #EFEFEF;
    left: 10px;
    /*rtl:raw:
    height: 50px;
    width: 100px;*/
}
 
/*rtl:raw:.test2 {
    color: #EFEFEF;
    left: 10px;
    width: 100%;    
}
 
.test3 {
    transform: translate(10px, 20px);
}
*/
output
.test1 {
    color: #EFEFEF;
}
 
[dir="ltr"] .test1 {
    left: 10px;
}
 
[dir="rtl"] .test1 {
    right: 10px;
    height: 50px;
    width: 100px;
}
 
[dir="rtl"] .test2 {
    color: #EFEFEF;
    left: 10px;
    width: 100%;    
}
 
[dir="rtl"] .test3 {
    transform: translate(10px20px);
}


Value Directives

Value directives are placed anywhere inside the declaration value. They target the containing declaration node.

Directive Description
/*rtl:ignore*/ Ignores processing of the declaration
/*rtl:append{value}*/ Appends {value} to the end of the declaration value
/*rtl:insert:{value}*/ Inserts {value} to where the directive is located inside the declaration value
/*rtl:prepend:{value}*/ Prepends {value} to the begining of the declaration value
/*rtl:{value}*/ Replaces the declaration value with {value}

/*rtl:ignore*/

Expand

This directive ignores processing of the current declaration:

input
.test1.test2 {
    text-align: left /*rtl:ignore*/;
    left: 10px;
}
output
.test1.test2 {
    text-align: left;
}
 
[dir="ltr"] .test1[dir="ltr"] .test2 {
    left: 10px;
}
 
[dir="rtl"] .test1[dir="rtl"] .test2 {
    right: 10px;
}


/*rtl:append{value}*/

Expand

This directive appends {value} to the end of the declaration value:

input
.test1.test2 {
    padding: 10px /*rtl:append20px*/;
    left: 10px;
}
output
[dir="ltr"] .test1[dir="ltr"] .test2 {
    padding: 10px;
    left: 10px;
}
 
[dir="rtl"] .test1[dir="rtl"] .test2 {
    padding: 10px 20px;
    right: 10px;
}


/*rtl:insert:{value}*/

Expand

This directive inserts {value} to where the directive is located inside the declaration value:

input
.test1.test2 {
    padding: 10px/*rtl:insert 20px*/ 5px;
    left: 10px;
}
output
[dir="ltr"] .test1[dir="ltr"] .test2 {
    padding: 10px 5px;
    left: 10px;
}
 
[dir="rtl"] .test1[dir="rtl"] .test2 {
    padding: 10px 20px 5px;
    right: 10px;
}


/*rtl:prepend:{value}*/

Expand

This directive prepends {value} to the begining of the declaration value:

input
.test1.test2 {
    font-family: ArialHelvetica/*rtl:prepend:"Droid Arabic Kufi", */;
    left: 10px;
}
output
[dir="ltr"] .test1[dir="ltr"] .test2 {
    font-family: ArialHelvetica;
    left: 10px;
}
 
[dir="rtl"] .test1[dir="rtl"] .test2 {
    font-family: "Droid Arabic Kufi"ArialHelvetica;
    right: 10px;
}


/*rtl:{value}*/

Expand

This directive replaces the declaration value with {value}:

input
.test1.test2 {
    font-family: ArialHelvetica/*rtl:"Droid Arabic Kufi"*/;
    left: 10px;
}
output
[dir="ltr"] .test1[dir="ltr"] .test2 {
    font-family: ArialHelvetica;
    left: 10px;
}
 
[dir="rtl"] .test1[dir="rtl"] .test2 {
    font-family: "Droid Arabic Kufi";
    right: 10px;
}


If you do not use PostCSS, add it according to official docs and set this plugin in settings.

Install

npm i postcss-rtlcss

DownloadsWeekly Downloads

6

Version

1.6.1

License

MIT

Unpacked Size

112 kB

Total Files

17

Last publish

Collaborators

  • avatar