@almadar/ui 5.128.0 → 5.129.0

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.
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * WCAG contrast audit for theme CSS files.
4
+ * Parses each [data-theme="..."] block, resolves var() aliases and rgba
5
+ * compositing over the theme's own background, then checks the canonical
6
+ * foreground/background pairs every component relies on.
7
+ *
8
+ * Usage:
9
+ * node theme-contrast-audit.mjs [--dir <themes-dir>] [file-filter]
10
+ *
11
+ * Defaults to this package's own themes/ dir. Apps that ship their own
12
+ * [data-theme] files (e.g. apps/kflow's design-system/themes) run the same
13
+ * audit against that dir via --dir so app-local themes stay gated.
14
+ */
15
+ import { readFileSync, readdirSync } from 'node:fs';
16
+ import { resolve, dirname } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+
21
+ const args = process.argv.slice(2);
22
+ let themesDir = resolve(__dirname, '..', 'themes');
23
+ let filter;
24
+ for (let i = 0; i < args.length; i++) {
25
+ if (args[i] === '--dir') {
26
+ themesDir = resolve(process.cwd(), args[++i]);
27
+ } else {
28
+ filter = args[i];
29
+ }
30
+ }
31
+
32
+ const PAIRS = [
33
+ ['--color-foreground', '--color-background'],
34
+ ['--color-card-foreground', '--color-card'],
35
+ ['--color-primary-foreground', '--color-primary'],
36
+ ['--color-primary-foreground', '--color-primary-hover'],
37
+ ['--color-secondary-foreground', '--color-secondary'],
38
+ ['--color-secondary-foreground', '--color-secondary-hover'],
39
+ ['--color-accent-foreground', '--color-accent'],
40
+ ['--color-muted-foreground', '--color-muted'],
41
+ ['--color-muted-foreground', '--color-card'],
42
+ ['--color-error-foreground', '--color-error'],
43
+ ['--color-success-foreground', '--color-success'],
44
+ ['--color-warning-foreground', '--color-warning'],
45
+ ['--color-info-foreground', '--color-info'],
46
+ ];
47
+
48
+ function parseColor(raw) {
49
+ const s = raw.trim();
50
+ let m = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i);
51
+ if (m) {
52
+ let h = m[1];
53
+ if (h.length === 3) h = h.split('').map(c => c + c).join('');
54
+ const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
55
+ const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;
56
+ return { r, g, b, a };
57
+ }
58
+ m = s.match(/^rgba?\(([^)]+)\)$/i);
59
+ if (m) {
60
+ const parts = m[1].split(',').map(p => parseFloat(p));
61
+ return { r: parts[0], g: parts[1], b: parts[2], a: parts.length > 3 ? parts[3] : 1 };
62
+ }
63
+ return null;
64
+ }
65
+
66
+ function compositeOver(fg, bg) {
67
+ const a = fg.a + bg.a * (1 - fg.a);
68
+ if (a === 0) return { r: 0, g: 0, b: 0 };
69
+ return {
70
+ r: (fg.r * fg.a + bg.r * bg.a * (1 - fg.a)) / a,
71
+ g: (fg.g * fg.a + bg.g * bg.a * (1 - fg.a)) / a,
72
+ b: (fg.b * fg.a + bg.b * bg.a * (1 - fg.a)) / a,
73
+ };
74
+ }
75
+
76
+ function luminance({ r, g, b }) {
77
+ const f = c => {
78
+ const v = c / 255;
79
+ return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
80
+ };
81
+ return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
82
+ }
83
+
84
+ function contrast(c1, c2) {
85
+ const l1 = luminance(c1), l2 = luminance(c2);
86
+ return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
87
+ }
88
+
89
+ const files = readdirSync(themesDir).filter(f => f.endsWith('.css') && !f.startsWith('_') && f !== 'index.css' && (!filter || f === filter));
90
+ let violations = 0;
91
+
92
+ for (const file of files.sort()) {
93
+ const css = readFileSync(resolve(themesDir, file), 'utf8');
94
+ const blocks = [...css.matchAll(/\[data-theme="([^"]+)"\]\s*\{([^}]*)\}/gs)];
95
+ for (const [, themeName, body] of blocks) {
96
+ const vars = {};
97
+ for (const decl of body.matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)) {
98
+ vars[decl[1]] = decl[2].trim();
99
+ }
100
+ const resolveVar = (name, depth = 0) => {
101
+ let v = vars[name];
102
+ if (v === undefined || depth > 5) return undefined;
103
+ const ref = v.match(/^var\((--[\w-]+)\)$/);
104
+ if (ref) return resolveVar(ref[1], depth + 1);
105
+ return v;
106
+ };
107
+ const bgRaw = resolveVar('--color-background');
108
+ const bg = bgRaw ? parseColor(bgRaw) : null;
109
+ const results = [];
110
+ for (const [fgName, bgName] of PAIRS) {
111
+ const fgRaw = resolveVar(fgName);
112
+ const bgRaw2 = resolveVar(bgName);
113
+ if (!fgRaw || !bgRaw2) continue;
114
+ const fg = parseColor(fgRaw);
115
+ let bgc = parseColor(bgRaw2);
116
+ if (!fg || !bgc) continue;
117
+ // Composite translucent fills over the theme background
118
+ if (bgc.a < 1 && bg) bgc = { ...compositeOver(bgc, bg), a: 1 };
119
+ if (fg.a < 1 && bg) { const c = compositeOver(fg, bgc); fg.r = c.r; fg.g = c.g; fg.b = c.b; }
120
+ const ratio = contrast(fg, bgc);
121
+ // Text on filled surfaces needs 4.5:1 (WCAG AA normal text)
122
+ if (ratio < 4.5) results.push({ pair: `${fgName} on ${bgName}`, ratio: ratio.toFixed(2), fg: fgRaw, bg: bgRaw2 });
123
+ }
124
+ if (results.length) {
125
+ violations += results.length;
126
+ console.log(`\n${themeName} (${file})`);
127
+ for (const r of results) console.log(` FAIL ${r.ratio}:1 ${r.pair} fg=${r.fg} bg=${r.bg}`);
128
+ }
129
+ }
130
+ }
131
+ console.log(violations === 0 ? '\nAll theme pairs pass WCAG AA (4.5:1).' : `\n${violations} violation(s).`);
132
+ process.exit(violations === 0 ? 0 : 1);
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Design System Generator Types
3
+ *
4
+ * Input configuration for generating a customized design system
5
+ * that gets output to a project folder.
6
+ */
7
+
8
+ export interface DesignSystemConfig {
9
+ /** Project/client name (e.g., "winning-11") */
10
+ projectName: string;
11
+
12
+ /** Output directory for the generated design system */
13
+ outputDir: string;
14
+
15
+ /** Theme configuration */
16
+ theme: ThemeConfig;
17
+
18
+ /** Custom components not in the standard library */
19
+ customComponents?: CustomComponentConfig[];
20
+
21
+ /** Which standard components to include (default: all) */
22
+ includeComponents?: ComponentFilter;
23
+
24
+ /** Storybook configuration */
25
+ storybook?: StorybookConfig;
26
+ }
27
+
28
+ export interface ThemeConfig {
29
+ /** Theme name (used in data-design-theme attribute) */
30
+ name: string;
31
+
32
+ /** Theme display name for documentation */
33
+ displayName?: string;
34
+
35
+ /** Design metaphor/style description */
36
+ metaphor?: string;
37
+
38
+ /** Color palette */
39
+ colors: {
40
+ primary: string;
41
+ primaryHover?: string;
42
+ primaryForeground?: string;
43
+ secondary: string;
44
+ secondaryHover?: string;
45
+ secondaryForeground?: string;
46
+ accent: string;
47
+ accentForeground?: string;
48
+ muted: string;
49
+ mutedForeground?: string;
50
+ background: string;
51
+ foreground: string;
52
+ card?: string;
53
+ cardForeground?: string;
54
+ border: string;
55
+ input?: string;
56
+ ring?: string;
57
+
58
+ /** Semantic colors */
59
+ success?: string;
60
+ warning?: string;
61
+ error?: string;
62
+ info?: string;
63
+
64
+ /** Custom domain-specific colors */
65
+ custom?: Record<string, string>;
66
+ };
67
+
68
+ /** Shadow style preset or custom shadows */
69
+ shadows?: 'soft-organic' | 'minimal' | 'corporate' | 'playful' | 'dramatic' | 'soft' | ShadowConfig;
70
+
71
+ /** Border radius preset or custom radii */
72
+ radius?: 'organic' | 'gentle' | 'sharp' | 'rounded' | 'friendly' | RadiusConfig;
73
+
74
+ /** Typography overrides */
75
+ typography?: {
76
+ fontFamily?: string;
77
+ fontWeightNormal?: number;
78
+ fontWeightMedium?: number;
79
+ fontWeightBold?: number;
80
+ };
81
+
82
+ /** Dark mode variant */
83
+ darkMode?: Partial<ThemeConfig['colors']>;
84
+ }
85
+
86
+ export interface ShadowConfig {
87
+ main: string;
88
+ sm: string;
89
+ lg: string;
90
+ hover: string;
91
+ inner?: string;
92
+ active?: string;
93
+ }
94
+
95
+ export interface RadiusConfig {
96
+ sm: string;
97
+ md: string;
98
+ lg: string;
99
+ xl: string;
100
+ }
101
+
102
+ export interface CustomComponentConfig {
103
+ /** Component name (PascalCase) */
104
+ name: string;
105
+
106
+ /** Component description */
107
+ description: string;
108
+
109
+ /** Category in the component library */
110
+ category: 'atoms' | 'molecules' | 'organisms' | 'templates';
111
+
112
+ /** Component props */
113
+ props: ComponentProp[];
114
+
115
+ /** Why this component is needed */
116
+ reasoning?: string;
117
+
118
+ /** Priority for implementation */
119
+ priority?: 'high' | 'medium' | 'low';
120
+
121
+ /** Optional implementation code */
122
+ implementation?: string;
123
+
124
+ /** Optional Storybook stories */
125
+ stories?: string;
126
+ }
127
+
128
+ export interface ComponentProp {
129
+ name: string;
130
+ type: string;
131
+ description: string;
132
+ required?: boolean;
133
+ defaultValue?: string;
134
+ }
135
+
136
+ export interface ComponentFilter {
137
+ /** Include all components (default: true) */
138
+ all?: boolean;
139
+
140
+ /** Specific categories to include */
141
+ categories?: ('atoms' | 'molecules' | 'organisms' | 'templates')[];
142
+
143
+ /** Specific components to include (by name) */
144
+ include?: string[];
145
+
146
+ /** Specific components to exclude (by name) */
147
+ exclude?: string[];
148
+ }
149
+
150
+ export interface StorybookConfig {
151
+ /** Build Storybook (default: true) */
152
+ build?: boolean;
153
+
154
+ /** Include Storybook source in output (default: true) */
155
+ includeSource?: boolean;
156
+
157
+ /** Custom Storybook title */
158
+ title?: string;
159
+ }
160
+
161
+ /**
162
+ * Example configuration for Winning-11 (Garden theme)
163
+ */
164
+ export const WINNING_11_CONFIG: DesignSystemConfig = {
165
+ projectName: 'winning-11',
166
+ outputDir: '/path/to/winning-11/design-system',
167
+
168
+ theme: {
169
+ name: 'winning-11',
170
+ displayName: 'Winning 11 Garden Theme',
171
+ metaphor: 'organic growth, trust, cultivation',
172
+
173
+ colors: {
174
+ primary: '#15803d',
175
+ primaryHover: '#166534',
176
+ primaryForeground: '#ffffff',
177
+ secondary: '#fef7ed',
178
+ secondaryForeground: '#78350f',
179
+ accent: '#ca8a04',
180
+ accentForeground: '#ffffff',
181
+ muted: '#ecfccb',
182
+ mutedForeground: '#365314',
183
+ background: '#fefefe',
184
+ foreground: '#1a2e05',
185
+ border: '#bbf7d0',
186
+
187
+ success: '#22c55e',
188
+ warning: '#f59e0b',
189
+ error: '#dc2626',
190
+ info: '#0ea5e9',
191
+
192
+ custom: {
193
+ 'garden-leaf': '#22c55e',
194
+ 'garden-stem': '#15803d',
195
+ 'garden-soil': '#78350f',
196
+ 'garden-water': '#0ea5e9',
197
+ 'garden-sun': '#fbbf24',
198
+ 'trust-low': '#fbbf24',
199
+ 'trust-medium': '#22c55e',
200
+ 'trust-high': '#15803d',
201
+ 'trust-verified': '#0ea5e9',
202
+ },
203
+ },
204
+
205
+ shadows: 'soft-organic',
206
+ radius: 'organic',
207
+
208
+ darkMode: {
209
+ primary: '#4ade80',
210
+ primaryHover: '#86efac',
211
+ background: '#0f1a0a',
212
+ foreground: '#ecfccb',
213
+ card: '#14532d',
214
+ border: '#166534',
215
+ },
216
+ },
217
+
218
+ customComponents: [
219
+ {
220
+ name: 'GardenView',
221
+ description: 'Visual garden layout showing plants in a grid or organic arrangement',
222
+ category: 'organisms',
223
+ priority: 'high',
224
+ props: [
225
+ { name: 'plants', type: 'Plant[]', description: 'Array of plants to display', required: true },
226
+ { name: 'layout', type: "'grid' | 'organic'", description: 'Layout style', defaultValue: "'grid'" },
227
+ { name: 'onPlantClick', type: '(plant: Plant) => void', description: 'Plant selection handler' },
228
+ ],
229
+ reasoning: 'Core visualization for garden/agriculture domain',
230
+ },
231
+ {
232
+ name: 'PlantCard',
233
+ description: 'Card displaying a single plant with growth status and care indicators',
234
+ category: 'molecules',
235
+ priority: 'high',
236
+ props: [
237
+ { name: 'plant', type: 'Plant', description: 'Plant data to display', required: true },
238
+ { name: 'showCareIndicators', type: 'boolean', description: 'Show water/sun needs', defaultValue: 'true' },
239
+ { name: 'onClick', type: '() => void', description: 'Click handler' },
240
+ ],
241
+ reasoning: 'Essential card for displaying individual plant information',
242
+ },
243
+ {
244
+ name: 'GrowthMeter',
245
+ description: 'Progress indicator showing plant growth stage',
246
+ category: 'atoms',
247
+ priority: 'high',
248
+ props: [
249
+ { name: 'progress', type: 'number', description: 'Growth progress 0-100', required: true },
250
+ { name: 'stages', type: 'string[]', description: 'Growth stage labels' },
251
+ ],
252
+ reasoning: 'Visual feedback for growth/progress - core to garden metaphor',
253
+ },
254
+ {
255
+ name: 'TrustMeter',
256
+ description: 'Visual indicator for trust/relationship level between entities',
257
+ category: 'atoms',
258
+ priority: 'high',
259
+ props: [
260
+ { name: 'level', type: "'low' | 'medium' | 'high' | 'verified'", description: 'Trust level', required: true },
261
+ { name: 'showLabel', type: 'boolean', description: 'Show text label', defaultValue: 'true' },
262
+ ],
263
+ reasoning: 'Visualize trust between farmers and buyers in Winning-11',
264
+ },
265
+ {
266
+ name: 'CareIndicator',
267
+ description: 'Small icon indicator for plant care needs',
268
+ category: 'atoms',
269
+ priority: 'medium',
270
+ props: [
271
+ { name: 'type', type: "'water' | 'sun' | 'nutrients'", description: 'Care type', required: true },
272
+ { name: 'level', type: "'low' | 'medium' | 'high'", description: 'Need level', required: true },
273
+ ],
274
+ reasoning: 'Quick visual cues for plant care requirements',
275
+ },
276
+ ],
277
+
278
+ storybook: {
279
+ build: true,
280
+ title: 'Winning 11 Design System',
281
+ },
282
+ };
@@ -221,5 +221,5 @@ instantly regardless of theme.
221
221
  3. **No raw pixel literals in components**. All dimensional values flow through these tokens or through Tailwind utilities backed by them in `tailwind-preset.cjs`. Components that hardcode `text-[10px]` or `min-w-[44px]` are migration targets.
222
222
  4. **Icon family swap is runtime-resolved**. `--icon-family` is read by the icon resolver in `@almadar/ui`; setting the variable in a theme does not require component edits.
223
223
  5. **Wireframe and trait-wars exceptions**. Wireframe's `--easing-standard` is `linear` not the default cubic-bezier; trait-wars's iconography may remain `lucide` even if its visual personality diverges. Document any per-theme override at the top of the theme file.
224
- 6. **Contrast is gated.** Every theme's canonical foreground/fill pairs must pass WCAG AA 4.5:1 — enforced by `scripts/theme-contrast-audit.mjs` (`pnpm verify:themes`), which runs in CI and before every npm publish. A new or edited theme that fails the audit blocks the publish.
224
+ 6. **Contrast is gated.** Every theme's canonical foreground/fill pairs must pass WCAG AA 4.5:1 — enforced by `scripts/theme-contrast-audit.mjs` (`pnpm verify:themes`), which runs in CI and before every npm publish and exits non-zero on any violation. A new or edited theme that fails the audit blocks the publish. The script also accepts `--dir <themes-dir>` (`scripts/` is published) so another package can audit its own theme dir with the same rules.
225
225
  7. **No hardcoded palette colors in components.** Components must consume these tokens only (no `text-white`, `bg-gray-800`, `border-indigo-300`, `text-[#hex]`). Enforced by the `almadar/no-hardcoded-colors` ESLint rule (`@almadar/eslint-plugin`, enabled in this package's `eslint.config.cjs`). The single sanctioned exception is media-overlay chrome (white text / dark scrims painted over images or video), which requires an inline `eslint-disable-next-line almadar/no-hardcoded-colors` comment stating the reason.