@bizrk/leancss 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adam Birkner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ import type { PluginCreator } from 'postcss';
2
+ interface LeanCssOptions {
3
+ }
4
+ declare const leancss: PluginCreator<LeanCssOptions>;
5
+ export default leancss;
package/dist/index.js ADDED
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const leancss = (opts = {}) => {
4
+ return {
5
+ postcssPlugin: 'leancss',
6
+ Once(root, { result }) {
7
+ const sets = new Map();
8
+ // 1. Collect and Validate sets
9
+ root.walkAtRules('set', (atRule) => {
10
+ const name = atRule.params.trim();
11
+ if (!name) {
12
+ atRule.warn(result, 'Missing name for @set');
13
+ return;
14
+ }
15
+ if (sets.has(name)) {
16
+ throw atRule.error(`Duplicate @set definition for "${name}"`);
17
+ }
18
+ let isAlias = false;
19
+ // Validate contents
20
+ atRule.walk((node) => {
21
+ if (node.type === 'atrule') {
22
+ const atNode = node;
23
+ if (atNode.name !== 'lift') {
24
+ throw node.error(`Unsupported at-rule @${atNode.name} inside @set. Only @lift is allowed.`);
25
+ }
26
+ isAlias = true;
27
+ }
28
+ else if (node.type === 'rule') {
29
+ throw node.error(`Nested selectors are not supported inside @set in v1.`);
30
+ }
31
+ else if (node.type !== 'decl' && node.type !== 'comment') {
32
+ throw node.error(`Unsupported node type "${node.type}" inside @set.`);
33
+ }
34
+ });
35
+ sets.set(name, {
36
+ name,
37
+ sourceFile: atRule.source?.input.file,
38
+ node: atRule,
39
+ isAlias,
40
+ resolvedDeclarations: [] // Will be populated in resolution phase
41
+ });
42
+ });
43
+ // 3. Resolve aliases
44
+ function resolveSet(name, resolutionChain, originNode) {
45
+ const setDef = sets.get(name);
46
+ if (!setDef) {
47
+ throw originNode.error(`Unknown set "${name}" referenced in @lift`);
48
+ }
49
+ // Cache hit
50
+ if (setDef.resolvedDeclarations.length > 0) {
51
+ return setDef.resolvedDeclarations;
52
+ }
53
+ // It might be empty, let's distinguish between empty resolution and unresolved by adding a flag or just always resolving.
54
+ // Circular detection
55
+ if (resolutionChain.includes(name)) {
56
+ const chain = [...resolutionChain, name].join(' -> ');
57
+ throw originNode.error(`Circular alias reference detected: ${chain}`);
58
+ }
59
+ const currentChain = [...resolutionChain, name];
60
+ const resolved = [];
61
+ setDef.node.walk((node) => {
62
+ if (node.type === 'decl') {
63
+ resolved.push(node.clone());
64
+ }
65
+ else if (node.type === 'atrule') {
66
+ const atNode = node;
67
+ if (atNode.name === 'lift') {
68
+ const refs = atNode.params.split(/\s+/).filter(Boolean);
69
+ for (const ref of refs) {
70
+ const expanded = resolveSet(ref, currentChain, atNode);
71
+ for (const decl of expanded) {
72
+ resolved.push(decl.clone());
73
+ }
74
+ }
75
+ }
76
+ }
77
+ });
78
+ setDef.resolvedDeclarations = resolved;
79
+ return resolved;
80
+ }
81
+ // Resolve all sets eagerly (or could be done lazily when expanding selector-level @lift)
82
+ for (const [name, setDef] of sets.entries()) {
83
+ if (setDef.resolvedDeclarations.length === 0) {
84
+ resolveSet(name, [], setDef.node);
85
+ }
86
+ }
87
+ // 4. Expand selector-level @lift
88
+ root.walkAtRules('lift', (atRule) => {
89
+ // Ignore @lift inside @set
90
+ let parent = atRule.parent;
91
+ let inSet = false;
92
+ while (parent) {
93
+ if (parent.type === 'atrule' && parent.name === 'set') {
94
+ inSet = true;
95
+ break;
96
+ }
97
+ parent = parent.parent;
98
+ }
99
+ if (inSet)
100
+ return;
101
+ if (atRule.parent?.type !== 'rule') {
102
+ throw atRule.error(`@lift is only allowed inside standard rules or @set blocks in v1.`);
103
+ }
104
+ const refs = atRule.params.split(/\s+/).filter(Boolean);
105
+ const declsToInsert = [];
106
+ for (const ref of refs) {
107
+ const setDef = sets.get(ref);
108
+ if (!setDef) {
109
+ throw atRule.error(`Unknown set "${ref}" referenced in @lift`);
110
+ }
111
+ for (const decl of setDef.resolvedDeclarations) {
112
+ declsToInsert.push(decl.clone());
113
+ }
114
+ }
115
+ atRule.replaceWith(...declsToInsert);
116
+ });
117
+ // 5. Remove @set definitions
118
+ root.walkAtRules('set', (atRule) => {
119
+ atRule.remove();
120
+ });
121
+ }
122
+ };
123
+ };
124
+ leancss.postcss = true;
125
+ exports.default = leancss;
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@bizrk/leancss",
3
+ "version": "0.1.0",
4
+ "description": "CSS-first utility composition with @set and @lift that provides a clean, layer-aware alternative to Sass mixins and Tailwind @apply for atomic compositions.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "directories": {
8
+ "test": "tests"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest"
14
+ },
15
+ "keywords": [
16
+ "css",
17
+ "postcss",
18
+ "postcss-plugin",
19
+ "utility",
20
+ "atomic",
21
+ "composition",
22
+ "design-system",
23
+ "leancss",
24
+ "set",
25
+ "lift"
26
+ ],
27
+ "files": [
28
+ "dist",
29
+ "readme.md"
30
+ ],
31
+ "author": "Adam Birkner",
32
+ "license": "MIT",
33
+ "peerDependencies": {
34
+ "postcss": "^8.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "postcss": "^8.4.38",
38
+ "@types/node": "^20.12.7",
39
+ "typescript": "^5.4.5",
40
+ "vitest": "^1.5.0"
41
+ }
42
+ }
package/readme.md ADDED
@@ -0,0 +1,410 @@
1
+ # LeanCSS
2
+
3
+ **Define sets. Lift them into components. Ship lean CSS.**
4
+
5
+ LeanCSS is a CSS-first utility composition tool.
6
+
7
+ It lets you define reusable style bundles with `@set`, then expand them inside selectors using `@lift`.
8
+
9
+ Unlike utility-class frameworks, LeanCSS keeps styling in CSS instead of HTML while still enabling composable, reusable patterns.
10
+
11
+ LeanCSS runs at build time and outputs plain CSS.
12
+
13
+ * * *
14
+
15
+ # Why LeanCSS?
16
+
17
+ Modern CSS tooling tends to fall into two camps.
18
+
19
+ ### Utility frameworks
20
+
21
+ Example:
22
+
23
+ &lt;button class="inline-flex items-center gap-2 px-4 py-2 rounded-md"&gt;
24
+
25
+ What is this?
26
+
27
+ Pros
28
+
29
+ - fast to prototype
30
+
31
+ - composable
32
+
33
+
34
+ Cons
35
+
36
+ - styles move into markup
37
+
38
+ - semantics become unclear
39
+
40
+ - component styles become fragmented
41
+
42
+
43
+ * * *
44
+
45
+ ### Sass mixins
46
+
47
+ Example:
48
+
49
+ .button {
50
+ @include flex-center;
51
+ }
52
+
53
+ Pros
54
+
55
+ - reusable abstractions
56
+
57
+ Cons
58
+
59
+ - hidden logic
60
+
61
+ - hard to inspect
62
+
63
+ - often grows into a mini programming language
64
+
65
+
66
+ * * *
67
+
68
+ ### LeanCSS approach
69
+
70
+ LeanCSS keeps composition **inside CSS itself**.
71
+
72
+ Example:
73
+
74
+ @set inline-flex {
75
+ display: inline-flex;
76
+ }
77
+ <br/>@set items-center {
78
+ align-items: center;
79
+ }
80
+ <br/>@set gap-2 {
81
+ gap: var(--space-2);
82
+ }
83
+ <br/>.button {
84
+ @lift inline-flex items-center gap-2;
85
+ }
86
+
87
+ Output:
88
+
89
+ .button {
90
+ display: inline-flex;
91
+ align-items: center;
92
+ gap: var(--space-2);
93
+ }
94
+
95
+ * * *
96
+
97
+ # Features
98
+
99
+ LeanCSS is intentionally simple.
100
+
101
+ • CSS-first authoring
102
+ • reusable style bundles via `@set`
103
+ • composition via `@lift`
104
+ • alias sets
105
+ • cascade layer friendly
106
+ • works with `.css` and `.scss`
107
+ • outputs plain CSS
108
+ • zero runtime
109
+
110
+ * * *
111
+
112
+ # Installation
113
+
114
+ npm install @bizrk/leancss
115
+
116
+ Add LeanCSS to your PostCSS configuration.
117
+
118
+ Example:
119
+
120
+ ```javascript
121
+ import leancss from "@bizrk/leancss"
122
+ ```
123
+ export default {
124
+ plugins: \[
125
+ leancss()
126
+ \]
127
+ }
128
+
129
+ LeanCSS runs during the CSS build process.
130
+
131
+ * * *
132
+
133
+ # Editor Support (VS Code)
134
+
135
+ To prevent VS Code from reporting "Unknown at-rule" warnings for `@set` and `@lift`, you can configure custom CSS data.
136
+
137
+ Create `.vscode/leancss.css-data.json`:
138
+
139
+ ```json
140
+ {
141
+ "version": 1.1,
142
+ "atDirectives": [
143
+ {
144
+ "name": "@set",
145
+ "description": "LeanCSS: Defines a reusable declaration bundle or alias bundle."
146
+ },
147
+ {
148
+ "name": "@lift",
149
+ "description": "LeanCSS: Expands one or more sets into the current selector rule."
150
+ }
151
+ ]
152
+ }
153
+ ```
154
+
155
+ Then tell VS Code to use it in `.vscode/settings.json`:
156
+
157
+ ```json
158
+ {
159
+ "css.customData": ["./.vscode/leancss.css-data.json"],
160
+ "scss.customData": ["./.vscode/leancss.css-data.json"]
161
+ }
162
+ ```
163
+
164
+ * * *
165
+
166
+ # Basic Usage
167
+
168
+ Define reusable sets.
169
+
170
+ @set inline-flex {
171
+ display: inline-flex;
172
+ }
173
+ <br/>@set items-center {
174
+ align-items: center;
175
+ }
176
+ <br/>@set gap-2 {
177
+ gap: var(--space-2);
178
+ }
179
+
180
+ Use them in selectors.
181
+
182
+ .button {
183
+ @lift inline-flex items-center gap-2;
184
+ }
185
+
186
+ LeanCSS expands the sets during compilation.
187
+
188
+ * * *
189
+
190
+ # Alias Sets
191
+
192
+ Sets can compose other sets.
193
+
194
+ Example:
195
+
196
+ @set flex-center {
197
+ @lift inline-flex items-center justify-center;
198
+ }
199
+
200
+ Usage:
201
+
202
+ .icon {
203
+ @lift flex-center;
204
+ }
205
+
206
+ Output:
207
+
208
+ .icon {
209
+ display: inline-flex;
210
+ align-items: center;
211
+ justify-content: center;
212
+ }
213
+
214
+ * * *
215
+
216
+ # Cascade Layers
217
+
218
+ LeanCSS works well with CSS cascade layers.
219
+
220
+ Example:
221
+
222
+ @layer reset, tokens, base, utilities, components, overrides;
223
+
224
+ Define sets in a utilities layer:
225
+
226
+ @layer utilities {
227
+ @set inline-flex {
228
+ display: inline-flex;
229
+ }
230
+ <br/>@set items-center {
231
+ align-items: center;
232
+ }
233
+ }
234
+
235
+ Consume them in components:
236
+
237
+ @layer components {
238
+ .button {
239
+ @lift inline-flex items-center;
240
+ }
241
+ }
242
+
243
+ LeanCSS preserves layer ordering.
244
+
245
+ * * *
246
+
247
+ # Example Project Structure
248
+
249
+ src/styles
250
+ ├─ tokens.css
251
+ ├─ utilities.css
252
+ ├─ base.css
253
+ ├─ components
254
+ │ ├─ button.css
255
+ │ ├─ card.css
256
+ │ └─ hero.css
257
+ └─ app.css
258
+
259
+ Example utilities file:
260
+
261
+ ```css
262
+ @layer utilities {
263
+ @set inline-flex {
264
+ display: inline-flex;
265
+ }
266
+
267
+ @set items-center {
268
+ align-items: center;
269
+ }
270
+
271
+ @set gap-2 {
272
+ gap: var(--space-2);
273
+ }
274
+
275
+ @set rounded-md {
276
+ border-radius: var(--radius-md);
277
+ }
278
+ }
279
+ ```
280
+
281
+ Example component:
282
+
283
+ ```css
284
+ .button {
285
+ @lift inline-flex items-center gap-2 rounded-md;
286
+ }
287
+ ```
288
+
289
+ ### Running PostCSS
290
+
291
+ You can use the `postcss-cli` package to compile your CSS. Add these scripts to your `package.json`:
292
+
293
+ ```json
294
+ {
295
+ "scripts": {
296
+ "build:css": "postcss src/styles/app.css -o dist/output.css",
297
+ "watch:css": "postcss src/styles/app.css -o dist/output.css --watch"
298
+ }
299
+ }
300
+ ```
301
+
302
+ Run `npm run build:css` to build your styles once for production, or `npm run watch:css` while actively developing to automatically re-compile whenever you save your CSS files.
303
+
304
+ * * *
305
+
306
+ # Philosophy
307
+
308
+ LeanCSS focuses on small, composable primitives.
309
+
310
+ It encourages:
311
+
312
+ • semantic selectors
313
+ • design token usage
314
+ • cascade layers
315
+ • small reusable patterns
316
+
317
+ It intentionally avoids:
318
+
319
+ • HTML class scanning
320
+ • runtime styling engines
321
+ • heavy configuration files
322
+
323
+ LeanCSS is designed to remain small, predictable, and easy to reason about.
324
+
325
+ * * *
326
+
327
+ # Error Handling
328
+
329
+ LeanCSS validates styles during compilation.
330
+
331
+ ### Unknown set
332
+
333
+ .button {
334
+ @lift missing-set;
335
+ }
336
+
337
+ Error:
338
+
339
+ Unknown LeanCSS set: missing-set
340
+
341
+ * * *
342
+
343
+ ### Duplicate set
344
+
345
+ @set panel { ... }
346
+ @set panel
347
+
348
+ Error:
349
+
350
+ Duplicate LeanCSS set definition: panel
351
+
352
+ * * *
353
+
354
+ ### Circular reference
355
+
356
+ @set a { @lift b }
357
+ @set b
358
+
359
+ Error:
360
+
361
+ Circular set reference detected
362
+
363
+ * * *
364
+
365
+ # Comparison
366
+
367
+ | Tool | Composition Location |
368
+ | --- | --- |
369
+ | Tailwind | HTML |
370
+ | Sass mixins | Sass |
371
+ | LeanCSS | CSS |
372
+
373
+ LeanCSS combines composability with semantic CSS architecture.
374
+
375
+ * * *
376
+
377
+ # License
378
+
379
+ LeanCSS is released under the MIT License.
380
+
381
+ * * *
382
+
383
+ # Contributing
384
+
385
+ Contributions are welcome.
386
+
387
+ Areas that benefit from community help:
388
+
389
+ • integrations
390
+ • preset libraries
391
+ • documentation
392
+ • testing
393
+
394
+ * * *
395
+
396
+ # Future Ideas
397
+
398
+ LeanCSS v1 focuses on a minimal core.
399
+
400
+ Possible future additions include:
401
+
402
+ • preset utility libraries
403
+ • devtools for inspecting `@lift` expansion
404
+ • design system integrations
405
+
406
+ * * *
407
+
408
+ # LeanCSS
409
+
410
+ **Define sets. Lift them into components.**