@seyuna/postcss 1.0.0-canary.15 → 1.0.0-canary.17

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +168 -72
  3. package/dist/at-rules/color-scheme.d.ts +1 -1
  4. package/dist/at-rules/color.d.ts +1 -1
  5. package/dist/at-rules/color.js +1 -1
  6. package/dist/at-rules/container.d.ts +1 -1
  7. package/dist/at-rules/index.d.ts +1 -1
  8. package/dist/at-rules/index.js +4 -13
  9. package/dist/config.d.ts +2 -4
  10. package/dist/errors.d.ts +1 -1
  11. package/dist/functions/color.d.ts +1 -1
  12. package/dist/functions/color.js +27 -74
  13. package/dist/functions/index.d.ts +1 -1
  14. package/dist/functions/index.js +2 -2
  15. package/dist/functions/theme.d.ts +1 -1
  16. package/dist/helpers.d.ts +1 -1
  17. package/dist/helpers.js +1 -1
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +2 -2
  20. package/dist/parser.d.ts +1 -1
  21. package/dist/parser.js +1 -1
  22. package/dist/plugin.d.ts +1 -1
  23. package/dist/plugin.js +4 -5
  24. package/package.json +3 -1
  25. package/release.config.mjs +6 -1
  26. package/src/at-rules/color-scheme.ts +1 -1
  27. package/src/at-rules/color.ts +2 -2
  28. package/src/at-rules/container.ts +1 -1
  29. package/src/at-rules/index.ts +5 -14
  30. package/src/config.ts +2 -5
  31. package/src/errors.ts +1 -1
  32. package/src/functions/color.ts +46 -99
  33. package/src/functions/index.ts +3 -3
  34. package/src/functions/theme.ts +1 -1
  35. package/src/helpers.ts +2 -2
  36. package/src/index.ts +2 -2
  37. package/src/parser.ts +2 -2
  38. package/src/plugin.ts +4 -5
  39. package/test-import.mjs +2 -0
  40. package/tests/plugin.test.ts +36 -144
  41. package/tsconfig.json +2 -2
  42. package/dist/at-rules/conditional.d.ts +0 -6
  43. package/dist/at-rules/conditional.js +0 -29
  44. package/dist/at-rules/custom-media.d.ts +0 -15
  45. package/dist/at-rules/custom-media.js +0 -40
  46. package/dist/at-rules/mixin.d.ts +0 -10
  47. package/dist/at-rules/mixin.js +0 -37
  48. package/src/at-rules/conditional.ts +0 -34
  49. package/src/at-rules/custom-media.ts +0 -50
  50. package/src/at-rules/mixin.ts +0 -46
@@ -9,6 +9,10 @@ const mockConfig = {
9
9
  primary: "200",
10
10
  secondary: "100"
11
11
  },
12
+ colors: {
13
+ white: { lightness: 1, chroma: 0, hue: 0 },
14
+ black: { lightness: 0, chroma: 0, hue: 0 }
15
+ },
12
16
  light: {
13
17
  lightness: 0.66,
14
18
  colors: {
@@ -28,8 +32,9 @@ const mockConfig = {
28
32
  }
29
33
  };
30
34
 
31
- async function run(input: string, opts = {}) {
32
- return postcss([plugin(opts)]).process(input, { from: undefined });
35
+ async function run(input: string, opts: any = {}) {
36
+ const mergedOpts = { config: mockConfig, ...opts };
37
+ return postcss([plugin(mergedOpts)]).process(input, { from: undefined });
33
38
  }
34
39
 
35
40
  describe('Seyuna PostCSS Plugin', () => {
@@ -47,23 +52,23 @@ describe('Seyuna PostCSS Plugin', () => {
47
52
  expect(result.css).toContain(output);
48
53
  });
49
54
 
50
- it('processes alpha() function', async () => {
51
- const input = '.test { color: alpha(oklch(0.5 0.1 200 / 1), 0.5); }';
52
- const output = 'color: oklch(0.5 0.1 200 / 0.5)';
55
+ it('processes alpha() function with color name', async () => {
56
+ const input = '.test { color: alpha(primary, 0.5); }';
57
+ const output = 'color: oklch(var(--lightness) var(--chroma) var(--primary-hue) / 0.5)';
53
58
  const result = await run(input);
54
59
  expect(result.css).toContain(output);
55
60
  });
56
61
 
57
- it('processes lighten() function', async () => {
58
- const input = '.test { color: lighten(oklch(0.5 0.1 200 / 1), 0.1); }';
59
- const output = 'color: oklch(0.6 0.1 200 / 1)';
62
+ it('processes lighten() function with color name', async () => {
63
+ const input = '.test { color: lighten(primary, 0.1); }';
64
+ const output = 'color: oklch(calc(var(--lightness) + 0.1) var(--chroma) var(--primary-hue) / 1)';
60
65
  const result = await run(input);
61
66
  expect(result.css).toContain(output);
62
67
  });
63
68
 
64
- it('processes lighten() with CSS variables', async () => {
65
- const input = '.test { color: lighten(oklch(var(--l) 0.1 200 / 1), 0.1); }';
66
- const output = 'color: oklch(calc(var(--l) + 0.1) 0.1 200 / 1)';
69
+ it('processes darken() function with color name', async () => {
70
+ const input = '.test { color: darken(primary, 0.1); }';
71
+ const output = 'color: oklch(calc(var(--lightness) - 0.1) var(--chroma) var(--primary-hue) / 1)';
67
72
  const result = await run(input);
68
73
  expect(result.css).toContain(output);
69
74
  });
@@ -90,108 +95,21 @@ describe('Seyuna PostCSS Plugin', () => {
90
95
  expect(result.css).toContain('[data-mode="light"] &');
91
96
  });
92
97
 
93
- it('processes mixins using @define-mixin and @apply', async () => {
94
- const input = `
95
- @define-mixin btn {
96
- padding: 1rem;
97
- border-radius: 4px;
98
- }
99
- .primary-btn {
100
- @apply btn;
101
- color: white;
102
- }
103
- `;
104
- const result = await run(input);
105
- expect(result.css).toContain('.primary-btn {');
106
- expect(result.css).toContain('padding: 1rem');
107
- expect(result.css).toContain('border-radius: 4px');
108
- expect(result.css).toContain('color: white');
109
- expect(result.css).not.toContain('@define-mixin');
110
- });
111
-
112
- it('processes @if directive with literal', async () => {
113
- const input = `
114
- .test {
115
- @if (true) {
116
- color: red;
117
- }
118
- @if (false) {
119
- color: blue;
120
- }
121
- }
122
- `;
123
- const result = await run(input);
124
- expect(result.css).toContain('color: red');
125
- expect(result.css).not.toContain('color: blue');
126
- });
127
-
128
- it('processes @if directive with theme()', async () => {
129
- const input = `
130
- .glass {
131
- @if (theme(ui.features.glass)) {
132
- backdrop-filter: blur(10px);
133
- }
134
- }
135
- `;
136
- const config = {
137
- ui: {
138
- features: {
139
- glass: "true"
140
- }
141
- }
142
- };
143
- const result = await run(input, { config });
144
- expect(result.css).toContain('backdrop-filter: blur(10px)');
145
- });
146
-
147
- it('resolves custom media tokens', async () => {
148
- const input = `
149
- @media (--tablet) {
150
- .sidebar { display: block; }
151
- }
152
- `;
153
- const config = {
154
- media: {
155
- tablet: "(min-width: 768px)"
156
- }
157
- };
158
- const result = await run(input, { config });
159
- expect(result.css).toContain('@media (min-width: 768px)');
160
- });
161
-
162
- it('handles @custom-media directive', async () => {
163
- const input = `
164
- @custom-media --mobile (max-width: 480px);
165
- @media (--mobile) {
166
- .full-width { width: 100%; }
167
- }
168
- `;
169
- const result = await run(input);
170
- expect(result.css).toContain('@media (max-width: 480px)');
171
- });
172
-
173
- it('processes alpha() with color name', async () => {
98
+ it('processes alpha() with standard color', async () => {
174
99
  const input = '.test { color: alpha(primary, 0.5); }';
175
- const output = 'color: oklch(var(--primary-lightness) var(--primary-chroma) var(--primary-hue) / 0.5)';
176
- const result = await run(input);
177
- expect(result.css).toContain(output);
178
- });
179
-
180
- it('processes lighten() with color name', async () => {
181
- const input = '.test { color: lighten(primary, 0.1); }';
182
- const output = 'color: oklch(calc(var(--primary-lightness) + 0.1) var(--primary-chroma) var(--primary-hue) / 1)';
183
- const result = await run(input);
100
+ const output = 'color: oklch(var(--lightness) var(--chroma) var(--primary-hue) / 0.5)';
101
+ const result = await run(input, { config: mockConfig });
184
102
  expect(result.css).toContain(output);
185
103
  });
186
104
 
187
- it('processes darken() with color name', async () => {
188
- const input = '.test { color: darken(primary, 0.1); }';
189
- const output = 'color: oklch(calc(var(--primary-lightness) - 0.1) var(--primary-chroma) var(--primary-hue) / 1)';
190
- const result = await run(input);
105
+ it('processes alpha() with fixed color', async () => {
106
+ const input = '.test { color: alpha(surface, 0.5); }';
107
+ const output = 'color: oklch(var(--surface-lightness) var(--surface-chroma) var(--surface-hue) / 0.5)';
108
+ const result = await run(input, { config: mockConfig });
191
109
  expect(result.css).toContain(output);
192
110
  });
193
111
 
194
- it('processes contrast() with color name', async () => {
112
+ it('processes contrast() with fixed color', async () => {
195
113
  const input = '.test { color: contrast(surface); }';
196
114
  const output = 'color: oklch(calc((var(--surface-lightness) - 0.6) * -1000) 0 0)';
197
115
  const result = await run(input, { config: mockConfig });
@@ -205,47 +123,21 @@ describe('Seyuna PostCSS Plugin', () => {
205
123
  expect(result.css).toContain(output);
206
124
  });
207
125
 
208
- it('processes alpha() with standard color (sc)', async () => {
209
- const input = '.test { color: alpha(primary, 0.5); }';
210
- // 'primary' is in hues in mockConfig, so it should use var(--lightness) and var(--chroma)
211
- const output = 'color: oklch(var(--lightness) var(--chroma) var(--primary-hue) / 0.5)';
212
- const result = await run(input, { config: mockConfig });
213
- expect(result.css).toContain(output);
126
+ it('throws error for unknown standard color in strict mode', async () => {
127
+ const input = '.test { color: sc(unknown); }';
128
+ await expect(run(input, { config: mockConfig, strict: true }))
129
+ .rejects.toThrow(/Standard color 'unknown' not found/);
214
130
  });
215
131
 
216
- it('processes alpha() with fixed color (fc)', async () => {
217
- const input = '.test { color: alpha(surface, 0.5); }';
218
- // 'surface' is in light.colors/dark.colors in mockConfig, so it should use its own lightness/chroma
219
- const output = 'color: oklch(var(--surface-lightness) var(--surface-chroma) var(--surface-hue) / 0.5)';
220
- const result = await run(input, { config: mockConfig });
221
- expect(result.css).toContain(output);
222
- });
223
-
224
- it('processes nested functions: contrast(fc(white))', async () => {
225
- const input = '.test { color: contrast(fc(white)); }';
226
- // fc(white) -> oklch(var(--white-lightness) ...)
227
- // contrast extracts lightness -> var(--white-lightness)
228
- const output = 'color: oklch(calc((var(--white-lightness) - 0.6) * -1000) 0 0)';
229
- const result = await run(input, { config: mockConfig });
230
- expect(result.css).toContain(output);
132
+ it('throws error for unknown fixed color in strict mode', async () => {
133
+ const input = '.test { color: fc(unknown); }';
134
+ await expect(run(input, { config: mockConfig, strict: true }))
135
+ .rejects.toThrow(/Fixed color 'unknown' not found/);
231
136
  });
232
137
 
233
- it('processes nested functions: alpha(sc(primary), 0.5)', async () => {
234
- const input = '.test { color: alpha(sc(primary), 0.5); }';
235
- // sc(primary) -> oklch(var(--lightness) var(--chroma) var(--primary-hue) / 1)
236
- // alpha(..., 0.5) extracts components and reapplies alpha
237
- const output = 'color: oklch(var(--lightness) var(--chroma) var(--primary-hue) / 0.5)';
238
- const result = await run(input, { config: mockConfig });
239
- expect(result.css).toContain(output);
240
- });
241
-
242
- it('processes multiple nested functions: contrast(alpha(fc(surface), 1))', async () => {
243
- const input = '.test { color: contrast(alpha(fc(surface), 1)); }';
244
- // inner-most: fc(surface) -> oklch(var(--surface-lightness) ...)
245
- // next: alpha(..., 1) -> oklch(var(--surface-lightness) ...)
246
- // outer: contrast extracts lightness -> var(--surface-lightness)
247
- const output = 'color: oklch(calc((var(--surface-lightness) - 0.6) * -1000) 0 0)';
248
- const result = await run(input, { config: mockConfig });
249
- expect(result.css).toContain(output);
138
+ it('throws error for unknown color in alpha() in strict mode', async () => {
139
+ const input = '.test { color: alpha(unknown, 0.5); }';
140
+ await expect(run(input, { config: mockConfig, strict: true }))
141
+ .rejects.toThrow(/Color 'unknown' not found in seyuna.json/);
250
142
  });
251
143
  });
package/tsconfig.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ES2020",
4
- "module": "ES2020",
4
+ "module": "NodeNext",
5
5
  "lib": ["ES2020"],
6
6
  "declaration": true,
7
7
  "outDir": "dist",
8
8
  "strict": true,
9
- "moduleResolution": "bundler",
9
+ "moduleResolution": "NodeNext",
10
10
  "esModuleInterop": true,
11
11
  "skipLibCheck": true
12
12
  },
@@ -1,6 +0,0 @@
1
- import { AtRule } from "postcss";
2
- import { PluginContext } from "../config";
3
- /**
4
- * Handler for @if (condition) { ... }
5
- */
6
- export declare function conditional(atRule: AtRule, context: PluginContext): void;
@@ -1,29 +0,0 @@
1
- import { processFunctions } from "../parser";
2
- /**
3
- * Handler for @if (condition) { ... }
4
- */
5
- export function conditional(atRule, context) {
6
- const { functions: fnMap } = context;
7
- // Clean up params (remove parentheses if present)
8
- let condition = atRule.params.trim().replace(/^\(|\)$/g, '').trim();
9
- // Process functions in the condition (e.g., theme access)
10
- const evaluated = processFunctions(condition, fnMap, atRule, context).trim();
11
- // Simple truthy evaluation
12
- const isFalsy = !evaluated ||
13
- evaluated === 'false' ||
14
- evaluated === '0' ||
15
- evaluated === 'null' ||
16
- evaluated === 'undefined';
17
- if (!isFalsy) {
18
- // Ungroup the nodes
19
- if (atRule.nodes && atRule.nodes.length > 0) {
20
- atRule.replaceWith(...atRule.nodes.map(n => n.clone()));
21
- }
22
- else {
23
- atRule.remove();
24
- }
25
- }
26
- else {
27
- atRule.remove();
28
- }
29
- }
@@ -1,15 +0,0 @@
1
- import { AtRule } from "postcss";
2
- import { PluginContext } from "../config";
3
- /**
4
- * Global store for CSS-defined custom media (if needed)
5
- * However, we primarily use the config.
6
- */
7
- /**
8
- * Handler for @media at-rules to resolve custom media tokens
9
- */
10
- export declare function resolveCustomMedia(atRule: AtRule, context: PluginContext): void;
11
- /**
12
- * [Future-proofing] Handler for @custom-media --name query
13
- * Adds to the config-like store for the current run
14
- */
15
- export declare function defineCustomMedia(atRule: AtRule, context: PluginContext): void;
@@ -1,40 +0,0 @@
1
- /**
2
- * Global store for CSS-defined custom media (if needed)
3
- * However, we primarily use the config.
4
- */
5
- /**
6
- * Handler for @media at-rules to resolve custom media tokens
7
- */
8
- export function resolveCustomMedia(atRule, context) {
9
- const { config } = context;
10
- const mediaTokens = config.media || {};
11
- // Regex to find --tokens anywhere
12
- let params = atRule.params;
13
- const tokenRegex = /--[a-zA-Z0-9\-_]+/g;
14
- params = params.replace(tokenRegex, (match) => {
15
- const tokenName = match.slice(2);
16
- if (mediaTokens[tokenName]) {
17
- return mediaTokens[tokenName];
18
- }
19
- return match;
20
- });
21
- // Clean up double parentheses like ((...)) if any
22
- params = params.replace(/\(\((.+)\)\)/g, '($1)');
23
- atRule.params = params;
24
- }
25
- /**
26
- * [Future-proofing] Handler for @custom-media --name query
27
- * Adds to the config-like store for the current run
28
- */
29
- export function defineCustomMedia(atRule, context) {
30
- const match = atRule.params.match(/^(--[a-zA-Z0-9\-_]+)\s+(.+)$/);
31
- if (match) {
32
- const name = match[1].slice(2);
33
- const query = match[2];
34
- if (!context.config.media) {
35
- context.config.media = {};
36
- }
37
- context.config.media[name] = query;
38
- }
39
- atRule.remove();
40
- }
@@ -1,10 +0,0 @@
1
- import { AtRule } from "postcss";
2
- import { PluginContext } from "../config";
3
- /**
4
- * Handler for @define-mixin [name] { ... }
5
- */
6
- export declare function defineMixin(atRule: AtRule, context: PluginContext): void;
7
- /**
8
- * Handler for @apply [name]
9
- */
10
- export declare function applyMixin(atRule: AtRule, context: PluginContext): void;
@@ -1,37 +0,0 @@
1
- import { reportError } from "../errors";
2
- /**
3
- * Handler for @define-mixin [name] { ... }
4
- */
5
- export function defineMixin(atRule, context) {
6
- const name = atRule.params.trim();
7
- if (!name) {
8
- reportError("Mixin name is required", atRule, context);
9
- return;
10
- }
11
- // Store the nodes (cloned)
12
- context.mixins[name] = atRule.nodes?.map(n => n.clone()) || [];
13
- // Remove the at-rule from the output
14
- atRule.remove();
15
- }
16
- /**
17
- * Handler for @apply [name]
18
- */
19
- export function applyMixin(atRule, context) {
20
- const name = atRule.params.trim();
21
- if (!name) {
22
- reportError("Mixin name is required for @apply", atRule, context);
23
- return;
24
- }
25
- const mixinNodes = context.mixins[name];
26
- if (!mixinNodes) {
27
- reportError(`Mixin "${name}" not found`, atRule, context);
28
- return;
29
- }
30
- // Inject the nodes, ensuring they have the correct source for mapping
31
- const nodesToInject = mixinNodes.map(n => {
32
- const cloned = n.clone();
33
- cloned.source = atRule.source;
34
- return cloned;
35
- });
36
- atRule.replaceWith(...nodesToInject);
37
- }
@@ -1,34 +0,0 @@
1
- import { AtRule } from "postcss";
2
- import { PluginContext } from "../config";
3
- import { processFunctions } from "../parser";
4
-
5
- /**
6
- * Handler for @if (condition) { ... }
7
- */
8
- export function conditional(atRule: AtRule, context: PluginContext) {
9
- const { functions: fnMap } = context;
10
-
11
- // Clean up params (remove parentheses if present)
12
- let condition = atRule.params.trim().replace(/^\(|\)$/g, '').trim();
13
-
14
- // Process functions in the condition (e.g., theme access)
15
- const evaluated = processFunctions(condition, fnMap, atRule, context).trim();
16
-
17
- // Simple truthy evaluation
18
- const isFalsy = !evaluated ||
19
- evaluated === 'false' ||
20
- evaluated === '0' ||
21
- evaluated === 'null' ||
22
- evaluated === 'undefined';
23
-
24
- if (!isFalsy) {
25
- // Ungroup the nodes
26
- if (atRule.nodes && atRule.nodes.length > 0) {
27
- atRule.replaceWith(...atRule.nodes.map(n => n.clone()));
28
- } else {
29
- atRule.remove();
30
- }
31
- } else {
32
- atRule.remove();
33
- }
34
- }
@@ -1,50 +0,0 @@
1
- import { AtRule } from "postcss";
2
- import { PluginContext } from "../config";
3
-
4
- /**
5
- * Global store for CSS-defined custom media (if needed)
6
- * However, we primarily use the config.
7
- */
8
-
9
- /**
10
- * Handler for @media at-rules to resolve custom media tokens
11
- */
12
- export function resolveCustomMedia(atRule: AtRule, context: PluginContext) {
13
- const { config } = context;
14
- const mediaTokens = (config as any).media || {};
15
-
16
- // Regex to find --tokens anywhere
17
- let params = atRule.params;
18
- const tokenRegex = /--[a-zA-Z0-9\-_]+/g;
19
-
20
- params = params.replace(tokenRegex, (match) => {
21
- const tokenName = match.slice(2);
22
- if (mediaTokens[tokenName]) {
23
- return mediaTokens[tokenName];
24
- }
25
- return match;
26
- });
27
-
28
- // Clean up double parentheses like ((...)) if any
29
- params = params.replace(/\(\((.+)\)\)/g, '($1)');
30
-
31
- atRule.params = params;
32
- }
33
-
34
- /**
35
- * [Future-proofing] Handler for @custom-media --name query
36
- * Adds to the config-like store for the current run
37
- */
38
- export function defineCustomMedia(atRule: AtRule, context: PluginContext) {
39
- const match = atRule.params.match(/^(--[a-zA-Z0-9\-_]+)\s+(.+)$/);
40
- if (match) {
41
- const name = match[1].slice(2);
42
- const query = match[2];
43
-
44
- if (!(context.config as any).media) {
45
- (context.config as any).media = {};
46
- }
47
- (context.config as any).media[name] = query;
48
- }
49
- atRule.remove();
50
- }
@@ -1,46 +0,0 @@
1
- import { AtRule, ChildNode } from "postcss";
2
- import { PluginContext } from "../config";
3
- import { reportError } from "../errors";
4
-
5
- /**
6
- * Handler for @define-mixin [name] { ... }
7
- */
8
- export function defineMixin(atRule: AtRule, context: PluginContext) {
9
- const name = atRule.params.trim();
10
- if (!name) {
11
- reportError("Mixin name is required", atRule, context);
12
- return;
13
- }
14
-
15
- // Store the nodes (cloned)
16
- context.mixins[name] = atRule.nodes?.map(n => n.clone()) || [];
17
-
18
- // Remove the at-rule from the output
19
- atRule.remove();
20
- }
21
-
22
- /**
23
- * Handler for @apply [name]
24
- */
25
- export function applyMixin(atRule: AtRule, context: PluginContext) {
26
- const name = atRule.params.trim();
27
- if (!name) {
28
- reportError("Mixin name is required for @apply", atRule, context);
29
- return;
30
- }
31
-
32
- const mixinNodes = context.mixins[name];
33
- if (!mixinNodes) {
34
- reportError(`Mixin "${name}" not found`, atRule, context);
35
- return;
36
- }
37
-
38
- // Inject the nodes, ensuring they have the correct source for mapping
39
- const nodesToInject = mixinNodes.map(n => {
40
- const cloned = n.clone();
41
- cloned.source = atRule.source;
42
- return cloned;
43
- });
44
-
45
- atRule.replaceWith(...nodesToInject);
46
- }