Skip to content

Commit 0de1419

Browse files
committed
init
1 parent 622f1ee commit 0de1419

13 files changed

+4971
-1
lines changed

.github/workflows/ci.yml

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
workflow_dispatch:
7+
8+
jobs:
9+
test:
10+
name: Node.js ${{ matrix.node-version }}
11+
runs-on: ubuntu-latest
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
node-version:
16+
- 22
17+
- 20
18+
- 18
19+
steps:
20+
- uses: actions/checkout@v4
21+
- uses: pnpm/action-setup@v4
22+
- uses: actions/setup-node@v4
23+
with:
24+
node-version: ${{ matrix.node-version }}
25+
- run: pnpm install
26+
- run: pnpm run "/^lint:.*/"
27+
- run: pnpm run build
28+
- run: pnpm run test

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,6 @@ dist
128128
.yarn/build-state.yml
129129
.yarn/install-state.gz
130130
.pnp.*
131+
132+
# PostCSS Tape
133+
*.result.css

.npmrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
use-node-version=18.20.7

README.md

+233-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,233 @@
1-
# postcss-custom-functions
1+
# @yuheiy/postcss-custom-functions
2+
3+
[PostCSS](https://github.com/postcss/postcss) plugin that allows users to define custom functions using JavaScript. This plugin is inspired by [custom functions](https://drafts.csswg.org/css-mixins-1/#custom-function) in CSS.
4+
5+
For example, define a custom function:
6+
7+
```js
8+
module.exports = {
9+
plugins: {
10+
'@yuheiy/postcss-custom-functions': {
11+
functions: {
12+
'--negative': (value) => `calc(-1 * ${value})`,
13+
},
14+
},
15+
},
16+
};
17+
```
18+
19+
Use the custom function you have defined:
20+
21+
```css
22+
html {
23+
--gap: 1em;
24+
padding: --negative(var(--gap));
25+
/* or by passing the value explicitly, like: */
26+
padding: --negative(1em);
27+
}
28+
```
29+
30+
will be processed to:
31+
32+
```css
33+
html {
34+
--gap: 1em;
35+
padding: calc(-1 * var(--gap));
36+
/* or by passing the value explicitly, like: */
37+
padding: calc(-1 * 1em);
38+
}
39+
```
40+
41+
## Usage
42+
43+
**Step 1:** Install plugin:
44+
45+
```sh
46+
npm install --save-dev postcss @yuheiy/postcss-custom-functions
47+
```
48+
49+
**Step 2:** Check you project for existed PostCSS config: `postcss.config.js`
50+
in the project root, `"postcss"` section in `package.json`
51+
or `postcss` in bundle config.
52+
53+
If you do not use PostCSS, add it according to [official docs](https://github.com/postcss/postcss#usage)
54+
and set this plugin in settings.
55+
56+
**Step 3:** Add the plugin to plugins list:
57+
58+
```diff
59+
module.exports = {
60+
plugins: {
61+
+ '@yuheiy/postcss-custom-functions': {},
62+
'autoprefixer': {},
63+
},
64+
};
65+
```
66+
67+
## Options
68+
69+
### `functions`
70+
71+
Type: `{ [key: string]: (...values: string[]) => string }`
72+
Default: `{}`
73+
74+
Define custom functions that can be used in your CSS.
75+
76+
## Tips
77+
78+
### Optional arguments with fallback
79+
80+
You can define custom functions with optional arguments and fallback values.
81+
82+
```js
83+
function shadow(shadowColor) {
84+
return `2px 2px ${shadowColor ? shadowColor : 'var(--shadow-color, black)'}`;
85+
}
86+
87+
module.exports = {
88+
plugins: {
89+
'@yuheiy/postcss-custom-functions': {
90+
functions: {
91+
'--shadow': shadow,
92+
},
93+
},
94+
},
95+
};
96+
```
97+
98+
Use the custom function you have defined:
99+
100+
```css
101+
.foo {
102+
--shadow-color: blue;
103+
box-shadow: --shadow(); /* produces a blue shadow */
104+
/* or just */
105+
box-shadow: --shadow(blue);
106+
}
107+
```
108+
109+
will be processed to:
110+
111+
```css
112+
.foo {
113+
--shadow-color: blue;
114+
box-shadow: 2px 2px var(--shadow-color, black); /* produces a blue shadow */
115+
/* or just */
116+
box-shadow: 2px 2px blue;
117+
}
118+
```
119+
120+
### Arguments validation
121+
122+
You can validate functions arguments and output warnings if they are incorrect.
123+
124+
```js
125+
function negative(value, ...rest) {
126+
if (!value) {
127+
throw new Error(
128+
`The --negative(…) function requires an argument, but received none.`,
129+
);
130+
}
131+
132+
if (rest.length > 0) {
133+
throw new Error(
134+
`The --negative(…) function only accepts a single argument, but received ${
135+
rest.length + 1
136+
}.`,
137+
);
138+
}
139+
140+
return `calc(-1 * ${value})`;
141+
}
142+
143+
module.exports = {
144+
plugins: {
145+
'@yuheiy/postcss-custom-functions': {
146+
functions: {
147+
'--negative': (value, ...rest) => negative,
148+
},
149+
},
150+
},
151+
};
152+
```
153+
154+
This implementation is inspired by [Tailwind CSS’s `css-functions.js`](https://github.com/tailwindlabs/tailwindcss/blob/v4.0.0/packages/tailwindcss/src/css-functions.ts).
155+
156+
### Fluid Typography
157+
158+
You can also implement [Fluid Typography](https://www.smashingmagazine.com/2022/01/modern-fluid-typography-css-clamp/) as a custom function, using the [`tan(atan2())` technique](https://dev.to/janeori/css-type-casting-to-numeric-tanatan2-scalars-582j) to remove px units and calculate them in CSS.
159+
160+
```js
161+
function fluid(
162+
minSize,
163+
maxSize,
164+
minBreakpoint = 'var(--breakpoint-sm)',
165+
maxBreakpoint = 'var(--breakpoint-xl)',
166+
...rest
167+
) {
168+
if (!minSize || !maxSize) {
169+
throw new Error(
170+
'The --fluid(…) function requires 2–4 arguments, but received none.',
171+
);
172+
}
173+
174+
if (rest.length > 0) {
175+
throw new Error(
176+
`The --fluid(…) function only accepts 4 arguments, but received ${
177+
rest.length + 1
178+
}.`,
179+
);
180+
}
181+
182+
const slope = `calc(tan(atan2(${maxSize} - ${minSize}, 1px)) / tan(atan2(${maxBreakpoint} - ${minBreakpoint}, 1px)))`;
183+
const intercept = `calc(tan(atan2(${minSize}, 1px)) - ${slope} * tan(atan2(${minBreakpoint}, 1px)))`;
184+
185+
return `clamp(${[
186+
`min(${minSize}, ${maxSize})`,
187+
`${slope} * 100lvi + ${intercept} / 16 * 1rem`,
188+
`max(${minSize}, ${maxSize})`,
189+
].join(', ')})`;
190+
}
191+
192+
module.exports = {
193+
plugins: {
194+
'@yuheiy/postcss-custom-functions': {
195+
functions: {
196+
'--fluid': fluid,
197+
},
198+
},
199+
},
200+
};
201+
```
202+
203+
Use the custom function you have defined:
204+
205+
```css
206+
:root {
207+
--breakpoint-sm: 40rem;
208+
--breakpoint-md: 48rem;
209+
--breakpoint-lg: 64rem;
210+
--breakpoint-xl: 80rem;
211+
--breakpoint-2xl: 96rem;
212+
}
213+
214+
h1 {
215+
font-size: --fluid(2rem, 4rem);
216+
}
217+
```
218+
219+
will be processed to:
220+
221+
<!-- prettier-ignore -->
222+
```css
223+
:root {
224+
--breakpoint-sm: 40rem;
225+
--breakpoint-md: 48rem;
226+
--breakpoint-lg: 64rem;
227+
--breakpoint-xl: 80rem;
228+
--breakpoint-2xl: 96rem;
229+
}
230+
231+
h1 {
232+
font-size: clamp(min(2rem, 4rem), calc(tan(atan2(4rem - 2rem, 1px)) / tan(atan2(var(--breakpoint-xl) - var(--breakpoint-sm), 1px))) * 100lvi + calc(tan(atan2(2rem, 1px)) - calc(tan(atan2(4rem - 2rem, 1px)) / tan(atan2(var(--breakpoint-xl) - var(--breakpoint-sm), 1px))) * tan(atan2(var(--breakpoint-sm), 1px))) / 16 * 1rem, max(2rem, 4rem));}
233+
```

biome.json

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3+
"vcs": {
4+
"enabled": true,
5+
"clientKind": "git",
6+
"useIgnoreFile": true,
7+
"defaultBranch": "main"
8+
},
9+
"files": {
10+
"ignoreUnknown": false,
11+
"ignore": []
12+
},
13+
"formatter": {
14+
"enabled": true,
15+
"indentStyle": "tab"
16+
},
17+
"organizeImports": {
18+
"enabled": true
19+
},
20+
"linter": {
21+
"enabled": true,
22+
"rules": {
23+
"recommended": true
24+
}
25+
},
26+
"javascript": {
27+
"formatter": {
28+
"quoteStyle": "single"
29+
}
30+
}
31+
}

package.json

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"name": "@yuheiy/postcss-custom-functions",
3+
"version": "0.0.0",
4+
"description": "PostCSS plugin that allows users to define custom functions using JavaScript.",
5+
"keywords": ["postcss-plugin"],
6+
"homepage": "https://github.com/yuheiy/postcss-custom-functions",
7+
"bugs": {
8+
"url": "https://github.com/yuheiy/postcss-custom-functions/issues"
9+
},
10+
"repository": {
11+
"type": "git",
12+
"url": "git+https://github.com/yuheiy/postcss-custom-functions.git"
13+
},
14+
"license": "MIT",
15+
"author": "Yuhei Yasuda <yuhei.yasuda1003@gmail.com> (https://yuheiy.com/)",
16+
"type": "module",
17+
"exports": {
18+
".": {
19+
"import": "./dist/index.mjs",
20+
"require": "./dist/index.cjs"
21+
},
22+
"./package.json": "./package.json"
23+
},
24+
"main": "./dist/index.cjs",
25+
"types": "./dist/index.d.ts",
26+
"files": ["dist"],
27+
"scripts": {
28+
"build": "unbuild",
29+
"dev": "unbuild --stub && pnpm run test --watch",
30+
"fix": "biome check . --write",
31+
"lint:biome": "biome lint .",
32+
"lint:typecheck": "tsc --noEmit",
33+
"prepublishOnly": "pnpm run \"/^lint:.*/\" && pnpm run build && pnpm run test",
34+
"release": "np --no-tests",
35+
"test": "node --test",
36+
"test:rewrite-expects": "REWRITE_EXPECTS=true node --test"
37+
},
38+
"dependencies": {
39+
"@csstools/css-parser-algorithms": "^3.0.4",
40+
"@csstools/css-tokenizer": "^3.0.3"
41+
},
42+
"devDependencies": {
43+
"@biomejs/biome": "1.9.4",
44+
"@csstools/postcss-tape": "6.0.0",
45+
"@types/node": "22.13.5",
46+
"np": "10.2.0",
47+
"postcss": "8.5.3",
48+
"typescript": "5.7.3",
49+
"unbuild": "3.3.1"
50+
},
51+
"peerDependencies": {
52+
"postcss": "^8.4"
53+
},
54+
"publishConfig": {
55+
"access": "public"
56+
},
57+
"packageManager": "pnpm@10.4.1",
58+
"unbuild": {
59+
"declaration": true,
60+
"sourcemap": true
61+
}
62+
}

0 commit comments

Comments
 (0)