@seyuna/postcss 1.0.0-canary.1 → 1.0.0-canary.11

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/CHANGELOG.md CHANGED
@@ -1,3 +1,83 @@
1
+ # [1.0.0-canary.11](https://github.com/seyuna-corp/seyuna-postcss/compare/v1.0.0-canary.10...v1.0.0-canary.11) (2025-09-15)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * nested rules for at-each-standard-color & at-each-fixed-color ([197a755](https://github.com/seyuna-corp/seyuna-postcss/commit/197a75542798ecacaa071802b2abbf962bcd6538))
7
+
8
+ # [1.0.0-canary.10](https://github.com/seyuna-corp/seyuna-postcss/compare/v1.0.0-canary.9...v1.0.0-canary.10) (2025-09-10)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * registered each breakpoint to the same handler for at-rule `container` ([f3f5b36](https://github.com/seyuna-corp/seyuna-postcss/commit/f3f5b3665291dc94e92f3f7c2b2a576b4bdb4517))
14
+
15
+ # [1.0.0-canary.9](https://github.com/seyuna-corp/seyuna-postcss/compare/v1.0.0-canary.8...v1.0.0-canary.9) (2025-09-10)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * colors selector in at-rule `each-fixed-color` ([31adb28](https://github.com/seyuna-corp/seyuna-postcss/commit/31adb28f3576a0dcfb78a9aadf331ee4b7ef3e0c))
21
+
22
+ # [1.0.0-canary.8](https://github.com/seyuna-corp/seyuna-postcss/compare/v1.0.0-canary.7...v1.0.0-canary.8) (2025-09-10)
23
+
24
+
25
+ ### Features
26
+
27
+ * added at-rule `each-fixed-color` ([699ee0d](https://github.com/seyuna-corp/seyuna-postcss/commit/699ee0defd1fbb0ff91a90c1e13358b1ef9832b2))
28
+
29
+
30
+ ### BREAKING CHANGES
31
+
32
+ * at-rule `each-seyuna-color` renamed to `each-standard-color`
33
+
34
+ # [1.0.0-canary.7](https://github.com/seyuna-corp/seyuna-postcss/compare/v1.0.0-canary.6...v1.0.0-canary.7) (2025-09-10)
35
+
36
+
37
+ ### Features
38
+
39
+ * Added at-rule ([8a08289](https://github.com/seyuna-corp/seyuna-postcss/commit/8a08289023f4aa6f65d56e10697e64d02444f118))
40
+
41
+ # [1.0.0-canary.6](https://github.com/seyuna-corp/seyuna-postcss/compare/v1.0.0-canary.5...v1.0.0-canary.6) (2025-09-09)
42
+
43
+
44
+ ### Features
45
+
46
+ * add new color function 'fc' ([70e961f](https://github.com/seyuna-corp/seyuna-postcss/commit/70e961fb0b0a13e358de15b42a87b7890f3fc5c0))
47
+
48
+
49
+ ### BREAKING CHANGES
50
+
51
+ * changed how 'sc' functions, not backward-compatible
52
+
53
+ # [1.0.0-canary.5](https://github.com/seyuna-corp/seyuna-postcss/compare/v1.0.0-canary.4...v1.0.0-canary.5) (2025-08-25)
54
+
55
+
56
+ ### Bug Fixes
57
+
58
+ * changed color function name to sc to avoid conflicts with the default css color() function ([a46e96c](https://github.com/seyuna-corp/seyuna-postcss/commit/a46e96c74839f930d39a2c273c822a689a942783))
59
+
60
+ # [1.0.0-canary.4](https://github.com/seyuna-corp/seyuna-postcss/compare/v1.0.0-canary.3...v1.0.0-canary.4) (2025-08-24)
61
+
62
+
63
+ ### Bug Fixes
64
+
65
+ * at-rules for mode now ensure that [data-mode=system] before enforcing prefers-color-scheme ([475055d](https://github.com/seyuna-corp/seyuna-postcss/commit/475055db1d5662d25631953af669bf64b2e0468e))
66
+
67
+ # [1.0.0-canary.3](https://github.com/seyuna-corp/seyuna-postcss/compare/v1.0.0-canary.2...v1.0.0-canary.3) (2025-08-24)
68
+
69
+
70
+ ### Bug Fixes
71
+
72
+ * prioritized data-mode over prefers-color-scheme for [@dark](https://github.com/dark) & [@light](https://github.com/light) rules ([1dafbe7](https://github.com/seyuna-corp/seyuna-postcss/commit/1dafbe74c2ceae8faf28f55ac64846e9e752405b))
73
+
74
+ # [1.0.0-canary.2](https://github.com/seyuna-corp/seyuna-postcss/compare/v1.0.0-canary.1...v1.0.0-canary.2) (2025-08-08)
75
+
76
+
77
+ ### Bug Fixes
78
+
79
+ * added os mode selectors to dark and light at-rules ([aab8f42](https://github.com/seyuna-corp/seyuna-postcss/commit/aab8f42f05d8bfedf45b19352134254f2da4d9f0))
80
+
1
81
  # 1.0.0-canary.1 (2025-08-07)
2
82
 
3
83
 
@@ -0,0 +1,34 @@
1
+ import { AtRule } from "postcss";
2
+ /**
3
+ * Custom PostCSS plugin handler for `@each-standard-color` at-rules.
4
+ *
5
+ * Example usage:
6
+ *
7
+ * @each-standard-color {
8
+ * color: white;
9
+ * }
10
+ *
11
+ * Will generate:
12
+ *
13
+ * .alpha { color: white; }
14
+ * .beta { color: white; }
15
+ * .gamma { color: white; }
16
+ * ...
17
+ */
18
+ export declare function eachStandardColor(atRule: AtRule): void;
19
+ /**
20
+ * Custom PostCSS plugin handler for `@each-fixed-color` at-rules.
21
+ *
22
+ * Example usage:
23
+ *
24
+ * @each-fixed-color {
25
+ * color: white;
26
+ * }
27
+ *
28
+ * Will generate:
29
+ *
30
+ * .primary { color: white; }
31
+ * .secondary { color: white; }
32
+ * ...
33
+ */
34
+ export declare function eachFixedColor(atRule: AtRule): void;
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.eachStandardColor = eachStandardColor;
7
+ exports.eachFixedColor = eachFixedColor;
8
+ const postcss_1 = require("postcss");
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ /**
12
+ * Custom PostCSS plugin handler for `@each-standard-color` at-rules.
13
+ *
14
+ * Example usage:
15
+ *
16
+ * @each-standard-color {
17
+ * color: white;
18
+ * }
19
+ *
20
+ * Will generate:
21
+ *
22
+ * .alpha { color: white; }
23
+ * .beta { color: white; }
24
+ * .gamma { color: white; }
25
+ * ...
26
+ */
27
+ function eachStandardColor(atRule) {
28
+ // Read seyuna.json from project root
29
+ const jsonPath = path_1.default.resolve(process.cwd(), "seyuna.json");
30
+ const fileContents = fs_1.default.readFileSync(jsonPath, "utf-8");
31
+ const data = JSON.parse(fileContents);
32
+ const hues = data.ui.theme.hues;
33
+ const hueNamesSet = new Set(Object.keys(hues));
34
+ // Guard against atRule.nodes being undefined
35
+ const nodes = atRule.nodes ?? [];
36
+ const generatedRules = [];
37
+ // Helper to clone nodes and replace {name} placeholder
38
+ const cloneNodesWithName = (name, nodeList = nodes) => nodeList.map((node) => {
39
+ const cloned = node.clone();
40
+ if (cloned.type === "decl") {
41
+ const decl = cloned;
42
+ decl.value = decl.value.replace(/\{name\}/g, name);
43
+ }
44
+ else if (cloned.type === "rule") {
45
+ const rule = cloned;
46
+ rule.selector = rule.selector.replace(/\{name\}/g, name);
47
+ rule.nodes = cloneNodesWithName(name, rule.nodes || []);
48
+ }
49
+ else if (cloned.type === "atrule") {
50
+ // If you want {name} in selectors inside at-rules too
51
+ cloned.params = cloned.params.replace(/\{name\}/g, name);
52
+ cloned.nodes = cloneNodesWithName(name, cloned.nodes || []);
53
+ }
54
+ return cloned;
55
+ });
56
+ // Generate rules for each hue
57
+ for (const hueName of hueNamesSet) {
58
+ const rule = new postcss_1.Rule({ selector: `.${hueName}` });
59
+ cloneNodesWithName(hueName).forEach((n) => rule.append(n));
60
+ generatedRules.push(rule);
61
+ }
62
+ // Replace the original @each-seyuna-color at-rule with all the generated rules
63
+ atRule.replaceWith(...generatedRules);
64
+ }
65
+ /**
66
+ * Custom PostCSS plugin handler for `@each-fixed-color` at-rules.
67
+ *
68
+ * Example usage:
69
+ *
70
+ * @each-fixed-color {
71
+ * color: white;
72
+ * }
73
+ *
74
+ * Will generate:
75
+ *
76
+ * .primary { color: white; }
77
+ * .secondary { color: white; }
78
+ * ...
79
+ */
80
+ function eachFixedColor(atRule) {
81
+ // Read seyuna.json from project root
82
+ const jsonPath = path_1.default.resolve(process.cwd(), "seyuna.json");
83
+ const fileContents = fs_1.default.readFileSync(jsonPath, "utf-8");
84
+ const data = JSON.parse(fileContents);
85
+ const light_colors = data.ui.theme.light.colors;
86
+ const dark_colors = data.ui.theme.dark.colors;
87
+ const lightColorNamesSet = new Set(Object.keys(light_colors));
88
+ const darkColorNamesSet = new Set(Object.keys(dark_colors));
89
+ const mergedColorNamesSet = new Set([
90
+ ...lightColorNamesSet,
91
+ ...darkColorNamesSet,
92
+ ]);
93
+ // Guard against atRule.nodes being undefined
94
+ const nodes = atRule.nodes ?? [];
95
+ const generatedRules = [];
96
+ // Helper to clone nodes and replace {name} placeholder
97
+ const cloneNodesWithName = (name, nodeList = nodes) => nodeList.map((node) => {
98
+ const cloned = node.clone();
99
+ if (cloned.type === "decl") {
100
+ const decl = cloned;
101
+ decl.value = decl.value.replace(/\{name\}/g, name);
102
+ }
103
+ else if (cloned.type === "rule") {
104
+ const rule = cloned;
105
+ rule.selector = rule.selector.replace(/\{name\}/g, name);
106
+ rule.nodes = cloneNodesWithName(name, rule.nodes || []);
107
+ }
108
+ else if (cloned.type === "atrule") {
109
+ // If you want {name} in selectors inside at-rules too
110
+ cloned.params = cloned.params.replace(/\{name\}/g, name);
111
+ cloned.nodes = cloneNodesWithName(name, cloned.nodes || []);
112
+ }
113
+ return cloned;
114
+ });
115
+ // Generate rules for mergedColorNamesSet
116
+ for (const colorName of mergedColorNamesSet) {
117
+ const rule = new postcss_1.Rule({ selector: `.${colorName}` });
118
+ cloneNodesWithName(colorName).forEach((n) => rule.append(n));
119
+ generatedRules.push(rule);
120
+ }
121
+ // Replace the original @each-seyuna-color at-rule with all the generated rules
122
+ atRule.replaceWith(...generatedRules);
123
+ }
@@ -0,0 +1,18 @@
1
+ import { AtRule } from "postcss";
2
+ /**
3
+ * Custom PostCSS plugin handler for responsive at-rules.
4
+ *
5
+ * Example:
6
+ *
7
+ * @xs {
8
+ * .box { color: red; }
9
+ * }
10
+ *
11
+ * Into:
12
+ *
13
+ * @xs (min-width: 234px) {
14
+ * .box { color: red; }
15
+ * }
16
+ *
17
+ */
18
+ export default function container(atRule: AtRule): void;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = container;
4
+ const postcss_1 = require("postcss");
5
+ /**
6
+ * Custom PostCSS plugin handler for responsive at-rules.
7
+ *
8
+ * Example:
9
+ *
10
+ * @xs {
11
+ * .box { color: red; }
12
+ * }
13
+ *
14
+ * Into:
15
+ *
16
+ * @xs (min-width: 234px) {
17
+ * .box { color: red; }
18
+ * }
19
+ *
20
+ */
21
+ function container(atRule) {
22
+ // Map of shortcuts → container widths
23
+ const breakpoints = {
24
+ xs: "20rem",
25
+ sm: "40rem",
26
+ md: "48rem",
27
+ lg: "64rem",
28
+ xl: "80rem",
29
+ "2xl": "96rem",
30
+ };
31
+ if (Object.keys(breakpoints).includes(atRule.name)) {
32
+ const minWidth = breakpoints[atRule.name];
33
+ const clonedNodes = [];
34
+ atRule.each((node) => {
35
+ clonedNodes.push(node.clone());
36
+ });
37
+ const containerAtRule = new postcss_1.AtRule({
38
+ name: "container",
39
+ params: `(min-width: ${minWidth})`,
40
+ });
41
+ clonedNodes.forEach((node) => containerAtRule.append(node));
42
+ atRule.replaceWith(containerAtRule);
43
+ }
44
+ }
@@ -1,2 +1,23 @@
1
- import { type AtRule } from "postcss";
1
+ import { AtRule } from "postcss";
2
+ /**
3
+ * Custom PostCSS plugin handler for `@dark` at-rules.
4
+ *
5
+ * Transforms:
6
+ *
7
+ * @dark {
8
+ * color: white;
9
+ * }
10
+ *
11
+ * Into:
12
+ *
13
+ * @media (prefers-color-scheme: dark) {
14
+ * [data-mode="system"] & {
15
+ * color: white;
16
+ * }
17
+ * }
18
+ *
19
+ * [data-mode="dark"] & {
20
+ * color: white;
21
+ * }
22
+ */
2
23
  export default function dark(atRule: AtRule): void;
@@ -2,14 +2,64 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = dark;
4
4
  const postcss_1 = require("postcss");
5
+ /**
6
+ * Custom PostCSS plugin handler for `@dark` at-rules.
7
+ *
8
+ * Transforms:
9
+ *
10
+ * @dark {
11
+ * color: white;
12
+ * }
13
+ *
14
+ * Into:
15
+ *
16
+ * @media (prefers-color-scheme: dark) {
17
+ * [data-mode="system"] & {
18
+ * color: white;
19
+ * }
20
+ * }
21
+ *
22
+ * [data-mode="dark"] & {
23
+ * color: white;
24
+ * }
25
+ */
5
26
  function dark(atRule) {
6
- const nestedRule = new postcss_1.Rule({
27
+ const clonedNodes = [];
28
+ // Clone all child nodes inside the @dark block
29
+ // (so we can reuse them in both generated rules).
30
+ atRule.each((node) => {
31
+ clonedNodes.push(node.clone());
32
+ });
33
+ /**
34
+ * Rule 1: [data-mode="dark"] & { ... }
35
+ *
36
+ * This applies the styles when the user explicitly sets dark mode.
37
+ */
38
+ const darkRule = new postcss_1.Rule({
7
39
  selector: `[data-mode="dark"] &`,
8
40
  });
9
- // Clone all child nodes to prevent them from being detached
10
- atRule.each((node) => {
11
- nestedRule.append(node.clone());
41
+ clonedNodes.forEach((node) => darkRule.append(node.clone()));
42
+ /**
43
+ * Rule 2: @media (prefers-color-scheme: dark) { [data-mode="system"] & { ... } }
44
+ *
45
+ * This applies the styles only when:
46
+ * - The user’s OS prefers dark mode
47
+ * - AND the app is in "system" mode (i.e. follow system preference)
48
+ */
49
+ const mediaAtRule = new postcss_1.AtRule({
50
+ name: "media",
51
+ params: "(prefers-color-scheme: dark)",
52
+ });
53
+ // Wrap cloned rules under `[data-mode="system"]`
54
+ const systemRule = new postcss_1.Rule({
55
+ selector: `[data-mode="system"] &`,
12
56
  });
13
- // Replace @dark atRule with the new nested rule
14
- atRule.replaceWith(nestedRule);
57
+ clonedNodes.forEach((node) => systemRule.append(node.clone()));
58
+ // Nest `[data-mode="system"]` inside the @media block
59
+ mediaAtRule.append(systemRule);
60
+ /**
61
+ * Replace the original @dark rule in the CSS tree
62
+ * with our two new generated rules.
63
+ */
64
+ atRule.replaceWith(mediaAtRule, darkRule);
15
65
  }
@@ -1,3 +1,6 @@
1
1
  import type { AtRule } from "postcss";
2
- export type AtRuleHandler = (atRule: AtRule) => void;
3
- export declare const atRuleHandlers: Record<string, AtRuleHandler>;
2
+ export interface AtRuleHandler {
3
+ name: string;
4
+ handler: (atRule: AtRule) => void;
5
+ }
6
+ export declare const atRuleHandlers: AtRuleHandler[];
@@ -4,10 +4,22 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.atRuleHandlers = void 0;
7
+ // atRuleHandlers.ts
7
8
  const dark_1 = __importDefault(require("./dark"));
8
9
  const light_1 = __importDefault(require("./light"));
9
- exports.atRuleHandlers = {
10
- light: light_1.default,
11
- dark: dark_1.default,
10
+ const container_1 = __importDefault(require("./container"));
11
+ const color_1 = require("./color");
12
+ // Ordered array ensures execution order
13
+ exports.atRuleHandlers = [
14
+ { name: "each-standard-color", handler: color_1.eachStandardColor }, // first
15
+ { name: "each-fixed-color", handler: color_1.eachFixedColor },
16
+ { name: "light", handler: light_1.default },
17
+ { name: "dark", handler: dark_1.default },
18
+ { name: "xs", handler: container_1.default },
19
+ { name: "sm", handler: container_1.default },
20
+ { name: "md", handler: container_1.default },
21
+ { name: "lg", handler: container_1.default },
22
+ { name: "xl", handler: container_1.default },
23
+ { name: "2xl", handler: container_1.default },
12
24
  // add more handlers here as needed
13
- };
25
+ ];
@@ -1,2 +1,23 @@
1
- import { type AtRule } from "postcss";
1
+ import { AtRule } from "postcss";
2
+ /**
3
+ * Custom PostCSS plugin handler for `@light` at-rules.
4
+ *
5
+ * Transforms:
6
+ *
7
+ * @light {
8
+ * color: white;
9
+ * }
10
+ *
11
+ * Into:
12
+ *
13
+ * @media (prefers-color-scheme: light) {
14
+ * [data-mode="system"] & {
15
+ * color: white;
16
+ * }
17
+ * }
18
+ *
19
+ * [data-mode="light"] & {
20
+ * color: white;
21
+ * }
22
+ */
2
23
  export default function light(atRule: AtRule): void;
@@ -2,14 +2,64 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = light;
4
4
  const postcss_1 = require("postcss");
5
+ /**
6
+ * Custom PostCSS plugin handler for `@light` at-rules.
7
+ *
8
+ * Transforms:
9
+ *
10
+ * @light {
11
+ * color: white;
12
+ * }
13
+ *
14
+ * Into:
15
+ *
16
+ * @media (prefers-color-scheme: light) {
17
+ * [data-mode="system"] & {
18
+ * color: white;
19
+ * }
20
+ * }
21
+ *
22
+ * [data-mode="light"] & {
23
+ * color: white;
24
+ * }
25
+ */
5
26
  function light(atRule) {
6
- const nestedRule = new postcss_1.Rule({
27
+ const clonedNodes = [];
28
+ // Clone all child nodes inside the @light block
29
+ // (so we can reuse them in both generated rules).
30
+ atRule.each((node) => {
31
+ clonedNodes.push(node.clone());
32
+ });
33
+ /**
34
+ * Rule 1: [data-mode="light"] & { ... }
35
+ *
36
+ * This applies the styles when the user explicitly sets light mode.
37
+ */
38
+ const lightRule = new postcss_1.Rule({
7
39
  selector: `[data-mode="light"] &`,
8
40
  });
9
- // Clone all child nodes to prevent them from being detached
10
- atRule.each((node) => {
11
- nestedRule.append(node.clone());
41
+ clonedNodes.forEach((node) => lightRule.append(node.clone()));
42
+ /**
43
+ * Rule 2: @media (prefers-color-scheme: light) { [data-mode="system"] & { ... } }
44
+ *
45
+ * This applies the styles only when:
46
+ * - The user’s OS prefers light mode
47
+ * - AND the app is in "system" mode (i.e. follow system preference)
48
+ */
49
+ const mediaAtRule = new postcss_1.AtRule({
50
+ name: "media",
51
+ params: "(prefers-color-scheme: light)",
52
+ });
53
+ // Wrap cloned rules under `[data-mode="system"]`
54
+ const systemRule = new postcss_1.Rule({
55
+ selector: `[data-mode="system"] &`,
12
56
  });
13
- // Replace @light atRule with the new nested rule
14
- atRule.replaceWith(nestedRule);
57
+ clonedNodes.forEach((node) => systemRule.append(node.clone()));
58
+ // Nest `[data-mode="system"]` inside the @media block
59
+ mediaAtRule.append(systemRule);
60
+ /**
61
+ * Replace the original @light rule in the CSS tree
62
+ * with our two new generated rules.
63
+ */
64
+ atRule.replaceWith(mediaAtRule, lightRule);
15
65
  }
@@ -1 +1,2 @@
1
- export default function color(name: string, alpha?: string, lightness?: string, chroma?: string): string;
1
+ export declare function sc(name: string, alpha?: string, lightness?: string, chroma?: string): string;
2
+ export declare function fc(name: string, alpha?: string, lightness?: string, chroma?: string): string;
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = color;
4
- function color(name, alpha, lightness, chroma) {
3
+ exports.sc = sc;
4
+ exports.fc = fc;
5
+ function sc(name, alpha, lightness, chroma) {
5
6
  let a = "1";
6
7
  let l = "var(--lightness)";
7
8
  let c = "var(--chroma)";
@@ -14,5 +15,20 @@ function color(name, alpha, lightness, chroma) {
14
15
  if (chroma && chroma !== "null") {
15
16
  c = chroma;
16
17
  }
17
- return `oklch(${l} ${c} var(--${name}) / ${a})`;
18
+ return `oklch(${l} ${c} var(--${name}-hue) / ${a})`;
19
+ }
20
+ function fc(name, alpha, lightness, chroma) {
21
+ let a = "1";
22
+ let l = `var(--${name}-lightness)`;
23
+ let c = `var(--${name}-chroma)`;
24
+ if (alpha && alpha !== "null") {
25
+ a = alpha;
26
+ }
27
+ if (lightness && lightness !== "null") {
28
+ l = lightness;
29
+ }
30
+ if (chroma && chroma !== "null") {
31
+ c = chroma;
32
+ }
33
+ return `oklch(${l} ${c} var(--${name}-hue) / ${a})`;
18
34
  }
@@ -4,9 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.functions = void 0;
7
- const color_1 = __importDefault(require("./color"));
7
+ const color_1 = require("./color");
8
8
  const spacing_1 = __importDefault(require("./spacing"));
9
9
  exports.functions = {
10
- color: color_1.default,
10
+ sc: color_1.sc,
11
+ fc: color_1.fc,
11
12
  spacing: spacing_1.default,
12
13
  };
package/dist/plugin.js CHANGED
@@ -20,8 +20,14 @@ const dynamicFunctionsPlugin = (opts = {}) => {
20
20
  }
21
21
  decl.value = value;
22
22
  },
23
- AtRule: {
24
- ...at_rules_1.atRuleHandlers,
23
+ // Override AtRule handler to ensure ordered execution
24
+ AtRule(atRule) {
25
+ // Iterate over handlers in order (array) instead of object spread
26
+ for (const { name, handler } of at_rules_1.atRuleHandlers) {
27
+ if (atRule.name === name) {
28
+ handler(atRule);
29
+ }
30
+ }
25
31
  },
26
32
  };
27
33
  };
@@ -0,0 +1,19 @@
1
+ export interface SeyunaConfig {
2
+ ui: {
3
+ theme: {
4
+ hues: Record<string, number>;
5
+ light: {
6
+ colors: Record<string, Color>;
7
+ };
8
+ dark: {
9
+ colors: Record<string, Color>;
10
+ };
11
+ };
12
+ };
13
+ }
14
+ type Color = {
15
+ lightness: number;
16
+ chroma: number;
17
+ hue: number;
18
+ };
19
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seyuna/postcss",
3
- "version": "1.0.0-canary.1",
3
+ "version": "1.0.0-canary.11",
4
4
  "description": "Seyuna UI's postcss plugin",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,138 @@
1
+ import { AtRule, Rule, ChildNode, Declaration } from "postcss";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { SeyunaConfig } from "../types";
5
+
6
+ /**
7
+ * Custom PostCSS plugin handler for `@each-standard-color` at-rules.
8
+ *
9
+ * Example usage:
10
+ *
11
+ * @each-standard-color {
12
+ * color: white;
13
+ * }
14
+ *
15
+ * Will generate:
16
+ *
17
+ * .alpha { color: white; }
18
+ * .beta { color: white; }
19
+ * .gamma { color: white; }
20
+ * ...
21
+ */
22
+ export function eachStandardColor(atRule: AtRule) {
23
+ // Read seyuna.json from project root
24
+ const jsonPath = path.resolve(process.cwd(), "seyuna.json");
25
+ const fileContents = fs.readFileSync(jsonPath, "utf-8");
26
+ const data: SeyunaConfig = JSON.parse(fileContents);
27
+ const hues = data.ui.theme.hues;
28
+ const hueNamesSet = new Set(Object.keys(hues));
29
+
30
+ // Guard against atRule.nodes being undefined
31
+ const nodes = atRule.nodes ?? [];
32
+
33
+ const generatedRules: Rule[] = [];
34
+
35
+ // Helper to clone nodes and replace {name} placeholder
36
+ const cloneNodesWithName = (
37
+ name: string,
38
+ nodeList: ChildNode[] = nodes
39
+ ): ChildNode[] =>
40
+ nodeList.map((node) => {
41
+ const cloned = node.clone();
42
+
43
+ if (cloned.type === "decl") {
44
+ const decl = cloned as Declaration;
45
+ decl.value = decl.value.replace(/\{name\}/g, name);
46
+ } else if (cloned.type === "rule") {
47
+ const rule = cloned as Rule;
48
+ rule.selector = rule.selector.replace(/\{name\}/g, name);
49
+ rule.nodes = cloneNodesWithName(name, rule.nodes || []);
50
+ } else if (cloned.type === "atrule") {
51
+ // If you want {name} in selectors inside at-rules too
52
+ cloned.params = cloned.params.replace(/\{name\}/g, name);
53
+ cloned.nodes = cloneNodesWithName(name, cloned.nodes || []);
54
+ }
55
+
56
+ return cloned;
57
+ });
58
+
59
+ // Generate rules for each hue
60
+ for (const hueName of hueNamesSet) {
61
+ const rule = new Rule({ selector: `.${hueName}` });
62
+ cloneNodesWithName(hueName).forEach((n) => rule.append(n));
63
+ generatedRules.push(rule);
64
+ }
65
+
66
+ // Replace the original @each-seyuna-color at-rule with all the generated rules
67
+ atRule.replaceWith(...generatedRules);
68
+ }
69
+
70
+ /**
71
+ * Custom PostCSS plugin handler for `@each-fixed-color` at-rules.
72
+ *
73
+ * Example usage:
74
+ *
75
+ * @each-fixed-color {
76
+ * color: white;
77
+ * }
78
+ *
79
+ * Will generate:
80
+ *
81
+ * .primary { color: white; }
82
+ * .secondary { color: white; }
83
+ * ...
84
+ */
85
+ export function eachFixedColor(atRule: AtRule) {
86
+ // Read seyuna.json from project root
87
+ const jsonPath = path.resolve(process.cwd(), "seyuna.json");
88
+ const fileContents = fs.readFileSync(jsonPath, "utf-8");
89
+ const data: SeyunaConfig = JSON.parse(fileContents);
90
+ const light_colors = data.ui.theme.light.colors;
91
+ const dark_colors = data.ui.theme.dark.colors;
92
+ const lightColorNamesSet = new Set(Object.keys(light_colors));
93
+ const darkColorNamesSet = new Set(Object.keys(dark_colors));
94
+
95
+ const mergedColorNamesSet = new Set([
96
+ ...lightColorNamesSet,
97
+ ...darkColorNamesSet,
98
+ ]);
99
+
100
+ // Guard against atRule.nodes being undefined
101
+ const nodes = atRule.nodes ?? [];
102
+
103
+ const generatedRules: Rule[] = [];
104
+
105
+ // Helper to clone nodes and replace {name} placeholder
106
+ const cloneNodesWithName = (
107
+ name: string,
108
+ nodeList: ChildNode[] = nodes
109
+ ): ChildNode[] =>
110
+ nodeList.map((node) => {
111
+ const cloned = node.clone();
112
+
113
+ if (cloned.type === "decl") {
114
+ const decl = cloned as Declaration;
115
+ decl.value = decl.value.replace(/\{name\}/g, name);
116
+ } else if (cloned.type === "rule") {
117
+ const rule = cloned as Rule;
118
+ rule.selector = rule.selector.replace(/\{name\}/g, name);
119
+ rule.nodes = cloneNodesWithName(name, rule.nodes || []);
120
+ } else if (cloned.type === "atrule") {
121
+ // If you want {name} in selectors inside at-rules too
122
+ cloned.params = cloned.params.replace(/\{name\}/g, name);
123
+ cloned.nodes = cloneNodesWithName(name, cloned.nodes || []);
124
+ }
125
+
126
+ return cloned;
127
+ });
128
+
129
+ // Generate rules for mergedColorNamesSet
130
+ for (const colorName of mergedColorNamesSet) {
131
+ const rule = new Rule({ selector: `.${colorName}` });
132
+ cloneNodesWithName(colorName).forEach((n) => rule.append(n));
133
+ generatedRules.push(rule);
134
+ }
135
+
136
+ // Replace the original @each-seyuna-color at-rule with all the generated rules
137
+ atRule.replaceWith(...generatedRules);
138
+ }
@@ -0,0 +1,47 @@
1
+ import { AtRule, ChildNode } from "postcss";
2
+
3
+ /**
4
+ * Custom PostCSS plugin handler for responsive at-rules.
5
+ *
6
+ * Example:
7
+ *
8
+ * @xs {
9
+ * .box { color: red; }
10
+ * }
11
+ *
12
+ * Into:
13
+ *
14
+ * @xs (min-width: 234px) {
15
+ * .box { color: red; }
16
+ * }
17
+ *
18
+ */
19
+ export default function container(atRule: AtRule) {
20
+ // Map of shortcuts → container widths
21
+ const breakpoints: Record<string, string> = {
22
+ xs: "20rem",
23
+ sm: "40rem",
24
+ md: "48rem",
25
+ lg: "64rem",
26
+ xl: "80rem",
27
+ "2xl": "96rem",
28
+ };
29
+
30
+ if (Object.keys(breakpoints).includes(atRule.name)) {
31
+ const minWidth = breakpoints[atRule.name];
32
+
33
+ const clonedNodes: ChildNode[] = [];
34
+ atRule.each((node: ChildNode) => {
35
+ clonedNodes.push(node.clone());
36
+ });
37
+
38
+ const containerAtRule = new AtRule({
39
+ name: "container",
40
+ params: `(min-width: ${minWidth})`,
41
+ });
42
+
43
+ clonedNodes.forEach((node) => containerAtRule.append(node));
44
+
45
+ atRule.replaceWith(containerAtRule);
46
+ }
47
+ }
@@ -1,15 +1,69 @@
1
- import { Rule, type AtRule, type ChildNode } from "postcss";
1
+ import { Rule, AtRule, ChildNode } from "postcss";
2
2
 
3
+ /**
4
+ * Custom PostCSS plugin handler for `@dark` at-rules.
5
+ *
6
+ * Transforms:
7
+ *
8
+ * @dark {
9
+ * color: white;
10
+ * }
11
+ *
12
+ * Into:
13
+ *
14
+ * @media (prefers-color-scheme: dark) {
15
+ * [data-mode="system"] & {
16
+ * color: white;
17
+ * }
18
+ * }
19
+ *
20
+ * [data-mode="dark"] & {
21
+ * color: white;
22
+ * }
23
+ */
3
24
  export default function dark(atRule: AtRule) {
4
- const nestedRule = new Rule({
25
+ const clonedNodes: ChildNode[] = [];
26
+
27
+ // Clone all child nodes inside the @dark block
28
+ // (so we can reuse them in both generated rules).
29
+ atRule.each((node: ChildNode) => {
30
+ clonedNodes.push(node.clone());
31
+ });
32
+
33
+ /**
34
+ * Rule 1: [data-mode="dark"] & { ... }
35
+ *
36
+ * This applies the styles when the user explicitly sets dark mode.
37
+ */
38
+ const darkRule = new Rule({
5
39
  selector: `[data-mode="dark"] &`,
6
40
  });
41
+ clonedNodes.forEach((node) => darkRule.append(node.clone()));
7
42
 
8
- // Clone all child nodes to prevent them from being detached
9
- atRule.each((node: ChildNode) => {
10
- nestedRule.append(node.clone());
43
+ /**
44
+ * Rule 2: @media (prefers-color-scheme: dark) { [data-mode="system"] & { ... } }
45
+ *
46
+ * This applies the styles only when:
47
+ * - The user’s OS prefers dark mode
48
+ * - AND the app is in "system" mode (i.e. follow system preference)
49
+ */
50
+ const mediaAtRule = new AtRule({
51
+ name: "media",
52
+ params: "(prefers-color-scheme: dark)",
53
+ });
54
+
55
+ // Wrap cloned rules under `[data-mode="system"]`
56
+ const systemRule = new Rule({
57
+ selector: `[data-mode="system"] &`,
11
58
  });
59
+ clonedNodes.forEach((node) => systemRule.append(node.clone()));
60
+
61
+ // Nest `[data-mode="system"]` inside the @media block
62
+ mediaAtRule.append(systemRule);
12
63
 
13
- // Replace @dark atRule with the new nested rule
14
- atRule.replaceWith(nestedRule);
64
+ /**
65
+ * Replace the original @dark rule in the CSS tree
66
+ * with our two new generated rules.
67
+ */
68
+ atRule.replaceWith(mediaAtRule, darkRule);
15
69
  }
@@ -1,11 +1,27 @@
1
+ // atRuleHandlers.ts
1
2
  import dark from "./dark";
2
3
  import light from "./light";
4
+ import container from "./container";
5
+ import { eachStandardColor, eachFixedColor } from "./color";
3
6
  import type { AtRule } from "postcss";
4
7
 
5
- export type AtRuleHandler = (atRule: AtRule) => void;
8
+ // Each handler has a name (matches the at-rule) and the function
9
+ export interface AtRuleHandler {
10
+ name: string;
11
+ handler: (atRule: AtRule) => void;
12
+ }
6
13
 
7
- export const atRuleHandlers: Record<string, AtRuleHandler> = {
8
- light,
9
- dark,
14
+ // Ordered array ensures execution order
15
+ export const atRuleHandlers: AtRuleHandler[] = [
16
+ { name: "each-standard-color", handler: eachStandardColor }, // first
17
+ { name: "each-fixed-color", handler: eachFixedColor },
18
+ { name: "light", handler: light },
19
+ { name: "dark", handler: dark },
20
+ { name: "xs", handler: container },
21
+ { name: "sm", handler: container },
22
+ { name: "md", handler: container },
23
+ { name: "lg", handler: container },
24
+ { name: "xl", handler: container },
25
+ { name: "2xl", handler: container },
10
26
  // add more handlers here as needed
11
- };
27
+ ];
@@ -1,15 +1,69 @@
1
- import { Rule, type AtRule, type ChildNode } from "postcss";
1
+ import { Rule, AtRule, ChildNode } from "postcss";
2
2
 
3
+ /**
4
+ * Custom PostCSS plugin handler for `@light` at-rules.
5
+ *
6
+ * Transforms:
7
+ *
8
+ * @light {
9
+ * color: white;
10
+ * }
11
+ *
12
+ * Into:
13
+ *
14
+ * @media (prefers-color-scheme: light) {
15
+ * [data-mode="system"] & {
16
+ * color: white;
17
+ * }
18
+ * }
19
+ *
20
+ * [data-mode="light"] & {
21
+ * color: white;
22
+ * }
23
+ */
3
24
  export default function light(atRule: AtRule) {
4
- const nestedRule = new Rule({
25
+ const clonedNodes: ChildNode[] = [];
26
+
27
+ // Clone all child nodes inside the @light block
28
+ // (so we can reuse them in both generated rules).
29
+ atRule.each((node: ChildNode) => {
30
+ clonedNodes.push(node.clone());
31
+ });
32
+
33
+ /**
34
+ * Rule 1: [data-mode="light"] & { ... }
35
+ *
36
+ * This applies the styles when the user explicitly sets light mode.
37
+ */
38
+ const lightRule = new Rule({
5
39
  selector: `[data-mode="light"] &`,
6
40
  });
41
+ clonedNodes.forEach((node) => lightRule.append(node.clone()));
7
42
 
8
- // Clone all child nodes to prevent them from being detached
9
- atRule.each((node: ChildNode) => {
10
- nestedRule.append(node.clone());
43
+ /**
44
+ * Rule 2: @media (prefers-color-scheme: light) { [data-mode="system"] & { ... } }
45
+ *
46
+ * This applies the styles only when:
47
+ * - The user’s OS prefers light mode
48
+ * - AND the app is in "system" mode (i.e. follow system preference)
49
+ */
50
+ const mediaAtRule = new AtRule({
51
+ name: "media",
52
+ params: "(prefers-color-scheme: light)",
53
+ });
54
+
55
+ // Wrap cloned rules under `[data-mode="system"]`
56
+ const systemRule = new Rule({
57
+ selector: `[data-mode="system"] &`,
11
58
  });
59
+ clonedNodes.forEach((node) => systemRule.append(node.clone()));
60
+
61
+ // Nest `[data-mode="system"]` inside the @media block
62
+ mediaAtRule.append(systemRule);
12
63
 
13
- // Replace @light atRule with the new nested rule
14
- atRule.replaceWith(nestedRule);
64
+ /**
65
+ * Replace the original @light rule in the CSS tree
66
+ * with our two new generated rules.
67
+ */
68
+ atRule.replaceWith(mediaAtRule, lightRule);
15
69
  }
@@ -1,4 +1,4 @@
1
- export default function color(
1
+ export function sc(
2
2
  name: string,
3
3
  alpha?: string,
4
4
  lightness?: string,
@@ -20,5 +20,30 @@ export default function color(
20
20
  c = chroma;
21
21
  }
22
22
 
23
- return `oklch(${l} ${c} var(--${name}) / ${a})`;
23
+ return `oklch(${l} ${c} var(--${name}-hue) / ${a})`;
24
+ }
25
+
26
+ export function fc(
27
+ name: string,
28
+ alpha?: string,
29
+ lightness?: string,
30
+ chroma?: string
31
+ ) {
32
+ let a: string = "1";
33
+ let l: string = `var(--${name}-lightness)`;
34
+ let c: string = `var(--${name}-chroma)`;
35
+
36
+ if (alpha && alpha !== "null") {
37
+ a = alpha;
38
+ }
39
+
40
+ if (lightness && lightness !== "null") {
41
+ l = lightness;
42
+ }
43
+
44
+ if (chroma && chroma !== "null") {
45
+ c = chroma;
46
+ }
47
+
48
+ return `oklch(${l} ${c} var(--${name}-hue) / ${a})`;
24
49
  }
@@ -1,9 +1,10 @@
1
- import color from "./color";
1
+ import { sc, fc } from "./color";
2
2
  import spacing from "./spacing";
3
3
 
4
4
  export type FnHandler = (...args: string[]) => string;
5
5
 
6
6
  export const functions: Record<string, FnHandler> = {
7
- color,
7
+ sc,
8
+ fc,
8
9
  spacing,
9
10
  };
package/src/plugin.ts CHANGED
@@ -31,8 +31,14 @@ export const dynamicFunctionsPlugin: PluginCreator<PluginOptions> = (
31
31
  decl.value = value;
32
32
  },
33
33
 
34
- AtRule: {
35
- ...atRuleHandlers,
34
+ // Override AtRule handler to ensure ordered execution
35
+ AtRule(atRule) {
36
+ // Iterate over handlers in order (array) instead of object spread
37
+ for (const { name, handler } of atRuleHandlers) {
38
+ if (atRule.name === name) {
39
+ handler(atRule);
40
+ }
41
+ }
36
42
  },
37
43
  };
38
44
  };
package/src/types.ts ADDED
@@ -0,0 +1,19 @@
1
+ export interface SeyunaConfig {
2
+ ui: {
3
+ theme: {
4
+ hues: Record<string, number>;
5
+ light: {
6
+ colors: Record<string, Color>;
7
+ };
8
+ dark: {
9
+ colors: Record<string, Color>;
10
+ };
11
+ };
12
+ };
13
+ }
14
+
15
+ type Color = {
16
+ lightness: number;
17
+ chroma: number;
18
+ hue: number;
19
+ };