@seyuna/postcss 0.0.1-dev.9 → 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.
@@ -1,156 +1,41 @@
1
- name: Build, Release, and Publish
1
+ name: Release
2
2
 
3
3
  on:
4
4
  push:
5
5
  branches:
6
- - stable
7
- - next
8
- - dev
9
- workflow_dispatch:
6
+ - main
7
+ - canary
10
8
 
11
9
  permissions:
12
10
  contents: write
11
+ issues: write
12
+ pull-requests: write
13
13
  id-token: write
14
14
 
15
15
  jobs:
16
- build:
17
- name: Build Plugin
16
+ release:
17
+ name: Semantic Release
18
18
  runs-on: ubuntu-latest
19
- outputs:
20
- version: ${{ steps.version.outputs.version }}
21
-
22
19
  steps:
23
- - name: Checkout repository
24
- uses: actions/checkout@v4
25
-
26
- - name: Read version from package.json
27
- id: version
28
- run: |
29
- VERSION=$(node -p "require('./package.json').version")
30
- echo "version=$VERSION" >> $GITHUB_OUTPUT
31
-
32
- - name: Set up Node.js
33
- uses: actions/setup-node@v4
34
- with:
35
- node-version: 20
36
- registry-url: https://registry.npmjs.org
37
-
38
- - name: Install dependencies
39
- run: npm ci
40
-
41
- - name: Build TypeScript
42
- run: npm run build
43
-
44
- - name: List dist contents before upload
45
- run: ls -l dist
46
-
47
- - name: Upload build artifact
48
- uses: actions/upload-artifact@v4
20
+ - uses: actions/checkout@v4
49
21
  with:
50
- name: plugin-dist
51
- path: dist
22
+ fetch-depth: 0
52
23
 
53
- publish:
54
- name: Publish to npm
55
- runs-on: ubuntu-latest
56
- needs: build
57
-
58
- steps:
59
- - name: Checkout repository
60
- uses: actions/checkout@v4
61
-
62
- - name: Set up Node.js
24
+ - name: Setup Node.js with npm auth
63
25
  uses: actions/setup-node@v4
64
26
  with:
65
27
  node-version: 20
66
- registry-url: https://registry.npmjs.org
67
-
68
- - name: Download build artifact
69
- uses: actions/download-artifact@v4
70
- with:
71
- name: plugin-dist
72
- path: dist
73
-
74
- - name: Authenticate with npm
75
- run: npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
76
-
77
- - name: Determine npm tag
78
- id: tag
79
- run: |
80
- BRANCH="${GITHUB_REF##*/}"
81
- if [ "$BRANCH" = "stable" ]; then
82
- echo "tag=latest" >> $GITHUB_OUTPUT
83
- else
84
- echo "tag=$BRANCH" >> $GITHUB_OUTPUT
85
- fi
28
+ registry-url: https://registry.npmjs.org/
29
+ scope: "@seyuna"
30
+ always-auth: true
31
+ env:
32
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
86
33
 
87
34
  - name: Install dependencies
88
35
  run: npm ci
89
36
 
90
- - name: Check if version exists on npm
91
- id: version_check
92
- run: |
93
- PKG_NAME=$(node -p "require('./package.json').name")
94
- PKG_VERSION=$(node -p "require('./package.json').version")
95
- if npm view "$PKG_NAME@$PKG_VERSION" > /dev/null 2>&1; then
96
- echo "exists=true" >> $GITHUB_OUTPUT
97
- else
98
- echo "exists=false" >> $GITHUB_OUTPUT
99
- fi
100
-
101
- - name: Publish package
102
- if: steps.version_check.outputs.exists != 'true'
103
- run: npm publish --access public --tag ${{ steps.tag.outputs.tag }}
104
-
105
- release:
106
- name: Create GitHub Release
107
- runs-on: ubuntu-latest
108
- needs: [build, publish]
109
-
110
- steps:
111
- - name: Checkout repository
112
- uses: actions/checkout@v4
113
-
114
- - name: Download build artifact
115
- uses: actions/download-artifact@v4
116
- with:
117
- name: plugin-dist
118
- path: dist
119
-
120
- - name: Generate changelog
121
- run: |
122
- last_tag=$(git tag --sort=-creatordate | grep '^v' | grep -v "v${{ needs.build.outputs.version }}" | head -n 1)
123
-
124
- if [ -z "$last_tag" ]; then
125
- changelog=$(git log --merges --pretty=format:"* %s (%h)")
126
- else
127
- changelog=$(git log "$last_tag"..HEAD --merges --pretty=format:"* %s (%h)")
128
- fi
129
-
130
- {
131
- echo "Automated release for version v${{ needs.build.outputs.version }}"
132
- echo ""
133
- echo "## Changes"
134
- echo ""
135
- echo "$changelog"
136
- } > RELEASE_NOTES.md
137
-
138
- - name: Check if GitHub release exists
139
- id: release_check
140
- run: |
141
- if gh release view "v${{ needs.build.outputs.version }}" >/dev/null 2>&1; then
142
- echo "exists=true" >> $GITHUB_OUTPUT
143
- else
144
- echo "exists=false" >> $GITHUB_OUTPUT
145
- fi
146
- env:
147
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
148
-
149
- - name: Create GitHub Release
150
- if: steps.release_check.outputs.exists != 'true'
151
- run: |
152
- gh release create "v${{ needs.build.outputs.version }}" \
153
- --title "v${{ needs.build.outputs.version }}" \
154
- --notes-file RELEASE_NOTES.md
37
+ - name: Run Semantic Release
155
38
  env:
156
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
41
+ run: npx semantic-release
package/CHANGELOG.md ADDED
@@ -0,0 +1,79 @@
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
+
74
+ # 1.0.0-canary.1 (2025-08-07)
75
+
76
+
77
+ ### Bug Fixes
78
+
79
+ * Clone all child nodes inside [@rules](https://github.com/rules) to prevent them from being detached ([6697d34](https://github.com/seyuna-corp/seyuna-postcss/commit/6697d34e604b1de1f319eee060604c4efc201251))
@@ -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,12 +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
- // Create a new nested rule: [data-mode="dark"] & { ... }
7
- 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({
8
39
  selector: `[data-mode="dark"] &`,
9
- nodes: atRule.nodes, // move children inside
10
40
  });
11
- // Replace @dark atRule with the new nested rule
12
- atRule.replaceWith(nestedRule);
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"] &`,
56
+ });
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);
13
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,12 +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
- // Create a new nested rule: [data-mode="light"] & { ... }
7
- 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({
8
39
  selector: `[data-mode="light"] &`,
9
- nodes: atRule.nodes, // move children inside
10
40
  });
11
- // Replace @light atRule with the new nested rule
12
- atRule.replaceWith(nestedRule);
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"] &`,
56
+ });
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);
13
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,12 +1,11 @@
1
1
  {
2
2
  "name": "@seyuna/postcss",
3
- "version": "0.0.1-dev.9",
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",
7
7
  "scripts": {
8
- "build": "tsc",
9
- "prepare": "npm run build"
8
+ "build": "tsc"
10
9
  },
11
10
  "keywords": [
12
11
  "postcss",
@@ -14,12 +13,24 @@
14
13
  "seyuna"
15
14
  ],
16
15
  "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/seyuna-corp/seyuna-postcss.git"
19
+ },
17
20
  "peerDependencies": {
18
21
  "postcss": "^8.5.6"
19
22
  },
20
23
  "devDependencies": {
24
+ "@semantic-release/changelog": "^6.0.3",
25
+ "@semantic-release/commit-analyzer": "^13.0.1",
26
+ "@semantic-release/exec": "^7.1.0",
27
+ "@semantic-release/git": "^10.0.1",
28
+ "@semantic-release/github": "^11.0.3",
29
+ "@semantic-release/npm": "^12.0.2",
30
+ "@semantic-release/release-notes-generator": "^14.0.3",
21
31
  "@types/node": "^20.0.0",
22
- "typescript": "^5.0.0",
23
- "postcss": "^8.5.6"
32
+ "postcss": "^8.5.6",
33
+ "semantic-release": "^24.2.7",
34
+ "typescript": "^5.0.0"
24
35
  }
25
36
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @type {import('semantic-release').GlobalConfig}
3
+ */
4
+ export default {
5
+ branches: [{ name: "canary", prerelease: "canary" }, "main"],
6
+ plugins: [
7
+ "@semantic-release/commit-analyzer",
8
+ "@semantic-release/release-notes-generator",
9
+ ["@semantic-release/changelog", { changelogFile: "CHANGELOG.md" }],
10
+ [
11
+ "@semantic-release/exec", // Runs build before publishing
12
+ {
13
+ prepareCmd: "npm run build",
14
+ },
15
+ ],
16
+ [
17
+ "@semantic-release/npm", // Publishes to npm
18
+ {
19
+ npmPublish: true,
20
+ },
21
+ ],
22
+ [
23
+ "@semantic-release/git", // Commits updated files
24
+ {
25
+ assets: ["package.json", "package-lock.json", "CHANGELOG.md"],
26
+ message:
27
+ "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}",
28
+ },
29
+ ],
30
+ "@semantic-release/github", // Creates GitHub release
31
+ ],
32
+ };
@@ -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,12 +1,69 @@
1
- import { Rule, type AtRule } 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
- // Create a new nested rule: [data-mode="dark"] & { ... }
5
- 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({
6
39
  selector: `[data-mode="dark"] &`,
7
- nodes: atRule.nodes, // move children inside
8
40
  });
41
+ clonedNodes.forEach((node) => darkRule.append(node.clone()));
42
+
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"] &`,
58
+ });
59
+ clonedNodes.forEach((node) => systemRule.append(node.clone()));
60
+
61
+ // Nest `[data-mode="system"]` inside the @media block
62
+ mediaAtRule.append(systemRule);
9
63
 
10
- // Replace @dark atRule with the new nested rule
11
- 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);
12
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,12 +1,69 @@
1
- import { Rule, type AtRule } 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
- // Create a new nested rule: [data-mode="light"] & { ... }
5
- 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({
6
39
  selector: `[data-mode="light"] &`,
7
- nodes: atRule.nodes, // move children inside
8
40
  });
41
+ clonedNodes.forEach((node) => lightRule.append(node.clone()));
42
+
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"] &`,
58
+ });
59
+ clonedNodes.forEach((node) => systemRule.append(node.clone()));
60
+
61
+ // Nest `[data-mode="system"]` inside the @media block
62
+ mediaAtRule.append(systemRule);
9
63
 
10
- // Replace @light atRule with the new nested rule
11
- 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);
12
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
+ };