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

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,76 @@
1
+ # [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)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * registered each breakpoint to the same handler for at-rule `container` ([f3f5b36](https://github.com/seyuna-corp/seyuna-postcss/commit/f3f5b3665291dc94e92f3f7c2b2a576b4bdb4517))
7
+
8
+ # [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)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * colors selector in at-rule `each-fixed-color` ([31adb28](https://github.com/seyuna-corp/seyuna-postcss/commit/31adb28f3576a0dcfb78a9aadf331ee4b7ef3e0c))
14
+
15
+ # [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)
16
+
17
+
18
+ ### Features
19
+
20
+ * added at-rule `each-fixed-color` ([699ee0d](https://github.com/seyuna-corp/seyuna-postcss/commit/699ee0defd1fbb0ff91a90c1e13358b1ef9832b2))
21
+
22
+
23
+ ### BREAKING CHANGES
24
+
25
+ * at-rule `each-seyuna-color` renamed to `each-standard-color`
26
+
27
+ # [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)
28
+
29
+
30
+ ### Features
31
+
32
+ * Added at-rule ([8a08289](https://github.com/seyuna-corp/seyuna-postcss/commit/8a08289023f4aa6f65d56e10697e64d02444f118))
33
+
34
+ # [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)
35
+
36
+
37
+ ### Features
38
+
39
+ * add new color function 'fc' ([70e961f](https://github.com/seyuna-corp/seyuna-postcss/commit/70e961fb0b0a13e358de15b42a87b7890f3fc5c0))
40
+
41
+
42
+ ### BREAKING CHANGES
43
+
44
+ * changed how 'sc' functions, not backward-compatible
45
+
46
+ # [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)
47
+
48
+
49
+ ### Bug Fixes
50
+
51
+ * 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))
52
+
53
+ # [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)
54
+
55
+
56
+ ### Bug Fixes
57
+
58
+ * 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))
59
+
60
+ # [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)
61
+
62
+
63
+ ### Bug Fixes
64
+
65
+ * 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))
66
+
67
+ # [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)
68
+
69
+
70
+ ### Bug Fixes
71
+
72
+ * added os mode selectors to dark and light at-rules ([aab8f42](https://github.com/seyuna-corp/seyuna-postcss/commit/aab8f42f05d8bfedf45b19352134254f2da4d9f0))
73
+
1
74
  # 1.0.0-canary.1 (2025-08-07)
2
75
 
3
76
 
@@ -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,105 @@
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) => nodes.map((node) => {
39
+ const cloned = node.clone();
40
+ // Only process declarations
41
+ if (cloned.type === "decl") {
42
+ const decl = cloned;
43
+ decl.value = decl.value.replace(/\{name\}/g, name);
44
+ }
45
+ return cloned;
46
+ });
47
+ // Generate rules for each hue
48
+ for (const hueName of hueNamesSet) {
49
+ const rule = new postcss_1.Rule({ selector: `.${hueName}` });
50
+ cloneNodesWithName(hueName).forEach((n) => rule.append(n));
51
+ generatedRules.push(rule);
52
+ }
53
+ // Replace the original @each-seyuna-color at-rule with all the generated rules
54
+ atRule.replaceWith(...generatedRules);
55
+ }
56
+ /**
57
+ * Custom PostCSS plugin handler for `@each-fixed-color` at-rules.
58
+ *
59
+ * Example usage:
60
+ *
61
+ * @each-fixed-color {
62
+ * color: white;
63
+ * }
64
+ *
65
+ * Will generate:
66
+ *
67
+ * .primary { color: white; }
68
+ * .secondary { color: white; }
69
+ * ...
70
+ */
71
+ function eachFixedColor(atRule) {
72
+ // Read seyuna.json from project root
73
+ const jsonPath = path_1.default.resolve(process.cwd(), "seyuna.json");
74
+ const fileContents = fs_1.default.readFileSync(jsonPath, "utf-8");
75
+ const data = JSON.parse(fileContents);
76
+ const light_colors = data.ui.theme.light.colors;
77
+ const dark_colors = data.ui.theme.dark.colors;
78
+ const lightColorNamesSet = new Set(Object.keys(light_colors));
79
+ const darkColorNamesSet = new Set(Object.keys(dark_colors));
80
+ const mergedColorNamesSet = new Set([
81
+ ...lightColorNamesSet,
82
+ ...darkColorNamesSet,
83
+ ]);
84
+ // Guard against atRule.nodes being undefined
85
+ const nodes = atRule.nodes ?? [];
86
+ const generatedRules = [];
87
+ // Helper to clone nodes and replace {name} placeholder
88
+ const cloneNodesWithName = (name) => nodes.map((node) => {
89
+ const cloned = node.clone();
90
+ // Only process declarations
91
+ if (cloned.type === "decl") {
92
+ const decl = cloned;
93
+ decl.value = decl.value.replace(/\{name\}/g, name);
94
+ }
95
+ return cloned;
96
+ });
97
+ // Generate rules for mergedColorNamesSet
98
+ for (const colorName of mergedColorNamesSet) {
99
+ const rule = new postcss_1.Rule({ selector: `.${colorName}` });
100
+ cloneNodesWithName(colorName).forEach((n) => rule.append(n));
101
+ generatedRules.push(rule);
102
+ }
103
+ // Replace the original @each-seyuna-color at-rule with all the generated rules
104
+ atRule.replaceWith(...generatedRules);
105
+ }
@@ -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.10",
4
4
  "description": "Seyuna UI's postcss plugin",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,118 @@
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 = (name: string) =>
37
+ nodes.map((node) => {
38
+ const cloned = node.clone();
39
+
40
+ // Only process declarations
41
+ if (cloned.type === "decl") {
42
+ const decl = cloned as Declaration;
43
+ decl.value = decl.value.replace(/\{name\}/g, name);
44
+ }
45
+
46
+ return cloned;
47
+ });
48
+
49
+ // Generate rules for each hue
50
+ for (const hueName of hueNamesSet) {
51
+ const rule = new Rule({ selector: `.${hueName}` });
52
+ cloneNodesWithName(hueName).forEach((n) => rule.append(n));
53
+ generatedRules.push(rule);
54
+ }
55
+
56
+ // Replace the original @each-seyuna-color at-rule with all the generated rules
57
+ atRule.replaceWith(...generatedRules);
58
+ }
59
+
60
+ /**
61
+ * Custom PostCSS plugin handler for `@each-fixed-color` at-rules.
62
+ *
63
+ * Example usage:
64
+ *
65
+ * @each-fixed-color {
66
+ * color: white;
67
+ * }
68
+ *
69
+ * Will generate:
70
+ *
71
+ * .primary { color: white; }
72
+ * .secondary { color: white; }
73
+ * ...
74
+ */
75
+ export function eachFixedColor(atRule: AtRule) {
76
+ // Read seyuna.json from project root
77
+ const jsonPath = path.resolve(process.cwd(), "seyuna.json");
78
+ const fileContents = fs.readFileSync(jsonPath, "utf-8");
79
+ const data: SeyunaConfig = JSON.parse(fileContents);
80
+ const light_colors = data.ui.theme.light.colors;
81
+ const dark_colors = data.ui.theme.dark.colors;
82
+ const lightColorNamesSet = new Set(Object.keys(light_colors));
83
+ const darkColorNamesSet = new Set(Object.keys(dark_colors));
84
+
85
+ const mergedColorNamesSet = new Set([
86
+ ...lightColorNamesSet,
87
+ ...darkColorNamesSet,
88
+ ]);
89
+
90
+ // Guard against atRule.nodes being undefined
91
+ const nodes = atRule.nodes ?? [];
92
+
93
+ const generatedRules: Rule[] = [];
94
+
95
+ // Helper to clone nodes and replace {name} placeholder
96
+ const cloneNodesWithName = (name: string) =>
97
+ nodes.map((node) => {
98
+ const cloned = node.clone();
99
+
100
+ // Only process declarations
101
+ if (cloned.type === "decl") {
102
+ const decl = cloned as Declaration;
103
+ decl.value = decl.value.replace(/\{name\}/g, name);
104
+ }
105
+
106
+ return cloned;
107
+ });
108
+
109
+ // Generate rules for mergedColorNamesSet
110
+ for (const colorName of mergedColorNamesSet) {
111
+ const rule = new Rule({ selector: `.${colorName}` });
112
+ cloneNodesWithName(colorName).forEach((n) => rule.append(n));
113
+ generatedRules.push(rule);
114
+ }
115
+
116
+ // Replace the original @each-seyuna-color at-rule with all the generated rules
117
+ atRule.replaceWith(...generatedRules);
118
+ }
@@ -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
+ };