@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 +7 -0
- package/README.md +171 -135
- package/dist/at-rules/config.d.ts +6 -0
- package/dist/at-rules/config.js +69 -0
- package/dist/at-rules/import.js +6 -11
- package/dist/at-rules/index.js +2 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.js +29 -74
- package/dist/types.d.ts +0 -2
- package/package.json +1 -1
- package/src/at-rules/config.ts +76 -0
- package/src/at-rules/import.ts +6 -12
- package/src/at-rules/index.ts +2 -0
- package/src/config.ts +45 -86
- package/src/types.ts +0 -2
- package/tests/plugin.test.ts +116 -76
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
|
[](https://www.npmjs.com/package/@seyuna/postcss)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
>
|
|
6
|
+
> Design once. Deploy everywhere. Light and dark, effortlessly.
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Philosophy
|
|
11
11
|
|
|
12
|
-
Seyuna PostCSS is
|
|
12
|
+
Seyuna PostCSS is not just another CSS preprocessor. It's a design system compiler that embraces four core principles:
|
|
13
13
|
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
Theme switching shouldn't require duplicating your entire stylesheet. With Seyuna, colors are defined once and adapt automatically:
|
|
27
29
|
|
|
28
|
-
```
|
|
29
|
-
|
|
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
|
-
##
|
|
80
|
+
## Quick Start
|
|
81
|
+
|
|
82
|
+
### Installation
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm install @seyuna/postcss postcss --save-dev
|
|
86
|
+
```
|
|
35
87
|
|
|
36
|
-
|
|
88
|
+
### PostCSS Configuration
|
|
37
89
|
|
|
38
90
|
```javascript
|
|
39
|
-
|
|
40
|
-
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
143
|
+
## Color System
|
|
67
144
|
|
|
68
|
-
Seyuna operates entirely in the OKLCH color space
|
|
145
|
+
Seyuna operates entirely in the **OKLCH color space**.
|
|
69
146
|
|
|
70
147
|
### Standard Colors
|
|
71
148
|
|
|
72
|
-
Standard colors
|
|
149
|
+
Standard colors define only a hue angle in the config and inherit theme lightness.
|
|
73
150
|
|
|
74
151
|
```css
|
|
75
|
-
|
|
76
|
-
|
|
152
|
+
@config "seyuna" {
|
|
153
|
+
--hue-primary: 240;
|
|
77
154
|
}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
Compiles to:
|
|
81
155
|
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
163
|
+
Fixed colors have explicit lightness, chroma, and hue values.
|
|
91
164
|
|
|
92
165
|
```css
|
|
93
|
-
|
|
94
|
-
|
|
166
|
+
@config "seyuna" {
|
|
167
|
+
--color-white: 1 0 0;
|
|
95
168
|
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Compiles to:
|
|
99
169
|
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
177
|
+
## Color Functions
|
|
109
178
|
|
|
110
|
-
|
|
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
|
-
|
|
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`.
|
|
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
|
-
|
|
200
|
+
### In-CSS Theme Overrides
|
|
132
201
|
|
|
133
|
-
|
|
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:
|
|
206
|
+
background: SeyunaFixedColor(white);
|
|
154
207
|
|
|
155
208
|
@dark {
|
|
156
|
-
background:
|
|
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
|
|
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` |
|
|
240
|
+
| `@sm` | 40rem |
|
|
188
241
|
| `@md` | 48rem |
|
|
189
|
-
| `@lg` |
|
|
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
|
|
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:
|
|
257
|
+
background-color: SeyunaStandardColor({name});
|
|
245
258
|
}
|
|
246
259
|
}
|
|
247
260
|
|
|
248
261
|
@each-fixed-color {
|
|
249
262
|
.text-{name} {
|
|
250
|
-
color:
|
|
263
|
+
color: SeyunaFixedColor({name});
|
|
251
264
|
}
|
|
252
265
|
}
|
|
253
266
|
```
|
|
254
267
|
|
|
255
268
|
---
|
|
256
269
|
|
|
257
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Accessing Config Values
|
|
298
|
+
|
|
299
|
+
Use `SeyunaTheme()` to access any value from your configuration:
|
|
262
300
|
|
|
263
301
|
```css
|
|
264
|
-
|
|
265
|
-
|
|
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,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
|
+
}
|
package/dist/at-rules/import.js
CHANGED
|
@@ -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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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)) {
|
package/dist/at-rules/index.js
CHANGED
|
@@ -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
|
|
2
|
-
export type { PluginOptions, PluginContext, FunctionMap } from
|
|
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:
|
|
5
|
-
modeAttribute:
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
+
}
|
package/src/at-rules/import.ts
CHANGED
|
@@ -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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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) {
|
package/src/at-rules/index.ts
CHANGED
|
@@ -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
|
|
2
|
-
import path from
|
|
3
|
-
import {
|
|
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
|
|
11
|
+
export type { PluginOptions, PluginContext, FunctionMap } from "./types.js";
|
|
7
12
|
|
|
8
13
|
const DEFAULT_OPTIONS: Required<PluginOptions> = {
|
|
9
|
-
configPath:
|
|
10
|
-
modeAttribute:
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
package/tests/plugin.test.ts
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
import { describe, it, expect } from
|
|
2
|
-
import postcss from
|
|
3
|
-
import plugin from
|
|
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(
|
|
41
|
-
it(
|
|
42
|
-
const input =
|
|
43
|
-
const output =
|
|
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(
|
|
49
|
-
const input =
|
|
50
|
-
const output =
|
|
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(
|
|
56
|
-
const input =
|
|
57
|
-
const output =
|
|
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(
|
|
63
|
-
const input =
|
|
64
|
-
const output =
|
|
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(
|
|
70
|
-
const input =
|
|
71
|
-
const output =
|
|
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(
|
|
81
|
+
it("processes SeyunaTheme() function", async () => {
|
|
77
82
|
// We pass the config via opts for testing
|
|
78
|
-
const input =
|
|
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(
|
|
86
|
+
expect(result.css).toContain(".test { border-radius: 48rem");
|
|
81
87
|
});
|
|
82
88
|
|
|
83
|
-
it(
|
|
84
|
-
const input =
|
|
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(
|
|
87
|
-
expect(result.css).toContain(
|
|
92
|
+
expect(result.css).toContain("@container (min-width: 20rem)");
|
|
93
|
+
expect(result.css).toContain(".box { color: red");
|
|
88
94
|
});
|
|
89
95
|
|
|
90
|
-
it(
|
|
91
|
-
const input =
|
|
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(
|
|
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(
|
|
99
|
-
const input =
|
|
100
|
-
const output =
|
|
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(
|
|
106
|
-
const input =
|
|
107
|
-
const output =
|
|
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(
|
|
113
|
-
const input =
|
|
114
|
-
const output =
|
|
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(
|
|
120
|
-
const input =
|
|
121
|
-
const output =
|
|
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(
|
|
127
|
-
const input =
|
|
128
|
-
await expect(
|
|
129
|
-
|
|
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(
|
|
133
|
-
const input =
|
|
134
|
-
await expect(
|
|
135
|
-
|
|
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(
|
|
139
|
-
const input =
|
|
140
|
-
await expect(
|
|
141
|
-
|
|
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(
|
|
150
|
-
expect(result.css).toContain(
|
|
151
|
-
expect(result.css).toContain(
|
|
152
|
-
expect(result.css).toContain(
|
|
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(
|
|
158
|
-
|
|
169
|
+
expect(result.css).toContain("--lightness: 0.66");
|
|
170
|
+
|
|
159
171
|
// Check for system preference media queries
|
|
160
|
-
expect(result.css).toContain(
|
|
161
|
-
expect(result.css).toContain(
|
|
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(
|
|
165
|
-
expect(result.css).toContain(
|
|
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(
|
|
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
|
});
|