diff --git a/.github/ISSUE_TEMPLATE/css-issue.yml b/.github/ISSUE_TEMPLATE/css-issue.yml
index 135995ae7..9e5322213 100644
--- a/.github/ISSUE_TEMPLATE/css-issue.yml
+++ b/.github/ISSUE_TEMPLATE/css-issue.yml
@@ -87,6 +87,7 @@ body:
- PostCSS Is Pseudo Class
- PostCSS Lab Function
- PostCSS Logical
+ - PostCSS Logical Viewport Units
- PostCSS Media Queries Aspect-Ratio Number Values
- PostCSS Media Query Ranges
- PostCSS Nested Calc
diff --git a/.github/ISSUE_TEMPLATE/plugin-issue.yml b/.github/ISSUE_TEMPLATE/plugin-issue.yml
index 2fa9f2915..c3fb620c3 100644
--- a/.github/ISSUE_TEMPLATE/plugin-issue.yml
+++ b/.github/ISSUE_TEMPLATE/plugin-issue.yml
@@ -89,6 +89,7 @@ body:
- PostCSS Is Pseudo Class
- PostCSS Lab Function
- PostCSS Logical
+ - PostCSS Logical Viewport Units
- PostCSS Media Queries Aspect-Ratio Number Values
- PostCSS Media Query Ranges
- PostCSS Nested Calc
diff --git a/.github/labeler.yml b/.github/labeler.yml
index c874c1372..c762356b6 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -136,6 +136,10 @@
- plugins/postcss-logical/**
- experimental/postcss-logical/**
+"plugins/postcss-logical-viewport-units":
+ - plugins/postcss-logical-viewport-units/**
+ - experimental/postcss-logical-viewport-units/**
+
"plugins/media-queries-aspect-ratio-number-values":
- plugins/postcss-media-queries-aspect-ratio-number-values/**
- experimental/postcss-media-queries-aspect-ratio-number-values/**
diff --git a/package-lock.json b/package-lock.json
index 54480558c..898244fba 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1961,6 +1961,10 @@
"resolved": "plugins/postcss-is-pseudo-class",
"link": true
},
+ "node_modules/@csstools/postcss-logical-viewport-units": {
+ "resolved": "plugins/postcss-logical-viewport-units",
+ "link": true
+ },
"node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": {
"resolved": "plugins/postcss-media-queries-aspect-ratio-number-values",
"link": true
@@ -7797,6 +7801,24 @@
"postcss": "^8.4"
}
},
+ "plugins/postcss-logical-viewport-units": {
+ "name": "@csstools/postcss-logical-viewport-units",
+ "version": "1.0.0",
+ "license": "CC0-1.0",
+ "devDependencies": {
+ "@csstools/css-tokenizer": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ }
+ },
"plugins/postcss-media-queries-aspect-ratio-number-values": {
"name": "@csstools/postcss-media-queries-aspect-ratio-number-values",
"version": "1.0.0",
@@ -9420,6 +9442,12 @@
"puppeteer": "^19.5.2"
}
},
+ "@csstools/postcss-logical-viewport-units": {
+ "version": "file:plugins/postcss-logical-viewport-units",
+ "requires": {
+ "@csstools/css-tokenizer": "^1.0.0"
+ }
+ },
"@csstools/postcss-media-queries-aspect-ratio-number-values": {
"version": "file:plugins/postcss-media-queries-aspect-ratio-number-values",
"requires": {
diff --git a/plugins/postcss-logical-viewport-units/.gitignore b/plugins/postcss-logical-viewport-units/.gitignore
new file mode 100644
index 000000000..e5b28db4a
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+package-lock.json
+yarn.lock
+*.result.css
+*.result.css.map
+*.result.html
diff --git a/plugins/postcss-logical-viewport-units/.nvmrc b/plugins/postcss-logical-viewport-units/.nvmrc
new file mode 100644
index 000000000..39e593ebe
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/.nvmrc
@@ -0,0 +1 @@
+v18.8.0
diff --git a/plugins/postcss-logical-viewport-units/.tape.mjs b/plugins/postcss-logical-viewport-units/.tape.mjs
new file mode 100644
index 000000000..ac23bcc9d
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/.tape.mjs
@@ -0,0 +1,35 @@
+import postcssTape from '../../packages/postcss-tape/dist/index.mjs';
+import plugin from '@csstools/postcss-logical-viewport-units';
+
+postcssTape(plugin)({
+ basic: {
+ message: "supports basic usage",
+ },
+ 'basic:hebrew': {
+ message: "supports { inlineDirection: 'right-to-left' }",
+ options: {
+ inlineDirection: 'right-to-left'
+ }
+ },
+ 'basic:vertical': {
+ message: "supports { inlineDirection: 'top-to-bottom' }",
+ options: {
+ inlineDirection: 'top-to-bottom'
+ }
+ },
+ 'examples/example': {
+ message: 'minimal example',
+ },
+ 'examples/example:vertical': {
+ message: 'minimal example',
+ options: {
+ inlineDirection: 'top-to-bottom'
+ }
+ },
+ 'examples/example:preserve-false': {
+ message: 'minimal example',
+ options: {
+ preserve: false
+ }
+ },
+});
diff --git a/plugins/postcss-logical-viewport-units/CHANGELOG.md b/plugins/postcss-logical-viewport-units/CHANGELOG.md
new file mode 100644
index 000000000..dd92bbe73
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changes to PostCSS Logical Viewport Units
+
+### 1.0.0 (Unreleased)
+
+- Initial version
diff --git a/plugins/postcss-logical-viewport-units/INSTALL.md b/plugins/postcss-logical-viewport-units/INSTALL.md
new file mode 100644
index 000000000..625923c4a
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/INSTALL.md
@@ -0,0 +1,235 @@
+# Installing PostCSS Logical Viewport Units
+
+[PostCSS Logical Viewport Units] runs in all Node environments, with special instructions for:
+
+- [Node](#node)
+- [PostCSS CLI](#postcss-cli)
+- [PostCSS Load Config](#postcss-load-config)
+- [Webpack](#webpack)
+- [Next.js](#nextjs)
+- [Gulp](#gulp)
+- [Grunt](#grunt)
+
+
+
+## Node
+
+Add [PostCSS Logical Viewport Units] to your project:
+
+```bash
+npm install postcss @csstools/postcss-logical-viewport-units --save-dev
+```
+
+Use it as a [PostCSS] plugin:
+
+```js
+// commonjs
+const postcss = require('postcss');
+const postcssLogicalViewportUnits = require('@csstools/postcss-logical-viewport-units');
+
+postcss([
+ postcssLogicalViewportUnits(/* pluginOptions */)
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+```js
+// esm
+import postcss from 'postcss';
+import postcssLogicalViewportUnits from '@csstools/postcss-logical-viewport-units';
+
+postcss([
+ postcssLogicalViewportUnits(/* pluginOptions */)
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+## PostCSS CLI
+
+Add [PostCSS CLI] to your project:
+
+```bash
+npm install postcss-cli @csstools/postcss-logical-viewport-units --save-dev
+```
+
+Use [PostCSS Logical Viewport Units] in your `postcss.config.js` configuration file:
+
+```js
+const postcssLogicalViewportUnits = require('@csstools/postcss-logical-viewport-units');
+
+module.exports = {
+ plugins: [
+ postcssLogicalViewportUnits(/* pluginOptions */)
+ ]
+}
+```
+
+## PostCSS Load Config
+
+If your framework/CLI supports [`postcss-load-config`](https://github.com/postcss/postcss-load-config).
+
+```bash
+npm install @csstools/postcss-logical-viewport-units --save-dev
+```
+
+`package.json`:
+
+```json
+{
+ "postcss": {
+ "plugins": {
+ "@csstools/postcss-logical-viewport-units": {}
+ }
+ }
+}
+```
+
+`.postcssrc.json`:
+
+```json
+{
+ "plugins": {
+ "@csstools/postcss-logical-viewport-units": {}
+ }
+}
+```
+
+_See the [README of `postcss-load-config`](https://github.com/postcss/postcss-load-config#usage) for more usage options._
+
+## Webpack
+
+_Webpack version 5_
+
+Add [PostCSS Loader] to your project:
+
+```bash
+npm install postcss-loader @csstools/postcss-logical-viewport-units --save-dev
+```
+
+Use [PostCSS Logical Viewport Units] in your Webpack configuration:
+
+```js
+module.exports = {
+ module: {
+ rules: [
+ {
+ test: /\.css$/i,
+ use: [
+ "style-loader",
+ {
+ loader: "css-loader",
+ options: { importLoaders: 1 },
+ },
+ {
+ loader: "postcss-loader",
+ options: {
+ postcssOptions: {
+ plugins: [
+ // Other plugins,
+ [
+ "@csstools/postcss-logical-viewport-units",
+ {
+ // Options
+ },
+ ],
+ ],
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+};
+```
+
+## Next.js
+
+Read the instructions on how to [customize the PostCSS configuration in Next.js](https://nextjs.org/docs/advanced-features/customizing-postcss-config)
+
+```bash
+npm install @csstools/postcss-logical-viewport-units --save-dev
+```
+
+Use [PostCSS Logical Viewport Units] in your `postcss.config.json` file:
+
+```json
+{
+ "plugins": [
+ "@csstools/postcss-logical-viewport-units"
+ ]
+}
+```
+
+```json5
+{
+ "plugins": [
+ [
+ "@csstools/postcss-logical-viewport-units",
+ {
+ // Optionally add plugin options
+ }
+ ]
+ ]
+}
+```
+
+## Gulp
+
+Add [Gulp PostCSS] to your project:
+
+```bash
+npm install gulp-postcss @csstools/postcss-logical-viewport-units --save-dev
+```
+
+Use [PostCSS Logical Viewport Units] in your Gulpfile:
+
+```js
+const postcss = require('gulp-postcss');
+const postcssLogicalViewportUnits = require('@csstools/postcss-logical-viewport-units');
+
+gulp.task('css', function () {
+ var plugins = [
+ postcssLogicalViewportUnits(/* pluginOptions */)
+ ];
+
+ return gulp.src('./src/*.css')
+ .pipe(postcss(plugins))
+ .pipe(gulp.dest('.'));
+});
+```
+
+## Grunt
+
+Add [Grunt PostCSS] to your project:
+
+```bash
+npm install grunt-postcss @csstools/postcss-logical-viewport-units --save-dev
+```
+
+Use [PostCSS Logical Viewport Units] in your Gruntfile:
+
+```js
+const postcssLogicalViewportUnits = require('@csstools/postcss-logical-viewport-units');
+
+grunt.loadNpmTasks('grunt-postcss');
+
+grunt.initConfig({
+ postcss: {
+ options: {
+ processors: [
+ postcssLogicalViewportUnits(/* pluginOptions */)
+ ]
+ },
+ dist: {
+ src: '*.css'
+ }
+ }
+});
+```
+
+[Gulp PostCSS]: https://github.com/postcss/gulp-postcss
+[Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss
+[PostCSS]: https://github.com/postcss/postcss
+[PostCSS CLI]: https://github.com/postcss/postcss-cli
+[PostCSS Loader]: https://github.com/postcss/postcss-loader
+[PostCSS Logical Viewport Units]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-logical-viewport-units
+[Next.js]: https://nextjs.org
diff --git a/plugins/postcss-logical-viewport-units/LICENSE.md b/plugins/postcss-logical-viewport-units/LICENSE.md
new file mode 100644
index 000000000..0bc1fa706
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/LICENSE.md
@@ -0,0 +1,108 @@
+# CC0 1.0 Universal
+
+## Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator and
+subsequent owner(s) (each and all, an “owner”) of an original work of
+authorship and/or a database (each, a “Work”).
+
+Certain owners wish to permanently relinquish those rights to a Work for the
+purpose of contributing to a commons of creative, cultural and scientific works
+(“Commons”) that the public can reliably and without fear of later claims of
+infringement build upon, modify, incorporate in other works, reuse and
+redistribute as freely as possible in any form whatsoever and for any purposes,
+including without limitation commercial purposes. These owners may contribute
+to the Commons to promote the ideal of a free culture and the further
+production of creative, cultural and scientific works, or to gain reputation or
+greater distribution for their Work in part through the use and efforts of
+others.
+
+For these and/or other purposes and motivations, and without any expectation of
+additional consideration or compensation, the person associating CC0 with a
+Work (the “Affirmer”), to the extent that he or she is an owner of Copyright
+and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and
+publicly distribute the Work under its terms, with knowledge of his or her
+Copyright and Related Rights in the Work and the meaning and intended legal
+effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+ protected by copyright and related or neighboring rights (“Copyright and
+ Related Rights”). Copyright and Related Rights include, but are not limited
+ to, the following:
+ 1. the right to reproduce, adapt, distribute, perform, display, communicate,
+ and translate a Work;
+ 2. moral rights retained by the original author(s) and/or performer(s);
+ 3. publicity and privacy rights pertaining to a person’s image or likeness
+ depicted in a Work;
+ 4. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(i), below;
+ 5. rights protecting the extraction, dissemination, use and reuse of data in
+ a Work;
+ 6. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation thereof,
+ including any amended or successor version of such directive); and
+ 7. other similar, equivalent or corresponding rights throughout the world
+ based on applicable law or treaty, and any national implementations
+ thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention of,
+ applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
+ unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright
+ and Related Rights and associated claims and causes of action, whether now
+ known or unknown (including existing as well as future claims and causes of
+ action), in the Work (i) in all territories worldwide, (ii) for the maximum
+ duration provided by applicable law or treaty (including future time
+ extensions), (iii) in any current or future medium and for any number of
+ copies, and (iv) for any purpose whatsoever, including without limitation
+ commercial, advertising or promotional purposes (the “Waiver”). Affirmer
+ makes the Waiver for the benefit of each member of the public at large and
+ to the detriment of Affirmer’s heirs and successors, fully intending that
+ such Waiver shall not be subject to revocation, rescission, cancellation,
+ termination, or any other legal or equitable action to disrupt the quiet
+ enjoyment of the Work by the public as contemplated by Affirmer’s express
+ Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason be
+ judged legally invalid or ineffective under applicable law, then the Waiver
+ shall be preserved to the maximum extent permitted taking into account
+ Affirmer’s express Statement of Purpose. In addition, to the extent the
+ Waiver is so judged Affirmer hereby grants to each affected person a
+ royalty-free, non transferable, non sublicensable, non exclusive,
+ irrevocable and unconditional license to exercise Affirmer’s Copyright and
+ Related Rights in the Work (i) in all territories worldwide, (ii) for the
+ maximum duration provided by applicable law or treaty (including future time
+ extensions), (iii) in any current or future medium and for any number of
+ copies, and (iv) for any purpose whatsoever, including without limitation
+ commercial, advertising or promotional purposes (the “License”). The License
+ shall be deemed effective as of the date CC0 was applied by Affirmer to the
+ Work. Should any part of the License for any reason be judged legally
+ invalid or ineffective under applicable law, such partial invalidity or
+ ineffectiveness shall not invalidate the remainder of the License, and in
+ such case Affirmer hereby affirms that he or she will not (i) exercise any
+ of his or her remaining Copyright and Related Rights in the Work or (ii)
+ assert any associated claims and causes of action with respect to the Work,
+ in either case contrary to Affirmer’s express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+ 1. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+ 2. Affirmer offers the Work as-is and makes no representations or warranties
+ of any kind concerning the Work, express, implied, statutory or
+ otherwise, including without limitation warranties of title,
+ merchantability, fitness for a particular purpose, non infringement, or
+ the absence of latent or other defects, accuracy, or the present or
+ absence of errors, whether or not discoverable, all to the greatest
+ extent permissible under applicable law.
+ 3. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without
+ limitation any person’s Copyright and Related Rights in the Work.
+ Further, Affirmer disclaims responsibility for obtaining any necessary
+ consents, permissions or other rights required for any use of the Work.
+ 4. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to this
+ CC0 or use of the Work.
+
+For more information, please see
+http://creativecommons.org/publicdomain/zero/1.0/.
diff --git a/plugins/postcss-logical-viewport-units/README.md b/plugins/postcss-logical-viewport-units/README.md
new file mode 100644
index 000000000..2ccc78f71
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/README.md
@@ -0,0 +1,114 @@
+# PostCSS Logical Viewport Units [
][PostCSS]
+
+[
][npm-url] [
][css-url] [
][cli-url] [
][discord]
+
+[PostCSS Logical Viewport Units] lets you easily use `vb` and `vi` length units following the [CSS-Values-4 Specification].
+
+```pcss
+.foo {
+ margin: 10vi 20vb;
+}
+
+/* becomes */
+
+.foo {
+ margin: 10vw 20vh;
+ margin: 10vi 20vb;
+}
+```
+
+## Usage
+
+Add [PostCSS Logical Viewport Units] to your project:
+
+```bash
+npm install postcss @csstools/postcss-logical-viewport-units --save-dev
+```
+
+Use it as a [PostCSS] plugin:
+
+```js
+const postcss = require('postcss');
+const postcssLogicalViewportUnits = require('@csstools/postcss-logical-viewport-units');
+
+postcss([
+ postcssLogicalViewportUnits(/* pluginOptions */)
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+[PostCSS Logical Viewport Units] runs in all Node environments, with special
+instructions for:
+
+- [Node](INSTALL.md#node)
+- [PostCSS CLI](INSTALL.md#postcss-cli)
+- [PostCSS Load Config](INSTALL.md#postcss-load-config)
+- [Webpack](INSTALL.md#webpack)
+- [Next.js](INSTALL.md#nextjs)
+- [Gulp](INSTALL.md#gulp)
+- [Grunt](INSTALL.md#grunt)
+
+## Options
+
+ ### inlineDirection
+
+ The `inlineDirection` option allows you to specify the direction of the inline axe. The default value is `left-to-right` respectively which would match any latin language.
+
+ You might want to tweak these value if you are using a different writing system, such as Arabic, Hebrew or Chinese for example.
+
+ ```js
+ postcssLogicalViewportUnits({
+ inlineDirection: 'top-to-bottom'
+ })
+ ```
+
+ ```pcss
+ .foo {
+ margin: 10vi 20vb;
+}
+
+ /* becomes */
+
+ .foo {
+ margin: 10vh 20vw;
+ margin: 10vi 20vb;
+}
+ ```
+
+ Each direction must be one of the following:
+
+ - `top-to-bottom`
+ - `bottom-to-top`
+ - `left-to-right`
+ - `right-to-left`
+
+ Please do note that transformations won't do anything particular for `right-to-left` or `bottom-to-top`.
+
+### preserve
+
+The `preserve` option determines whether the original notation
+is preserved. By default, it is preserved.
+
+```js
+postcssLogicalViewportUnits({ preserve: false })
+```
+
+```pcss
+.foo {
+ margin: 10vi 20vb;
+}
+
+/* becomes */
+
+.foo {
+ margin: 10vw 20vh;
+}
+```
+
+[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test
+[css-url]: https://cssdb.org/#logical-viewport-units
+[discord]: https://discord.gg/bUadyRwkJS
+[npm-url]: https://www.npmjs.com/package/@csstools/postcss-logical-viewport-units
+
+[PostCSS]: https://github.com/postcss/postcss
+[PostCSS Logical Viewport Units]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-logical-viewport-units
+[CSS-Values-4 Specification]: https://www.w3.org/TR/css-values-4/#viewport-relative-units
diff --git a/plugins/postcss-logical-viewport-units/dist/has-feature.d.ts b/plugins/postcss-logical-viewport-units/dist/has-feature.d.ts
new file mode 100644
index 000000000..a902b0da2
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/dist/has-feature.d.ts
@@ -0,0 +1 @@
+export declare function hasFeature(source: string): boolean;
diff --git a/plugins/postcss-logical-viewport-units/dist/has-supports-at-rule-ancestor.d.ts b/plugins/postcss-logical-viewport-units/dist/has-supports-at-rule-ancestor.d.ts
new file mode 100644
index 000000000..a46009ecc
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/dist/has-supports-at-rule-ancestor.d.ts
@@ -0,0 +1,2 @@
+import type { Node } from 'postcss';
+export declare function hasSupportsAtRuleAncestor(node: Node): boolean;
diff --git a/plugins/postcss-logical-viewport-units/dist/index.cjs b/plugins/postcss-logical-viewport-units/dist/index.cjs
new file mode 100644
index 000000000..f94381309
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/dist/index.cjs
@@ -0,0 +1 @@
+"use strict";var e,t,o=require("@csstools/css-tokenizer");function hasFeature(e){{const t=e.toLowerCase();if(!t.includes("vb")&&!t.includes("vi"))return!1}const t=o.tokenizer({css:e});for(;;){const e=t.nextToken();if(e[0]===o.TokenType.EOF)break;if(e[0]!==o.TokenType.Dimension)continue;const n=e[4].unit.toLowerCase();if("vb"===n||"vi"===n)return!0}return!1}function hasSupportsAtRuleAncestor(e){let t=e.parent;for(;t;)if("atrule"===t.type){if("supports"===t.name.toLowerCase()&&hasFeature(t.params))return!0;t=t.parent}else t=t.parent;return!1}function transform(e,t){const n=o.tokenizer({css:e}),r=[];let i=!1;for(;;){const e=n.nextToken();if(r.push(e),e[0]===o.TokenType.EOF)break;if(e[0]!==o.TokenType.Dimension)continue;const s=t[e[4].unit.toLowerCase()];s&&(e[1]=e[4].value.toString()+s,e[4].unit=s,i=!0)}return i?o.stringify(...r):e}!function(e){e.TopToBottom="top-to-bottom",e.BottomToTop="bottom-to-top",e.RightToLeft="right-to-left",e.LeftToRight="left-to-right"}(e||(e={})),function(e){e.Top="top",e.Right="right",e.Bottom="bottom",e.Left="left"}(t||(t={}));const creator=t=>{const o=Object.assign({inlineDirection:e.LeftToRight,preserve:!0},t),n=Object.values(e);if(!n.includes(o.inlineDirection))throw new Error(`[postcss-logical-viewport-units] "inlineDirection" must be one of ${n.join(", ")}`);const r=[e.LeftToRight,e.RightToLeft].includes(o.inlineDirection),i={vb:"vh",vi:"vw"};return r||(i.vb="vw",i.vi="vh"),{postcssPlugin:"postcss-logical-viewport-units",Declaration(e,{atRule:t}){{const t=e.value.toLowerCase();if(!t.includes("vb")&&!t.includes("vi"))return;const o=e.prev();if(o&&"decl"===o.type&&o.prop===e.prop)return;if(hasSupportsAtRuleAncestor(e))return}const n=transform(e.value,i);if(n===e.value)return;if(e.cloneBefore({value:n}),!o.preserve)return void e.remove();if(!e.variable)return;const r=t({name:"supports",params:"(top: 1vi)",source:e.source}),s=e.parent,u=e.parent.cloneAfter({nodes:[]});u.append(e),r.append(u),s.after(r)}}};creator.postcss=!0,module.exports=creator;
diff --git a/plugins/postcss-logical-viewport-units/dist/index.d.ts b/plugins/postcss-logical-viewport-units/dist/index.d.ts
new file mode 100644
index 000000000..bf4c6641b
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/dist/index.d.ts
@@ -0,0 +1,11 @@
+import type { PluginCreator } from 'postcss';
+import { DirectionFlow } from './lib/types';
+/** postcss-logical-viewport-units plugin options */
+export type pluginOptions = {
+ /** Preserve the original notation. default: false */
+ preserve?: boolean;
+ /** Sets the direction for inline. default: left-to-right */
+ inlineDirection?: DirectionFlow;
+};
+declare const creator: PluginCreator;
+export default creator;
diff --git a/plugins/postcss-logical-viewport-units/dist/index.mjs b/plugins/postcss-logical-viewport-units/dist/index.mjs
new file mode 100644
index 000000000..e8e18bad5
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/dist/index.mjs
@@ -0,0 +1 @@
+import{tokenizer as t,TokenType as e,stringify as o}from"@csstools/css-tokenizer";function hasFeature(o){{const t=o.toLowerCase();if(!t.includes("vb")&&!t.includes("vi"))return!1}const n=t({css:o});for(;;){const t=n.nextToken();if(t[0]===e.EOF)break;if(t[0]!==e.Dimension)continue;const o=t[4].unit.toLowerCase();if("vb"===o||"vi"===o)return!0}return!1}function hasSupportsAtRuleAncestor(t){let e=t.parent;for(;e;)if("atrule"===e.type){if("supports"===e.name.toLowerCase()&&hasFeature(e.params))return!0;e=e.parent}else e=e.parent;return!1}var n,r;function transform(n,r){const i=t({css:n}),s=[];let u=!1;for(;;){const t=i.nextToken();if(s.push(t),t[0]===e.EOF)break;if(t[0]!==e.Dimension)continue;const o=r[t[4].unit.toLowerCase()];o&&(t[1]=t[4].value.toString()+o,t[4].unit=o,u=!0)}return u?o(...s):n}!function(t){t.TopToBottom="top-to-bottom",t.BottomToTop="bottom-to-top",t.RightToLeft="right-to-left",t.LeftToRight="left-to-right"}(n||(n={})),function(t){t.Top="top",t.Right="right",t.Bottom="bottom",t.Left="left"}(r||(r={}));const creator=t=>{const e=Object.assign({inlineDirection:n.LeftToRight,preserve:!0},t),o=Object.values(n);if(!o.includes(e.inlineDirection))throw new Error(`[postcss-logical-viewport-units] "inlineDirection" must be one of ${o.join(", ")}`);const r=[n.LeftToRight,n.RightToLeft].includes(e.inlineDirection),i={vb:"vh",vi:"vw"};return r||(i.vb="vw",i.vi="vh"),{postcssPlugin:"postcss-logical-viewport-units",Declaration(t,{atRule:o}){{const e=t.value.toLowerCase();if(!e.includes("vb")&&!e.includes("vi"))return;const o=t.prev();if(o&&"decl"===o.type&&o.prop===t.prop)return;if(hasSupportsAtRuleAncestor(t))return}const n=transform(t.value,i);if(n===t.value)return;if(t.cloneBefore({value:n}),!e.preserve)return void t.remove();if(!t.variable)return;const r=o({name:"supports",params:"(top: 1vi)",source:t.source}),s=t.parent,u=t.parent.cloneAfter({nodes:[]});u.append(t),r.append(u),s.after(r)}}};creator.postcss=!0;export{creator as default};
diff --git a/plugins/postcss-logical-viewport-units/dist/lib/types.d.ts b/plugins/postcss-logical-viewport-units/dist/lib/types.d.ts
new file mode 100644
index 000000000..2b3c47f9b
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/dist/lib/types.d.ts
@@ -0,0 +1,18 @@
+export declare const DirectionValues: {
+ BlockStart: string;
+ BlockEnd: string;
+ InlineStart: string;
+ InlineEnd: string;
+};
+export declare enum DirectionFlow {
+ TopToBottom = "top-to-bottom",
+ BottomToTop = "bottom-to-top",
+ RightToLeft = "right-to-left",
+ LeftToRight = "left-to-right"
+}
+export declare enum Axes {
+ Top = "top",
+ Right = "right",
+ Bottom = "bottom",
+ Left = "left"
+}
diff --git a/plugins/postcss-logical-viewport-units/dist/transform.d.ts b/plugins/postcss-logical-viewport-units/dist/transform.d.ts
new file mode 100644
index 000000000..0d007ffa5
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/dist/transform.d.ts
@@ -0,0 +1,4 @@
+export declare function transform(source: string, replacements: {
+ vi: 'vw' | 'vh';
+ vb: 'vw' | 'vh';
+}): string;
diff --git a/plugins/postcss-logical-viewport-units/docs/README.md b/plugins/postcss-logical-viewport-units/docs/README.md
new file mode 100644
index 000000000..1072b4f42
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/docs/README.md
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[] lets you easily use `vb` and `vi` length units following the [CSS-Values-4 Specification].
+
+```pcss
+
+
+/* becomes */
+
+
+```
+
+
+
+
+
+## Options
+
+ ### inlineDirection
+
+ The `inlineDirection` option allows you to specify the direction of the inline axe. The default value is `left-to-right` respectively which would match any latin language.
+
+ You might want to tweak these value if you are using a different writing system, such as Arabic, Hebrew or Chinese for example.
+
+ ```js
+ ({
+ inlineDirection: 'top-to-bottom'
+ })
+ ```
+
+ ```pcss
+
+
+ /* becomes */
+
+
+ ```
+
+ Each direction must be one of the following:
+
+ - `top-to-bottom`
+ - `bottom-to-top`
+ - `left-to-right`
+ - `right-to-left`
+
+ Please do note that transformations won't do anything particular for `right-to-left` or `bottom-to-top`.
+
+### preserve
+
+The `preserve` option determines whether the original notation
+is preserved. By default, it is preserved.
+
+```js
+({ preserve: false })
+```
+
+```pcss
+
+
+/* becomes */
+
+
+```
+
+
+[CSS-Values-4 Specification]:
diff --git a/plugins/postcss-logical-viewport-units/package.json b/plugins/postcss-logical-viewport-units/package.json
new file mode 100644
index 000000000..6f4a933fb
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/package.json
@@ -0,0 +1,78 @@
+{
+ "name": "@csstools/postcss-logical-viewport-units",
+ "description": "Use vb and vi length units in CSS",
+ "version": "1.0.0",
+ "contributors": [
+ {
+ "name": "Antonio Laguna",
+ "email": "antonio@laguna.es",
+ "url": "https://antonio.laguna.es"
+ },
+ {
+ "name": "Romain Menke",
+ "email": "romainmenke@gmail.com"
+ }
+ ],
+ "license": "CC0-1.0",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "main": "dist/index.cjs",
+ "module": "dist/index.mjs",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.cjs",
+ "default": "./dist/index.mjs"
+ }
+ },
+ "files": [
+ "CHANGELOG.md",
+ "LICENSE.md",
+ "README.md",
+ "dist"
+ ],
+ "peerDependencies": {
+ "postcss": "^8.4"
+ },
+ "devDependencies": {
+ "@csstools/css-tokenizer": "^1.0.0"
+ },
+ "scripts": {
+ "prebuild": "npm run clean",
+ "build": "rollup -c ../../rollup/default.mjs",
+ "clean": "node -e \"fs.rmSync('./dist', { recursive: true, force: true }); fs.mkdirSync('./dist');\"",
+ "docs": "node ../../.github/bin/generate-docs/install.mjs && node ../../.github/bin/generate-docs/readme.mjs",
+ "lint": "npm run lint:eslint && npm run lint:package-json",
+ "lint:eslint": "eslint ./src --ext .js --ext .ts --ext .mjs --no-error-on-unmatched-pattern",
+ "lint:package-json": "node ../../.github/bin/format-package-json.mjs",
+ "prepublishOnly": "npm run clean && npm run build && npm run test",
+ "test": "node .tape.mjs && npm run test:exports",
+ "test:exports": "node ./test/_import.mjs && node ./test/_require.cjs",
+ "test:rewrite-expects": "REWRITE_EXPECTS=true node .tape.mjs"
+ },
+ "homepage": "https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-logical-viewport-units#readme",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/csstools/postcss-plugins.git",
+ "directory": "plugins/postcss-logical-viewport-units"
+ },
+ "bugs": "https://github.com/csstools/postcss-plugins/issues",
+ "keywords": [
+ "postcss-plugin"
+ ],
+ "csstools": {
+ "cssdbId": "logical-viewport-units",
+ "exportName": "postcssLogicalViewportUnits",
+ "humanReadableName": "PostCSS Logical Viewport Units",
+ "specUrl": "https://www.w3.org/TR/css-values-4/#viewport-relative-units"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/plugins/postcss-logical-viewport-units/src/has-feature.ts b/plugins/postcss-logical-viewport-units/src/has-feature.ts
new file mode 100644
index 000000000..5e8c3a35e
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/src/has-feature.ts
@@ -0,0 +1,31 @@
+import { tokenizer, TokenType } from '@csstools/css-tokenizer';
+
+export function hasFeature(source: string): boolean {
+ {
+ const lowerCaseValue = source.toLowerCase();
+ if (!(lowerCaseValue.includes('vb') || lowerCaseValue.includes('vi'))) {
+ return false;
+ }
+ }
+
+ const t = tokenizer({ css: source });
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const token = t.nextToken();
+ if (token[0] === TokenType.EOF) {
+ break;
+ }
+
+ if (token[0] !== TokenType.Dimension) {
+ continue;
+ }
+
+ const unit = token[4].unit.toLowerCase();
+ if (unit === 'vb' || unit === 'vi') {
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/plugins/postcss-logical-viewport-units/src/has-supports-at-rule-ancestor.ts b/plugins/postcss-logical-viewport-units/src/has-supports-at-rule-ancestor.ts
new file mode 100644
index 000000000..3141478b7
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/src/has-supports-at-rule-ancestor.ts
@@ -0,0 +1,22 @@
+import type { Node, AtRule } from 'postcss';
+import { hasFeature } from './has-feature';
+
+export function hasSupportsAtRuleAncestor(node: Node): boolean {
+ let parent = node.parent;
+ while (parent) {
+ if (parent.type !== 'atrule') {
+ parent = parent.parent;
+ continue;
+ }
+
+ if ((parent as AtRule).name.toLowerCase() === 'supports') {
+ if (hasFeature((parent as AtRule).params)) {
+ return true;
+ }
+ }
+
+ parent = parent.parent;
+ }
+
+ return false;
+}
diff --git a/plugins/postcss-logical-viewport-units/src/index.ts b/plugins/postcss-logical-viewport-units/src/index.ts
new file mode 100644
index 000000000..5868e6a41
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/src/index.ts
@@ -0,0 +1,101 @@
+import type { PluginCreator } from 'postcss';
+import { hasSupportsAtRuleAncestor } from './has-supports-at-rule-ancestor';
+import { DirectionFlow } from './lib/types';
+import { transform } from './transform';
+
+/** postcss-logical-viewport-units plugin options */
+export type pluginOptions = {
+ /** Preserve the original notation. default: false */
+ preserve?: boolean,
+ /** Sets the direction for inline. default: left-to-right */
+ inlineDirection?: DirectionFlow,
+};
+
+const creator: PluginCreator = (opts?: pluginOptions) => {
+ const options = Object.assign(
+ // Default options
+ {
+ inlineDirection: DirectionFlow.LeftToRight,
+ preserve: true,
+ },
+ // Provided options
+ opts,
+ );
+
+ const directionValues = Object.values(DirectionFlow);
+ if (!directionValues.includes(options.inlineDirection)) {
+ throw new Error(`[postcss-logical-viewport-units] "inlineDirection" must be one of ${directionValues.join(', ')}`);
+ }
+
+ const isHorizontal = [DirectionFlow.LeftToRight, DirectionFlow.RightToLeft].includes(options.inlineDirection);
+
+ const replacements: { vi: 'vw' | 'vh', vb: 'vw' | 'vh' } = {
+ vb: 'vh',
+ vi: 'vw',
+ };
+
+ if (!isHorizontal) {
+ replacements.vb = 'vw';
+ replacements.vi = 'vh';
+ }
+
+ return {
+ postcssPlugin: 'postcss-logical-viewport-units',
+ Declaration(decl, { atRule }) {
+ {
+ // Fast check
+ const lowerCaseValue = decl.value.toLowerCase();
+ if (!(lowerCaseValue.includes('vb') || lowerCaseValue.includes('vi'))) {
+ return;
+ }
+
+ // Declaration already has a fallback
+ const prev = decl.prev();
+ if (prev && prev.type === 'decl' && prev.prop === decl.prop) {
+ return;
+ }
+
+ // Is wrapped in a relevant `@supports`
+ if (hasSupportsAtRuleAncestor(decl)) {
+ return;
+ }
+ }
+
+ const modifiedValue = transform(decl.value, replacements);
+ if (modifiedValue === decl.value) {
+ return;
+ }
+
+ decl.cloneBefore({
+ value: modifiedValue,
+ });
+
+ if (!options.preserve) {
+ decl.remove();
+ return;
+ }
+
+ if (!decl.variable) {
+ return;
+ }
+
+ const supports = atRule({
+ name: 'supports',
+ params: '(top: 1vi)',
+ source: decl.source,
+ });
+
+ const parent = decl.parent;
+ const parentClone = decl.parent.cloneAfter({ nodes: [] });
+
+ parentClone.append(decl);
+ supports.append(parentClone);
+
+ parent.after(supports);
+ },
+ };
+};
+
+creator.postcss = true;
+
+export default creator;
diff --git a/plugins/postcss-logical-viewport-units/src/lib/types.ts b/plugins/postcss-logical-viewport-units/src/lib/types.ts
new file mode 100644
index 000000000..06807b9c0
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/src/lib/types.ts
@@ -0,0 +1,19 @@
+export const DirectionValues = {
+ BlockStart: 'block-start',
+ BlockEnd: 'block-end',
+ InlineStart: 'inline-start',
+ InlineEnd: 'inline-end',
+};
+export enum DirectionFlow {
+ TopToBottom = 'top-to-bottom',
+ BottomToTop = 'bottom-to-top',
+ RightToLeft = 'right-to-left',
+ LeftToRight = 'left-to-right',
+}
+
+export enum Axes {
+ Top = 'top',
+ Right = 'right',
+ Bottom = 'bottom',
+ Left = 'left',
+}
diff --git a/plugins/postcss-logical-viewport-units/src/transform.ts b/plugins/postcss-logical-viewport-units/src/transform.ts
new file mode 100644
index 000000000..0b0c2e697
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/src/transform.ts
@@ -0,0 +1,37 @@
+import { stringify, tokenizer, TokenType } from '@csstools/css-tokenizer';
+
+export function transform(source: string, replacements: { vi: 'vw' | 'vh', vb: 'vw' | 'vh' }): string {
+ const t = tokenizer({ css: source });
+ const tokens = [];
+ let didTransformUnits = false;
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const token = t.nextToken();
+ tokens.push(token);
+
+ if (token[0] === TokenType.EOF) {
+ break;
+ }
+
+ if (token[0] !== TokenType.Dimension) {
+ continue;
+ }
+
+ const unit = token[4].unit.toLowerCase();
+ const replacement = replacements[unit];
+ if (!replacement) {
+ continue;
+ }
+
+ token[1] = token[4].value.toString() + replacement;
+ token[4].unit = replacement;
+ didTransformUnits = true;
+ }
+
+ if (!didTransformUnits) {
+ return source;
+ }
+
+ return stringify(...tokens);
+}
diff --git a/plugins/postcss-logical-viewport-units/test/_import.mjs b/plugins/postcss-logical-viewport-units/test/_import.mjs
new file mode 100644
index 000000000..0f1eedf3a
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/test/_import.mjs
@@ -0,0 +1,6 @@
+import assert from 'assert';
+import plugin from '@csstools/postcss-logical-viewport-units';
+plugin();
+
+assert.ok(plugin.postcss, 'should have "postcss flag"');
+assert.equal(typeof plugin, 'function', 'should return a function');
diff --git a/plugins/postcss-logical-viewport-units/test/_require.cjs b/plugins/postcss-logical-viewport-units/test/_require.cjs
new file mode 100644
index 000000000..12d9ec20e
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/test/_require.cjs
@@ -0,0 +1,6 @@
+const assert = require('assert');
+const plugin = require('@csstools/postcss-logical-viewport-units');
+plugin();
+
+assert.ok(plugin.postcss, 'should have "postcss flag"');
+assert.equal(typeof plugin, 'function', 'should return a function');
diff --git a/plugins/postcss-logical-viewport-units/test/basic.css b/plugins/postcss-logical-viewport-units/test/basic.css
new file mode 100644
index 000000000..0cc0ba9ac
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/test/basic.css
@@ -0,0 +1,30 @@
+.foo {
+ margin: 10vi 20vb;
+}
+
+.foo {
+ margin: calc(10vi + 5vb);
+}
+
+.vb {
+ left: 5vb;
+}
+
+.vi {
+ left: 5vi;
+}
+
+@supports (width: 100vb) {
+ .vi {
+ left: 5vi;
+ }
+}
+
+:root {
+ --var: 5vi;
+}
+
+.fallback {
+ left: 5vw;
+ left: 5vi;
+}
diff --git a/plugins/postcss-logical-viewport-units/test/basic.expect.css b/plugins/postcss-logical-viewport-units/test/basic.expect.css
new file mode 100644
index 000000000..79dc02cd2
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/test/basic.expect.css
@@ -0,0 +1,41 @@
+.foo {
+ margin: 10vw 20vh;
+ margin: 10vi 20vb;
+}
+
+.foo {
+ margin: calc(10vw + 5vh);
+ margin: calc(10vi + 5vb);
+}
+
+.vb {
+ left: 5vh;
+ left: 5vb;
+}
+
+.vi {
+ left: 5vw;
+ left: 5vi;
+}
+
+@supports (width: 100vb) {
+ .vi {
+ left: 5vi;
+ }
+}
+
+:root {
+ --var: 5vw;
+}
+
+@supports (top: 1vi) {
+
+:root {
+ --var: 5vi;
+}
+}
+
+.fallback {
+ left: 5vw;
+ left: 5vi;
+}
diff --git a/plugins/postcss-logical-viewport-units/test/basic.hebrew.expect.css b/plugins/postcss-logical-viewport-units/test/basic.hebrew.expect.css
new file mode 100644
index 000000000..79dc02cd2
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/test/basic.hebrew.expect.css
@@ -0,0 +1,41 @@
+.foo {
+ margin: 10vw 20vh;
+ margin: 10vi 20vb;
+}
+
+.foo {
+ margin: calc(10vw + 5vh);
+ margin: calc(10vi + 5vb);
+}
+
+.vb {
+ left: 5vh;
+ left: 5vb;
+}
+
+.vi {
+ left: 5vw;
+ left: 5vi;
+}
+
+@supports (width: 100vb) {
+ .vi {
+ left: 5vi;
+ }
+}
+
+:root {
+ --var: 5vw;
+}
+
+@supports (top: 1vi) {
+
+:root {
+ --var: 5vi;
+}
+}
+
+.fallback {
+ left: 5vw;
+ left: 5vi;
+}
diff --git a/plugins/postcss-logical-viewport-units/test/basic.vertical.expect.css b/plugins/postcss-logical-viewport-units/test/basic.vertical.expect.css
new file mode 100644
index 000000000..dd691b29e
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/test/basic.vertical.expect.css
@@ -0,0 +1,41 @@
+.foo {
+ margin: 10vh 20vw;
+ margin: 10vi 20vb;
+}
+
+.foo {
+ margin: calc(10vh + 5vw);
+ margin: calc(10vi + 5vb);
+}
+
+.vb {
+ left: 5vw;
+ left: 5vb;
+}
+
+.vi {
+ left: 5vh;
+ left: 5vi;
+}
+
+@supports (width: 100vb) {
+ .vi {
+ left: 5vi;
+ }
+}
+
+:root {
+ --var: 5vh;
+}
+
+@supports (top: 1vi) {
+
+:root {
+ --var: 5vi;
+}
+}
+
+.fallback {
+ left: 5vw;
+ left: 5vi;
+}
diff --git a/plugins/postcss-logical-viewport-units/test/examples/example.css b/plugins/postcss-logical-viewport-units/test/examples/example.css
new file mode 100644
index 000000000..1950f801f
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/test/examples/example.css
@@ -0,0 +1,3 @@
+.foo {
+ margin: 10vi 20vb;
+}
diff --git a/plugins/postcss-logical-viewport-units/test/examples/example.expect.css b/plugins/postcss-logical-viewport-units/test/examples/example.expect.css
new file mode 100644
index 000000000..ebe269938
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/test/examples/example.expect.css
@@ -0,0 +1,4 @@
+.foo {
+ margin: 10vw 20vh;
+ margin: 10vi 20vb;
+}
diff --git a/plugins/postcss-logical-viewport-units/test/examples/example.preserve-false.expect.css b/plugins/postcss-logical-viewport-units/test/examples/example.preserve-false.expect.css
new file mode 100644
index 000000000..ef698fbc2
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/test/examples/example.preserve-false.expect.css
@@ -0,0 +1,3 @@
+.foo {
+ margin: 10vw 20vh;
+}
diff --git a/plugins/postcss-logical-viewport-units/test/examples/example.vertical.expect.css b/plugins/postcss-logical-viewport-units/test/examples/example.vertical.expect.css
new file mode 100644
index 000000000..bfd40e32c
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/test/examples/example.vertical.expect.css
@@ -0,0 +1,4 @@
+.foo {
+ margin: 10vh 20vw;
+ margin: 10vi 20vb;
+}
diff --git a/plugins/postcss-logical-viewport-units/tsconfig.json b/plugins/postcss-logical-viewport-units/tsconfig.json
new file mode 100644
index 000000000..e0d06239c
--- /dev/null
+++ b/plugins/postcss-logical-viewport-units/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "declarationDir": "."
+ },
+ "include": ["./src/**/*"],
+ "exclude": ["dist"],
+}