@seyuna/postcss 1.0.0-canary.27 → 1.0.0-canary.28

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,10 @@
1
+ # [1.0.0-canary.28](https://github.com/seyuna-corp/seyuna-postcss/compare/v1.0.0-canary.27...v1.0.0-canary.28) (2026-01-21)
2
+
3
+
4
+ ### Features
5
+
6
+ * move configuration from JSON to in-CSS [@config](https://github.com/config) at-rule ([964eabd](https://github.com/seyuna-corp/seyuna-postcss/commit/964eabdef0b1f63d600d0d1f2171387c2932f0b2))
7
+
1
8
  # [1.0.0-canary.27](https://github.com/seyuna-corp/seyuna-postcss/compare/v1.0.0-canary.26...v1.0.0-canary.27) (2026-01-20)
2
9
 
3
10
 
package/README.md CHANGED
@@ -3,15 +3,17 @@
3
3
  [![NPM Version](https://img.shields.io/npm/v/@seyuna/postcss.svg)](https://www.npmjs.com/package/@seyuna/postcss)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- > Build interfaces that transcend resolution.
6
+ > Design once. Deploy everywhere. Light and dark, effortlessly.
7
7
 
8
8
  ---
9
9
 
10
- ## What is this
10
+ ## Philosophy
11
11
 
12
- Seyuna PostCSS is the engine behind the Seyuna design system. It compiles a declarative color configuration into fully functional CSS, handles theme switching, and provides a suite of utilities that make authoring stylesheets faster and more intuitive.
12
+ Seyuna PostCSS is not just another CSS preprocessor. It's a design system compiler that embraces four core principles:
13
13
 
14
- The core philosophy is simple: **design once at 1080p, deploy everywhere**. Seyuna UI handles upscaling automatically through viewport-relative font sizing. The result is pixel-perfect consistency from a laptop to a 4K display without breakpoints, without media queries, without you lifting a finger.
14
+ ### 1. Design Once at 1080p, Deploy Everywhere
15
+
16
+ Traditional responsive design means designing the same component three or four times for different screen sizes. Seyuna takes a different approach: viewport-relative scaling.
15
17
 
16
18
  ```css
17
19
  html {
@@ -19,150 +21,203 @@ html {
19
21
  }
20
22
  ```
21
23
 
22
- This single line is the foundation. At 1920px viewport width, `0.833vw` equals exactly `16px`. Everything scales proportionally. No more designing three versions of every component.
24
+ At 1920px viewport width, `0.833vw` equals exactly `16px`. On a 4K display, everything scales proportionally. Your 1080p design becomes a 4K design automatically. No breakpoints. No media queries. No redesigning.
23
25
 
24
- ---
26
+ ### 2. Seamless Dark / Light Mode
25
27
 
26
- ## Installation
28
+ Theme switching shouldn't require duplicating your entire stylesheet. With Seyuna, colors are defined once and adapt automatically:
27
29
 
28
- ```bash
29
- npm install @seyuna/postcss postcss-import postcss-advanced-variables postcss-preset-env --save-dev
30
+ ```css
31
+ .button {
32
+ background: SeyunaStandardColor(primary);
33
+ }
34
+ ```
35
+
36
+ When the user switches from light to dark mode, every standard color updates its lightness and chroma automatically. No conditional classes. No theme-specific stylesheets.
37
+
38
+ ### 3. Cleaner Codebase
39
+
40
+ CSS shouldn't be bloated with repetitive color definitions and hardcoded values. Seyuna keeps your stylesheets lean:
41
+
42
+ ```css
43
+ /* Instead of this */
44
+ .card {
45
+ background: rgba(66, 133, 244, 0.5);
46
+ border: 1px solid rgba(66, 133, 244, 0.8);
47
+ }
48
+
49
+ /* Write this */
50
+ .card {
51
+ background: SeyunaAlpha(primary, 0.5);
52
+ border: 1px solid SeyunaAlpha(primary, 0.8);
53
+ }
54
+ ```
55
+
56
+ Your color palette lives in `seyuna.json`. Your stylesheets reference it by name. Change the primary color once, and it updates everywhere.
57
+
58
+ ### 4. Seamless Color Functions
59
+
60
+ Need a hover state that's slightly lighter? A disabled state that's more transparent? Seyuna provides color manipulation functions that work with your design tokens:
61
+
62
+ ```css
63
+ .button {
64
+ background: SeyunaStandardColor(primary);
65
+ }
66
+
67
+ .button:hover {
68
+ background: SeyunaLighten(primary, 0.1);
69
+ }
70
+
71
+ .button:disabled {
72
+ background: SeyunaAlpha(primary, 0.3);
73
+ }
30
74
  ```
31
75
 
76
+ These functions compile to pure CSS using `calc()` and CSS custom properties. No JavaScript runtime. No color processing at page load.
77
+
32
78
  ---
33
79
 
34
- ## Configuration
80
+ ## Quick Start
81
+
82
+ ### Installation
83
+
84
+ ```bash
85
+ npm install @seyuna/postcss postcss --save-dev
86
+ ```
35
87
 
36
- Create a `postcss.config.js` at your project root:
88
+ ### PostCSS Configuration
37
89
 
38
90
  ```javascript
39
- import seyunaPostcss from '@seyuna/postcss';
40
- import postcssImport from 'postcss-import';
41
- import postcssAdvancedVariables from 'postcss-advanced-variables';
42
- import postcssPresetEnv from 'postcss-preset-env';
91
+ // postcss.config.js
92
+ import seyunaPostcss from "@seyuna/postcss";
43
93
 
44
94
  export default {
45
95
  plugins: [
46
96
  seyunaPostcss({
47
- configPath: 'seyuna.json',
48
- modeAttribute: 'data-mode',
49
- }),
50
- postcssImport,
51
- postcssAdvancedVariables,
52
- postcssPresetEnv({
53
- stage: 3,
54
- features: {
55
- 'nesting-rules': true,
56
- },
97
+ modeAttribute: "data-mode",
57
98
  }),
58
99
  ],
59
100
  };
60
101
  ```
61
102
 
62
- The plugin reads from `seyuna.json` at build time. This file is your single source of truth for colors, themes, and any other design tokens you want accessible in CSS.
103
+ ### CSS Setup
104
+
105
+ Configure your design tokens directly in your CSS using the `@config "seyuna"` at-rule, then import the design system:
106
+
107
+ ```css
108
+ @config "seyuna" {
109
+ /* Define hues (0-360) */
110
+ --hue-primary: 240;
111
+ --hue-secondary: 30;
112
+
113
+ /* Define fixed colors (lightness chroma hue) */
114
+ --color-white: 1 0 0;
115
+ --color-black: 0 0 0;
116
+
117
+ /* Configure theme modes */
118
+ --light-lightness: 0.66;
119
+ --light-chroma: 0.26;
120
+ --light-background: 1 0 0;
121
+ --light-text: 0 0 0;
122
+
123
+ --dark-lightness: 0.66;
124
+ --dark-chroma: 0.26;
125
+ --dark-background: 0 0 0;
126
+ --dark-text: 1 0 0;
127
+ }
128
+
129
+ @import "seyuna";
130
+
131
+ /* Your styles here */
132
+ ```
133
+
134
+ The `@config` rule is parsed and removed at build time. It populates:
135
+
136
+ - CSS custom properties for all your colors and hues
137
+ - Theme mode selectors (`[data-mode="light"]`, `[data-mode="dark"]`)
138
+ - System preference media queries
139
+ - A modern CSS reset with cascade layers
63
140
 
64
141
  ---
65
142
 
66
- ## The Color Model
143
+ ## Color System
67
144
 
68
- Seyuna operates entirely in the OKLCH color space. Unlike HSL or RGB, OKLCH is perceptually uniform. A lightness of `0.66` looks equally bright across all hues. This makes it possible to build harmonious palettes programmatically.
145
+ Seyuna operates entirely in the **OKLCH color space**.
69
146
 
70
147
  ### Standard Colors
71
148
 
72
- Standard colors are hue-only tokens. They inherit the global `--lightness` and `--chroma` values from the current theme mode. When the user switches from light to dark, every standard color adapts automatically.
149
+ Standard colors define only a hue angle in the config and inherit theme lightness.
73
150
 
74
151
  ```css
75
- .button {
76
- background: sc(primary);
152
+ @config "seyuna" {
153
+ --hue-primary: 240;
77
154
  }
78
- ```
79
-
80
- Compiles to:
81
155
 
82
- ```css
83
- .button {
84
- background: oklch(var(--lightness) var(--chroma) var(--primary-hue) / 1);
156
+ .badge {
157
+ background: SeyunaStandardColor(primary);
85
158
  }
86
159
  ```
87
160
 
88
161
  ### Fixed Colors
89
162
 
90
- Fixed colors are absolute. They define their own lightness, chroma, and hue. Use these for colors that must remain constant regardless of theme, like brand logos or semantic status indicators.
163
+ Fixed colors have explicit lightness, chroma, and hue values.
91
164
 
92
165
  ```css
93
- .badge {
94
- background: fc(white);
166
+ @config "seyuna" {
167
+ --color-white: 1 0 0;
95
168
  }
96
- ```
97
-
98
- Compiles to:
99
169
 
100
- ```css
101
- .badge {
102
- background: oklch(var(--white-lightness) var(--white-chroma) var(--white-hue) / 1);
170
+ .logo {
171
+ color: SeyunaFixedColor(white);
103
172
  }
104
173
  ```
105
174
 
106
175
  ---
107
176
 
108
- ## Color Manipulation
177
+ ## Color Functions
109
178
 
110
- All color functions accept a color name from your configuration.
179
+ | Function | Purpose | Example |
180
+ | :----------------------------- | :---------------------------------------- | :----------------------------- |
181
+ | `SeyunaStandardColor(name)` | Standard color with theme-aware lightness | `SeyunaStandardColor(primary)` |
182
+ | `SeyunaFixedColor(name)` | Fixed color with explicit values | `SeyunaFixedColor(white)` |
183
+ | `SeyunaAlpha(color, value)` | Adjusts opacity | `SeyunaAlpha(primary, 0.5)` |
184
+ | `SeyunaLighten(color, amount)` | Increases lightness | `SeyunaLighten(primary, 0.1)` |
185
+ | `SeyunaDarken(color, amount)` | Decreases lightness | `SeyunaDarken(primary, 0.1)` |
186
+ | `SeyunaContrast(color)` | Returns black or white for readability | `SeyunaContrast(surface)` |
111
187
 
112
- | Function | Purpose | Example |
113
- |:----------------------|:-----------------------------------------|:----------------------------|
114
- | `alpha(color, value)` | Adjusts opacity | `alpha(primary, 0.5)` |
115
- | `lighten(color, amt)` | Increases lightness by `amt` | `lighten(primary, 0.1)` |
116
- | `darken(color, amt)` | Decreases lightness by `amt` | `darken(primary, 0.1)` |
117
- | `contrast(color)` | Returns black or white based on lightness| `color: contrast(surface);` |
118
-
119
- The `contrast()` function uses a dynamic CSS calculation internally. It does not bake in a static value. If `--surface-lightness` changes at runtime, the contrast color updates with it.
188
+ The `SeyunaContrast()` function uses dynamic CSS calculations. If the underlying color changes at runtime, the contrast color updates automatically.
120
189
 
121
190
  ---
122
191
 
123
192
  ## Theme Switching
124
193
 
125
- Seyuna supports three modes: `light`, `dark`, and `system`. The system mode respects `prefers-color-scheme`. All switching is handled via the `data-mode` attribute on your root element.
194
+ Seyuna supports three modes: `light`, `dark`, and `system`. Control the active mode via the `data-mode` attribute:
126
195
 
127
196
  ```html
128
- <html data-mode="system">
197
+ <html data-mode="system"></html>
129
198
  ```
130
199
 
131
- The compiled CSS contains rules for all three states:
200
+ ### In-CSS Theme Overrides
132
201
 
133
- ```css
134
- [data-mode="light"] {
135
- --lightness: 0.66;
136
- --chroma: 0.26;
137
- }
138
-
139
- @media (prefers-color-scheme: dark) {
140
- [data-mode="system"] {
141
- --lightness: 0.66;
142
- --chroma: 0.26;
143
- }
144
- }
145
- ```
146
-
147
- ### In-CSS Overrides
148
-
149
- Need a component to look different in dark mode? Use the `@dark` and `@light` at-rules directly in your stylesheet:
202
+ Need a component to look different in dark mode? Use the `@dark` and `@light` at-rules:
150
203
 
151
204
  ```css
152
205
  .card {
153
- background: fc(white);
206
+ background: SeyunaFixedColor(white);
154
207
 
155
208
  @dark {
156
- background: fc(black);
209
+ background: SeyunaFixedColor(black);
157
210
  }
158
211
  }
159
212
  ```
160
213
 
214
+ Compiles to rules that target both explicit mode selection and system preferences.
215
+
161
216
  ---
162
217
 
163
218
  ## Container Queries
164
219
 
165
- Seyuna ships with shorthand breakpoints that compile to CSS Container Queries instead of traditional media queries. This means components respond to their container size, not the viewport.
220
+ Seyuna includes shorthand breakpoints that compile to CSS Container Queries:
166
221
 
167
222
  ```css
168
223
  .grid {
@@ -179,95 +234,76 @@ Seyuna ships with shorthand breakpoints that compile to CSS Container Queries in
179
234
  }
180
235
  ```
181
236
 
182
- Available breakpoints:
183
-
184
237
  | At-Rule | Container Width |
185
- |:--------|:----------------|
238
+ | :------ | :-------------- |
186
239
  | `@xs` | 20rem |
187
- | `@sm` | 30rem |
240
+ | `@sm` | 40rem |
188
241
  | `@md` | 48rem |
189
- | `@lg` | 62rem |
242
+ | `@lg` | 64rem |
190
243
  | `@xl` | 80rem |
191
244
  | `@2xl` | 96rem |
192
245
 
193
- ---
194
-
195
- ## Configuration Reference
196
-
197
- ```json
198
- {
199
- "ui": {
200
- "theme": {
201
- "hues": {
202
- "primary": 240,
203
- "secondary": 30,
204
- "accent": 150
205
- },
206
- "colors": {
207
- "white": { "lightness": 1, "chroma": 0, "hue": 0 },
208
- "black": { "lightness": 0, "chroma": 0, "hue": 0 }
209
- },
210
- "light": {
211
- "lightness": 0.66,
212
- "chroma": 0.26,
213
- "background": { "lightness": 1, "chroma": 0, "hue": 0 },
214
- "text": { "lightness": 0, "chroma": 0, "hue": 0 }
215
- },
216
- "dark": {
217
- "lightness": 0.66,
218
- "chroma": 0.26,
219
- "background": { "lightness": 0, "chroma": 0, "hue": 0 },
220
- "text": { "lightness": 1, "chroma": 0, "hue": 0 }
221
- }
222
- },
223
- "mode": "system",
224
- "output_dir": "src/styles"
225
- }
226
- }
227
- ```
228
-
229
- `hues` defines standard colors. Only the hue angle is specified because lightness and chroma come from the theme mode.
230
-
231
- `colors` defines fixed colors with explicit lightness, chroma, and hue values.
232
-
233
- `light` and `dark` set the global lightness and chroma for standard colors, plus any mode-specific fixed colors like background and text.
246
+ Components respond to their container size, not the viewport. True component-based responsive design.
234
247
 
235
248
  ---
236
249
 
237
250
  ## Palette Iteration
238
251
 
239
- Generate utility classes by iterating over your entire color configuration:
252
+ Generate utility classes by iterating over your color configuration:
240
253
 
241
254
  ```css
242
255
  @each-standard-color {
243
256
  .bg-{name} {
244
- background-color: sc({name});
257
+ background-color: SeyunaStandardColor({name});
245
258
  }
246
259
  }
247
260
 
248
261
  @each-fixed-color {
249
262
  .text-{name} {
250
- color: fc({name});
263
+ color: SeyunaFixedColor({name});
251
264
  }
252
265
  }
253
266
  ```
254
267
 
255
268
  ---
256
269
 
257
- ## The Reset Layer
270
+ ## Configuration Properties
271
+
272
+ Within the `@config "seyuna"` block, you can use the following property prefixes:
273
+
274
+ | Prefix | Description | Example |
275
+ | :---------- | :--------------------------------------------------------------- | :---------------------------- |
276
+ | `--hue-*` | Defines a hue angle for a standard color | `--hue-primary: 240;` |
277
+ | `--color-*` | Defines a fixed color (L C H) | `--color-brand: 0.6 0.2 120;` |
278
+ | `--light-*` | Configures the light theme (lightness, chroma, background, text) | `--light-lightness: 0.8;` |
279
+ | `--dark-*` | Configures the dark theme | `--dark-background: 0 0 0;` |
280
+
281
+ For colors and background/text, use a space-separated list of values: `lightness chroma hue`.
282
+
283
+ ---
284
+
285
+ ## Plugin Options
258
286
 
259
- Seyuna UI generates a CSS file that includes a minimal, opinionated reset. It zeroes out margins and padding, removes default list styles, and sets sensible defaults for text rendering. The reset is applied to the `reset` layer, so your styles can override it without specificity battles.
287
+ ```javascript
288
+ seyunaPostcss({
289
+ configPath: "seyuna.json", // Path to your config file
290
+ modeAttribute: "data-mode", // Attribute for theme switching
291
+ strict: false, // Throw errors for missing colors
292
+ });
293
+ ```
260
294
 
261
- The upscaling magic happens here:
295
+ ---
296
+
297
+ ## Accessing Config Values
298
+
299
+ Use `SeyunaTheme()` to access any value from your configuration:
262
300
 
263
301
  ```css
264
- html {
265
- font-size: max(1rem, 0.833vw);
302
+ .container {
303
+ max-width: SeyunaTheme(ui.theme.breakpoints.lg);
266
304
  }
267
305
  ```
268
306
 
269
- At 1920px, the computed font size is 16px. At 2560px, it becomes roughly 21px. Every `rem` value in your stylesheet scales accordingly. Design at 1080p. Ship at any resolution.
270
-
271
307
  ---
272
308
 
273
309
  ## License
@@ -0,0 +1,6 @@
1
+ import { PluginContext } from "../types.js";
2
+ /**
3
+ * Handler for @config "seyuna"
4
+ * Parses the configuration defined directly in CSS.
5
+ */
6
+ export declare function handleConfig(atRule: any, context: PluginContext): void;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Handler for @config "seyuna"
3
+ * Parses the configuration defined directly in CSS.
4
+ */
5
+ export function handleConfig(atRule, context) {
6
+ const params = atRule.params.replace(/['"]/g, "");
7
+ if (params === "seyuna") {
8
+ const { config } = context;
9
+ if (!config.ui) {
10
+ config.ui = {
11
+ theme: {
12
+ hues: {},
13
+ colors: {},
14
+ light: {
15
+ chroma: 0,
16
+ lightness: 0,
17
+ background: { lightness: 0, chroma: 0, hue: 0 },
18
+ text: { lightness: 0, chroma: 0, hue: 0 },
19
+ colors: {},
20
+ },
21
+ dark: {
22
+ chroma: 0,
23
+ lightness: 0,
24
+ background: { lightness: 0, chroma: 0, hue: 0 },
25
+ text: { lightness: 0, chroma: 0, hue: 0 },
26
+ colors: {},
27
+ },
28
+ },
29
+ };
30
+ }
31
+ const theme = config.ui.theme;
32
+ atRule.walkDecls((decl) => {
33
+ const prop = decl.prop;
34
+ const value = decl.value;
35
+ // Hues: --hue-alpha: 0;
36
+ if (prop.startsWith("--hue-")) {
37
+ const name = prop.replace("--hue-", "");
38
+ theme.hues[name] = parseFloat(value);
39
+ }
40
+ // Fixed Colors: --color-primary: 0.66 0.26 240;
41
+ else if (prop.startsWith("--color-")) {
42
+ const name = prop.replace("--color-", "");
43
+ const [l, c, h] = value.split(/\s+/).map(parseFloat);
44
+ theme.colors[name] = { lightness: l, chroma: c, hue: h };
45
+ }
46
+ // Theme Palettes: --light-lightness: 0.66;
47
+ else if (prop.startsWith("--light-") || prop.startsWith("--dark-")) {
48
+ const isLight = prop.startsWith("--light-");
49
+ const palette = isLight ? theme.light : theme.dark;
50
+ const key = prop.replace(isLight ? "--light-" : "--dark-", "");
51
+ if (key === "lightness")
52
+ palette.lightness = parseFloat(value);
53
+ else if (key === "chroma")
54
+ palette.chroma = parseFloat(value);
55
+ else if (key === "background" || key === "text") {
56
+ const [l, c, h] = value.split(/\s+/).map(parseFloat);
57
+ palette[key] = { lightness: l, chroma: c, hue: h };
58
+ }
59
+ else {
60
+ // Custom colors in palette: --light-surface: 1 0 0;
61
+ const [l, c, h] = value.split(/\s+/).map(parseFloat);
62
+ palette.colors[key] = { lightness: l, chroma: c, hue: h };
63
+ }
64
+ }
65
+ });
66
+ // Remove the at-rule from CSS output
67
+ atRule.remove();
68
+ }
69
+ }
@@ -7,9 +7,6 @@ import { fileURLToPath } from "url";
7
7
  import postcss from "postcss";
8
8
  const __filename = fileURLToPath(import.meta.url);
9
9
  const __dirname = path.dirname(__filename);
10
- const STANDARD_HUES = [
11
- 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta', 'theta', 'iota', 'kappa', 'lambda', 'mu', 'nu', 'xi', 'omicron', 'pi', 'rho', 'sigma', 'tau', 'upsilon', 'phi', 'chi', 'psi', 'omega'
12
- ];
13
10
  /**
14
11
  * Handler for @import "seyuna"
15
12
  * Injects the core Seyuna Design System variables and base styles
@@ -48,14 +45,12 @@ export function handleImport(atRule, context) {
48
45
  }
49
46
  // 2. Global Hues and Base Colors (:root)
50
47
  const rootRule = new Rule({ selector: ":root", source: atRule.source });
51
- // Process all hues from config plus defaults
52
- const allHues = new Set([...STANDARD_HUES, ...Object.keys(theme.hues || {})]);
53
- allHues.forEach((name) => {
54
- const i = STANDARD_HUES.indexOf(name);
55
- const defaultValue = i !== -1 ? i * 15 : 0;
56
- const value = (theme.hues && theme.hues[name] !== undefined) ? theme.hues[name] : defaultValue;
57
- rootRule.append(new Declaration({ prop: `--${name}-hue`, value: String(value) }));
58
- });
48
+ // Process all hues from config
49
+ if (theme.hues) {
50
+ for (const [name, value] of Object.entries(theme.hues)) {
51
+ rootRule.append(new Declaration({ prop: `--${name}-hue`, value: String(value) }));
52
+ }
53
+ }
59
54
  // Add shared colors from theme.colors
60
55
  if (theme.colors) {
61
56
  for (const [name, color] of Object.entries(theme.colors)) {
@@ -3,8 +3,10 @@ import { eachStandardColor, eachFixedColor } from "./color.js";
3
3
  import container from "./container.js";
4
4
  import { light, dark } from "./color-scheme.js";
5
5
  import { handleImport } from "./import.js";
6
+ import { handleConfig } from "./config.js";
6
7
  // Ordered array ensures execution order
7
8
  export const atRuleHandlers = [
9
+ { name: "config", handler: handleConfig },
8
10
  { name: "import", handler: handleImport },
9
11
  { name: "each-standard-color", handler: eachStandardColor },
10
12
  { name: "each-fixed-color", handler: eachFixedColor },
package/dist/config.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { SeyunaConfig, PluginOptions } from './types.js';
2
- export type { PluginOptions, PluginContext, FunctionMap } from './types.js';
1
+ import { SeyunaConfig, PluginOptions } from "./types.js";
2
+ export type { PluginOptions, PluginContext, FunctionMap } from "./types.js";
3
3
  export declare function loadConfig(options?: PluginOptions): {
4
4
  config: SeyunaConfig;
5
5
  options: Required<PluginOptions>;
package/dist/config.js CHANGED
@@ -1,87 +1,42 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
1
  const DEFAULT_OPTIONS = {
4
- configPath: 'seyuna.json',
5
- modeAttribute: 'data-mode',
2
+ configPath: "seyuna.json",
3
+ modeAttribute: "data-mode",
6
4
  strict: false,
7
5
  config: undefined,
8
6
  functions: undefined,
9
7
  };
10
- let cachedConfig = null;
11
- let cachedConfigPath = null;
12
8
  export function loadConfig(options = {}) {
13
9
  const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
14
10
  if (mergedOptions.config) {
15
11
  return { config: mergedOptions.config, options: mergedOptions };
16
12
  }
17
- const configPath = path.resolve(process.cwd(), mergedOptions.configPath);
18
- // Cache config if it's the same path
19
- if (cachedConfig && cachedConfigPath === configPath) {
20
- return { config: cachedConfig, options: mergedOptions };
21
- }
22
- try {
23
- if (!fs.existsSync(configPath)) {
24
- if (mergedOptions.strict) {
25
- throw new Error(`Seyuna config not found at ${configPath}`);
26
- }
27
- return {
28
- config: { ui: { theme: { hues: {}, light: { colors: {} }, dark: { colors: {} } } } },
29
- options: mergedOptions,
30
- };
31
- }
32
- const data = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
33
- cachedConfig = data;
34
- cachedConfigPath = configPath;
35
- return { config: data, options: mergedOptions };
36
- }
37
- catch (error) {
38
- if (mergedOptions.strict) {
39
- throw error;
40
- }
41
- console.warn(`[Seyuna PostCSS] Warning: Failed to load config: ${error instanceof Error ? error.message : String(error)}`);
42
- return {
43
- config: { ui: { theme: { hues: {}, light: { colors: {} }, dark: { colors: {} } } } },
44
- options: mergedOptions,
45
- };
46
- }
13
+ // Return base empty config - we'll populate it from CSS
14
+ return {
15
+ config: {
16
+ ui: {
17
+ theme: {
18
+ hues: {},
19
+ colors: {},
20
+ light: {
21
+ chroma: 0,
22
+ lightness: 0,
23
+ background: { lightness: 0, chroma: 0, hue: 0 },
24
+ text: { lightness: 0, chroma: 0, hue: 0 },
25
+ colors: {},
26
+ },
27
+ dark: {
28
+ chroma: 0,
29
+ lightness: 0,
30
+ background: { lightness: 0, chroma: 0, hue: 0 },
31
+ text: { lightness: 0, chroma: 0, hue: 0 },
32
+ colors: {},
33
+ },
34
+ },
35
+ },
36
+ },
37
+ options: mergedOptions,
38
+ };
47
39
  }
48
40
  export async function loadConfigAsync(options = {}) {
49
- const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
50
- if (mergedOptions.config) {
51
- return { config: mergedOptions.config, options: mergedOptions };
52
- }
53
- const configPath = path.resolve(process.cwd(), mergedOptions.configPath);
54
- // Cache config if it's the same path
55
- if (cachedConfig && cachedConfigPath === configPath) {
56
- return { config: cachedConfig, options: mergedOptions };
57
- }
58
- try {
59
- try {
60
- await fs.promises.access(configPath);
61
- }
62
- catch {
63
- if (mergedOptions.strict) {
64
- throw new Error(`Seyuna config not found at ${configPath}`);
65
- }
66
- return {
67
- config: { ui: { theme: { hues: {}, light: { colors: {} }, dark: { colors: {} } } } },
68
- options: mergedOptions,
69
- };
70
- }
71
- const content = await fs.promises.readFile(configPath, 'utf-8');
72
- const data = JSON.parse(content);
73
- cachedConfig = data;
74
- cachedConfigPath = configPath;
75
- return { config: data, options: mergedOptions };
76
- }
77
- catch (error) {
78
- if (mergedOptions.strict) {
79
- throw error;
80
- }
81
- console.warn(`[Seyuna PostCSS] Warning: Failed to load config: ${error instanceof Error ? error.message : String(error)}`);
82
- return {
83
- config: { ui: { theme: { hues: {}, light: { colors: {} }, dark: { colors: {} } } } },
84
- options: mergedOptions,
85
- };
86
- }
41
+ return loadConfig(options);
87
42
  }
package/dist/types.d.ts CHANGED
@@ -34,14 +34,12 @@ export type Mode = "system" | "light" | "dark";
34
34
  */
35
35
  export interface UI {
36
36
  theme: Theme;
37
- mode: Mode;
38
37
  output_dir?: string;
39
38
  }
40
39
  /**
41
40
  * Root configuration structure for a Seyuna project.
42
41
  */
43
42
  export interface SeyunaConfig {
44
- license?: string;
45
43
  ui?: UI;
46
44
  }
47
45
  export type FunctionMap = Record<string, (context: PluginContext, ...args: string[]) => string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seyuna/postcss",
3
- "version": "1.0.0-canary.27",
3
+ "version": "1.0.0-canary.28",
4
4
  "description": "Seyuna UI's postcss plugin",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,76 @@
1
+ import { PluginContext } from "../types.js";
2
+
3
+ /**
4
+ * Handler for @config "seyuna"
5
+ * Parses the configuration defined directly in CSS.
6
+ */
7
+ export function handleConfig(atRule: any, context: PluginContext) {
8
+ const params = atRule.params.replace(/['"]/g, "");
9
+
10
+ if (params === "seyuna") {
11
+ const { config } = context;
12
+ if (!config.ui) {
13
+ config.ui = {
14
+ theme: {
15
+ hues: {},
16
+ colors: {},
17
+ light: {
18
+ chroma: 0,
19
+ lightness: 0,
20
+ background: { lightness: 0, chroma: 0, hue: 0 },
21
+ text: { lightness: 0, chroma: 0, hue: 0 },
22
+ colors: {},
23
+ },
24
+ dark: {
25
+ chroma: 0,
26
+ lightness: 0,
27
+ background: { lightness: 0, chroma: 0, hue: 0 },
28
+ text: { lightness: 0, chroma: 0, hue: 0 },
29
+ colors: {},
30
+ },
31
+ },
32
+ };
33
+ }
34
+
35
+ const theme = config.ui.theme;
36
+
37
+ atRule.walkDecls((decl: any) => {
38
+ const prop = decl.prop;
39
+ const value = decl.value;
40
+
41
+ // Hues: --hue-alpha: 0;
42
+ if (prop.startsWith("--hue-")) {
43
+ const name = prop.replace("--hue-", "");
44
+ theme.hues[name] = parseFloat(value);
45
+ }
46
+
47
+ // Fixed Colors: --color-primary: 0.66 0.26 240;
48
+ else if (prop.startsWith("--color-")) {
49
+ const name = prop.replace("--color-", "");
50
+ const [l, c, h] = value.split(/\s+/).map(parseFloat);
51
+ theme.colors[name] = { lightness: l, chroma: c, hue: h };
52
+ }
53
+
54
+ // Theme Palettes: --light-lightness: 0.66;
55
+ else if (prop.startsWith("--light-") || prop.startsWith("--dark-")) {
56
+ const isLight = prop.startsWith("--light-");
57
+ const palette = isLight ? theme.light : theme.dark;
58
+ const key = prop.replace(isLight ? "--light-" : "--dark-", "");
59
+
60
+ if (key === "lightness") palette.lightness = parseFloat(value);
61
+ else if (key === "chroma") palette.chroma = parseFloat(value);
62
+ else if (key === "background" || key === "text") {
63
+ const [l, c, h] = value.split(/\s+/).map(parseFloat);
64
+ (palette as any)[key] = { lightness: l, chroma: c, hue: h };
65
+ } else {
66
+ // Custom colors in palette: --light-surface: 1 0 0;
67
+ const [l, c, h] = value.split(/\s+/).map(parseFloat);
68
+ palette.colors[key] = { lightness: l, chroma: c, hue: h };
69
+ }
70
+ }
71
+ });
72
+
73
+ // Remove the at-rule from CSS output
74
+ atRule.remove();
75
+ }
76
+ }
@@ -10,10 +10,6 @@ import postcss from "postcss";
10
10
  const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = path.dirname(__filename);
12
12
 
13
- const STANDARD_HUES = [
14
- 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta', 'theta', 'iota', 'kappa', 'lambda', 'mu', 'nu', 'xi', 'omicron', 'pi', 'rho', 'sigma', 'tau', 'upsilon', 'phi', 'chi', 'psi', 'omega'
15
- ];
16
-
17
13
  /**
18
14
  * Handler for @import "seyuna"
19
15
  * Injects the core Seyuna Design System variables and base styles
@@ -56,14 +52,12 @@ export function handleImport(atRule: any, context: PluginContext) {
56
52
  // 2. Global Hues and Base Colors (:root)
57
53
  const rootRule = new Rule({ selector: ":root", source: atRule.source });
58
54
 
59
- // Process all hues from config plus defaults
60
- const allHues = new Set([...STANDARD_HUES, ...Object.keys(theme.hues || {})]);
61
- allHues.forEach((name) => {
62
- const i = STANDARD_HUES.indexOf(name);
63
- const defaultValue = i !== -1 ? i * 15 : 0;
64
- const value = (theme.hues && theme.hues[name] !== undefined) ? theme.hues[name] : defaultValue;
65
- rootRule.append(new Declaration({ prop: `--${name}-hue`, value: String(value) }));
66
- });
55
+ // Process all hues from config
56
+ if (theme.hues) {
57
+ for (const [name, value] of Object.entries(theme.hues)) {
58
+ rootRule.append(new Declaration({ prop: `--${name}-hue`, value: String(value) }));
59
+ }
60
+ }
67
61
 
68
62
  // Add shared colors from theme.colors
69
63
  if (theme.colors) {
@@ -3,6 +3,7 @@ import { eachStandardColor, eachFixedColor } from "./color.js";
3
3
  import container from "./container.js";
4
4
  import { light, dark } from "./color-scheme.js";
5
5
  import { handleImport } from "./import.js";
6
+ import { handleConfig } from "./config.js";
6
7
  import { PluginContext } from "../types.js";
7
8
 
8
9
  // Each handler has a name (matches the at-rule) and the function
@@ -13,6 +14,7 @@ export interface AtRuleHandler {
13
14
 
14
15
  // Ordered array ensures execution order
15
16
  export const atRuleHandlers: AtRuleHandler[] = [
17
+ { name: "config", handler: handleConfig },
16
18
  { name: "import", handler: handleImport },
17
19
  { name: "each-standard-color", handler: eachStandardColor },
18
20
  { name: "each-fixed-color", handler: eachFixedColor },
package/src/config.ts CHANGED
@@ -1,104 +1,63 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { SeyunaConfig, PluginOptions, PluginContext, FunctionMap } from './types.js';
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import {
4
+ SeyunaConfig,
5
+ PluginOptions,
6
+ PluginContext,
7
+ FunctionMap,
8
+ } from "./types.js";
4
9
 
5
10
  // Re-export types for backwards compatibility
6
- export type { PluginOptions, PluginContext, FunctionMap } from './types.js';
11
+ export type { PluginOptions, PluginContext, FunctionMap } from "./types.js";
7
12
 
8
13
  const DEFAULT_OPTIONS: Required<PluginOptions> = {
9
- configPath: 'seyuna.json',
10
- modeAttribute: 'data-mode',
14
+ configPath: "seyuna.json",
15
+ modeAttribute: "data-mode",
11
16
  strict: false,
12
17
  config: undefined as any,
13
18
  functions: undefined as any,
14
19
  };
15
20
 
16
- let cachedConfig: SeyunaConfig | null = null;
17
- let cachedConfigPath: string | null = null;
18
-
19
- export function loadConfig(options: PluginOptions = {}): { config: SeyunaConfig, options: Required<PluginOptions> } {
21
+ export function loadConfig(options: PluginOptions = {}): {
22
+ config: SeyunaConfig;
23
+ options: Required<PluginOptions>;
24
+ } {
20
25
  const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
21
26
 
22
27
  if (mergedOptions.config) {
23
28
  return { config: mergedOptions.config, options: mergedOptions };
24
29
  }
25
30
 
26
- const configPath = path.resolve(process.cwd(), mergedOptions.configPath);
27
-
28
- // Cache config if it's the same path
29
- if (cachedConfig && cachedConfigPath === configPath) {
30
- return { config: cachedConfig, options: mergedOptions };
31
- }
32
-
33
- try {
34
- if (!fs.existsSync(configPath)) {
35
- if (mergedOptions.strict) {
36
- throw new Error(`Seyuna config not found at ${configPath}`);
37
- }
38
- return {
39
- config: { ui: { theme: { hues: {}, light: { colors: {} }, dark: { colors: {} } } } } as any,
40
- options: mergedOptions,
41
- };
42
- }
43
-
44
- const data = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
45
- cachedConfig = data;
46
- cachedConfigPath = configPath;
47
-
48
- return { config: data, options: mergedOptions };
49
- } catch (error) {
50
- if (mergedOptions.strict) {
51
- throw error;
52
- }
53
- console.warn(`[Seyuna PostCSS] Warning: Failed to load config: ${error instanceof Error ? error.message : String(error)}`);
54
- return {
55
- config: { ui: { theme: { hues: {}, light: { colors: {} }, dark: { colors: {} } } } } as any,
56
- options: mergedOptions,
57
- };
58
- }
31
+ // Return base empty config - we'll populate it from CSS
32
+ return {
33
+ config: {
34
+ ui: {
35
+ theme: {
36
+ hues: {},
37
+ colors: {},
38
+ light: {
39
+ chroma: 0,
40
+ lightness: 0,
41
+ background: { lightness: 0, chroma: 0, hue: 0 },
42
+ text: { lightness: 0, chroma: 0, hue: 0 },
43
+ colors: {},
44
+ },
45
+ dark: {
46
+ chroma: 0,
47
+ lightness: 0,
48
+ background: { lightness: 0, chroma: 0, hue: 0 },
49
+ text: { lightness: 0, chroma: 0, hue: 0 },
50
+ colors: {},
51
+ },
52
+ },
53
+ },
54
+ } as any,
55
+ options: mergedOptions,
56
+ };
59
57
  }
60
58
 
61
- export async function loadConfigAsync(options: PluginOptions = {}): Promise<{ config: SeyunaConfig, options: Required<PluginOptions> }> {
62
- const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
63
-
64
- if (mergedOptions.config) {
65
- return { config: mergedOptions.config, options: mergedOptions };
66
- }
67
-
68
- const configPath = path.resolve(process.cwd(), mergedOptions.configPath);
69
-
70
- // Cache config if it's the same path
71
- if (cachedConfig && cachedConfigPath === configPath) {
72
- return { config: cachedConfig, options: mergedOptions };
73
- }
74
-
75
- try {
76
- try {
77
- await fs.promises.access(configPath);
78
- } catch {
79
- if (mergedOptions.strict) {
80
- throw new Error(`Seyuna config not found at ${configPath}`);
81
- }
82
- return {
83
- config: { ui: { theme: { hues: {}, light: { colors: {} }, dark: { colors: {} } } } } as any,
84
- options: mergedOptions,
85
- };
86
- }
87
-
88
- const content = await fs.promises.readFile(configPath, 'utf-8');
89
- const data = JSON.parse(content);
90
- cachedConfig = data;
91
- cachedConfigPath = configPath;
92
-
93
- return { config: data, options: mergedOptions };
94
- } catch (error) {
95
- if (mergedOptions.strict) {
96
- throw error;
97
- }
98
- console.warn(`[Seyuna PostCSS] Warning: Failed to load config: ${error instanceof Error ? error.message : String(error)}`);
99
- return {
100
- config: { ui: { theme: { hues: {}, light: { colors: {} }, dark: { colors: {} } } } } as any,
101
- options: mergedOptions,
102
- };
103
- }
59
+ export async function loadConfigAsync(
60
+ options: PluginOptions = {},
61
+ ): Promise<{ config: SeyunaConfig; options: Required<PluginOptions> }> {
62
+ return loadConfig(options);
104
63
  }
package/src/types.ts CHANGED
@@ -41,7 +41,6 @@ export type Mode = "system" | "light" | "dark";
41
41
  */
42
42
  export interface UI {
43
43
  theme: Theme;
44
- mode: Mode;
45
44
  output_dir?: string;
46
45
  }
47
46
 
@@ -49,7 +48,6 @@ export interface UI {
49
48
  * Root configuration structure for a Seyuna project.
50
49
  */
51
50
  export interface SeyunaConfig {
52
- license?: string;
53
51
  ui?: UI;
54
52
  }
55
53
 
@@ -1,35 +1,35 @@
1
- import { describe, it, expect } from 'vitest';
2
- import postcss from 'postcss';
3
- import plugin from '../src/index';
1
+ import { describe, it, expect } from "vitest";
2
+ import postcss from "postcss";
3
+ import plugin from "../src/index";
4
4
 
5
5
  const mockConfig = {
6
6
  ui: {
7
7
  theme: {
8
8
  hues: {
9
9
  primary: "200",
10
- secondary: "100"
10
+ secondary: "100",
11
11
  },
12
12
  colors: {
13
13
  white: { lightness: 1, chroma: 0, hue: 0 },
14
- black: { lightness: 0, chroma: 0, hue: 0 }
14
+ black: { lightness: 0, chroma: 0, hue: 0 },
15
15
  },
16
16
  light: {
17
17
  lightness: 0.66,
18
18
  colors: {
19
- surface: { lightness: 1, chroma: 0, hue: 0 }
20
- }
19
+ surface: { lightness: 1, chroma: 0, hue: 0 },
20
+ },
21
21
  },
22
22
  dark: {
23
23
  lightness: 0.66,
24
24
  colors: {
25
- surface: { lightness: 0, chroma: 0, hue: 0 }
26
- }
25
+ surface: { lightness: 0, chroma: 0, hue: 0 },
26
+ },
27
27
  },
28
28
  breakpoints: {
29
- tablet: "48rem"
30
- }
31
- }
32
- }
29
+ tablet: "48rem",
30
+ },
31
+ },
32
+ },
33
33
  };
34
34
 
35
35
  async function run(input: string, opts: any = {}) {
@@ -37,134 +37,174 @@ async function run(input: string, opts: any = {}) {
37
37
  return postcss([plugin(mergedOpts)]).process(input, { from: undefined });
38
38
  }
39
39
 
40
- describe('Seyuna PostCSS Plugin', () => {
41
- it('processes SeyunaStandardColor() function', async () => {
42
- const input = '.test { color: SeyunaStandardColor(primary); }';
43
- const output = '.test { color: oklch(var(--lightness) var(--chroma) var(--primary-hue) / 1)';
40
+ describe("Seyuna PostCSS Plugin", () => {
41
+ it("processes SeyunaStandardColor() function", async () => {
42
+ const input = ".test { color: SeyunaStandardColor(primary); }";
43
+ const output =
44
+ ".test { color: oklch(var(--lightness) var(--chroma) var(--primary-hue) / 1)";
44
45
  const result = await run(input);
45
46
  expect(result.css).toContain(output);
46
47
  });
47
48
 
48
- it('processes SeyunaStandardColor() with alpha', async () => {
49
- const input = '.test { color: SeyunaStandardColor(primary, 0.5); }';
50
- const output = '.test { color: oklch(var(--lightness) var(--chroma) var(--primary-hue) / 0.5)';
49
+ it("processes SeyunaStandardColor() with alpha", async () => {
50
+ const input = ".test { color: SeyunaStandardColor(primary, 0.5); }";
51
+ const output =
52
+ ".test { color: oklch(var(--lightness) var(--chroma) var(--primary-hue) / 0.5)";
51
53
  const result = await run(input);
52
54
  expect(result.css).toContain(output);
53
55
  });
54
56
 
55
- it('processes SeyunaAlpha() function with color name', async () => {
56
- const input = '.test { color: SeyunaAlpha(primary, 0.5); }';
57
- const output = 'color: oklch(var(--lightness) var(--chroma) var(--primary-hue) / 0.5)';
57
+ it("processes SeyunaAlpha() function with color name", async () => {
58
+ const input = ".test { color: SeyunaAlpha(primary, 0.5); }";
59
+ const output =
60
+ "color: oklch(var(--lightness) var(--chroma) var(--primary-hue) / 0.5)";
58
61
  const result = await run(input);
59
62
  expect(result.css).toContain(output);
60
63
  });
61
64
 
62
- it('processes SeyunaLighten() function with color name', async () => {
63
- const input = '.test { color: SeyunaLighten(primary, 0.1); }';
64
- const output = 'color: oklch(calc(var(--lightness) + 0.1) var(--chroma) var(--primary-hue) / 1)';
65
+ it("processes SeyunaLighten() function with color name", async () => {
66
+ const input = ".test { color: SeyunaLighten(primary, 0.1); }";
67
+ const output =
68
+ "color: oklch(calc(var(--lightness) + 0.1) var(--chroma) var(--primary-hue) / 1)";
65
69
  const result = await run(input);
66
70
  expect(result.css).toContain(output);
67
71
  });
68
72
 
69
- it('processes SeyunaDarken() function with color name', async () => {
70
- const input = '.test { color: SeyunaDarken(primary, 0.1); }';
71
- const output = 'color: oklch(calc(var(--lightness) - 0.1) var(--chroma) var(--primary-hue) / 1)';
73
+ it("processes SeyunaDarken() function with color name", async () => {
74
+ const input = ".test { color: SeyunaDarken(primary, 0.1); }";
75
+ const output =
76
+ "color: oklch(calc(var(--lightness) - 0.1) var(--chroma) var(--primary-hue) / 1)";
72
77
  const result = await run(input);
73
78
  expect(result.css).toContain(output);
74
79
  });
75
80
 
76
- it('processes SeyunaTheme() function', async () => {
81
+ it("processes SeyunaTheme() function", async () => {
77
82
  // We pass the config via opts for testing
78
- const input = '.test { border-radius: SeyunaTheme(ui.theme.breakpoints.tablet); }';
83
+ const input =
84
+ ".test { border-radius: SeyunaTheme(ui.theme.breakpoints.tablet); }";
79
85
  const result = await run(input, { config: mockConfig });
80
- expect(result.css).toContain('.test { border-radius: 48rem');
86
+ expect(result.css).toContain(".test { border-radius: 48rem");
81
87
  });
82
88
 
83
- it('processes @xs container at-rule', async () => {
84
- const input = '@xs { .box { color: red; } }';
89
+ it("processes @xs container at-rule", async () => {
90
+ const input = "@xs { .box { color: red; } }";
85
91
  const result = await run(input);
86
- expect(result.css).toContain('@container (min-width: 20rem)');
87
- expect(result.css).toContain('.box { color: red');
92
+ expect(result.css).toContain("@container (min-width: 20rem)");
93
+ expect(result.css).toContain(".box { color: red");
88
94
  });
89
95
 
90
- it('processes @light at-rule', async () => {
91
- const input = '@light { .test { color: black; } }';
96
+ it("processes @light at-rule", async () => {
97
+ const input = "@light { .test { color: black; } }";
92
98
  const result = await run(input);
93
- expect(result.css).toContain('@media (prefers-color-scheme: light)');
99
+ expect(result.css).toContain("@media (prefers-color-scheme: light)");
94
100
  expect(result.css).toContain('[data-mode="system"] &');
95
101
  expect(result.css).toContain('[data-mode="light"] &');
96
102
  });
97
103
 
98
- it('processes SeyunaAlpha() with standard color', async () => {
99
- const input = '.test { color: SeyunaAlpha(primary, 0.5); }';
100
- const output = 'color: oklch(var(--lightness) var(--chroma) var(--primary-hue) / 0.5)';
104
+ it("processes SeyunaAlpha() with standard color", async () => {
105
+ const input = ".test { color: SeyunaAlpha(primary, 0.5); }";
106
+ const output =
107
+ "color: oklch(var(--lightness) var(--chroma) var(--primary-hue) / 0.5)";
101
108
  const result = await run(input, { config: mockConfig });
102
109
  expect(result.css).toContain(output);
103
110
  });
104
111
 
105
- it('processes SeyunaAlpha() with fixed color', async () => {
106
- const input = '.test { color: SeyunaAlpha(surface, 0.5); }';
107
- const output = 'color: oklch(var(--surface-lightness) var(--surface-chroma) var(--surface-hue) / 0.5)';
112
+ it("processes SeyunaAlpha() with fixed color", async () => {
113
+ const input = ".test { color: SeyunaAlpha(surface, 0.5); }";
114
+ const output =
115
+ "color: oklch(var(--surface-lightness) var(--surface-chroma) var(--surface-hue) / 0.5)";
108
116
  const result = await run(input, { config: mockConfig });
109
117
  expect(result.css).toContain(output);
110
118
  });
111
119
 
112
- it('processes SeyunaContrast() with fixed color', async () => {
113
- const input = '.test { color: SeyunaContrast(surface); }';
114
- const output = 'color: oklch(calc((var(--surface-lightness) - 0.6) * -1000) 0 0)';
120
+ it("processes SeyunaContrast() with fixed color", async () => {
121
+ const input = ".test { color: SeyunaContrast(surface); }";
122
+ const output =
123
+ "color: oklch(calc((var(--surface-lightness) - 0.6) * -1000) 0 0)";
115
124
  const result = await run(input, { config: mockConfig });
116
125
  expect(result.css).toContain(output);
117
126
  });
118
127
 
119
- it('processes SeyunaContrast() with standard color', async () => {
120
- const input = '.test { color: SeyunaContrast(primary); }';
121
- const output = 'color: oklch(calc((var(--lightness) - 0.6) * -1000) 0 0)';
128
+ it("processes SeyunaContrast() with standard color", async () => {
129
+ const input = ".test { color: SeyunaContrast(primary); }";
130
+ const output = "color: oklch(calc((var(--lightness) - 0.6) * -1000) 0 0)";
122
131
  const result = await run(input, { config: mockConfig });
123
132
  expect(result.css).toContain(output);
124
133
  });
125
134
 
126
- it('throws error for unknown standard color in strict mode', async () => {
127
- const input = '.test { color: SeyunaStandardColor(unknown); }';
128
- await expect(run(input, { config: mockConfig, strict: true }))
129
- .rejects.toThrow(/Standard color 'unknown' not found/);
135
+ it("throws error for unknown standard color in strict mode", async () => {
136
+ const input = ".test { color: SeyunaStandardColor(unknown); }";
137
+ await expect(
138
+ run(input, { config: mockConfig, strict: true }),
139
+ ).rejects.toThrow(/Standard color 'unknown' not found/);
130
140
  });
131
141
 
132
- it('throws error for unknown fixed color in strict mode', async () => {
133
- const input = '.test { color: SeyunaFixedColor(unknown); }';
134
- await expect(run(input, { config: mockConfig, strict: true }))
135
- .rejects.toThrow(/Fixed color 'unknown' not found/);
142
+ it("throws error for unknown fixed color in strict mode", async () => {
143
+ const input = ".test { color: SeyunaFixedColor(unknown); }";
144
+ await expect(
145
+ run(input, { config: mockConfig, strict: true }),
146
+ ).rejects.toThrow(/Fixed color 'unknown' not found/);
136
147
  });
137
148
 
138
- it('throws error for unknown color in SeyunaAlpha() in strict mode', async () => {
139
- const input = '.test { color: SeyunaAlpha(unknown, 0.5); }';
140
- await expect(run(input, { config: mockConfig, strict: true }))
141
- .rejects.toThrow(/Color 'unknown' not found in seyuna.json/);
149
+ it("throws error for unknown color in SeyunaAlpha() in strict mode", async () => {
150
+ const input = ".test { color: SeyunaAlpha(unknown, 0.5); }";
151
+ await expect(
152
+ run(input, { config: mockConfig, strict: true }),
153
+ ).rejects.toThrow(/Color 'unknown' not found in seyuna.json/);
142
154
  });
143
155
 
144
156
  it('processes @import "seyuna"', async () => {
145
157
  const input = '@import "seyuna";';
146
158
  const result = await run(input, { config: mockConfig });
147
-
159
+
148
160
  // Check for root variables (hues)
149
- expect(result.css).toContain(':root');
150
- expect(result.css).toContain('--alpha-hue: 0');
151
- expect(result.css).toContain('--omega-hue: 345');
152
- expect(result.css).toContain('--primary-hue: 200'); // Override from mockConfig
153
-
161
+ expect(result.css).toContain(":root");
162
+ expect(result.css).toContain("--primary-hue: 200");
163
+ expect(result.css).toContain("--secondary-hue: 100");
164
+ expect(result.css).not.toContain("--alpha-hue");
165
+
154
166
  // Check for mode selectors
155
167
  expect(result.css).toContain('[data-mode="light"]');
156
168
  expect(result.css).toContain('[data-mode="dark"]');
157
- expect(result.css).toContain('--lightness: 0.66');
158
-
169
+ expect(result.css).toContain("--lightness: 0.66");
170
+
159
171
  // Check for system preference media queries
160
- expect(result.css).toContain('@media (prefers-color-scheme: light)');
161
- expect(result.css).toContain('@media (prefers-color-scheme: dark)');
172
+ expect(result.css).toContain("@media (prefers-color-scheme: light)");
173
+ expect(result.css).toContain("@media (prefers-color-scheme: dark)");
162
174
 
163
175
  // Check for base styles (reset)
164
- expect(result.css).toContain('@layer reset');
165
- expect(result.css).toContain('-webkit-text-size-adjust: none');
176
+ expect(result.css).toContain("@layer reset");
177
+ expect(result.css).toContain("-webkit-text-size-adjust: none");
166
178
 
167
179
  // Check for palette colors
168
- expect(result.css).toContain('--surface-lightness: 1');
180
+ expect(result.css).toContain("--surface-lightness: 1");
181
+ });
182
+
183
+ it('configures via @config "seyuna"', async () => {
184
+ const input = `
185
+ @config "seyuna" {
186
+ --hue-custom: 123;
187
+ --color-brand: 0.5 0.2 100;
188
+ --light-lightness: 0.8;
189
+ }
190
+ @import "seyuna";
191
+ .test {
192
+ color: SeyunaStandardColor(custom);
193
+ background: SeyunaFixedColor(brand);
194
+ }
195
+ `;
196
+ const result = await postcss([plugin({ config: undefined })]).process(
197
+ input,
198
+ { from: undefined },
199
+ );
200
+
201
+ expect(result.css).not.toContain("@config");
202
+ expect(result.css).toContain("--custom-hue: 123");
203
+ expect(result.css).toContain(
204
+ "color: oklch(var(--lightness) var(--chroma) var(--custom-hue) / 1)",
205
+ );
206
+ expect(result.css).toContain(
207
+ "background: oklch(var(--brand-lightness) var(--brand-chroma) var(--brand-hue) / 1)",
208
+ );
169
209
  });
170
210
  });