oh-my-design-cli 0.1.2 → 1.0.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.
- package/.claude/hooks/post-edit-watch.cjs +99 -0
- package/.claude/hooks/session-end-foldin.cjs +96 -0
- package/.claude/hooks/session-state-loader.cjs +64 -0
- package/.claude/hooks/skill-activation.cjs +73 -0
- package/.claude/settings.json +55 -0
- package/.claude/skills/skill-rules.json +87 -0
- package/AGENTS.md +111 -0
- package/README.md +75 -202
- package/agents/AGENT.md +53 -0
- package/agents/omd-3d-blender.md +269 -0
- package/agents/omd-a11y-auditor.md +97 -0
- package/agents/omd-asset-curator.md +260 -0
- package/agents/omd-critic.md +181 -0
- package/agents/omd-master.md +548 -0
- package/agents/omd-microcopy.md +63 -0
- package/agents/omd-persona-tester.md +118 -0
- package/agents/omd-ui-junior.md +129 -0
- package/agents/omd-ux-engineer.md +265 -0
- package/agents/omd-ux-researcher.md +62 -0
- package/agents/omd-ux-writer.md +181 -0
- package/data/opt-out-corpus.json +141 -0
- package/data/reference-fingerprints.json +1495 -0
- package/dist/bin/oh-my-design.js +3 -818
- package/dist/bin/oh-my-design.js.map +1 -1
- package/dist/install-skills-SVIYKXOE.js +442 -0
- package/dist/install-skills-SVIYKXOE.js.map +1 -0
- package/package.json +23 -23
- package/scripts/context.cjs +91 -0
- package/scripts/postinstall.cjs +54 -0
- package/skills/omd-apply/SKILL.md +64 -53
- package/skills/omd-harness/SKILL.md +271 -0
- package/skills/omd-learn/SKILL.md +55 -35
- package/skills/omd-remember/SKILL.md +93 -15
- package/skills/omd-sync/SKILL.md +140 -16
- package/dist/chunk-6YNSV3VY.js +0 -35
- package/dist/chunk-6YNSV3VY.js.map +0 -1
- package/dist/chunk-MHFYGZSO.js +0 -337
- package/dist/chunk-MHFYGZSO.js.map +0 -1
- package/dist/chunk-N2JG6N4Q.js +0 -264
- package/dist/chunk-N2JG6N4Q.js.map +0 -1
- package/dist/chunk-OOQQEUGX.js +0 -46
- package/dist/chunk-OOQQEUGX.js.map +0 -1
- package/dist/chunk-OR5DHENY.js +0 -250
- package/dist/chunk-OR5DHENY.js.map +0 -1
- package/dist/customizer-CM76752R.js +0 -8
- package/dist/customizer-CM76752R.js.map +0 -1
- package/dist/index.d.ts +0 -559
- package/dist/index.js +0 -3113
- package/dist/index.js.map +0 -1
- package/dist/init-UMM4XIV5.js +0 -675
- package/dist/init-UMM4XIV5.js.map +0 -1
- package/dist/install-skills-CM6VXFZJ.js +0 -152
- package/dist/install-skills-CM6VXFZJ.js.map +0 -1
- package/dist/learn-33LHKEJA.js +0 -140
- package/dist/learn-33LHKEJA.js.map +0 -1
- package/dist/reference-YMNAOXJQ.js +0 -47
- package/dist/reference-YMNAOXJQ.js.map +0 -1
- package/dist/reference-parser-TM3CJPNE.js +0 -10
- package/dist/reference-parser-TM3CJPNE.js.map +0 -1
- package/dist/remember-UAFA5B2O.js +0 -78
- package/dist/remember-UAFA5B2O.js.map +0 -1
- package/dist/sync-FDYRKNFE.js +0 -417
- package/dist/sync-FDYRKNFE.js.map +0 -1
- package/dist/templates/templates/design-md.hbs +0 -44
- package/dist/templates/templates/partials/agent-prompt-guide.hbs +0 -28
- package/dist/templates/templates/partials/color-palette.hbs +0 -49
- package/dist/templates/templates/partials/component-stylings.hbs +0 -28
- package/dist/templates/templates/partials/depth-elevation.hbs +0 -31
- package/dist/templates/templates/partials/dos-donts.hbs +0 -13
- package/dist/templates/templates/partials/layout.hbs +0 -30
- package/dist/templates/templates/partials/responsive.hbs +0 -25
- package/dist/templates/templates/partials/shadcn-tokens.hbs +0 -64
- package/dist/templates/templates/partials/typography.hbs +0 -43
- package/dist/templates/templates/partials/visual-theme.hbs +0 -26
package/dist/index.js
DELETED
|
@@ -1,3113 +0,0 @@
|
|
|
1
|
-
// src/utils/color.ts
|
|
2
|
-
function hexToRgb(hex) {
|
|
3
|
-
const h = hex.replace("#", "");
|
|
4
|
-
return [
|
|
5
|
-
parseInt(h.slice(0, 2), 16),
|
|
6
|
-
parseInt(h.slice(2, 4), 16),
|
|
7
|
-
parseInt(h.slice(4, 6), 16)
|
|
8
|
-
];
|
|
9
|
-
}
|
|
10
|
-
function rgbToHex(r, g, b) {
|
|
11
|
-
return "#" + [r, g, b].map((v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, "0")).join("");
|
|
12
|
-
}
|
|
13
|
-
function hexToHsl(hex) {
|
|
14
|
-
const [r, g, b] = hexToRgb(hex).map((v) => v / 255);
|
|
15
|
-
const max = Math.max(r, g, b);
|
|
16
|
-
const min = Math.min(r, g, b);
|
|
17
|
-
const l = (max + min) / 2;
|
|
18
|
-
let h = 0;
|
|
19
|
-
let s = 0;
|
|
20
|
-
if (max !== min) {
|
|
21
|
-
const d = max - min;
|
|
22
|
-
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
23
|
-
switch (max) {
|
|
24
|
-
case r:
|
|
25
|
-
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
26
|
-
break;
|
|
27
|
-
case g:
|
|
28
|
-
h = ((b - r) / d + 2) / 6;
|
|
29
|
-
break;
|
|
30
|
-
case b:
|
|
31
|
-
h = ((r - g) / d + 4) / 6;
|
|
32
|
-
break;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
|
|
36
|
-
}
|
|
37
|
-
function hslToHex(h, s, l) {
|
|
38
|
-
const sn = s / 100;
|
|
39
|
-
const ln = l / 100;
|
|
40
|
-
const a = sn * Math.min(ln, 1 - ln);
|
|
41
|
-
const f = (n) => {
|
|
42
|
-
const k = (n + h / 30) % 12;
|
|
43
|
-
const color = ln - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
44
|
-
return Math.round(255 * color);
|
|
45
|
-
};
|
|
46
|
-
return rgbToHex(f(0), f(8), f(4));
|
|
47
|
-
}
|
|
48
|
-
function hslString(hex) {
|
|
49
|
-
const [h, s, l] = hexToHsl(hex);
|
|
50
|
-
return `${h} ${s}% ${l}%`;
|
|
51
|
-
}
|
|
52
|
-
function generateColorScale(hex) {
|
|
53
|
-
const [h, s] = hexToHsl(hex);
|
|
54
|
-
const lightnesses = {
|
|
55
|
-
50: 97,
|
|
56
|
-
100: 94,
|
|
57
|
-
200: 86,
|
|
58
|
-
300: 77,
|
|
59
|
-
400: 66,
|
|
60
|
-
500: 55,
|
|
61
|
-
600: 47,
|
|
62
|
-
700: 39,
|
|
63
|
-
800: 32,
|
|
64
|
-
900: 24,
|
|
65
|
-
950: 14
|
|
66
|
-
};
|
|
67
|
-
const scale = {};
|
|
68
|
-
for (const [key, l] of Object.entries(lightnesses)) {
|
|
69
|
-
scale[key] = hslToHex(h, s, l);
|
|
70
|
-
}
|
|
71
|
-
return scale;
|
|
72
|
-
}
|
|
73
|
-
function isLight(hex) {
|
|
74
|
-
const [, , l] = hexToHsl(hex);
|
|
75
|
-
return l > 55;
|
|
76
|
-
}
|
|
77
|
-
function contrastForeground(bgHex) {
|
|
78
|
-
return isLight(bgHex) ? "#09090b" : "#fafafa";
|
|
79
|
-
}
|
|
80
|
-
function semanticColor(base) {
|
|
81
|
-
return { base, foreground: contrastForeground(base) };
|
|
82
|
-
}
|
|
83
|
-
function lighten(hex, amount) {
|
|
84
|
-
const [h, s, l] = hexToHsl(hex);
|
|
85
|
-
return hslToHex(h, s, Math.min(100, l + amount));
|
|
86
|
-
}
|
|
87
|
-
function darken(hex, amount) {
|
|
88
|
-
const [h, s, l] = hexToHsl(hex);
|
|
89
|
-
return hslToHex(h, s, Math.max(0, l - amount));
|
|
90
|
-
}
|
|
91
|
-
function generateChartColors(primaryHex) {
|
|
92
|
-
const [h, s, l] = hexToHsl(primaryHex);
|
|
93
|
-
return [
|
|
94
|
-
primaryHex,
|
|
95
|
-
hslToHex((h + 40) % 360, s, l),
|
|
96
|
-
hslToHex((h + 80) % 360, s, l),
|
|
97
|
-
hslToHex((h + 160) % 360, s, l),
|
|
98
|
-
hslToHex((h + 220) % 360, s, l)
|
|
99
|
-
];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// src/utils/spacing.ts
|
|
103
|
-
var SPACING_CONFIGS = {
|
|
104
|
-
compact: { baseUnit: 4, multiplier: 1 },
|
|
105
|
-
comfortable: { baseUnit: 4, multiplier: 1.5 },
|
|
106
|
-
spacious: { baseUnit: 4, multiplier: 2 }
|
|
107
|
-
};
|
|
108
|
-
function generateSpacingTokens(density) {
|
|
109
|
-
const { baseUnit, multiplier } = SPACING_CONFIGS[density];
|
|
110
|
-
const steps = [0, 1, 2, 3, 4, 6, 8, 12, 16, 24];
|
|
111
|
-
const scale = steps.map((s) => Math.round(s * baseUnit * multiplier));
|
|
112
|
-
const px = (n) => `${n}px`;
|
|
113
|
-
return {
|
|
114
|
-
baseUnit: baseUnit * multiplier,
|
|
115
|
-
scale,
|
|
116
|
-
sectionGap: px(scale[8]),
|
|
117
|
-
componentPadding: {
|
|
118
|
-
sm: `${px(scale[2])} ${px(scale[3])}`,
|
|
119
|
-
md: `${px(scale[3])} ${px(scale[4])}`,
|
|
120
|
-
lg: `${px(scale[4])} ${px(scale[6])}`
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// src/utils/typography.ts
|
|
126
|
-
var FONT_STACKS = {
|
|
127
|
-
geometric: {
|
|
128
|
-
sans: '"Inter", "Geist", ui-sans-serif, system-ui, sans-serif',
|
|
129
|
-
mono: '"JetBrains Mono", "Fira Code", ui-monospace, monospace',
|
|
130
|
-
heading: '"Inter", "Geist", ui-sans-serif, system-ui, sans-serif'
|
|
131
|
-
},
|
|
132
|
-
humanist: {
|
|
133
|
-
sans: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
|
|
134
|
-
mono: '"SF Mono", "Cascadia Code", ui-monospace, monospace',
|
|
135
|
-
heading: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif'
|
|
136
|
-
},
|
|
137
|
-
monospace: {
|
|
138
|
-
sans: '"JetBrains Mono", "Fira Code", ui-monospace, monospace',
|
|
139
|
-
mono: '"JetBrains Mono", "Fira Code", ui-monospace, monospace',
|
|
140
|
-
heading: '"JetBrains Mono", "Fira Code", ui-monospace, monospace'
|
|
141
|
-
},
|
|
142
|
-
"serif-accent": {
|
|
143
|
-
sans: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
|
|
144
|
-
mono: '"SF Mono", ui-monospace, monospace',
|
|
145
|
-
heading: '"Playfair Display", "Georgia", "Times New Roman", serif'
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
var WEIGHT_PROFILES = {
|
|
149
|
-
geometric: { normal: 400, medium: 500, semibold: 600, bold: 700 },
|
|
150
|
-
humanist: { normal: 400, medium: 500, semibold: 600, bold: 700 },
|
|
151
|
-
monospace: { normal: 400, medium: 500, semibold: 600, bold: 700 },
|
|
152
|
-
"serif-accent": { normal: 400, medium: 500, semibold: 600, bold: 800 }
|
|
153
|
-
};
|
|
154
|
-
var LETTER_SPACING = {
|
|
155
|
-
geometric: { tight: "-0.025em", normal: "0em", wide: "0.05em" },
|
|
156
|
-
humanist: { tight: "-0.01em", normal: "0em", wide: "0.025em" },
|
|
157
|
-
monospace: { tight: "-0.02em", normal: "0em", wide: "0.1em" },
|
|
158
|
-
"serif-accent": { tight: "-0.02em", normal: "0.01em", wide: "0.075em" }
|
|
159
|
-
};
|
|
160
|
-
function generateTypographyTokens(style) {
|
|
161
|
-
return {
|
|
162
|
-
fontFamily: FONT_STACKS[style],
|
|
163
|
-
scale: {
|
|
164
|
-
xs: "0.75rem",
|
|
165
|
-
// 12px
|
|
166
|
-
sm: "0.875rem",
|
|
167
|
-
// 14px
|
|
168
|
-
base: "1rem",
|
|
169
|
-
// 16px
|
|
170
|
-
lg: "1.125rem",
|
|
171
|
-
// 18px
|
|
172
|
-
xl: "1.25rem",
|
|
173
|
-
// 20px
|
|
174
|
-
"2xl": "1.5rem",
|
|
175
|
-
// 24px
|
|
176
|
-
"3xl": "1.875rem",
|
|
177
|
-
// 30px
|
|
178
|
-
"4xl": "2.25rem"
|
|
179
|
-
// 36px
|
|
180
|
-
},
|
|
181
|
-
weight: WEIGHT_PROFILES[style],
|
|
182
|
-
lineHeight: {
|
|
183
|
-
tight: 1.25,
|
|
184
|
-
normal: 1.5,
|
|
185
|
-
relaxed: 1.75
|
|
186
|
-
},
|
|
187
|
-
letterSpacing: LETTER_SPACING[style]
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// src/core/token-resolver.ts
|
|
192
|
-
var MOOD_ADJUSTMENTS = {
|
|
193
|
-
clean: { secondaryHueShift: 0, accentHueShift: 30, saturationMod: -10, backgroundLightness: 100, mutedSaturation: 5 },
|
|
194
|
-
warm: { secondaryHueShift: 20, accentHueShift: 40, saturationMod: 0, backgroundLightness: 99, mutedSaturation: 8 },
|
|
195
|
-
bold: { secondaryHueShift: 180, accentHueShift: 60, saturationMod: 15, backgroundLightness: 100, mutedSaturation: 5 },
|
|
196
|
-
minimal: { secondaryHueShift: 0, accentHueShift: 0, saturationMod: -20, backgroundLightness: 100, mutedSaturation: 3 },
|
|
197
|
-
playful: { secondaryHueShift: 120, accentHueShift: 60, saturationMod: 10, backgroundLightness: 99, mutedSaturation: 10 },
|
|
198
|
-
dark: { secondaryHueShift: 30, accentHueShift: 180, saturationMod: 5, backgroundLightness: 7, mutedSaturation: 10 }
|
|
199
|
-
};
|
|
200
|
-
function resolveColors(primaryHex, mood) {
|
|
201
|
-
const adj = MOOD_ADJUSTMENTS[mood];
|
|
202
|
-
const [h, s] = hexToHsl(primaryHex);
|
|
203
|
-
const scale = generateColorScale(primaryHex);
|
|
204
|
-
const isDark = mood === "dark";
|
|
205
|
-
const secondaryHex = hslToHex(
|
|
206
|
-
(h + adj.secondaryHueShift) % 360,
|
|
207
|
-
Math.max(0, Math.min(100, s + adj.saturationMod - 30)),
|
|
208
|
-
isDark ? 20 : 96
|
|
209
|
-
);
|
|
210
|
-
const accentHex = hslToHex(
|
|
211
|
-
(h + adj.accentHueShift) % 360,
|
|
212
|
-
Math.max(0, Math.min(100, s + adj.saturationMod)),
|
|
213
|
-
isDark ? 30 : 55
|
|
214
|
-
);
|
|
215
|
-
const mutedHex = hslToHex(h, adj.mutedSaturation, isDark ? 15 : 96);
|
|
216
|
-
const bgHex = isDark ? hslToHex(h, 15, adj.backgroundLightness) : hslToHex(h, adj.mutedSaturation, adj.backgroundLightness);
|
|
217
|
-
const fgHex = isDark ? "#fafafa" : "#09090b";
|
|
218
|
-
const borderHex = isDark ? hslToHex(h, 10, 18) : hslToHex(h, 8, 90);
|
|
219
|
-
return {
|
|
220
|
-
primary: { ...semanticColor(primaryHex), scale },
|
|
221
|
-
secondary: semanticColor(secondaryHex),
|
|
222
|
-
accent: semanticColor(accentHex),
|
|
223
|
-
muted: semanticColor(mutedHex),
|
|
224
|
-
destructive: semanticColor("#ef4444"),
|
|
225
|
-
background: bgHex,
|
|
226
|
-
foreground: fgHex,
|
|
227
|
-
card: semanticColor(isDark ? lighten(bgHex, 3) : "#ffffff"),
|
|
228
|
-
popover: semanticColor(isDark ? lighten(bgHex, 5) : "#ffffff"),
|
|
229
|
-
border: borderHex,
|
|
230
|
-
input: isDark ? lighten(borderHex, 5) : darken(borderHex, 3),
|
|
231
|
-
ring: primaryHex,
|
|
232
|
-
chart: generateChartColors(primaryHex)
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
function resolveDarkColors(primaryHex, mood) {
|
|
236
|
-
return resolveColors(primaryHex, "dark");
|
|
237
|
-
}
|
|
238
|
-
var RADIUS_CONFIGS = {
|
|
239
|
-
sharp: { none: "0", sm: "0.125rem", md: "0.125rem", lg: "0.25rem", xl: "0.375rem", full: "9999px" },
|
|
240
|
-
moderate: { none: "0", sm: "0.25rem", md: "0.375rem", lg: "0.5rem", xl: "0.75rem", full: "9999px" },
|
|
241
|
-
rounded: { none: "0", sm: "0.375rem", md: "0.75rem", lg: "1rem", xl: "1.5rem", full: "9999px" },
|
|
242
|
-
pill: { none: "0", sm: "0.5rem", md: "9999px", lg: "9999px", xl: "9999px", full: "9999px" }
|
|
243
|
-
};
|
|
244
|
-
var SHADOW_CONFIGS = {
|
|
245
|
-
flat: {
|
|
246
|
-
none: "none",
|
|
247
|
-
sm: "none",
|
|
248
|
-
md: "none",
|
|
249
|
-
lg: "none",
|
|
250
|
-
xl: "none"
|
|
251
|
-
},
|
|
252
|
-
subtle: {
|
|
253
|
-
none: "none",
|
|
254
|
-
sm: "0 1px 2px 0 rgb(0 0 0 / 0.03)",
|
|
255
|
-
md: "0 1px 3px 0 rgb(0 0 0 / 0.06), 0 1px 2px -1px rgb(0 0 0 / 0.06)",
|
|
256
|
-
lg: "0 4px 6px -1px rgb(0 0 0 / 0.06), 0 2px 4px -2px rgb(0 0 0 / 0.06)",
|
|
257
|
-
xl: "0 10px 15px -3px rgb(0 0 0 / 0.06), 0 4px 6px -4px rgb(0 0 0 / 0.06)"
|
|
258
|
-
},
|
|
259
|
-
layered: {
|
|
260
|
-
none: "none",
|
|
261
|
-
sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
|
|
262
|
-
md: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
|
|
263
|
-
lg: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
|
|
264
|
-
xl: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)"
|
|
265
|
-
},
|
|
266
|
-
dramatic: {
|
|
267
|
-
none: "none",
|
|
268
|
-
sm: "0 1px 3px 0 rgb(0 0 0 / 0.12)",
|
|
269
|
-
md: "0 4px 6px -1px rgb(0 0 0 / 0.15), 0 2px 4px -2px rgb(0 0 0 / 0.15)",
|
|
270
|
-
lg: "0 10px 15px -3px rgb(0 0 0 / 0.2), 0 4px 6px -4px rgb(0 0 0 / 0.2)",
|
|
271
|
-
xl: "0 25px 50px -12px rgb(0 0 0 / 0.3)"
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
function resolveBorders(depth, borderColor) {
|
|
275
|
-
return {
|
|
276
|
-
width: depth === "flat" ? "1px" : "1px",
|
|
277
|
-
style: "solid",
|
|
278
|
-
color: borderColor
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
function resolveTokens(prefs) {
|
|
282
|
-
const colors = resolveColors(prefs.primaryColor, prefs.mood);
|
|
283
|
-
return {
|
|
284
|
-
name: prefs.preset ?? "Custom Design System",
|
|
285
|
-
colors,
|
|
286
|
-
typography: generateTypographyTokens(prefs.typography),
|
|
287
|
-
spacing: generateSpacingTokens(prefs.density),
|
|
288
|
-
radius: RADIUS_CONFIGS[prefs.roundness],
|
|
289
|
-
shadows: SHADOW_CONFIGS[prefs.depth],
|
|
290
|
-
borders: resolveBorders(prefs.depth, colors.border),
|
|
291
|
-
components: {},
|
|
292
|
-
darkColors: prefs.darkMode ? resolveDarkColors(prefs.primaryColor, prefs.mood) : void 0
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// src/core/renderer.ts
|
|
297
|
-
import Handlebars from "handlebars";
|
|
298
|
-
import { readFileSync } from "fs";
|
|
299
|
-
import { join, dirname } from "path";
|
|
300
|
-
import { fileURLToPath } from "url";
|
|
301
|
-
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
302
|
-
var TEMPLATE_DIR = join(__dirname, "..", "templates");
|
|
303
|
-
function registerHelpers() {
|
|
304
|
-
Handlebars.registerHelper("eq", (a, b) => a === b);
|
|
305
|
-
Handlebars.registerHelper("hsl", (hex) => {
|
|
306
|
-
if (typeof hex !== "string") return "";
|
|
307
|
-
return hslString(hex);
|
|
308
|
-
});
|
|
309
|
-
Handlebars.registerHelper("chartColor", (arr, index) => {
|
|
310
|
-
if (Array.isArray(arr) && typeof index === "number") {
|
|
311
|
-
return arr[index] ?? "";
|
|
312
|
-
}
|
|
313
|
-
return "";
|
|
314
|
-
});
|
|
315
|
-
Handlebars.registerHelper("multiply", (a, b) => a * b);
|
|
316
|
-
Handlebars.registerHelper("spacingUsage", (index) => {
|
|
317
|
-
const usages = [
|
|
318
|
-
"None",
|
|
319
|
-
"Tight inner gaps",
|
|
320
|
-
"Icon-to-text gap, inline padding",
|
|
321
|
-
"Small component padding",
|
|
322
|
-
"Default component padding",
|
|
323
|
-
"Component group spacing",
|
|
324
|
-
"Section inner padding",
|
|
325
|
-
"Large section padding",
|
|
326
|
-
"Section gap",
|
|
327
|
-
"Page-level section spacing"
|
|
328
|
-
];
|
|
329
|
-
return usages[index] ?? "";
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
function registerPartials() {
|
|
333
|
-
const partialNames = [
|
|
334
|
-
"visual-theme",
|
|
335
|
-
"color-palette",
|
|
336
|
-
"typography",
|
|
337
|
-
"component-stylings",
|
|
338
|
-
"layout",
|
|
339
|
-
"depth-elevation",
|
|
340
|
-
"dos-donts",
|
|
341
|
-
"responsive",
|
|
342
|
-
"agent-prompt-guide",
|
|
343
|
-
"shadcn-tokens"
|
|
344
|
-
];
|
|
345
|
-
for (const name of partialNames) {
|
|
346
|
-
const content = readFileSync(join(TEMPLATE_DIR, "partials", `${name}.hbs`), "utf-8");
|
|
347
|
-
Handlebars.registerPartial(name, content);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
var initialized = false;
|
|
351
|
-
function renderDesignMd(context) {
|
|
352
|
-
if (!initialized) {
|
|
353
|
-
registerHelpers();
|
|
354
|
-
registerPartials();
|
|
355
|
-
initialized = true;
|
|
356
|
-
}
|
|
357
|
-
const mainTemplate = readFileSync(join(TEMPLATE_DIR, "design-md.hbs"), "utf-8");
|
|
358
|
-
const template = Handlebars.compile(mainTemplate);
|
|
359
|
-
const result = template(context);
|
|
360
|
-
return result.replace(/\n{3,}/g, "\n\n").trim() + "\n";
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// src/core/components.ts
|
|
364
|
-
var COMPONENT_TITLES = {
|
|
365
|
-
button: "Button",
|
|
366
|
-
card: "Card",
|
|
367
|
-
dialog: "Dialog / Modal",
|
|
368
|
-
dropdown: "Dropdown Menu",
|
|
369
|
-
table: "Table",
|
|
370
|
-
input: "Input / Form Field",
|
|
371
|
-
navigation: "Navigation / Sidebar",
|
|
372
|
-
badge: "Badge / Tag",
|
|
373
|
-
"floating-button": "Floating Action Button",
|
|
374
|
-
toast: "Toast / Notification",
|
|
375
|
-
tabs: "Tabs",
|
|
376
|
-
select: "Select"
|
|
377
|
-
};
|
|
378
|
-
var generators = {
|
|
379
|
-
button: (t) => ({
|
|
380
|
-
description: "Primary interaction element. Use clear, action-oriented labels.",
|
|
381
|
-
variants: {
|
|
382
|
-
default: { background: t.colors.primary.base, foreground: t.colors.primary.foreground, radius: t.radius.md },
|
|
383
|
-
secondary: { background: t.colors.secondary.base, foreground: t.colors.secondary.foreground, border: t.colors.border, radius: t.radius.md },
|
|
384
|
-
outline: { background: "transparent", foreground: t.colors.foreground, border: t.colors.border, radius: t.radius.md },
|
|
385
|
-
ghost: { background: "transparent", foreground: t.colors.foreground, radius: t.radius.md },
|
|
386
|
-
destructive: { background: t.colors.destructive.base, foreground: t.colors.destructive.foreground, radius: t.radius.md }
|
|
387
|
-
},
|
|
388
|
-
states: {
|
|
389
|
-
hover: "Reduce opacity to 90% or shift background 1 stop darker",
|
|
390
|
-
active: "Scale down to 98% and shift background 2 stops darker",
|
|
391
|
-
focus: `Ring 2px offset-2 using ring color (${t.colors.ring})`,
|
|
392
|
-
disabled: "Opacity 50%, pointer-events none"
|
|
393
|
-
},
|
|
394
|
-
sizes: {
|
|
395
|
-
sm: { height: "32px", padding: t.spacing.componentPadding.sm, fontSize: t.typography.scale.sm },
|
|
396
|
-
md: { height: "40px", padding: t.spacing.componentPadding.md, fontSize: t.typography.scale.sm },
|
|
397
|
-
lg: { height: "48px", padding: t.spacing.componentPadding.lg, fontSize: t.typography.scale.base }
|
|
398
|
-
}
|
|
399
|
-
}),
|
|
400
|
-
card: (t) => ({
|
|
401
|
-
description: "Container for grouped content. Provides visual separation from the page background.",
|
|
402
|
-
variants: {
|
|
403
|
-
default: { background: t.colors.card.base, foreground: t.colors.card.foreground, border: t.colors.border, shadow: t.shadows.sm, radius: t.radius.lg },
|
|
404
|
-
elevated: { background: t.colors.card.base, foreground: t.colors.card.foreground, shadow: t.shadows.md, radius: t.radius.lg },
|
|
405
|
-
outline: { background: "transparent", foreground: t.colors.foreground, border: t.colors.border, radius: t.radius.lg }
|
|
406
|
-
},
|
|
407
|
-
states: {
|
|
408
|
-
hover: "Shift shadow up one level (sm \u2192 md) for interactive cards"
|
|
409
|
-
}
|
|
410
|
-
}),
|
|
411
|
-
dialog: (t) => ({
|
|
412
|
-
description: "Modal overlay for focused interactions. Always includes a backdrop.",
|
|
413
|
-
variants: {
|
|
414
|
-
default: { background: t.colors.card.base, foreground: t.colors.card.foreground, shadow: t.shadows.xl, radius: t.radius.lg }
|
|
415
|
-
},
|
|
416
|
-
states: {
|
|
417
|
-
hover: "N/A",
|
|
418
|
-
focus: "Trap focus within the dialog. Highlight focusable elements with ring."
|
|
419
|
-
}
|
|
420
|
-
}),
|
|
421
|
-
dropdown: (t) => ({
|
|
422
|
-
description: "Contextual menu triggered by a button or right-click.",
|
|
423
|
-
variants: {
|
|
424
|
-
default: { background: t.colors.popover.base, foreground: t.colors.popover.foreground, border: t.colors.border, shadow: t.shadows.lg, radius: t.radius.md }
|
|
425
|
-
},
|
|
426
|
-
states: {
|
|
427
|
-
hover: `Item background shifts to accent (${t.colors.accent.base})`,
|
|
428
|
-
focus: `Item receives subtle background tint and focus ring`
|
|
429
|
-
},
|
|
430
|
-
sizes: {
|
|
431
|
-
item: { height: "32px", padding: t.spacing.componentPadding.sm, fontSize: t.typography.scale.sm }
|
|
432
|
-
}
|
|
433
|
-
}),
|
|
434
|
-
table: (t) => ({
|
|
435
|
-
description: "Data display in rows and columns. Prioritize scanability and alignment.",
|
|
436
|
-
variants: {
|
|
437
|
-
default: { background: t.colors.card.base, foreground: t.colors.foreground, border: t.colors.border, radius: t.radius.lg }
|
|
438
|
-
},
|
|
439
|
-
states: {
|
|
440
|
-
hover: `Row background shifts to muted (${t.colors.muted.base})`
|
|
441
|
-
},
|
|
442
|
-
sizes: {
|
|
443
|
-
cell: { height: "44px", padding: t.spacing.componentPadding.sm, fontSize: t.typography.scale.sm },
|
|
444
|
-
header: { height: "44px", padding: t.spacing.componentPadding.sm, fontSize: t.typography.scale.sm }
|
|
445
|
-
}
|
|
446
|
-
}),
|
|
447
|
-
input: (t) => ({
|
|
448
|
-
description: "Text input, textarea, and form controls.",
|
|
449
|
-
variants: {
|
|
450
|
-
default: { background: t.colors.background, foreground: t.colors.foreground, border: t.colors.input, radius: t.radius.md },
|
|
451
|
-
error: { background: t.colors.background, foreground: t.colors.foreground, border: t.colors.destructive.base, radius: t.radius.md }
|
|
452
|
-
},
|
|
453
|
-
states: {
|
|
454
|
-
hover: "Border darkens by 1 stop",
|
|
455
|
-
focus: `Border shifts to ring color (${t.colors.ring}), add focus ring`,
|
|
456
|
-
disabled: "Background shifts to muted, opacity 60%"
|
|
457
|
-
},
|
|
458
|
-
sizes: {
|
|
459
|
-
sm: { height: "32px", padding: t.spacing.componentPadding.sm, fontSize: t.typography.scale.sm },
|
|
460
|
-
md: { height: "40px", padding: t.spacing.componentPadding.md, fontSize: t.typography.scale.sm },
|
|
461
|
-
lg: { height: "48px", padding: t.spacing.componentPadding.lg, fontSize: t.typography.scale.base }
|
|
462
|
-
}
|
|
463
|
-
}),
|
|
464
|
-
navigation: (t) => ({
|
|
465
|
-
description: "Primary navigation, sidebar, or top bar.",
|
|
466
|
-
variants: {
|
|
467
|
-
default: { background: t.colors.card.base, foreground: t.colors.foreground, border: t.colors.border }
|
|
468
|
-
},
|
|
469
|
-
states: {
|
|
470
|
-
hover: `Item background shifts to accent (${t.colors.accent.base})`,
|
|
471
|
-
active: `Item receives primary color indicator and semibold weight`
|
|
472
|
-
},
|
|
473
|
-
sizes: {
|
|
474
|
-
item: { height: "40px", padding: t.spacing.componentPadding.md, fontSize: t.typography.scale.sm }
|
|
475
|
-
}
|
|
476
|
-
}),
|
|
477
|
-
badge: (t) => ({
|
|
478
|
-
description: "Small label for status, category, or count.",
|
|
479
|
-
variants: {
|
|
480
|
-
default: { background: t.colors.primary.base, foreground: t.colors.primary.foreground, radius: t.radius.full },
|
|
481
|
-
secondary: { background: t.colors.secondary.base, foreground: t.colors.secondary.foreground, radius: t.radius.full },
|
|
482
|
-
outline: { background: "transparent", foreground: t.colors.foreground, border: t.colors.border, radius: t.radius.full },
|
|
483
|
-
destructive: { background: t.colors.destructive.base, foreground: t.colors.destructive.foreground, radius: t.radius.full }
|
|
484
|
-
},
|
|
485
|
-
states: {},
|
|
486
|
-
sizes: {
|
|
487
|
-
sm: { height: "20px", padding: "2px 8px", fontSize: t.typography.scale.xs },
|
|
488
|
-
md: { height: "24px", padding: "2px 10px", fontSize: t.typography.scale.sm }
|
|
489
|
-
}
|
|
490
|
-
}),
|
|
491
|
-
"floating-button": (t) => ({
|
|
492
|
-
description: "Fixed-position action button, typically in the bottom-right corner.",
|
|
493
|
-
variants: {
|
|
494
|
-
default: { background: t.colors.primary.base, foreground: t.colors.primary.foreground, shadow: t.shadows.lg, radius: t.radius.full }
|
|
495
|
-
},
|
|
496
|
-
states: {
|
|
497
|
-
hover: "Scale to 105%, shadow increases one level",
|
|
498
|
-
active: "Scale to 95%",
|
|
499
|
-
focus: `Focus ring with offset (${t.colors.ring})`
|
|
500
|
-
},
|
|
501
|
-
sizes: {
|
|
502
|
-
md: { height: "48px", padding: "12px", fontSize: t.typography.scale.base },
|
|
503
|
-
lg: { height: "56px", padding: "16px", fontSize: t.typography.scale.lg }
|
|
504
|
-
}
|
|
505
|
-
}),
|
|
506
|
-
toast: (t) => ({
|
|
507
|
-
description: "Temporary notification appearing at screen edge.",
|
|
508
|
-
variants: {
|
|
509
|
-
default: { background: t.colors.card.base, foreground: t.colors.card.foreground, border: t.colors.border, shadow: t.shadows.lg, radius: t.radius.md },
|
|
510
|
-
success: { background: "#10b981", foreground: "#ffffff", shadow: t.shadows.lg, radius: t.radius.md },
|
|
511
|
-
error: { background: t.colors.destructive.base, foreground: t.colors.destructive.foreground, shadow: t.shadows.lg, radius: t.radius.md }
|
|
512
|
-
},
|
|
513
|
-
states: {}
|
|
514
|
-
}),
|
|
515
|
-
tabs: (t) => ({
|
|
516
|
-
description: "Segmented navigation for switching between content panels.",
|
|
517
|
-
variants: {
|
|
518
|
-
default: { background: t.colors.muted.base, foreground: t.colors.muted.foreground, radius: t.radius.md },
|
|
519
|
-
active: { background: t.colors.background, foreground: t.colors.foreground, shadow: t.shadows.sm, radius: t.radius.md }
|
|
520
|
-
},
|
|
521
|
-
states: {
|
|
522
|
-
hover: `Background shifts to accent (${t.colors.accent.base})`
|
|
523
|
-
},
|
|
524
|
-
sizes: {
|
|
525
|
-
item: { height: "36px", padding: t.spacing.componentPadding.sm, fontSize: t.typography.scale.sm }
|
|
526
|
-
}
|
|
527
|
-
}),
|
|
528
|
-
select: (t) => ({
|
|
529
|
-
description: "Dropdown selector for choosing from a list of options.",
|
|
530
|
-
variants: {
|
|
531
|
-
default: { background: t.colors.background, foreground: t.colors.foreground, border: t.colors.input, radius: t.radius.md }
|
|
532
|
-
},
|
|
533
|
-
states: {
|
|
534
|
-
hover: "Border darkens by 1 stop",
|
|
535
|
-
focus: `Border shifts to ring color (${t.colors.ring}), add focus ring`,
|
|
536
|
-
disabled: "Background shifts to muted, opacity 60%"
|
|
537
|
-
},
|
|
538
|
-
sizes: {
|
|
539
|
-
sm: { height: "32px", padding: t.spacing.componentPadding.sm, fontSize: t.typography.scale.sm },
|
|
540
|
-
md: { height: "40px", padding: t.spacing.componentPadding.md, fontSize: t.typography.scale.sm }
|
|
541
|
-
}
|
|
542
|
-
})
|
|
543
|
-
};
|
|
544
|
-
function generateComponentTokens(componentNames, tokens) {
|
|
545
|
-
return componentNames.map((name) => ({
|
|
546
|
-
name,
|
|
547
|
-
title: COMPONENT_TITLES[name],
|
|
548
|
-
...generators[name](tokens)
|
|
549
|
-
}));
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// src/core/shadcn-mapper.ts
|
|
553
|
-
function mapColorTokens(colors, radius) {
|
|
554
|
-
return {
|
|
555
|
-
"--background": hslString(colors.background),
|
|
556
|
-
"--foreground": hslString(colors.foreground),
|
|
557
|
-
"--card": hslString(colors.card.base),
|
|
558
|
-
"--card-foreground": hslString(colors.card.foreground),
|
|
559
|
-
"--popover": hslString(colors.popover.base),
|
|
560
|
-
"--popover-foreground": hslString(colors.popover.foreground),
|
|
561
|
-
"--primary": hslString(colors.primary.base),
|
|
562
|
-
"--primary-foreground": hslString(colors.primary.foreground),
|
|
563
|
-
"--secondary": hslString(colors.secondary.base),
|
|
564
|
-
"--secondary-foreground": hslString(colors.secondary.foreground),
|
|
565
|
-
"--muted": hslString(colors.muted.base),
|
|
566
|
-
"--muted-foreground": hslString(colors.muted.foreground),
|
|
567
|
-
"--accent": hslString(colors.accent.base),
|
|
568
|
-
"--accent-foreground": hslString(colors.accent.foreground),
|
|
569
|
-
"--destructive": hslString(colors.destructive.base),
|
|
570
|
-
"--destructive-foreground": hslString(colors.destructive.foreground),
|
|
571
|
-
"--border": hslString(colors.border),
|
|
572
|
-
"--input": hslString(colors.input),
|
|
573
|
-
"--ring": hslString(colors.ring),
|
|
574
|
-
"--radius": radius,
|
|
575
|
-
"--chart-1": hslString(colors.chart[0]),
|
|
576
|
-
"--chart-2": hslString(colors.chart[1]),
|
|
577
|
-
"--chart-3": hslString(colors.chart[2]),
|
|
578
|
-
"--chart-4": hslString(colors.chart[3]),
|
|
579
|
-
"--chart-5": hslString(colors.chart[4]),
|
|
580
|
-
"--sidebar-background": hslString(colors.card.base),
|
|
581
|
-
"--sidebar-foreground": hslString(colors.foreground),
|
|
582
|
-
"--sidebar-primary": hslString(colors.primary.base),
|
|
583
|
-
"--sidebar-primary-foreground": hslString(colors.primary.foreground),
|
|
584
|
-
"--sidebar-accent": hslString(colors.accent.base),
|
|
585
|
-
"--sidebar-accent-foreground": hslString(colors.accent.foreground),
|
|
586
|
-
"--sidebar-border": hslString(colors.border),
|
|
587
|
-
"--sidebar-ring": hslString(colors.ring)
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
function mapToShadcn(tokens) {
|
|
591
|
-
const theme = {
|
|
592
|
-
light: mapColorTokens(tokens.colors, tokens.radius.md)
|
|
593
|
-
};
|
|
594
|
-
if (tokens.darkColors) {
|
|
595
|
-
theme.dark = mapColorTokens(tokens.darkColors, tokens.radius.md);
|
|
596
|
-
}
|
|
597
|
-
return theme;
|
|
598
|
-
}
|
|
599
|
-
function shadcnToCss(theme) {
|
|
600
|
-
const indent = " ";
|
|
601
|
-
const lines = ["@layer base {", " :root {"];
|
|
602
|
-
for (const [key, value] of Object.entries(theme.light)) {
|
|
603
|
-
lines.push(`${indent}${key}: ${value};`);
|
|
604
|
-
}
|
|
605
|
-
lines.push(" }");
|
|
606
|
-
if (theme.dark) {
|
|
607
|
-
lines.push("", " .dark {");
|
|
608
|
-
for (const [key, value] of Object.entries(theme.dark)) {
|
|
609
|
-
lines.push(`${indent}${key}: ${value};`);
|
|
610
|
-
}
|
|
611
|
-
lines.push(" }");
|
|
612
|
-
}
|
|
613
|
-
lines.push("}");
|
|
614
|
-
return lines.join("\n");
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// src/core/preview-generator.ts
|
|
618
|
-
function generatePreviewHtml(data) {
|
|
619
|
-
const {
|
|
620
|
-
name,
|
|
621
|
-
basedOn,
|
|
622
|
-
primary,
|
|
623
|
-
background,
|
|
624
|
-
foreground,
|
|
625
|
-
font,
|
|
626
|
-
headingWeight,
|
|
627
|
-
radius,
|
|
628
|
-
colors,
|
|
629
|
-
darkMode,
|
|
630
|
-
shadcnCss
|
|
631
|
-
} = data;
|
|
632
|
-
const isLightBg = isLight2(background);
|
|
633
|
-
const scale = generateColorScale(primary);
|
|
634
|
-
const radiusPx = radius === "9999px" ? "24px" : radius;
|
|
635
|
-
const borderColor = colors.border;
|
|
636
|
-
const accent = colors.accent;
|
|
637
|
-
const muted = colors.muted;
|
|
638
|
-
const chart = colors.chart;
|
|
639
|
-
const darkBg = hslToHex(hexToHsl(primary)[0], 15, 7);
|
|
640
|
-
const darkFg = "#fafafa";
|
|
641
|
-
const darkBorder = hslToHex(hexToHsl(primary)[0], 10, 18);
|
|
642
|
-
return `<!DOCTYPE html>
|
|
643
|
-
<html lang="en">
|
|
644
|
-
<head>
|
|
645
|
-
<meta charset="UTF-8">
|
|
646
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
647
|
-
<title>${esc(name)} \u2014 Design System Preview</title>
|
|
648
|
-
<style>
|
|
649
|
-
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
650
|
-
|
|
651
|
-
:root {
|
|
652
|
-
--bg: ${background};
|
|
653
|
-
--fg: ${foreground};
|
|
654
|
-
--primary: ${primary};
|
|
655
|
-
--primary-fg: ${contrastForeground(primary)};
|
|
656
|
-
--accent: ${accent};
|
|
657
|
-
--muted: ${muted};
|
|
658
|
-
--muted-fg: ${lighten(foreground, 40)};
|
|
659
|
-
--border: ${borderColor};
|
|
660
|
-
--destructive: #ef4444;
|
|
661
|
-
--card: ${isLightBg ? "#ffffff" : lighten(background, 3)};
|
|
662
|
-
--radius: ${radiusPx};
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
.dark {
|
|
666
|
-
--bg: ${darkBg};
|
|
667
|
-
--fg: ${darkFg};
|
|
668
|
-
--card: ${lighten(darkBg, 3)};
|
|
669
|
-
--muted: ${hslToHex(hexToHsl(primary)[0], 10, 15)};
|
|
670
|
-
--muted-fg: ${darken(darkFg, 35)};
|
|
671
|
-
--border: ${darkBorder};
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
body {
|
|
675
|
-
font-family: "${font}", "Inter", system-ui, sans-serif;
|
|
676
|
-
background: var(--bg);
|
|
677
|
-
color: var(--fg);
|
|
678
|
-
line-height: 1.5;
|
|
679
|
-
transition: background 0.25s, color 0.25s;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
.shell { max-width: 1100px; margin: 0 auto; padding: 40px 24px 80px; }
|
|
683
|
-
|
|
684
|
-
/* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
685
|
-
.header {
|
|
686
|
-
display: flex; justify-content: space-between; align-items: center;
|
|
687
|
-
padding-bottom: 24px; margin-bottom: 40px;
|
|
688
|
-
border-bottom: 1px solid var(--border);
|
|
689
|
-
}
|
|
690
|
-
.header h1 {
|
|
691
|
-
font-size: 2rem; font-weight: ${headingWeight};
|
|
692
|
-
letter-spacing: -0.02em;
|
|
693
|
-
}
|
|
694
|
-
.header .sub {
|
|
695
|
-
font-size: 0.875rem; color: var(--muted-fg); margin-top: 4px;
|
|
696
|
-
}
|
|
697
|
-
.controls { display: flex; gap: 8px; }
|
|
698
|
-
.ctrl-btn {
|
|
699
|
-
padding: 8px 16px; border-radius: var(--radius);
|
|
700
|
-
font-size: 0.8125rem; font-family: inherit; cursor: pointer;
|
|
701
|
-
transition: all 0.15s; border: 1px solid var(--border);
|
|
702
|
-
background: var(--card); color: var(--fg);
|
|
703
|
-
}
|
|
704
|
-
.ctrl-btn:hover { opacity: 0.85; }
|
|
705
|
-
.ctrl-btn.primary { background: var(--primary); color: var(--primary-fg); border-color: transparent; }
|
|
706
|
-
|
|
707
|
-
/* \u2500\u2500 Sections \u2500\u2500\u2500\u2500\u2500 */
|
|
708
|
-
.section { margin-bottom: 48px; }
|
|
709
|
-
.section-title {
|
|
710
|
-
font-size: 1.375rem; font-weight: ${headingWeight};
|
|
711
|
-
letter-spacing: -0.01em; margin-bottom: 16px;
|
|
712
|
-
}
|
|
713
|
-
.section-sub { font-size: 0.8125rem; color: var(--muted-fg); margin-bottom: 16px; }
|
|
714
|
-
|
|
715
|
-
/* \u2500\u2500 Color Scale \u2500\u2500 */
|
|
716
|
-
.scale-row {
|
|
717
|
-
display: flex; border-radius: var(--radius); overflow: hidden;
|
|
718
|
-
border: 1px solid var(--border); margin-bottom: 24px;
|
|
719
|
-
}
|
|
720
|
-
.scale-stop {
|
|
721
|
-
flex: 1; padding: 28px 4px 8px; text-align: center;
|
|
722
|
-
font-size: 10px; font-family: monospace; cursor: pointer;
|
|
723
|
-
transition: transform 0.1s; position: relative;
|
|
724
|
-
}
|
|
725
|
-
.scale-stop:hover { z-index: 1; transform: scaleY(1.12); }
|
|
726
|
-
.scale-stop .lbl { display: block; opacity: 0.7; margin-bottom: 2px; }
|
|
727
|
-
.scale-stop .hex {
|
|
728
|
-
background: rgba(0,0,0,0.2); color: #fff;
|
|
729
|
-
padding: 1px 4px; border-radius: 3px; font-size: 9px;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
/* \u2500\u2500 Color Chips \u2500\u2500 */
|
|
733
|
-
.chip-grid {
|
|
734
|
-
display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px;
|
|
735
|
-
}
|
|
736
|
-
.chip {
|
|
737
|
-
border-radius: var(--radius); overflow: hidden;
|
|
738
|
-
border: 1px solid var(--border); cursor: pointer;
|
|
739
|
-
transition: transform 0.15s, box-shadow 0.15s;
|
|
740
|
-
}
|
|
741
|
-
.chip:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
|
742
|
-
.chip .sw { height: 64px; display: flex; align-items: end; padding: 6px; }
|
|
743
|
-
.chip .sw span {
|
|
744
|
-
font-size: 10px; font-family: monospace;
|
|
745
|
-
background: rgba(0,0,0,0.25); color: #fff;
|
|
746
|
-
padding: 1px 5px; border-radius: 3px;
|
|
747
|
-
}
|
|
748
|
-
.chip .inf { padding: 8px; background: var(--card); }
|
|
749
|
-
.chip .inf .n { font-size: 0.75rem; font-weight: 500; }
|
|
750
|
-
.chip .inf .v { font-size: 10px; color: var(--muted-fg); font-family: monospace; }
|
|
751
|
-
|
|
752
|
-
/* \u2500\u2500 Typography \u2500\u2500 */
|
|
753
|
-
.type-rows { display: flex; flex-direction: column; gap: 8px; }
|
|
754
|
-
.type-row {
|
|
755
|
-
display: flex; align-items: baseline; gap: 16px;
|
|
756
|
-
padding: 12px 16px; border-radius: var(--radius);
|
|
757
|
-
background: var(--card); border: 1px solid var(--border);
|
|
758
|
-
}
|
|
759
|
-
.type-row .lbl {
|
|
760
|
-
min-width: 48px; font-size: 0.75rem; color: var(--muted-fg);
|
|
761
|
-
font-family: monospace; flex-shrink: 0;
|
|
762
|
-
}
|
|
763
|
-
.type-row .sample { flex: 1; }
|
|
764
|
-
|
|
765
|
-
/* \u2500\u2500 Components \u2500\u2500 */
|
|
766
|
-
.comp-card {
|
|
767
|
-
padding: 24px; border-radius: var(--radius);
|
|
768
|
-
background: var(--card); border: 1px solid var(--border);
|
|
769
|
-
margin-bottom: 16px;
|
|
770
|
-
}
|
|
771
|
-
.comp-card h3 { font-size: 1rem; font-weight: 600; margin-bottom: 16px; }
|
|
772
|
-
.comp-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 12px; }
|
|
773
|
-
|
|
774
|
-
.btn {
|
|
775
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
776
|
-
font-family: inherit; font-weight: 500; cursor: pointer;
|
|
777
|
-
transition: all 0.15s; border: none; font-size: 0.8125rem;
|
|
778
|
-
padding: 8px 16px; height: 36px; border-radius: var(--radius);
|
|
779
|
-
}
|
|
780
|
-
.btn-primary { background: var(--primary); color: var(--primary-fg); }
|
|
781
|
-
.btn-secondary { background: var(--muted); color: var(--fg); }
|
|
782
|
-
.btn-outline { background: transparent; color: var(--fg); border: 1px solid var(--border); }
|
|
783
|
-
.btn-ghost { background: transparent; color: var(--fg); }
|
|
784
|
-
.btn-destructive { background: var(--destructive); color: #fff; }
|
|
785
|
-
.btn:hover { opacity: 0.9; }
|
|
786
|
-
|
|
787
|
-
.badge {
|
|
788
|
-
display: inline-flex; font-size: 0.6875rem; font-weight: 500;
|
|
789
|
-
padding: 2px 10px; border-radius: 9999px;
|
|
790
|
-
}
|
|
791
|
-
.badge-primary { background: var(--primary); color: var(--primary-fg); }
|
|
792
|
-
.badge-secondary { background: var(--muted); color: var(--fg); }
|
|
793
|
-
.badge-outline { background: transparent; color: var(--fg); border: 1px solid var(--border); }
|
|
794
|
-
|
|
795
|
-
.input-demo {
|
|
796
|
-
width: 100%; max-width: 320px; font-family: inherit;
|
|
797
|
-
background: var(--bg); color: var(--fg);
|
|
798
|
-
border: 1px solid var(--border); border-radius: var(--radius);
|
|
799
|
-
padding: 8px 12px; font-size: 0.8125rem; outline: none;
|
|
800
|
-
transition: border-color 0.15s;
|
|
801
|
-
}
|
|
802
|
-
.input-demo:focus {
|
|
803
|
-
border-color: var(--primary);
|
|
804
|
-
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 20%, transparent);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
.demo-table {
|
|
808
|
-
width: 100%; border-collapse: collapse; font-size: 0.8125rem;
|
|
809
|
-
}
|
|
810
|
-
.demo-table th, .demo-table td {
|
|
811
|
-
padding: 10px 12px; text-align: left;
|
|
812
|
-
border-bottom: 1px solid var(--border);
|
|
813
|
-
}
|
|
814
|
-
.demo-table th {
|
|
815
|
-
font-weight: 500; color: var(--muted-fg);
|
|
816
|
-
font-size: 0.75rem; text-transform: uppercase;
|
|
817
|
-
letter-spacing: 0.05em;
|
|
818
|
-
}
|
|
819
|
-
.demo-table tr:hover td { background: var(--muted); }
|
|
820
|
-
|
|
821
|
-
.demo-card {
|
|
822
|
-
padding: 20px; border-radius: var(--radius);
|
|
823
|
-
background: var(--card); border: 1px solid var(--border);
|
|
824
|
-
max-width: 340px; box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
|
825
|
-
}
|
|
826
|
-
.demo-card h4 { font-weight: 600; margin-bottom: 6px; }
|
|
827
|
-
.demo-card p { font-size: 0.8125rem; color: var(--muted-fg); margin-bottom: 14px; }
|
|
828
|
-
|
|
829
|
-
.demo-dialog {
|
|
830
|
-
padding: 24px; border-radius: var(--radius);
|
|
831
|
-
background: var(--card); box-shadow: 0 20px 40px rgba(0,0,0,0.15);
|
|
832
|
-
max-width: 400px; border: 1px solid var(--border);
|
|
833
|
-
}
|
|
834
|
-
.demo-dialog h4 { font-weight: 600; margin-bottom: 8px; }
|
|
835
|
-
.demo-dialog p { font-size: 0.8125rem; color: var(--muted-fg); margin-bottom: 16px; }
|
|
836
|
-
.demo-dialog .acts { display: flex; justify-content: flex-end; gap: 8px; }
|
|
837
|
-
|
|
838
|
-
.tabs {
|
|
839
|
-
display: flex; background: var(--muted); border-radius: var(--radius); padding: 4px; gap: 2px;
|
|
840
|
-
}
|
|
841
|
-
.tab {
|
|
842
|
-
padding: 6px 14px; border-radius: calc(var(--radius) - 2px);
|
|
843
|
-
font-size: 0.8125rem; font-family: inherit; font-weight: 500;
|
|
844
|
-
border: none; cursor: pointer; background: transparent;
|
|
845
|
-
color: var(--muted-fg); transition: all 0.15s;
|
|
846
|
-
}
|
|
847
|
-
.tab.active {
|
|
848
|
-
background: var(--bg); color: var(--fg);
|
|
849
|
-
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
|
850
|
-
}
|
|
851
|
-
.tab:hover:not(.active) { color: var(--fg); }
|
|
852
|
-
|
|
853
|
-
/* \u2500\u2500 CSS Block \u2500\u2500\u2500\u2500 */
|
|
854
|
-
.css-block {
|
|
855
|
-
position: relative; background: var(--card);
|
|
856
|
-
border: 1px solid var(--border); border-radius: var(--radius);
|
|
857
|
-
overflow: hidden;
|
|
858
|
-
}
|
|
859
|
-
.css-block pre {
|
|
860
|
-
padding: 20px; overflow-x: auto; font-family: monospace;
|
|
861
|
-
font-size: 11px; line-height: 1.6; color: var(--fg);
|
|
862
|
-
}
|
|
863
|
-
.css-block .copy-css { position: absolute; top: 8px; right: 8px; }
|
|
864
|
-
|
|
865
|
-
/* \u2500\u2500 Spacing \u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
866
|
-
.spacing-vis {
|
|
867
|
-
display: flex; align-items: end; gap: 8px; padding: 24px;
|
|
868
|
-
background: var(--card); border: 1px solid var(--border);
|
|
869
|
-
border-radius: var(--radius);
|
|
870
|
-
}
|
|
871
|
-
.sp-bar { display: flex; flex-direction: column; align-items: center; gap: 4px; }
|
|
872
|
-
.sp-bar .bar { background: var(--primary); border-radius: 2px; width: 28px; }
|
|
873
|
-
.sp-bar .val { font-size: 10px; font-family: monospace; color: var(--muted-fg); }
|
|
874
|
-
|
|
875
|
-
/* \u2500\u2500 Radius \u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
876
|
-
.radius-grid { display: flex; gap: 16px; flex-wrap: wrap; }
|
|
877
|
-
.radius-demo {
|
|
878
|
-
width: 72px; height: 72px; background: var(--primary);
|
|
879
|
-
display: flex; align-items: center; justify-content: center;
|
|
880
|
-
color: var(--primary-fg); font-size: 0.6875rem; font-family: monospace;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
/* \u2500\u2500 Toast \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
884
|
-
.toast-popup {
|
|
885
|
-
position: fixed; bottom: 24px; right: 24px;
|
|
886
|
-
padding: 10px 20px; background: var(--fg); color: var(--bg);
|
|
887
|
-
border-radius: var(--radius); font-size: 0.8125rem;
|
|
888
|
-
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
|
|
889
|
-
transform: translateY(100px); opacity: 0;
|
|
890
|
-
transition: all 0.3s; z-index: 100;
|
|
891
|
-
}
|
|
892
|
-
.toast-popup.show { transform: translateY(0); opacity: 1; }
|
|
893
|
-
|
|
894
|
-
@media (max-width: 768px) {
|
|
895
|
-
.shell { padding: 16px 12px 48px; }
|
|
896
|
-
.header { flex-direction: column; gap: 12px; align-items: flex-start; }
|
|
897
|
-
.chip-grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); }
|
|
898
|
-
}
|
|
899
|
-
</style>
|
|
900
|
-
</head>
|
|
901
|
-
<body>
|
|
902
|
-
<div class="shell">
|
|
903
|
-
|
|
904
|
-
<div class="header">
|
|
905
|
-
<div>
|
|
906
|
-
<h1>${esc(name)}</h1>
|
|
907
|
-
<div class="sub">Based on ${esc(basedOn)} · Generated by oh-my-design</div>
|
|
908
|
-
</div>
|
|
909
|
-
<div class="controls">
|
|
910
|
-
${darkMode ? '<button class="ctrl-btn" onclick="toggleTheme()" id="theme-btn">\u{1F319} Dark</button>' : ""}
|
|
911
|
-
<button class="ctrl-btn primary" onclick="copyCss()">Copy CSS</button>
|
|
912
|
-
</div>
|
|
913
|
-
</div>
|
|
914
|
-
|
|
915
|
-
<!-- Primary Scale -->
|
|
916
|
-
<div class="section">
|
|
917
|
-
<h2 class="section-title">Primary Color Scale</h2>
|
|
918
|
-
<div class="section-sub">Click to copy hex value</div>
|
|
919
|
-
<div class="scale-row">
|
|
920
|
-
${Object.entries(scale).map(([stop, hex]) => ` <div class="scale-stop" style="background:${hex};color:${isLight2(hex) ? "#000" : "#fff"}" onclick="copy('${hex}')">
|
|
921
|
-
<span class="lbl">${stop}</span><span class="hex">${hex}</span>
|
|
922
|
-
</div>`).join("\n")}
|
|
923
|
-
</div>
|
|
924
|
-
</div>
|
|
925
|
-
|
|
926
|
-
<!-- Semantic Colors -->
|
|
927
|
-
<div class="section">
|
|
928
|
-
<h2 class="section-title">Semantic Colors</h2>
|
|
929
|
-
<div class="chip-grid">
|
|
930
|
-
${[
|
|
931
|
-
{ n: "Primary", bg: primary, v: "--primary" },
|
|
932
|
-
{ n: "Accent", bg: accent, v: "--accent" },
|
|
933
|
-
{ n: "Muted", bg: muted, v: "--muted" },
|
|
934
|
-
{ n: "Destructive", bg: "#ef4444", v: "--destructive" },
|
|
935
|
-
{ n: "Background", bg: background, v: "--background" },
|
|
936
|
-
{ n: "Foreground", bg: foreground, v: "--foreground" },
|
|
937
|
-
{ n: "Border", bg: borderColor, v: "--border" },
|
|
938
|
-
{ n: "Card", bg: isLightBg ? "#ffffff" : lighten(background, 3), v: "--card" }
|
|
939
|
-
].map((c) => ` <div class="chip" onclick="copy('${c.bg}')">
|
|
940
|
-
<div class="sw" style="background:${c.bg}"><span>${c.bg}</span></div>
|
|
941
|
-
<div class="inf"><div class="n">${c.n}</div><div class="v">${c.v}</div></div>
|
|
942
|
-
</div>`).join("\n")}
|
|
943
|
-
</div>
|
|
944
|
-
</div>
|
|
945
|
-
|
|
946
|
-
<!-- Chart Colors -->
|
|
947
|
-
<div class="section">
|
|
948
|
-
<h2 class="section-title">Chart Colors</h2>
|
|
949
|
-
<div style="display:flex;gap:8px;">
|
|
950
|
-
${chart.map((c, i) => ` <div style="width:48px;height:48px;border-radius:var(--radius);background:${c};cursor:pointer;" onclick="copy('${c}')" title="Chart ${i + 1}: ${c}"></div>`).join("\n")}
|
|
951
|
-
</div>
|
|
952
|
-
</div>
|
|
953
|
-
|
|
954
|
-
<!-- Typography -->
|
|
955
|
-
<div class="section">
|
|
956
|
-
<h2 class="section-title">Typography</h2>
|
|
957
|
-
<div class="type-rows">
|
|
958
|
-
<div class="type-row">
|
|
959
|
-
<span class="lbl">H1</span>
|
|
960
|
-
<span class="sample" style="font-size:2.25rem;font-weight:${headingWeight};letter-spacing:-0.02em;line-height:1.2">Page Title Heading</span>
|
|
961
|
-
</div>
|
|
962
|
-
<div class="type-row">
|
|
963
|
-
<span class="lbl">H2</span>
|
|
964
|
-
<span class="sample" style="font-size:1.875rem;font-weight:${headingWeight};letter-spacing:-0.01em;line-height:1.25">Section Heading</span>
|
|
965
|
-
</div>
|
|
966
|
-
<div class="type-row">
|
|
967
|
-
<span class="lbl">H3</span>
|
|
968
|
-
<span class="sample" style="font-size:1.5rem;font-weight:${headingWeight};line-height:1.3">Subsection</span>
|
|
969
|
-
</div>
|
|
970
|
-
<div class="type-row">
|
|
971
|
-
<span class="lbl">body</span>
|
|
972
|
-
<span class="sample" style="font-size:1rem;">The quick brown fox jumps over the lazy dog. \uB2E4\uB78C\uC950 \uD5CC \uCCC7\uBC14\uD034\uC5D0 \uD0C0\uACE0\uD30C.</span>
|
|
973
|
-
</div>
|
|
974
|
-
<div class="type-row">
|
|
975
|
-
<span class="lbl">sm</span>
|
|
976
|
-
<span class="sample" style="font-size:0.875rem;color:var(--muted-fg)">Secondary text, labels, and metadata</span>
|
|
977
|
-
</div>
|
|
978
|
-
<div class="type-row">
|
|
979
|
-
<span class="lbl">mono</span>
|
|
980
|
-
<span class="sample" style="font-family:monospace;font-size:0.875rem">const theme = generateDesignSystem();</span>
|
|
981
|
-
</div>
|
|
982
|
-
</div>
|
|
983
|
-
</div>
|
|
984
|
-
|
|
985
|
-
<!-- Radius -->
|
|
986
|
-
<div class="section">
|
|
987
|
-
<h2 class="section-title">Border Radius</h2>
|
|
988
|
-
<div class="radius-grid">
|
|
989
|
-
<div style="text-align:center"><div class="radius-demo" style="border-radius:0">0</div><div style="font-size:10px;margin-top:4px;color:var(--muted-fg)">none</div></div>
|
|
990
|
-
<div style="text-align:center"><div class="radius-demo" style="border-radius:${radiusPx}">${radiusPx}</div><div style="font-size:10px;margin-top:4px;color:var(--muted-fg)">base</div></div>
|
|
991
|
-
<div style="text-align:center"><div class="radius-demo" style="border-radius:${parseInt(radiusPx) * 2}px">${parseInt(radiusPx) * 2}px</div><div style="font-size:10px;margin-top:4px;color:var(--muted-fg)">lg</div></div>
|
|
992
|
-
<div style="text-align:center"><div class="radius-demo" style="border-radius:9999px">pill</div><div style="font-size:10px;margin-top:4px;color:var(--muted-fg)">full</div></div>
|
|
993
|
-
</div>
|
|
994
|
-
</div>
|
|
995
|
-
|
|
996
|
-
<!-- Components -->
|
|
997
|
-
<div class="section">
|
|
998
|
-
<h2 class="section-title">Components</h2>
|
|
999
|
-
|
|
1000
|
-
<!-- Buttons -->
|
|
1001
|
-
<div class="comp-card">
|
|
1002
|
-
<h3>Buttons</h3>
|
|
1003
|
-
<div class="comp-row">
|
|
1004
|
-
<button class="btn btn-primary">Primary</button>
|
|
1005
|
-
<button class="btn btn-secondary">Secondary</button>
|
|
1006
|
-
<button class="btn btn-outline">Outline</button>
|
|
1007
|
-
<button class="btn btn-ghost">Ghost</button>
|
|
1008
|
-
<button class="btn btn-destructive">Delete</button>
|
|
1009
|
-
</div>
|
|
1010
|
-
</div>
|
|
1011
|
-
|
|
1012
|
-
<!-- Badges -->
|
|
1013
|
-
<div class="comp-card">
|
|
1014
|
-
<h3>Badges</h3>
|
|
1015
|
-
<div class="comp-row">
|
|
1016
|
-
<span class="badge badge-primary">Active</span>
|
|
1017
|
-
<span class="badge badge-secondary">Draft</span>
|
|
1018
|
-
<span class="badge badge-outline">Archived</span>
|
|
1019
|
-
</div>
|
|
1020
|
-
</div>
|
|
1021
|
-
|
|
1022
|
-
<!-- Input -->
|
|
1023
|
-
<div class="comp-card">
|
|
1024
|
-
<h3>Input</h3>
|
|
1025
|
-
<div style="display:flex;flex-direction:column;gap:12px;max-width:400px;">
|
|
1026
|
-
<div>
|
|
1027
|
-
<label style="font-size:0.8125rem;font-weight:500;display:block;margin-bottom:4px;">Email</label>
|
|
1028
|
-
<input class="input-demo" type="email" placeholder="you@example.com">
|
|
1029
|
-
</div>
|
|
1030
|
-
<div>
|
|
1031
|
-
<label style="font-size:0.8125rem;font-weight:500;display:block;margin-bottom:4px;">Password</label>
|
|
1032
|
-
<input class="input-demo" type="password" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022">
|
|
1033
|
-
</div>
|
|
1034
|
-
</div>
|
|
1035
|
-
</div>
|
|
1036
|
-
|
|
1037
|
-
<!-- Card -->
|
|
1038
|
-
<div class="comp-card">
|
|
1039
|
-
<h3>Card</h3>
|
|
1040
|
-
<div class="comp-row" style="align-items:stretch;">
|
|
1041
|
-
<div class="demo-card">
|
|
1042
|
-
<h4>Project Overview</h4>
|
|
1043
|
-
<p>A summary card showing key metrics and status for your project.</p>
|
|
1044
|
-
<button class="btn btn-primary" style="height:32px;font-size:0.75rem;">View Details</button>
|
|
1045
|
-
</div>
|
|
1046
|
-
</div>
|
|
1047
|
-
</div>
|
|
1048
|
-
|
|
1049
|
-
<!-- Table -->
|
|
1050
|
-
<div class="comp-card">
|
|
1051
|
-
<h3>Table</h3>
|
|
1052
|
-
<div style="overflow-x:auto;">
|
|
1053
|
-
<table class="demo-table">
|
|
1054
|
-
<thead><tr><th>Name</th><th>Status</th><th>Role</th><th>Email</th></tr></thead>
|
|
1055
|
-
<tbody>
|
|
1056
|
-
<tr><td>Kim Minjae</td><td><span class="badge badge-primary" style="font-size:10px;">Active</span></td><td>Developer</td><td>minjae@example.com</td></tr>
|
|
1057
|
-
<tr><td>Lee Soyeon</td><td><span class="badge badge-secondary" style="font-size:10px;">Pending</span></td><td>Designer</td><td>soyeon@example.com</td></tr>
|
|
1058
|
-
<tr><td>Park Jiwoo</td><td><span class="badge badge-outline" style="font-size:10px;">Inactive</span></td><td>PM</td><td>jiwoo@example.com</td></tr>
|
|
1059
|
-
</tbody>
|
|
1060
|
-
</table>
|
|
1061
|
-
</div>
|
|
1062
|
-
</div>
|
|
1063
|
-
|
|
1064
|
-
<!-- Dialog -->
|
|
1065
|
-
<div class="comp-card">
|
|
1066
|
-
<h3>Dialog</h3>
|
|
1067
|
-
<div class="demo-dialog">
|
|
1068
|
-
<h4>Delete item?</h4>
|
|
1069
|
-
<p>This action cannot be undone.</p>
|
|
1070
|
-
<div class="acts">
|
|
1071
|
-
<button class="btn btn-outline" style="height:32px;">Cancel</button>
|
|
1072
|
-
<button class="btn btn-destructive" style="height:32px;">Delete</button>
|
|
1073
|
-
</div>
|
|
1074
|
-
</div>
|
|
1075
|
-
</div>
|
|
1076
|
-
|
|
1077
|
-
<!-- Tabs -->
|
|
1078
|
-
<div class="comp-card">
|
|
1079
|
-
<h3>Tabs</h3>
|
|
1080
|
-
<div class="tabs">
|
|
1081
|
-
<button class="tab active">Overview</button>
|
|
1082
|
-
<button class="tab">Analytics</button>
|
|
1083
|
-
<button class="tab">Settings</button>
|
|
1084
|
-
</div>
|
|
1085
|
-
</div>
|
|
1086
|
-
</div>
|
|
1087
|
-
|
|
1088
|
-
<!-- shadcn CSS -->
|
|
1089
|
-
<div class="section">
|
|
1090
|
-
<h2 class="section-title">shadcn/ui CSS Variables</h2>
|
|
1091
|
-
<div class="section-sub">Ready to paste into globals.css</div>
|
|
1092
|
-
<div class="css-block">
|
|
1093
|
-
<button class="ctrl-btn copy-css" onclick="copyCss()" style="position:absolute;top:8px;right:8px;">Copy</button>
|
|
1094
|
-
<pre id="css-output">${escHtml(shadcnCss)}</pre>
|
|
1095
|
-
</div>
|
|
1096
|
-
</div>
|
|
1097
|
-
|
|
1098
|
-
</div>
|
|
1099
|
-
|
|
1100
|
-
<div class="toast-popup" id="toast">Copied!</div>
|
|
1101
|
-
|
|
1102
|
-
<script>
|
|
1103
|
-
let isDark = false;
|
|
1104
|
-
function toggleTheme() {
|
|
1105
|
-
isDark = !isDark;
|
|
1106
|
-
document.body.classList.toggle('dark', isDark);
|
|
1107
|
-
const btn = document.getElementById('theme-btn');
|
|
1108
|
-
if (btn) btn.textContent = isDark ? '\u2600\uFE0F Light' : '\u{1F319} Dark';
|
|
1109
|
-
}
|
|
1110
|
-
function showToast(msg) {
|
|
1111
|
-
const el = document.getElementById('toast');
|
|
1112
|
-
el.textContent = msg;
|
|
1113
|
-
el.classList.add('show');
|
|
1114
|
-
setTimeout(() => el.classList.remove('show'), 1800);
|
|
1115
|
-
}
|
|
1116
|
-
function copy(val) {
|
|
1117
|
-
navigator.clipboard.writeText(val).then(() => showToast('Copied: ' + val));
|
|
1118
|
-
}
|
|
1119
|
-
function copyCss() {
|
|
1120
|
-
const css = document.getElementById('css-output').textContent;
|
|
1121
|
-
navigator.clipboard.writeText(css).then(() => showToast('CSS variables copied!'));
|
|
1122
|
-
}
|
|
1123
|
-
document.querySelectorAll('.tab').forEach(t => {
|
|
1124
|
-
t.addEventListener('click', () => {
|
|
1125
|
-
t.parentElement.querySelectorAll('.tab').forEach(x => x.classList.remove('active'));
|
|
1126
|
-
t.classList.add('active');
|
|
1127
|
-
});
|
|
1128
|
-
});
|
|
1129
|
-
</script>
|
|
1130
|
-
</body>
|
|
1131
|
-
</html>`;
|
|
1132
|
-
}
|
|
1133
|
-
function isLight2(hex) {
|
|
1134
|
-
const h = hex.replace("#", "");
|
|
1135
|
-
const r = parseInt(h.slice(0, 2), 16);
|
|
1136
|
-
const g = parseInt(h.slice(2, 4), 16);
|
|
1137
|
-
const b = parseInt(h.slice(4, 6), 16);
|
|
1138
|
-
return (r * 299 + g * 587 + b * 114) / 1e3 > 140;
|
|
1139
|
-
}
|
|
1140
|
-
function esc(s) {
|
|
1141
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1142
|
-
}
|
|
1143
|
-
function escHtml(s) {
|
|
1144
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
// src/core/reference-parser.ts
|
|
1148
|
-
import { readFileSync as readFileSync2, readdirSync, existsSync } from "fs";
|
|
1149
|
-
import { join as join2, dirname as dirname2 } from "path";
|
|
1150
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1151
|
-
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
1152
|
-
var CATEGORIES = {
|
|
1153
|
-
stripe: "Fintech",
|
|
1154
|
-
coinbase: "Fintech",
|
|
1155
|
-
revolut: "Fintech",
|
|
1156
|
-
wise: "Fintech",
|
|
1157
|
-
kraken: "Fintech",
|
|
1158
|
-
vercel: "Developer Tools",
|
|
1159
|
-
cursor: "Developer Tools",
|
|
1160
|
-
warp: "Developer Tools",
|
|
1161
|
-
expo: "Developer Tools",
|
|
1162
|
-
lovable: "Developer Tools",
|
|
1163
|
-
raycast: "Developer Tools",
|
|
1164
|
-
superhuman: "Developer Tools",
|
|
1165
|
-
supabase: "Backend & DevOps",
|
|
1166
|
-
mongodb: "Backend & DevOps",
|
|
1167
|
-
sentry: "Backend & DevOps",
|
|
1168
|
-
posthog: "Backend & DevOps",
|
|
1169
|
-
hashicorp: "Backend & DevOps",
|
|
1170
|
-
clickhouse: "Backend & DevOps",
|
|
1171
|
-
composio: "Backend & DevOps",
|
|
1172
|
-
sanity: "Backend & DevOps",
|
|
1173
|
-
notion: "Productivity",
|
|
1174
|
-
"linear.app": "Productivity",
|
|
1175
|
-
"cal": "Productivity",
|
|
1176
|
-
zapier: "Productivity",
|
|
1177
|
-
intercom: "Productivity",
|
|
1178
|
-
resend: "Productivity",
|
|
1179
|
-
mintlify: "Productivity",
|
|
1180
|
-
figma: "Design Tools",
|
|
1181
|
-
framer: "Design Tools",
|
|
1182
|
-
miro: "Design Tools",
|
|
1183
|
-
webflow: "Design Tools",
|
|
1184
|
-
airtable: "Design Tools",
|
|
1185
|
-
clay: "Design Tools",
|
|
1186
|
-
claude: "AI & LLM",
|
|
1187
|
-
cohere: "AI & LLM",
|
|
1188
|
-
mistral: "AI & LLM",
|
|
1189
|
-
"mistral.ai": "AI & LLM",
|
|
1190
|
-
ollama: "AI & LLM",
|
|
1191
|
-
"opencode.ai": "AI & LLM",
|
|
1192
|
-
replicate: "AI & LLM",
|
|
1193
|
-
"together.ai": "AI & LLM",
|
|
1194
|
-
"x.ai": "AI & LLM",
|
|
1195
|
-
elevenlabs: "AI & LLM",
|
|
1196
|
-
minimax: "AI & LLM",
|
|
1197
|
-
runwayml: "AI & LLM",
|
|
1198
|
-
voltagent: "AI & LLM",
|
|
1199
|
-
apple: "Consumer Tech",
|
|
1200
|
-
spotify: "Consumer Tech",
|
|
1201
|
-
uber: "Consumer Tech",
|
|
1202
|
-
airbnb: "Consumer Tech",
|
|
1203
|
-
pinterest: "Consumer Tech",
|
|
1204
|
-
nvidia: "Consumer Tech",
|
|
1205
|
-
ibm: "Consumer Tech",
|
|
1206
|
-
spacex: "Consumer Tech",
|
|
1207
|
-
shopify: "E-commerce",
|
|
1208
|
-
semrush: "Marketing",
|
|
1209
|
-
tesla: "Automotive",
|
|
1210
|
-
bmw: "Automotive",
|
|
1211
|
-
ferrari: "Automotive",
|
|
1212
|
-
lamborghini: "Automotive",
|
|
1213
|
-
renault: "Automotive",
|
|
1214
|
-
bugatti: "Automotive",
|
|
1215
|
-
karrot: "Korean Tech",
|
|
1216
|
-
toss: "Korean Tech",
|
|
1217
|
-
baemin: "Korean Tech",
|
|
1218
|
-
kakao: "Korean Tech"
|
|
1219
|
-
};
|
|
1220
|
-
var CATEGORY_ORDER = [
|
|
1221
|
-
"Korean Tech",
|
|
1222
|
-
"AI & LLM",
|
|
1223
|
-
"Design Tools",
|
|
1224
|
-
"Developer Tools",
|
|
1225
|
-
"Productivity",
|
|
1226
|
-
"Consumer Tech",
|
|
1227
|
-
"Fintech",
|
|
1228
|
-
"Backend & DevOps",
|
|
1229
|
-
"E-commerce",
|
|
1230
|
-
"Automotive",
|
|
1231
|
-
"Marketing"
|
|
1232
|
-
];
|
|
1233
|
-
function extractHexColors(text) {
|
|
1234
|
-
const matches = text.match(/#[0-9a-fA-F]{6}\b/g) || [];
|
|
1235
|
-
return [...new Set(matches)];
|
|
1236
|
-
}
|
|
1237
|
-
function extractPrimaryColor(md) {
|
|
1238
|
-
const section2Match = md.match(/## 2\. Color Palette.*?\n([\s\S]*?)(?=## 3\.)/);
|
|
1239
|
-
if (section2Match) {
|
|
1240
|
-
const section2 = section2Match[1];
|
|
1241
|
-
const primaryMatch = section2.match(/\*\*([^*]+)\*\*\s*\(`(#[0-9a-fA-F]{6})`\).*?(?:primary|brand|CTA|main)/i);
|
|
1242
|
-
if (primaryMatch) {
|
|
1243
|
-
return { hex: primaryMatch[2], name: primaryMatch[1] };
|
|
1244
|
-
}
|
|
1245
|
-
const firstHex = section2.match(/`(#[0-9a-fA-F]{6})`/);
|
|
1246
|
-
if (firstHex) {
|
|
1247
|
-
return { hex: firstHex[1], name: "Primary" };
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
const allHex = extractHexColors(md);
|
|
1251
|
-
return { hex: allHex[0] || "#6366f1", name: "Primary" };
|
|
1252
|
-
}
|
|
1253
|
-
function extractBackground(md) {
|
|
1254
|
-
const patterns = [
|
|
1255
|
-
/(?:page|canvas|marketing)\s+background.*?`(#[0-9a-fA-F]{6})`/i,
|
|
1256
|
-
/(?:Pure White|White).*?`(#[0-9a-fA-F]{6})`.*?(?:page background|background)/i,
|
|
1257
|
-
/Background:.*?Pure White.*?\(`(#[0-9a-fA-F]{6})`\)/i,
|
|
1258
|
-
/(?:Page|Site)\s+background.*?`(#[0-9a-fA-F]{6})`/i,
|
|
1259
|
-
// Match "hex: The primary page background" pattern (hex before description)
|
|
1260
|
-
/`(#[0-9a-fA-F]{6})`[^.]*?(?:primary\s+)?page\s+background/i
|
|
1261
|
-
];
|
|
1262
|
-
for (const pattern of patterns) {
|
|
1263
|
-
const match = md.match(pattern);
|
|
1264
|
-
if (match) return match[1];
|
|
1265
|
-
}
|
|
1266
|
-
const section2 = md.match(/## 2\. Color.*?\n([\s\S]*?)(?=## 3\.)/);
|
|
1267
|
-
if (section2) {
|
|
1268
|
-
const surfaceBg = section2[1].match(/(?:Pure White|Pure Black|page background|Background\b).*?`(#[0-9a-fA-F]{6})`/i);
|
|
1269
|
-
if (surfaceBg) return surfaceBg[1];
|
|
1270
|
-
}
|
|
1271
|
-
const quickRef = md.match(/Quick Color Reference[\s\S]*?(?:Page\s+)?Background.*?[(`](#[0-9a-fA-F]{6})[)`]/i);
|
|
1272
|
-
if (quickRef) return quickRef[1];
|
|
1273
|
-
if (md.match(/dark.mode.(?:native|first)/i)) {
|
|
1274
|
-
const darkBg = md.match(/(?:marketing|deepest|canvas).*?`(#[0-9a-fA-F]{6})`/i);
|
|
1275
|
-
if (darkBg) return darkBg[1];
|
|
1276
|
-
}
|
|
1277
|
-
return "#ffffff";
|
|
1278
|
-
}
|
|
1279
|
-
function extractForeground(md) {
|
|
1280
|
-
const fgMatch = md.match(/(?:heading|primary text).*?`(#[0-9a-fA-F]{6})`/i);
|
|
1281
|
-
return fgMatch ? fgMatch[1] : "#09090b";
|
|
1282
|
-
}
|
|
1283
|
-
function extractAccent(md) {
|
|
1284
|
-
const accentMatch = md.match(/(?:accent|secondary).*?`(#[0-9a-fA-F]{6})`/i);
|
|
1285
|
-
return accentMatch ? accentMatch[1] : void 0;
|
|
1286
|
-
}
|
|
1287
|
-
function extractBorder(md) {
|
|
1288
|
-
const borderMatch = md.match(/(?:border.*?default|border.*?standard).*?`(#[0-9a-fA-F]{6})`/i);
|
|
1289
|
-
return borderMatch ? borderMatch[1] : void 0;
|
|
1290
|
-
}
|
|
1291
|
-
function extractTypography(md) {
|
|
1292
|
-
const section3 = md.match(/## 3\. Typography.*?\n([\s\S]*?)(?=## 4\.)/);
|
|
1293
|
-
let primary = "Inter";
|
|
1294
|
-
let mono;
|
|
1295
|
-
let headingWeight = "600";
|
|
1296
|
-
if (section3) {
|
|
1297
|
-
const text = section3[1];
|
|
1298
|
-
const primaryMatch = text.match(/\*\*Primary\*\*:\s*`([^`]+)`/i);
|
|
1299
|
-
if (primaryMatch) primary = primaryMatch[1].split(",")[0].trim();
|
|
1300
|
-
const monoMatch = text.match(/\*\*Monospace\*\*:\s*`([^`]+)`/i);
|
|
1301
|
-
if (monoMatch) mono = monoMatch[1].split(",")[0].trim();
|
|
1302
|
-
const weightMatch = text.match(/Display.*?\|\s*(\d{3})\s*\|/);
|
|
1303
|
-
if (weightMatch) headingWeight = weightMatch[1];
|
|
1304
|
-
}
|
|
1305
|
-
return { primary, mono, headingWeight };
|
|
1306
|
-
}
|
|
1307
|
-
function extractRadius(md) {
|
|
1308
|
-
const radiusMatch = md.match(/(?:border-radius|radius).*?(\d+px(?:\s*[-–]\s*\d+px)?)/i);
|
|
1309
|
-
return radiusMatch ? radiusMatch[1] : "6px";
|
|
1310
|
-
}
|
|
1311
|
-
function extractMood(md) {
|
|
1312
|
-
const section1 = md.match(/## 1\. Visual Theme.*?\n([\s\S]*?)(?=## 2\.)/);
|
|
1313
|
-
if (!section1) return "";
|
|
1314
|
-
const text = section1[1].trim();
|
|
1315
|
-
const firstParagraph = text.split("\n\n")[0];
|
|
1316
|
-
return firstParagraph.slice(0, 300);
|
|
1317
|
-
}
|
|
1318
|
-
function toDisplayName(id) {
|
|
1319
|
-
const special = {
|
|
1320
|
-
"linear.app": "Linear",
|
|
1321
|
-
"cal": "Cal.com",
|
|
1322
|
-
"mistral.ai": "Mistral AI",
|
|
1323
|
-
"opencode.ai": "OpenCode AI",
|
|
1324
|
-
"together.ai": "Together AI",
|
|
1325
|
-
"x.ai": "xAI",
|
|
1326
|
-
ibm: "IBM",
|
|
1327
|
-
bmw: "BMW",
|
|
1328
|
-
nvidia: "NVIDIA",
|
|
1329
|
-
posthog: "PostHog",
|
|
1330
|
-
supabase: "Supabase",
|
|
1331
|
-
voltagent: "VoltAgent",
|
|
1332
|
-
elevenlabs: "ElevenLabs",
|
|
1333
|
-
runwayml: "RunwayML",
|
|
1334
|
-
spacex: "SpaceX",
|
|
1335
|
-
coinbase: "Coinbase",
|
|
1336
|
-
airbnb: "Airbnb",
|
|
1337
|
-
clickhouse: "ClickHouse",
|
|
1338
|
-
karrot: "Karrot",
|
|
1339
|
-
toss: "Toss",
|
|
1340
|
-
baemin: "Baemin",
|
|
1341
|
-
kakao: "Kakao"
|
|
1342
|
-
};
|
|
1343
|
-
return special[id] || id.charAt(0).toUpperCase() + id.slice(1);
|
|
1344
|
-
}
|
|
1345
|
-
function getReferencesDir() {
|
|
1346
|
-
const candidates = [
|
|
1347
|
-
join2(process.cwd(), "references"),
|
|
1348
|
-
join2(__dirname2, "..", "..", "references"),
|
|
1349
|
-
join2(__dirname2, "..", "references")
|
|
1350
|
-
];
|
|
1351
|
-
for (const dir of candidates) {
|
|
1352
|
-
if (existsSync(dir)) return dir;
|
|
1353
|
-
}
|
|
1354
|
-
throw new Error("references/ directory not found. Searched: " + candidates.join(", "));
|
|
1355
|
-
}
|
|
1356
|
-
function loadReference(id) {
|
|
1357
|
-
const dir = getReferencesDir();
|
|
1358
|
-
const mdPath = join2(dir, id, "DESIGN.md");
|
|
1359
|
-
if (!existsSync(mdPath)) {
|
|
1360
|
-
throw new Error(`Reference not found: ${id}`);
|
|
1361
|
-
}
|
|
1362
|
-
const designMd = readFileSync2(mdPath, "utf-8");
|
|
1363
|
-
const primary = extractPrimaryColor(designMd);
|
|
1364
|
-
return {
|
|
1365
|
-
id,
|
|
1366
|
-
name: toDisplayName(id),
|
|
1367
|
-
category: CATEGORIES[id] || "Other",
|
|
1368
|
-
designMd,
|
|
1369
|
-
colors: {
|
|
1370
|
-
primary: primary.hex,
|
|
1371
|
-
primaryName: primary.name,
|
|
1372
|
-
background: extractBackground(designMd),
|
|
1373
|
-
foreground: extractForeground(designMd),
|
|
1374
|
-
accent: extractAccent(designMd),
|
|
1375
|
-
border: extractBorder(designMd)
|
|
1376
|
-
},
|
|
1377
|
-
typography: extractTypography(designMd),
|
|
1378
|
-
radius: extractRadius(designMd),
|
|
1379
|
-
mood: extractMood(designMd)
|
|
1380
|
-
};
|
|
1381
|
-
}
|
|
1382
|
-
function listReferences() {
|
|
1383
|
-
const dir = getReferencesDir();
|
|
1384
|
-
const entries = readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync(join2(dir, d.name, "DESIGN.md"))).map((d) => {
|
|
1385
|
-
const mdPath = join2(dir, d.name, "DESIGN.md");
|
|
1386
|
-
const md = readFileSync2(mdPath, "utf-8");
|
|
1387
|
-
const primary = extractPrimaryColor(md);
|
|
1388
|
-
return {
|
|
1389
|
-
id: d.name,
|
|
1390
|
-
name: toDisplayName(d.name),
|
|
1391
|
-
category: CATEGORIES[d.name] || "Other",
|
|
1392
|
-
primaryColor: primary.hex
|
|
1393
|
-
};
|
|
1394
|
-
});
|
|
1395
|
-
return entries.sort((a, b) => {
|
|
1396
|
-
const ai = CATEGORY_ORDER.indexOf(a.category);
|
|
1397
|
-
const bi = CATEGORY_ORDER.indexOf(b.category);
|
|
1398
|
-
const oa = ai === -1 ? 999 : ai;
|
|
1399
|
-
const ob = bi === -1 ? 999 : bi;
|
|
1400
|
-
return oa - ob || a.name.localeCompare(b.name);
|
|
1401
|
-
});
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
// src/core/customizer.ts
|
|
1405
|
-
function applyOverrides(ref, overrides, mode, components) {
|
|
1406
|
-
let md = ref.designMd;
|
|
1407
|
-
const effectivePrimary = overrides.primaryColor || ref.colors.primary;
|
|
1408
|
-
const effectiveFont = overrides.fontFamily || ref.typography.primary;
|
|
1409
|
-
const effectiveWeight = overrides.headingWeight || ref.typography.headingWeight;
|
|
1410
|
-
const effectiveRadius = overrides.borderRadius || ref.radius.replace(/[-–].*/, "").trim();
|
|
1411
|
-
const effectiveBg = ref.colors.background;
|
|
1412
|
-
const effectiveFg = ref.colors.foreground;
|
|
1413
|
-
md = md.replace(/[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{FE00}-\u{FE0F}\u{1FA00}-\u{1FAFF}\u{200D}\u{20E3}\u{E0020}-\u{E007F}]/gu, "");
|
|
1414
|
-
if (mode === "customized") {
|
|
1415
|
-
md = md.replace(/^# .+$/m, `# Custom Design System (based on ${ref.name})`);
|
|
1416
|
-
if (overrides.primaryColor && overrides.primaryColor !== ref.colors.primary) {
|
|
1417
|
-
const re = new RegExp(ref.colors.primary.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
1418
|
-
md = re[Symbol.replace](md, overrides.primaryColor);
|
|
1419
|
-
}
|
|
1420
|
-
if (overrides.fontFamily && overrides.fontFamily !== ref.typography.primary) {
|
|
1421
|
-
md = md.replaceAll(ref.typography.primary, overrides.fontFamily);
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
if (components && components.length > 0) {
|
|
1425
|
-
md += `
|
|
1426
|
-
|
|
1427
|
-
---
|
|
1428
|
-
|
|
1429
|
-
## Included Components
|
|
1430
|
-
|
|
1431
|
-
The following components are part of this design system:
|
|
1432
|
-
|
|
1433
|
-
`;
|
|
1434
|
-
md += components.map((c) => `- ${c.charAt(0).toUpperCase() + c.slice(1).replace(/-/g, " ")}`).join("\n");
|
|
1435
|
-
md += "\n";
|
|
1436
|
-
}
|
|
1437
|
-
md += buildIconographySection();
|
|
1438
|
-
const shadcnCss = generateShadcnCss(effectivePrimary, effectiveBg, effectiveFg, effectiveRadius, ref, overrides);
|
|
1439
|
-
md += buildDocumentPolicies();
|
|
1440
|
-
const previewData = buildPreviewData(ref, overrides, effectivePrimary, effectiveBg, effectiveFg, effectiveFont, effectiveWeight, effectiveRadius);
|
|
1441
|
-
return { designMd: md, shadcnCss, previewData };
|
|
1442
|
-
}
|
|
1443
|
-
function generateShadcnCss(primary, background, foreground, radius, ref, overrides) {
|
|
1444
|
-
const scale = generateColorScale(primary);
|
|
1445
|
-
const accent = ref.colors.accent || hslToHex((hexToHsl(primary)[0] + 30) % 360, 60, 55);
|
|
1446
|
-
const border = ref.colors.border || lighten(foreground, 75);
|
|
1447
|
-
const muted = lighten(background === "#ffffff" ? "#f5f5f5" : background, 5);
|
|
1448
|
-
const destructive = "#ef4444";
|
|
1449
|
-
const chart = generateChartColors(primary);
|
|
1450
|
-
const radiusRem = radius === "9999px" ? "9999px" : `${parseInt(radius) / 16}rem`;
|
|
1451
|
-
const vars = {
|
|
1452
|
-
"--background": hslString(background),
|
|
1453
|
-
"--foreground": hslString(foreground),
|
|
1454
|
-
"--card": hslString(background === "#ffffff" ? "#ffffff" : lighten(background, 3)),
|
|
1455
|
-
"--card-foreground": hslString(foreground),
|
|
1456
|
-
"--popover": hslString(background === "#ffffff" ? "#ffffff" : lighten(background, 5)),
|
|
1457
|
-
"--popover-foreground": hslString(foreground),
|
|
1458
|
-
"--primary": hslString(primary),
|
|
1459
|
-
"--primary-foreground": hslString(contrastForeground(primary)),
|
|
1460
|
-
"--secondary": hslString(scale[100]),
|
|
1461
|
-
"--secondary-foreground": hslString(foreground),
|
|
1462
|
-
"--muted": hslString(muted),
|
|
1463
|
-
"--muted-foreground": hslString(lighten(foreground, 40)),
|
|
1464
|
-
"--accent": hslString(accent),
|
|
1465
|
-
"--accent-foreground": hslString(contrastForeground(accent)),
|
|
1466
|
-
"--destructive": hslString(destructive),
|
|
1467
|
-
"--destructive-foreground": hslString(contrastForeground(destructive)),
|
|
1468
|
-
"--border": hslString(border),
|
|
1469
|
-
"--input": hslString(border),
|
|
1470
|
-
"--ring": hslString(primary),
|
|
1471
|
-
"--radius": radiusRem,
|
|
1472
|
-
"--chart-1": hslString(chart[0]),
|
|
1473
|
-
"--chart-2": hslString(chart[1]),
|
|
1474
|
-
"--chart-3": hslString(chart[2]),
|
|
1475
|
-
"--chart-4": hslString(chart[3]),
|
|
1476
|
-
"--chart-5": hslString(chart[4])
|
|
1477
|
-
};
|
|
1478
|
-
const lines = ["@layer base {", " :root {"];
|
|
1479
|
-
for (const [k, v] of Object.entries(vars)) {
|
|
1480
|
-
lines.push(` ${k}: ${v};`);
|
|
1481
|
-
}
|
|
1482
|
-
lines.push(" }");
|
|
1483
|
-
if (overrides.darkMode) {
|
|
1484
|
-
const darkBg = hslToHex(hexToHsl(primary)[0], 15, 7);
|
|
1485
|
-
const darkFg = "#fafafa";
|
|
1486
|
-
const darkBorder = hslToHex(hexToHsl(primary)[0], 10, 18);
|
|
1487
|
-
const darkMuted = hslToHex(hexToHsl(primary)[0], 10, 15);
|
|
1488
|
-
lines.push("", " .dark {");
|
|
1489
|
-
const darkVars = {
|
|
1490
|
-
"--background": hslString(darkBg),
|
|
1491
|
-
"--foreground": hslString(darkFg),
|
|
1492
|
-
"--card": hslString(lighten(darkBg, 3)),
|
|
1493
|
-
"--card-foreground": hslString(darkFg),
|
|
1494
|
-
"--popover": hslString(lighten(darkBg, 5)),
|
|
1495
|
-
"--popover-foreground": hslString(darkFg),
|
|
1496
|
-
"--primary": hslString(primary),
|
|
1497
|
-
"--primary-foreground": hslString(contrastForeground(primary)),
|
|
1498
|
-
"--secondary": hslString(hslToHex(hexToHsl(primary)[0], 15, 20)),
|
|
1499
|
-
"--secondary-foreground": hslString(darkFg),
|
|
1500
|
-
"--muted": hslString(darkMuted),
|
|
1501
|
-
"--muted-foreground": hslString(darken(darkFg, 35)),
|
|
1502
|
-
"--accent": hslString(accent),
|
|
1503
|
-
"--accent-foreground": hslString(contrastForeground(accent)),
|
|
1504
|
-
"--destructive": hslString(destructive),
|
|
1505
|
-
"--destructive-foreground": hslString(contrastForeground(destructive)),
|
|
1506
|
-
"--border": hslString(darkBorder),
|
|
1507
|
-
"--input": hslString(lighten(darkBorder, 5)),
|
|
1508
|
-
"--ring": hslString(primary),
|
|
1509
|
-
"--chart-1": hslString(chart[0]),
|
|
1510
|
-
"--chart-2": hslString(chart[1]),
|
|
1511
|
-
"--chart-3": hslString(chart[2]),
|
|
1512
|
-
"--chart-4": hslString(chart[3]),
|
|
1513
|
-
"--chart-5": hslString(chart[4])
|
|
1514
|
-
};
|
|
1515
|
-
for (const [k, v] of Object.entries(darkVars)) {
|
|
1516
|
-
lines.push(` ${k}: ${v};`);
|
|
1517
|
-
}
|
|
1518
|
-
lines.push(" }");
|
|
1519
|
-
}
|
|
1520
|
-
lines.push("}");
|
|
1521
|
-
return lines.join("\n");
|
|
1522
|
-
}
|
|
1523
|
-
function buildPreviewData(ref, overrides, primary, background, foreground, font, headingWeight, radius) {
|
|
1524
|
-
const scale = generateColorScale(primary);
|
|
1525
|
-
const accent = ref.colors.accent || hslToHex((hexToHsl(primary)[0] + 30) % 360, 60, 55);
|
|
1526
|
-
const border = ref.colors.border || lighten(foreground, 75);
|
|
1527
|
-
const chart = generateChartColors(primary);
|
|
1528
|
-
return {
|
|
1529
|
-
name: overrides.primaryColor || overrides.fontFamily ? `Custom (based on ${ref.name})` : ref.name,
|
|
1530
|
-
basedOn: ref.name,
|
|
1531
|
-
primary,
|
|
1532
|
-
background,
|
|
1533
|
-
foreground,
|
|
1534
|
-
font,
|
|
1535
|
-
headingWeight,
|
|
1536
|
-
radius,
|
|
1537
|
-
shadcnCss: "",
|
|
1538
|
-
// filled later
|
|
1539
|
-
designMd: "",
|
|
1540
|
-
// filled later
|
|
1541
|
-
colors: {
|
|
1542
|
-
primary,
|
|
1543
|
-
accent,
|
|
1544
|
-
muted: lighten(background === "#ffffff" ? "#f5f5f5" : background, 5),
|
|
1545
|
-
destructive: "#ef4444",
|
|
1546
|
-
border,
|
|
1547
|
-
scale,
|
|
1548
|
-
chart
|
|
1549
|
-
},
|
|
1550
|
-
darkMode: overrides.darkMode
|
|
1551
|
-
};
|
|
1552
|
-
}
|
|
1553
|
-
function buildIconographySection() {
|
|
1554
|
-
return `
|
|
1555
|
-
|
|
1556
|
-
---
|
|
1557
|
-
|
|
1558
|
-
## Iconography & SVG Guidelines
|
|
1559
|
-
|
|
1560
|
-
### Icon Library
|
|
1561
|
-
|
|
1562
|
-
Use a single, consistent icon library throughout the project. Recommended options:
|
|
1563
|
-
|
|
1564
|
-
- **Lucide React** (\`lucide-react\`): Default for shadcn/ui projects. 1,400+ icons, tree-shakeable, consistent 24x24 grid.
|
|
1565
|
-
- **Radix Icons** (\`@radix-ui/react-icons\`): 300+ icons, 15x15 grid, minimal and geometric.
|
|
1566
|
-
- **Heroicons** (\`@heroicons/react\`): 300+ icons by Tailwind team, outline and solid variants.
|
|
1567
|
-
|
|
1568
|
-
Pick ONE library and use it everywhere. Do not mix icon libraries within the same project.
|
|
1569
|
-
|
|
1570
|
-
### SVG Usage Rules
|
|
1571
|
-
|
|
1572
|
-
- All icons must be inline SVG components (not \`<img>\` tags) for color and size control.
|
|
1573
|
-
- Icon size follows the type scale: 16px (inline), 20px (buttons), 24px (standalone).
|
|
1574
|
-
- Icon color inherits from \`currentColor\` -- never hard-code fill/stroke colors.
|
|
1575
|
-
- For custom/brand icons, export as SVG components with \`currentColor\` fills.
|
|
1576
|
-
- Stroke width: 1.5px-2px for outline icons. Keep consistent across the project.
|
|
1577
|
-
|
|
1578
|
-
### Icon Sizing Scale
|
|
1579
|
-
|
|
1580
|
-
| Context | Size | Usage |
|
|
1581
|
-
|---------|------|-------|
|
|
1582
|
-
| Inline text | 16px (1rem) | Badges, labels, breadcrumbs |
|
|
1583
|
-
| Button icon | 18px (1.125rem) | Icon buttons, CTA icons |
|
|
1584
|
-
| Standalone | 24px (1.5rem) | Navigation, card icons |
|
|
1585
|
-
| Feature | 32-48px | Hero sections, empty states |
|
|
1586
|
-
|
|
1587
|
-
### SVG Optimization
|
|
1588
|
-
|
|
1589
|
-
- Run all custom SVGs through SVGO before committing.
|
|
1590
|
-
- Remove unnecessary attributes: \`xmlns\`, \`xml:space\`, editor metadata.
|
|
1591
|
-
- Use \`viewBox\` instead of fixed \`width\`/\`height\` for scalability.
|
|
1592
|
-
`;
|
|
1593
|
-
}
|
|
1594
|
-
function buildDocumentPolicies() {
|
|
1595
|
-
return `
|
|
1596
|
-
|
|
1597
|
-
---
|
|
1598
|
-
|
|
1599
|
-
## Document Policies
|
|
1600
|
-
|
|
1601
|
-
### No Emojis
|
|
1602
|
-
|
|
1603
|
-
This design system must not use emojis in any UI element, component, label, status indicator, or documentation.
|
|
1604
|
-
Use SVG icons from the chosen icon library instead. Emojis render inconsistently across platforms and break visual coherence.
|
|
1605
|
-
|
|
1606
|
-
- Status indicators: use colored dots or icon components, not emoji.
|
|
1607
|
-
- Section markers: use text prefixes ("DO:" / "DON'T:") or icons, not checkmark/cross emojis.
|
|
1608
|
-
- Navigation: use icon components, not emoji.
|
|
1609
|
-
|
|
1610
|
-
### Format Compliance
|
|
1611
|
-
|
|
1612
|
-
This document follows the Google Stitch DESIGN.md 9-section format:
|
|
1613
|
-
1. Visual Theme & Atmosphere
|
|
1614
|
-
2. Color Palette & Roles
|
|
1615
|
-
3. Typography Rules
|
|
1616
|
-
4. Component Stylings
|
|
1617
|
-
5. Layout Principles
|
|
1618
|
-
6. Depth & Elevation
|
|
1619
|
-
7. Do's and Don'ts
|
|
1620
|
-
8. Responsive Behavior
|
|
1621
|
-
9. Agent Prompt Guide
|
|
1622
|
-
|
|
1623
|
-
Extended with:
|
|
1624
|
-
- Iconography & SVG Guidelines
|
|
1625
|
-
- Document Policies
|
|
1626
|
-
|
|
1627
|
-
Total target length: 250-400 lines. Keep sections concise and actionable.
|
|
1628
|
-
`;
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
// src/presets/_base.ts
|
|
1632
|
-
var basePreset = {
|
|
1633
|
-
name: "base",
|
|
1634
|
-
displayName: "Base",
|
|
1635
|
-
description: "Shared defaults inherited by all presets",
|
|
1636
|
-
preferences: {
|
|
1637
|
-
mood: "clean",
|
|
1638
|
-
primaryColor: "#2563eb",
|
|
1639
|
-
roundness: "moderate",
|
|
1640
|
-
density: "comfortable",
|
|
1641
|
-
typography: "geometric",
|
|
1642
|
-
depth: "subtle",
|
|
1643
|
-
darkMode: false,
|
|
1644
|
-
components: ["button", "card", "input"]
|
|
1645
|
-
},
|
|
1646
|
-
dosAndDonts: {
|
|
1647
|
-
dos: [
|
|
1648
|
-
"Maintain consistent spacing throughout all components",
|
|
1649
|
-
"Use semantic color tokens instead of raw hex values",
|
|
1650
|
-
"Ensure all interactive elements have visible focus indicators",
|
|
1651
|
-
"Keep text contrast ratio at WCAG AA minimum (4.5:1)",
|
|
1652
|
-
"Use the defined type scale \u2014 avoid arbitrary font sizes"
|
|
1653
|
-
],
|
|
1654
|
-
donts: [
|
|
1655
|
-
"Mix border-radius styles within the same surface level",
|
|
1656
|
-
"Use more than 3 font weights on a single page",
|
|
1657
|
-
"Apply shadows and borders simultaneously on the same element",
|
|
1658
|
-
"Override semantic colors with hard-coded values",
|
|
1659
|
-
"Nest elevated surfaces more than 2 levels deep"
|
|
1660
|
-
]
|
|
1661
|
-
}
|
|
1662
|
-
};
|
|
1663
|
-
|
|
1664
|
-
// src/presets/fintech-premium.ts
|
|
1665
|
-
var fintechPremiumPreset = {
|
|
1666
|
-
name: "fintech-premium",
|
|
1667
|
-
displayName: "Fintech Premium",
|
|
1668
|
-
description: "Refined, trustworthy aesthetic with blue-tinted shadows and conservative radius \u2014 inspired by fintech dashboards",
|
|
1669
|
-
preferences: {
|
|
1670
|
-
mood: "clean",
|
|
1671
|
-
primaryColor: "#635bff",
|
|
1672
|
-
roundness: "moderate",
|
|
1673
|
-
density: "comfortable",
|
|
1674
|
-
typography: "geometric",
|
|
1675
|
-
depth: "layered",
|
|
1676
|
-
darkMode: true,
|
|
1677
|
-
components: ["button", "card", "input", "table", "badge", "dropdown", "tabs"]
|
|
1678
|
-
},
|
|
1679
|
-
dosAndDonts: {
|
|
1680
|
-
dos: [
|
|
1681
|
-
"Use generous whitespace to convey premium feel",
|
|
1682
|
-
"Rely on typography hierarchy over decorative elements",
|
|
1683
|
-
"Keep interactive states subtle \u2014 opacity shifts over color jumps",
|
|
1684
|
-
"Use the primary color sparingly for key CTAs only",
|
|
1685
|
-
"Present data in clean, well-aligned tables with minimal borders"
|
|
1686
|
-
],
|
|
1687
|
-
donts: [
|
|
1688
|
-
"Use bright, saturated accent colors for large surface areas",
|
|
1689
|
-
"Mix warm and cool grays \u2014 stick to one temperature",
|
|
1690
|
-
"Add decorative gradients or patterns to functional surfaces",
|
|
1691
|
-
"Use pill-shaped buttons for primary actions",
|
|
1692
|
-
"Over-decorate table rows \u2014 let the data breathe"
|
|
1693
|
-
]
|
|
1694
|
-
}
|
|
1695
|
-
};
|
|
1696
|
-
|
|
1697
|
-
// src/presets/developer-dark.ts
|
|
1698
|
-
var developerDarkPreset = {
|
|
1699
|
-
name: "developer-dark",
|
|
1700
|
-
displayName: "Developer Dark",
|
|
1701
|
-
description: "High-contrast dark theme with monospace accents and sharp edges \u2014 inspired by developer tools and terminals",
|
|
1702
|
-
preferences: {
|
|
1703
|
-
mood: "dark",
|
|
1704
|
-
primaryColor: "#ffffff",
|
|
1705
|
-
roundness: "sharp",
|
|
1706
|
-
density: "compact",
|
|
1707
|
-
typography: "monospace",
|
|
1708
|
-
depth: "subtle",
|
|
1709
|
-
darkMode: false,
|
|
1710
|
-
// already dark by default
|
|
1711
|
-
components: ["button", "card", "input", "table", "badge", "tabs", "toast"]
|
|
1712
|
-
},
|
|
1713
|
-
dosAndDonts: {
|
|
1714
|
-
dos: [
|
|
1715
|
-
"Use high contrast between foreground text and dark backgrounds",
|
|
1716
|
-
"Leverage monospace type for code, data, and labels",
|
|
1717
|
-
"Keep borders thin (1px) and use subtle color separation",
|
|
1718
|
-
"Use accent color only for interactive elements and focus rings",
|
|
1719
|
-
"Maintain sharp corners throughout \u2014 consistency matters"
|
|
1720
|
-
],
|
|
1721
|
-
donts: [
|
|
1722
|
-
"Use rounded or pill-shaped elements \u2014 they conflict with the sharp aesthetic",
|
|
1723
|
-
"Apply warm tones to the neutral palette",
|
|
1724
|
-
"Use heavy shadows on dark backgrounds \u2014 they become invisible",
|
|
1725
|
-
"Mix serif fonts into the interface",
|
|
1726
|
-
"Use light gray text on dark backgrounds below 4.5:1 contrast ratio"
|
|
1727
|
-
]
|
|
1728
|
-
}
|
|
1729
|
-
};
|
|
1730
|
-
|
|
1731
|
-
// src/presets/productivity-warm.ts
|
|
1732
|
-
var productivityWarmPreset = {
|
|
1733
|
-
name: "productivity-warm",
|
|
1734
|
-
displayName: "Productivity Warm",
|
|
1735
|
-
description: "Warm neutrals with friendly radius and comfortable density \u2014 inspired by modern productivity tools",
|
|
1736
|
-
preferences: {
|
|
1737
|
-
mood: "warm",
|
|
1738
|
-
primaryColor: "#2f81f7",
|
|
1739
|
-
roundness: "rounded",
|
|
1740
|
-
density: "comfortable",
|
|
1741
|
-
typography: "humanist",
|
|
1742
|
-
depth: "subtle",
|
|
1743
|
-
darkMode: true,
|
|
1744
|
-
components: ["button", "card", "input", "dropdown", "dialog", "navigation", "badge"]
|
|
1745
|
-
},
|
|
1746
|
-
dosAndDonts: {
|
|
1747
|
-
dos: [
|
|
1748
|
-
"Use warm gray tones (#faf9f7, #37352f) over pure black/white",
|
|
1749
|
-
"Keep interactions gentle \u2014 soft hover states, smooth transitions",
|
|
1750
|
-
"Use rounded corners consistently for a friendly, approachable feel",
|
|
1751
|
-
"Emphasize readability with generous line-height (1.6+)",
|
|
1752
|
-
"Layer content with subtle background tints rather than hard borders"
|
|
1753
|
-
],
|
|
1754
|
-
donts: [
|
|
1755
|
-
"Use sharp corners or angular design elements",
|
|
1756
|
-
"Apply cold, saturated blues for large background areas",
|
|
1757
|
-
"Use heavy shadows that make the UI feel oppressive",
|
|
1758
|
-
"Over-use the primary color \u2014 warm UIs need muted palettes",
|
|
1759
|
-
"Make interactive elements too small \u2014 comfortable density means generous targets"
|
|
1760
|
-
]
|
|
1761
|
-
}
|
|
1762
|
-
};
|
|
1763
|
-
|
|
1764
|
-
// src/presets/data-dense.ts
|
|
1765
|
-
var dataDensePreset = {
|
|
1766
|
-
name: "data-dense",
|
|
1767
|
-
displayName: "Data Dense",
|
|
1768
|
-
description: "Compact spacing with flat depth and geometric sans \u2014 inspired by project management and data-heavy interfaces",
|
|
1769
|
-
preferences: {
|
|
1770
|
-
mood: "clean",
|
|
1771
|
-
primaryColor: "#5e6ad2",
|
|
1772
|
-
roundness: "sharp",
|
|
1773
|
-
density: "compact",
|
|
1774
|
-
typography: "geometric",
|
|
1775
|
-
depth: "flat",
|
|
1776
|
-
darkMode: true,
|
|
1777
|
-
components: ["button", "card", "input", "table", "badge", "tabs", "select", "dropdown"]
|
|
1778
|
-
},
|
|
1779
|
-
dosAndDonts: {
|
|
1780
|
-
dos: [
|
|
1781
|
-
"Pack information densely \u2014 every pixel should earn its place",
|
|
1782
|
-
"Use small type sizes (12-13px) for data cells and metadata",
|
|
1783
|
-
"Rely on borders and background tints over shadows for separation",
|
|
1784
|
-
"Keep interactive elements compact but above 28px minimum height",
|
|
1785
|
-
"Use keyboard shortcuts and dense navigation patterns"
|
|
1786
|
-
],
|
|
1787
|
-
donts: [
|
|
1788
|
-
"Add decorative spacing between dense data elements",
|
|
1789
|
-
"Use large rounded corners \u2014 they waste space in tight layouts",
|
|
1790
|
-
"Apply heavy shadows that add visual weight to a dense interface",
|
|
1791
|
-
"Use more than 2 levels of visual nesting",
|
|
1792
|
-
"Mix compact and spacious components on the same surface"
|
|
1793
|
-
]
|
|
1794
|
-
}
|
|
1795
|
-
};
|
|
1796
|
-
|
|
1797
|
-
// src/presets/playful-creative.ts
|
|
1798
|
-
var playfulCreativePreset = {
|
|
1799
|
-
name: "playful-creative",
|
|
1800
|
-
displayName: "Playful Creative",
|
|
1801
|
-
description: "Rounded corners, vibrant colors, and layered depth \u2014 inspired by creative and design tools",
|
|
1802
|
-
preferences: {
|
|
1803
|
-
mood: "playful",
|
|
1804
|
-
primaryColor: "#a855f7",
|
|
1805
|
-
roundness: "rounded",
|
|
1806
|
-
density: "comfortable",
|
|
1807
|
-
typography: "humanist",
|
|
1808
|
-
depth: "layered",
|
|
1809
|
-
darkMode: true,
|
|
1810
|
-
components: ["button", "card", "input", "badge", "dialog", "toast", "floating-button"]
|
|
1811
|
-
},
|
|
1812
|
-
dosAndDonts: {
|
|
1813
|
-
dos: [
|
|
1814
|
-
"Use vibrant, saturated colors for interactive elements",
|
|
1815
|
-
"Apply generous border-radius to convey friendliness",
|
|
1816
|
-
"Layer surfaces with distinct shadows for spatial clarity",
|
|
1817
|
-
"Use playful micro-interactions (scale, bounce, color shift)",
|
|
1818
|
-
"Mix 2-3 accent colors for visual variety within the palette"
|
|
1819
|
-
],
|
|
1820
|
-
donts: [
|
|
1821
|
-
"Use sharp corners \u2014 they contradict the playful mood",
|
|
1822
|
-
"Make the palette too muted or desaturated",
|
|
1823
|
-
"Use flat depth \u2014 layered shadows are part of the personality",
|
|
1824
|
-
"Overcrowd layouts \u2014 playful needs room to breathe",
|
|
1825
|
-
"Use formal serif fonts for UI text"
|
|
1826
|
-
]
|
|
1827
|
-
}
|
|
1828
|
-
};
|
|
1829
|
-
|
|
1830
|
-
// src/presets/brutalist.ts
|
|
1831
|
-
var brutalistPreset = {
|
|
1832
|
-
name: "brutalist",
|
|
1833
|
-
displayName: "Brutalist",
|
|
1834
|
-
description: "No radius, flat surfaces, monospace type, high-contrast borders \u2014 anti-design aesthetic",
|
|
1835
|
-
preferences: {
|
|
1836
|
-
mood: "bold",
|
|
1837
|
-
primaryColor: "#000000",
|
|
1838
|
-
roundness: "sharp",
|
|
1839
|
-
density: "comfortable",
|
|
1840
|
-
typography: "monospace",
|
|
1841
|
-
depth: "flat",
|
|
1842
|
-
darkMode: false,
|
|
1843
|
-
components: ["button", "card", "input", "table", "badge"]
|
|
1844
|
-
},
|
|
1845
|
-
dosAndDonts: {
|
|
1846
|
-
dos: [
|
|
1847
|
-
"Use thick borders (2-3px) as the primary visual separator",
|
|
1848
|
-
"Keep all corners sharp \u2014 0px radius everywhere",
|
|
1849
|
-
"Use monospace type for all text, including headings",
|
|
1850
|
-
"Embrace high contrast: black on white, white on black",
|
|
1851
|
-
"Let raw structure show \u2014 no decorative gradients or shadows"
|
|
1852
|
-
],
|
|
1853
|
-
donts: [
|
|
1854
|
-
"Add rounded corners, gradients, or subtle shadows",
|
|
1855
|
-
"Use pastel or muted color palettes",
|
|
1856
|
-
"Apply smooth transitions \u2014 prefer instant state changes",
|
|
1857
|
-
"Mix serif or humanist fonts into the interface",
|
|
1858
|
-
"Over-polish the UI \u2014 imperfection is intentional"
|
|
1859
|
-
]
|
|
1860
|
-
}
|
|
1861
|
-
};
|
|
1862
|
-
|
|
1863
|
-
// src/presets/corporate-clean.ts
|
|
1864
|
-
var corporateCleanPreset = {
|
|
1865
|
-
name: "corporate-clean",
|
|
1866
|
-
displayName: "Corporate Clean",
|
|
1867
|
-
description: "System fonts, subtle shadows, blue primary \u2014 reliable enterprise SaaS aesthetic",
|
|
1868
|
-
preferences: {
|
|
1869
|
-
mood: "clean",
|
|
1870
|
-
primaryColor: "#2563eb",
|
|
1871
|
-
roundness: "moderate",
|
|
1872
|
-
density: "comfortable",
|
|
1873
|
-
typography: "humanist",
|
|
1874
|
-
depth: "subtle",
|
|
1875
|
-
darkMode: true,
|
|
1876
|
-
components: ["button", "card", "input", "table", "dialog", "dropdown", "navigation", "tabs", "select", "badge"]
|
|
1877
|
-
},
|
|
1878
|
-
dosAndDonts: {
|
|
1879
|
-
dos: [
|
|
1880
|
-
"Use system fonts for maximum cross-platform consistency",
|
|
1881
|
-
"Keep the blue primary for trust and authority",
|
|
1882
|
-
"Apply subtle shadows for gentle depth without drama",
|
|
1883
|
-
"Maintain strict visual hierarchy with consistent spacing",
|
|
1884
|
-
"Design for accessibility first \u2014 WCAG AA minimum everywhere"
|
|
1885
|
-
],
|
|
1886
|
-
donts: [
|
|
1887
|
-
"Use trendy or experimental design patterns",
|
|
1888
|
-
"Apply vibrant accent colors to large surfaces",
|
|
1889
|
-
"Use custom or decorative fonts \u2014 system fonts are intentional",
|
|
1890
|
-
"Create dramatic visual effects (large shadows, animations)",
|
|
1891
|
-
"Sacrifice clarity for visual flair"
|
|
1892
|
-
]
|
|
1893
|
-
}
|
|
1894
|
-
};
|
|
1895
|
-
|
|
1896
|
-
// src/presets/index.ts
|
|
1897
|
-
var presetRegistry = /* @__PURE__ */ new Map([
|
|
1898
|
-
["fintech-premium", fintechPremiumPreset],
|
|
1899
|
-
["developer-dark", developerDarkPreset],
|
|
1900
|
-
["productivity-warm", productivityWarmPreset],
|
|
1901
|
-
["data-dense", dataDensePreset],
|
|
1902
|
-
["playful-creative", playfulCreativePreset],
|
|
1903
|
-
["brutalist", brutalistPreset],
|
|
1904
|
-
["corporate-clean", corporateCleanPreset]
|
|
1905
|
-
]);
|
|
1906
|
-
function getPreset(name) {
|
|
1907
|
-
return presetRegistry.get(name);
|
|
1908
|
-
}
|
|
1909
|
-
function getAllPresets() {
|
|
1910
|
-
return Array.from(presetRegistry.values());
|
|
1911
|
-
}
|
|
1912
|
-
function mergeWithBase(preset) {
|
|
1913
|
-
return {
|
|
1914
|
-
preferences: { ...basePreset.preferences, ...preset.preferences },
|
|
1915
|
-
dosAndDonts: {
|
|
1916
|
-
dos: [...basePreset.dosAndDonts?.dos ?? [], ...preset.dosAndDonts?.dos ?? []],
|
|
1917
|
-
donts: [...basePreset.dosAndDonts?.donts ?? [], ...preset.dosAndDonts?.donts ?? []]
|
|
1918
|
-
}
|
|
1919
|
-
};
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
// src/core/sync-marker.ts
|
|
1923
|
-
import { createHash } from "crypto";
|
|
1924
|
-
var BLOCK_RE = /<!-- omd:start v=(\d+) hash=([a-f0-9]+) -->\n?([\s\S]*?)\n?<!-- omd:end -->/;
|
|
1925
|
-
function hashContent(content) {
|
|
1926
|
-
return createHash("sha256").update(content, "utf8").digest("hex").slice(0, 12);
|
|
1927
|
-
}
|
|
1928
|
-
function parseBlock(fileContent) {
|
|
1929
|
-
const m = BLOCK_RE.exec(fileContent);
|
|
1930
|
-
if (!m) return null;
|
|
1931
|
-
return {
|
|
1932
|
-
version: Number(m[1]),
|
|
1933
|
-
hash: m[2],
|
|
1934
|
-
content: m[3],
|
|
1935
|
-
rawStart: m.index,
|
|
1936
|
-
rawEnd: m.index + m[0].length
|
|
1937
|
-
};
|
|
1938
|
-
}
|
|
1939
|
-
function hasDrift(block) {
|
|
1940
|
-
return hashContent(block.content) !== block.hash;
|
|
1941
|
-
}
|
|
1942
|
-
function writeBlock(fileContent, managedContent, version) {
|
|
1943
|
-
const hash = hashContent(managedContent);
|
|
1944
|
-
const block = `<!-- omd:start v=${version} hash=${hash} -->
|
|
1945
|
-
${managedContent}
|
|
1946
|
-
<!-- omd:end -->`;
|
|
1947
|
-
const existing = parseBlock(fileContent);
|
|
1948
|
-
if (existing) {
|
|
1949
|
-
const updated = fileContent.slice(0, existing.rawStart) + block + fileContent.slice(existing.rawEnd);
|
|
1950
|
-
return { updated, hash };
|
|
1951
|
-
}
|
|
1952
|
-
if (fileContent === "") {
|
|
1953
|
-
return { updated: block + "\n", hash };
|
|
1954
|
-
}
|
|
1955
|
-
const sep = fileContent.endsWith("\n\n") ? "" : fileContent.endsWith("\n") ? "\n" : "\n\n";
|
|
1956
|
-
return { updated: fileContent + sep + block + "\n", hash };
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
// src/core/sync-lock.ts
|
|
1960
|
-
import { readFileSync as readFileSync3, writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
|
|
1961
|
-
import { dirname as dirname3, join as join3 } from "path";
|
|
1962
|
-
import { z } from "zod";
|
|
1963
|
-
var SYNC_LOCK_VERSION = 1;
|
|
1964
|
-
var SYNC_LOCK_PATH = ".omd/sync.lock.json";
|
|
1965
|
-
var TargetSchema = z.object({
|
|
1966
|
-
managed_hash: z.string(),
|
|
1967
|
-
last_synced: z.string()
|
|
1968
|
-
});
|
|
1969
|
-
var SyncLockSchema = z.object({
|
|
1970
|
-
version: z.number().int().positive(),
|
|
1971
|
-
design_md_hash: z.string(),
|
|
1972
|
-
targets: z.record(z.string(), TargetSchema)
|
|
1973
|
-
});
|
|
1974
|
-
function lockPath(projectRoot) {
|
|
1975
|
-
return join3(projectRoot, SYNC_LOCK_PATH);
|
|
1976
|
-
}
|
|
1977
|
-
function readLock(projectRoot) {
|
|
1978
|
-
const path = lockPath(projectRoot);
|
|
1979
|
-
if (!existsSync2(path)) return null;
|
|
1980
|
-
const raw = readFileSync3(path, "utf8");
|
|
1981
|
-
const parsed = JSON.parse(raw);
|
|
1982
|
-
return SyncLockSchema.parse(parsed);
|
|
1983
|
-
}
|
|
1984
|
-
function writeLock(projectRoot, lock) {
|
|
1985
|
-
const validated = SyncLockSchema.parse(lock);
|
|
1986
|
-
const path = lockPath(projectRoot);
|
|
1987
|
-
mkdirSync(dirname3(path), { recursive: true });
|
|
1988
|
-
writeFileSync(path, JSON.stringify(validated, null, 2) + "\n", "utf8");
|
|
1989
|
-
}
|
|
1990
|
-
function createLock(designMdHash) {
|
|
1991
|
-
return {
|
|
1992
|
-
version: SYNC_LOCK_VERSION,
|
|
1993
|
-
design_md_hash: designMdHash,
|
|
1994
|
-
targets: {}
|
|
1995
|
-
};
|
|
1996
|
-
}
|
|
1997
|
-
function updateTarget(projectRoot, target, managedHash) {
|
|
1998
|
-
const existing = readLock(projectRoot) ?? createLock("");
|
|
1999
|
-
const next = {
|
|
2000
|
-
...existing,
|
|
2001
|
-
targets: {
|
|
2002
|
-
...existing.targets,
|
|
2003
|
-
[target]: {
|
|
2004
|
-
managed_hash: managedHash,
|
|
2005
|
-
last_synced: (/* @__PURE__ */ new Date()).toISOString()
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
};
|
|
2009
|
-
writeLock(projectRoot, next);
|
|
2010
|
-
return next;
|
|
2011
|
-
}
|
|
2012
|
-
function updateDesignMdHash(projectRoot, designMdHash) {
|
|
2013
|
-
const existing = readLock(projectRoot) ?? createLock(designMdHash);
|
|
2014
|
-
const next = { ...existing, design_md_hash: designMdHash };
|
|
2015
|
-
writeLock(projectRoot, next);
|
|
2016
|
-
return next;
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
// src/core/shims.ts
|
|
2020
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
|
|
2021
|
-
import { dirname as dirname4, join as join4 } from "path";
|
|
2022
|
-
var MANAGED_BLOCK_VERSION = 1;
|
|
2023
|
-
var CLAUDE_BODY = `# Design System (oh-my-design)
|
|
2024
|
-
|
|
2025
|
-
The authoritative brand & UI spec is **@./DESIGN.md**.
|
|
2026
|
-
Read before any UI/styling/microcopy/motion work.
|
|
2027
|
-
|
|
2028
|
-
Preference log (pending corrections): @./.omd/preferences.md
|
|
2029
|
-
|
|
2030
|
-
Precedence: DESIGN.md > preferences.md > your defaults.`;
|
|
2031
|
-
var AGENTS_BODY = `## Design System (oh-my-design)
|
|
2032
|
-
|
|
2033
|
-
**Before any UI, styling, copy, or motion change, open and read \`./DESIGN.md\` in full.** It is the authoritative brand/design spec. Treat its tokens, voice, and component rules as binding unless the user overrides in chat.
|
|
2034
|
-
|
|
2035
|
-
If present, read \`./.omd/preferences.md\` \u2014 pending corrections not yet folded into DESIGN.md. Apply them; flag conflicts.`;
|
|
2036
|
-
var CURSOR_FRONTMATTER = `---
|
|
2037
|
-
description: Authoritative brand & UI design system. Read DESIGN.md before UI work.
|
|
2038
|
-
globs:
|
|
2039
|
-
- "**/*.tsx"
|
|
2040
|
-
- "**/*.jsx"
|
|
2041
|
-
- "**/*.vue"
|
|
2042
|
-
- "**/*.svelte"
|
|
2043
|
-
- "**/*.css"
|
|
2044
|
-
- "**/*.scss"
|
|
2045
|
-
- "**/tailwind.config.*"
|
|
2046
|
-
- "**/components/**"
|
|
2047
|
-
- "**/app/**/page.*"
|
|
2048
|
-
alwaysApply: false
|
|
2049
|
-
---`;
|
|
2050
|
-
var CURSOR_BODY = `The authoritative design spec lives at \`@DESIGN.md\` (repo root). Open and read before generating/modifying UI.
|
|
2051
|
-
|
|
2052
|
-
Pending preference corrections: \`@.omd/preferences.md\`.
|
|
2053
|
-
|
|
2054
|
-
Precedence: DESIGN.md > preferences.md > framework defaults.`;
|
|
2055
|
-
var CLAUDE_SHIM = {
|
|
2056
|
-
id: "claude",
|
|
2057
|
-
relPath: "CLAUDE.md",
|
|
2058
|
-
mode: "block",
|
|
2059
|
-
render: () => CLAUDE_BODY
|
|
2060
|
-
};
|
|
2061
|
-
var AGENTS_SHIM = {
|
|
2062
|
-
id: "agents",
|
|
2063
|
-
relPath: "AGENTS.md",
|
|
2064
|
-
mode: "block",
|
|
2065
|
-
render: () => AGENTS_BODY
|
|
2066
|
-
};
|
|
2067
|
-
var CURSOR_SHIM = {
|
|
2068
|
-
id: "cursor",
|
|
2069
|
-
relPath: ".cursor/rules/omd-design.mdc",
|
|
2070
|
-
mode: "whole",
|
|
2071
|
-
render: () => {
|
|
2072
|
-
const hash = hashContent(CURSOR_BODY);
|
|
2073
|
-
return `${CURSOR_FRONTMATTER}
|
|
2074
|
-
|
|
2075
|
-
<!-- omd:start v=${MANAGED_BLOCK_VERSION} hash=${hash} -->
|
|
2076
|
-
${CURSOR_BODY}
|
|
2077
|
-
<!-- omd:end -->
|
|
2078
|
-
`;
|
|
2079
|
-
}
|
|
2080
|
-
};
|
|
2081
|
-
var ALL_SHIMS = [
|
|
2082
|
-
CLAUDE_SHIM,
|
|
2083
|
-
AGENTS_SHIM,
|
|
2084
|
-
CURSOR_SHIM
|
|
2085
|
-
];
|
|
2086
|
-
function resolvePath(projectRoot, relPath) {
|
|
2087
|
-
return join4(projectRoot, relPath);
|
|
2088
|
-
}
|
|
2089
|
-
function readFileOrEmpty(path) {
|
|
2090
|
-
return existsSync3(path) ? readFileSync4(path, "utf8") : "";
|
|
2091
|
-
}
|
|
2092
|
-
function ensureDir(path) {
|
|
2093
|
-
mkdirSync2(dirname4(path), { recursive: true });
|
|
2094
|
-
}
|
|
2095
|
-
function inspectShim(projectRoot, shim) {
|
|
2096
|
-
const abs = resolvePath(projectRoot, shim.relPath);
|
|
2097
|
-
const existing = readFileOrEmpty(abs);
|
|
2098
|
-
const fileExists = existing !== "";
|
|
2099
|
-
if (shim.mode === "whole") {
|
|
2100
|
-
const rendered = shim.render();
|
|
2101
|
-
if (!fileExists) {
|
|
2102
|
-
return { id: shim.id, path: abs, status: "missing", rendered };
|
|
2103
|
-
}
|
|
2104
|
-
if (existing === rendered) {
|
|
2105
|
-
return { id: shim.id, path: abs, status: "clean", existing, rendered };
|
|
2106
|
-
}
|
|
2107
|
-
return { id: shim.id, path: abs, status: "drifted", existing, rendered };
|
|
2108
|
-
}
|
|
2109
|
-
const managed = shim.render();
|
|
2110
|
-
const block = parseBlock(existing);
|
|
2111
|
-
if (!block) {
|
|
2112
|
-
return {
|
|
2113
|
-
id: shim.id,
|
|
2114
|
-
path: abs,
|
|
2115
|
-
status: "missing",
|
|
2116
|
-
existing: fileExists ? existing : void 0,
|
|
2117
|
-
rendered: managed
|
|
2118
|
-
};
|
|
2119
|
-
}
|
|
2120
|
-
if (hasDrift(block)) {
|
|
2121
|
-
return {
|
|
2122
|
-
id: shim.id,
|
|
2123
|
-
path: abs,
|
|
2124
|
-
status: "drifted",
|
|
2125
|
-
existing: block.content,
|
|
2126
|
-
rendered: managed
|
|
2127
|
-
};
|
|
2128
|
-
}
|
|
2129
|
-
if (block.content === managed) {
|
|
2130
|
-
return {
|
|
2131
|
-
id: shim.id,
|
|
2132
|
-
path: abs,
|
|
2133
|
-
status: "clean",
|
|
2134
|
-
existing: block.content,
|
|
2135
|
-
rendered: managed
|
|
2136
|
-
};
|
|
2137
|
-
}
|
|
2138
|
-
return {
|
|
2139
|
-
id: shim.id,
|
|
2140
|
-
path: abs,
|
|
2141
|
-
status: "out-of-date",
|
|
2142
|
-
existing: block.content,
|
|
2143
|
-
rendered: managed
|
|
2144
|
-
};
|
|
2145
|
-
}
|
|
2146
|
-
function inspectAllShims(projectRoot) {
|
|
2147
|
-
return ALL_SHIMS.map((s) => inspectShim(projectRoot, s));
|
|
2148
|
-
}
|
|
2149
|
-
function writeShim(projectRoot, shim, opts = {}) {
|
|
2150
|
-
const onDrift = opts.onDrift ?? "error";
|
|
2151
|
-
const abs = resolvePath(projectRoot, shim.relPath);
|
|
2152
|
-
const existing = readFileOrEmpty(abs);
|
|
2153
|
-
const fileExists = existing !== "";
|
|
2154
|
-
if (shim.mode === "whole") {
|
|
2155
|
-
const rendered = shim.render();
|
|
2156
|
-
const newHash = hashContent(rendered);
|
|
2157
|
-
if (fileExists && existing !== rendered) {
|
|
2158
|
-
const existingHash = hashContent(existing);
|
|
2159
|
-
if (onDrift === "error") {
|
|
2160
|
-
throw new Error(
|
|
2161
|
-
`drift detected in ${shim.relPath} (existing hash ${existingHash} != rendered ${newHash}); rerun with onDrift=overwrite to force`
|
|
2162
|
-
);
|
|
2163
|
-
}
|
|
2164
|
-
if (onDrift === "skip") {
|
|
2165
|
-
updateTarget(projectRoot, shim.relPath, existingHash);
|
|
2166
|
-
return {
|
|
2167
|
-
id: shim.id,
|
|
2168
|
-
path: abs,
|
|
2169
|
-
hash: existingHash,
|
|
2170
|
-
status: "skipped-drift"
|
|
2171
|
-
};
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
if (fileExists && existing === rendered) {
|
|
2175
|
-
updateTarget(projectRoot, shim.relPath, newHash);
|
|
2176
|
-
return { id: shim.id, path: abs, hash: newHash, status: "unchanged" };
|
|
2177
|
-
}
|
|
2178
|
-
ensureDir(abs);
|
|
2179
|
-
writeFileSync2(abs, rendered, "utf8");
|
|
2180
|
-
updateTarget(projectRoot, shim.relPath, newHash);
|
|
2181
|
-
return {
|
|
2182
|
-
id: shim.id,
|
|
2183
|
-
path: abs,
|
|
2184
|
-
hash: newHash,
|
|
2185
|
-
status: fileExists ? "updated" : "created"
|
|
2186
|
-
};
|
|
2187
|
-
}
|
|
2188
|
-
const managed = shim.render();
|
|
2189
|
-
const existingBlock = parseBlock(existing);
|
|
2190
|
-
if (existingBlock && hasDrift(existingBlock)) {
|
|
2191
|
-
if (onDrift === "error") {
|
|
2192
|
-
throw new Error(
|
|
2193
|
-
`managed block in ${shim.relPath} was hand-edited; rerun with onDrift=overwrite to force`
|
|
2194
|
-
);
|
|
2195
|
-
}
|
|
2196
|
-
if (onDrift === "skip") {
|
|
2197
|
-
updateTarget(projectRoot, shim.relPath, existingBlock.hash);
|
|
2198
|
-
return {
|
|
2199
|
-
id: shim.id,
|
|
2200
|
-
path: abs,
|
|
2201
|
-
hash: existingBlock.hash,
|
|
2202
|
-
status: "skipped-drift"
|
|
2203
|
-
};
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
if (existingBlock && existingBlock.content === managed && !hasDrift(existingBlock)) {
|
|
2207
|
-
updateTarget(projectRoot, shim.relPath, existingBlock.hash);
|
|
2208
|
-
return {
|
|
2209
|
-
id: shim.id,
|
|
2210
|
-
path: abs,
|
|
2211
|
-
hash: existingBlock.hash,
|
|
2212
|
-
status: "unchanged"
|
|
2213
|
-
};
|
|
2214
|
-
}
|
|
2215
|
-
const { updated, hash } = writeBlock(existing, managed, MANAGED_BLOCK_VERSION);
|
|
2216
|
-
ensureDir(abs);
|
|
2217
|
-
writeFileSync2(abs, updated, "utf8");
|
|
2218
|
-
updateTarget(projectRoot, shim.relPath, hash);
|
|
2219
|
-
return {
|
|
2220
|
-
id: shim.id,
|
|
2221
|
-
path: abs,
|
|
2222
|
-
hash,
|
|
2223
|
-
status: existingBlock ? "updated" : "created"
|
|
2224
|
-
};
|
|
2225
|
-
}
|
|
2226
|
-
function writeAllShims(projectRoot, opts = {}) {
|
|
2227
|
-
const results = ALL_SHIMS.map((shim) => writeShim(projectRoot, shim, opts));
|
|
2228
|
-
refreshDesignMdHash(projectRoot);
|
|
2229
|
-
return results;
|
|
2230
|
-
}
|
|
2231
|
-
function refreshDesignMdHash(projectRoot) {
|
|
2232
|
-
const designMdPath = join4(projectRoot, "DESIGN.md");
|
|
2233
|
-
if (!existsSync3(designMdPath)) return null;
|
|
2234
|
-
const hash = hashContent(readFileSync4(designMdPath, "utf8"));
|
|
2235
|
-
updateDesignMdHash(projectRoot, hash);
|
|
2236
|
-
return hash;
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
// src/core/preferences.ts
|
|
2240
|
-
import {
|
|
2241
|
-
readFileSync as readFileSync5,
|
|
2242
|
-
writeFileSync as writeFileSync3,
|
|
2243
|
-
mkdirSync as mkdirSync3,
|
|
2244
|
-
existsSync as existsSync4
|
|
2245
|
-
} from "fs";
|
|
2246
|
-
import { randomBytes } from "crypto";
|
|
2247
|
-
import { dirname as dirname5, join as join5 } from "path";
|
|
2248
|
-
var PREFERENCES_PATH = ".omd/preferences.md";
|
|
2249
|
-
var PREFERENCES_SCHEMA = "omd.preferences/v1";
|
|
2250
|
-
function prefPath(projectRoot) {
|
|
2251
|
-
return join5(projectRoot, PREFERENCES_PATH);
|
|
2252
|
-
}
|
|
2253
|
-
function generateId() {
|
|
2254
|
-
const ts = Date.now().toString(36);
|
|
2255
|
-
const rand = randomBytes(4).toString("hex");
|
|
2256
|
-
return `pref_${ts}_${rand}`;
|
|
2257
|
-
}
|
|
2258
|
-
function slugify(input, max = 40) {
|
|
2259
|
-
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, max) || "entry";
|
|
2260
|
-
}
|
|
2261
|
-
var SCOPE_KEYWORDS = [
|
|
2262
|
-
[/\b(buttons?|ctas?|btns?)\b/i, "components.button"],
|
|
2263
|
-
[/\b(cards?)\b/i, "components.card"],
|
|
2264
|
-
[/\b(dialogs?|modals?)\b/i, "components.dialog"],
|
|
2265
|
-
[/\b(inputs?|fields?|forms?)\b/i, "components.input"],
|
|
2266
|
-
[/\b(nav|navigation|headers?|menus?)\b/i, "components.navigation"],
|
|
2267
|
-
[/\b(badges?|chips?|pills?|tags?)\b/i, "components.badge"],
|
|
2268
|
-
[/\b(tables?|rows?|cells?)\b/i, "components.table"],
|
|
2269
|
-
[/\b(dropdowns?|selects?|comboboxes?)\b/i, "components.dropdown"],
|
|
2270
|
-
[/\b(toasts?|notifications?|snackbars?)\b/i, "components.toast"],
|
|
2271
|
-
[/\b(tabs?)\b/i, "components.tabs"],
|
|
2272
|
-
[/\b(colors?|palette|hex|hue|saturation|shades?|tints?|gradients?)\b/i, "color"],
|
|
2273
|
-
[/\b(font|typography|typeface|weight|leading|tracking|letter-?spacing)\b/i, "typography"],
|
|
2274
|
-
[/\b(spacing|gap|padding|margin|grid)\b/i, "spacing"],
|
|
2275
|
-
[/\b(voice|tone|copy|microcopy|wording|language)\b/i, "voice"],
|
|
2276
|
-
[/\b(motion|animation|transition|easing|duration)\b/i, "motion"],
|
|
2277
|
-
[/\b(layout|structure|hierarchy)\b/i, "layout"],
|
|
2278
|
-
[/\b(theme|aesthetic|vibe|mood|look|feel)\b/i, "visualTheme"]
|
|
2279
|
-
];
|
|
2280
|
-
function inferScope(note) {
|
|
2281
|
-
for (const [re, scope] of SCOPE_KEYWORDS) {
|
|
2282
|
-
if (re.test(note)) return scope;
|
|
2283
|
-
}
|
|
2284
|
-
return "visualTheme";
|
|
2285
|
-
}
|
|
2286
|
-
function renderMeta(meta) {
|
|
2287
|
-
const lines = [];
|
|
2288
|
-
lines.push(`id: ${meta.id}`);
|
|
2289
|
-
lines.push(`timestamp: ${meta.timestamp}`);
|
|
2290
|
-
lines.push(`scope: ${meta.scope}`);
|
|
2291
|
-
lines.push(`signal: ${meta.signal}`);
|
|
2292
|
-
lines.push(`confidence: ${meta.confidence}`);
|
|
2293
|
-
lines.push(`status: ${meta.status}`);
|
|
2294
|
-
if (meta.source_agent) lines.push(`source_agent: ${meta.source_agent}`);
|
|
2295
|
-
if (meta.source_context)
|
|
2296
|
-
lines.push(`source_context: ${JSON.stringify(meta.source_context)}`);
|
|
2297
|
-
if (meta.applied_design_md_hash)
|
|
2298
|
-
lines.push(`applied_design_md_hash: ${meta.applied_design_md_hash}`);
|
|
2299
|
-
if (meta.applied_at) lines.push(`applied_at: ${meta.applied_at}`);
|
|
2300
|
-
if (meta.rejected_reason)
|
|
2301
|
-
lines.push(`rejected_reason: ${JSON.stringify(meta.rejected_reason)}`);
|
|
2302
|
-
if (meta.superseded_by) lines.push(`superseded_by: ${meta.superseded_by}`);
|
|
2303
|
-
return lines.join("\n");
|
|
2304
|
-
}
|
|
2305
|
-
function renderEntry(entry) {
|
|
2306
|
-
return `## ${entry.heading}
|
|
2307
|
-
|
|
2308
|
-
\`\`\`omd-meta
|
|
2309
|
-
` + renderMeta(entry.meta) + "\n```\n\n" + entry.body.trim() + "\n";
|
|
2310
|
-
}
|
|
2311
|
-
function renderFile(file) {
|
|
2312
|
-
const header = `---
|
|
2313
|
-
schema: ${file.schema}
|
|
2314
|
-
design_md_hash_at_creation: ${file.design_md_hash_at_creation}
|
|
2315
|
-
---
|
|
2316
|
-
|
|
2317
|
-
# Preference Log
|
|
2318
|
-
|
|
2319
|
-
`;
|
|
2320
|
-
const body = file.entries.map(renderEntry).join("\n");
|
|
2321
|
-
return header + body;
|
|
2322
|
-
}
|
|
2323
|
-
var FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/;
|
|
2324
|
-
var ENTRY_SPLIT_RE = /^## /m;
|
|
2325
|
-
var META_BLOCK_RE = /```omd-meta\n([\s\S]*?)\n```/;
|
|
2326
|
-
function parseFrontmatter(raw) {
|
|
2327
|
-
const m = FRONTMATTER_RE.exec(raw);
|
|
2328
|
-
if (!m) return { fields: {}, rest: raw };
|
|
2329
|
-
const fields = {};
|
|
2330
|
-
for (const line of m[1].split("\n")) {
|
|
2331
|
-
const idx = line.indexOf(":");
|
|
2332
|
-
if (idx === -1) continue;
|
|
2333
|
-
const key = line.slice(0, idx).trim();
|
|
2334
|
-
const val = line.slice(idx + 1).trim();
|
|
2335
|
-
fields[key] = val;
|
|
2336
|
-
}
|
|
2337
|
-
return { fields, rest: raw.slice(m[0].length) };
|
|
2338
|
-
}
|
|
2339
|
-
function parseMetaBlock(text) {
|
|
2340
|
-
const m = META_BLOCK_RE.exec(text);
|
|
2341
|
-
if (!m) return {};
|
|
2342
|
-
const fields = {};
|
|
2343
|
-
for (const line of m[1].split("\n")) {
|
|
2344
|
-
const idx = line.indexOf(":");
|
|
2345
|
-
if (idx === -1) continue;
|
|
2346
|
-
const key = line.slice(0, idx).trim();
|
|
2347
|
-
let val = line.slice(idx + 1).trim();
|
|
2348
|
-
if (val.startsWith('"') && val.endsWith('"')) {
|
|
2349
|
-
try {
|
|
2350
|
-
val = JSON.parse(val);
|
|
2351
|
-
} catch {
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
fields[key] = val;
|
|
2355
|
-
}
|
|
2356
|
-
return fields;
|
|
2357
|
-
}
|
|
2358
|
-
function parseEntry(chunk) {
|
|
2359
|
-
const newline = chunk.indexOf("\n");
|
|
2360
|
-
const heading = (newline === -1 ? chunk : chunk.slice(0, newline)).trim();
|
|
2361
|
-
const rest = newline === -1 ? "" : chunk.slice(newline + 1);
|
|
2362
|
-
const metaFields = parseMetaBlock(rest);
|
|
2363
|
-
if (!metaFields.id) return null;
|
|
2364
|
-
const bodyStart = rest.search(META_BLOCK_RE);
|
|
2365
|
-
const metaEnd = bodyStart >= 0 ? bodyStart + (META_BLOCK_RE.exec(rest.slice(bodyStart))?.[0].length ?? 0) : 0;
|
|
2366
|
-
const body = rest.slice(metaEnd).trim();
|
|
2367
|
-
return {
|
|
2368
|
-
heading,
|
|
2369
|
-
body,
|
|
2370
|
-
meta: {
|
|
2371
|
-
id: metaFields.id,
|
|
2372
|
-
timestamp: metaFields.timestamp ?? "",
|
|
2373
|
-
scope: metaFields.scope ?? "visualTheme",
|
|
2374
|
-
signal: metaFields.signal ?? "user-statement",
|
|
2375
|
-
confidence: metaFields.confidence ?? "explicit",
|
|
2376
|
-
status: metaFields.status ?? "pending",
|
|
2377
|
-
source_agent: metaFields.source_agent,
|
|
2378
|
-
source_context: metaFields.source_context,
|
|
2379
|
-
applied_design_md_hash: metaFields.applied_design_md_hash,
|
|
2380
|
-
applied_at: metaFields.applied_at,
|
|
2381
|
-
rejected_reason: metaFields.rejected_reason,
|
|
2382
|
-
superseded_by: metaFields.superseded_by
|
|
2383
|
-
}
|
|
2384
|
-
};
|
|
2385
|
-
}
|
|
2386
|
-
function parseFile(raw) {
|
|
2387
|
-
const { fields, rest } = parseFrontmatter(raw);
|
|
2388
|
-
const schema = fields.schema || PREFERENCES_SCHEMA;
|
|
2389
|
-
const design_md_hash_at_creation = fields.design_md_hash_at_creation ?? "";
|
|
2390
|
-
const chunks = rest.split(ENTRY_SPLIT_RE).slice(1);
|
|
2391
|
-
const entries = chunks.map((c) => parseEntry(c)).filter((e) => e !== null);
|
|
2392
|
-
return { schema, design_md_hash_at_creation, entries };
|
|
2393
|
-
}
|
|
2394
|
-
function readFile(projectRoot) {
|
|
2395
|
-
const path = prefPath(projectRoot);
|
|
2396
|
-
if (!existsSync4(path)) return null;
|
|
2397
|
-
return parseFile(readFileSync5(path, "utf8"));
|
|
2398
|
-
}
|
|
2399
|
-
function writeFile(projectRoot, file) {
|
|
2400
|
-
const path = prefPath(projectRoot);
|
|
2401
|
-
mkdirSync3(dirname5(path), { recursive: true });
|
|
2402
|
-
writeFileSync3(path, renderFile(file), "utf8");
|
|
2403
|
-
}
|
|
2404
|
-
function buildEntry(input) {
|
|
2405
|
-
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
2406
|
-
const timestamp = now.toISOString();
|
|
2407
|
-
const scope = input.scope ?? inferScope(input.note);
|
|
2408
|
-
const slug = slugify(input.note);
|
|
2409
|
-
const heading = `${timestamp} \u2014 ${slug}`;
|
|
2410
|
-
return {
|
|
2411
|
-
heading,
|
|
2412
|
-
body: input.note.trim(),
|
|
2413
|
-
meta: {
|
|
2414
|
-
id: generateId(),
|
|
2415
|
-
timestamp,
|
|
2416
|
-
scope,
|
|
2417
|
-
signal: input.signal ?? "user-statement",
|
|
2418
|
-
confidence: input.confidence ?? "explicit",
|
|
2419
|
-
status: "pending",
|
|
2420
|
-
source_agent: input.source_agent,
|
|
2421
|
-
source_context: input.source_context
|
|
2422
|
-
}
|
|
2423
|
-
};
|
|
2424
|
-
}
|
|
2425
|
-
function updateEntryStatus(projectRoot, input) {
|
|
2426
|
-
const file = readFile(projectRoot);
|
|
2427
|
-
if (!file) {
|
|
2428
|
-
throw new Error(
|
|
2429
|
-
`no preferences file found at ${projectRoot}/${PREFERENCES_PATH}`
|
|
2430
|
-
);
|
|
2431
|
-
}
|
|
2432
|
-
const entry = file.entries.find((e) => e.meta.id === input.id);
|
|
2433
|
-
if (!entry) {
|
|
2434
|
-
throw new Error(`preference id not found: ${input.id}`);
|
|
2435
|
-
}
|
|
2436
|
-
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2437
|
-
entry.meta.status = input.status;
|
|
2438
|
-
if (input.status === "applied") {
|
|
2439
|
-
entry.meta.applied_at = now;
|
|
2440
|
-
if (input.applied_design_md_hash) {
|
|
2441
|
-
entry.meta.applied_design_md_hash = input.applied_design_md_hash;
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
if (input.status === "rejected" && input.rejected_reason) {
|
|
2445
|
-
entry.meta.rejected_reason = input.rejected_reason;
|
|
2446
|
-
}
|
|
2447
|
-
if (input.status === "superseded" && input.superseded_by) {
|
|
2448
|
-
entry.meta.superseded_by = input.superseded_by;
|
|
2449
|
-
}
|
|
2450
|
-
writeFile(projectRoot, file);
|
|
2451
|
-
return entry;
|
|
2452
|
-
}
|
|
2453
|
-
function groupByScope(entries) {
|
|
2454
|
-
const map = /* @__PURE__ */ new Map();
|
|
2455
|
-
for (const entry of entries) {
|
|
2456
|
-
const bucket = map.get(entry.meta.scope);
|
|
2457
|
-
if (bucket) bucket.push(entry);
|
|
2458
|
-
else map.set(entry.meta.scope, [entry]);
|
|
2459
|
-
}
|
|
2460
|
-
return map;
|
|
2461
|
-
}
|
|
2462
|
-
function filterByStatus(entries, status) {
|
|
2463
|
-
return entries.filter((e) => e.meta.status === status);
|
|
2464
|
-
}
|
|
2465
|
-
function appendEntry(projectRoot, input, designMdHashIfNew = "") {
|
|
2466
|
-
const existing = readFile(projectRoot);
|
|
2467
|
-
const entry = buildEntry(input);
|
|
2468
|
-
const file = existing ?? {
|
|
2469
|
-
schema: PREFERENCES_SCHEMA,
|
|
2470
|
-
design_md_hash_at_creation: designMdHashIfNew,
|
|
2471
|
-
entries: []
|
|
2472
|
-
};
|
|
2473
|
-
file.entries.push(entry);
|
|
2474
|
-
writeFile(projectRoot, file);
|
|
2475
|
-
return entry;
|
|
2476
|
-
}
|
|
2477
|
-
|
|
2478
|
-
// src/core/agent-detect.ts
|
|
2479
|
-
import { existsSync as existsSync5 } from "fs";
|
|
2480
|
-
import { join as join6 } from "path";
|
|
2481
|
-
function detectCallingAgent() {
|
|
2482
|
-
const env = process.env;
|
|
2483
|
-
if (env.CLAUDECODE === "1" || env.CLAUDE_CODE === "1" || env.CLAUDE_CODE_TASK_ID) {
|
|
2484
|
-
return "claude-code";
|
|
2485
|
-
}
|
|
2486
|
-
if (env.CODEX_SESSION_ID || env.CODEX || env.OPENAI_CODEX) {
|
|
2487
|
-
return "codex";
|
|
2488
|
-
}
|
|
2489
|
-
if (env.OPENCODE || env.OPENCODE_SESSION) {
|
|
2490
|
-
return "opencode";
|
|
2491
|
-
}
|
|
2492
|
-
if (env.CURSOR_SESSION_ID || env.CURSOR_AGENT) {
|
|
2493
|
-
return "cursor";
|
|
2494
|
-
}
|
|
2495
|
-
return "unknown";
|
|
2496
|
-
}
|
|
2497
|
-
function detectInstalledAgents(projectRoot) {
|
|
2498
|
-
return {
|
|
2499
|
-
claudeCode: existsSync5(join6(projectRoot, ".claude")) || existsSync5(join6(projectRoot, "CLAUDE.md")),
|
|
2500
|
-
codex: existsSync5(join6(projectRoot, ".codex")) || existsSync5(join6(projectRoot, "AGENTS.md")) || existsSync5(join6(projectRoot, "AGENTS.override.md")),
|
|
2501
|
-
opencode: existsSync5(join6(projectRoot, ".opencode")) || existsSync5(join6(projectRoot, "opencode.json")) || existsSync5(join6(projectRoot, "opencode.jsonc")),
|
|
2502
|
-
cursor: existsSync5(join6(projectRoot, ".cursor")) || existsSync5(join6(projectRoot, ".cursorrules"))
|
|
2503
|
-
};
|
|
2504
|
-
}
|
|
2505
|
-
|
|
2506
|
-
// src/core/vocabulary.ts
|
|
2507
|
-
import { readFileSync as readFileSync6 } from "fs";
|
|
2508
|
-
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2509
|
-
import { dirname as dirname6, join as join7 } from "path";
|
|
2510
|
-
var cachedVocab = null;
|
|
2511
|
-
var cachedSynonyms = null;
|
|
2512
|
-
function dataDir() {
|
|
2513
|
-
const here = dirname6(fileURLToPath3(import.meta.url));
|
|
2514
|
-
const candidates = [
|
|
2515
|
-
join7(here, "..", "data"),
|
|
2516
|
-
join7(here, "..", "..", "data")
|
|
2517
|
-
];
|
|
2518
|
-
for (const c of candidates) {
|
|
2519
|
-
try {
|
|
2520
|
-
readFileSync6(join7(c, "vocabulary.json"), "utf8");
|
|
2521
|
-
return c;
|
|
2522
|
-
} catch {
|
|
2523
|
-
}
|
|
2524
|
-
}
|
|
2525
|
-
throw new Error("data/vocabulary.json not found relative to " + here);
|
|
2526
|
-
}
|
|
2527
|
-
function loadVocabulary() {
|
|
2528
|
-
if (cachedVocab) return cachedVocab;
|
|
2529
|
-
const path = join7(dataDir(), "vocabulary.json");
|
|
2530
|
-
cachedVocab = JSON.parse(readFileSync6(path, "utf8"));
|
|
2531
|
-
return cachedVocab;
|
|
2532
|
-
}
|
|
2533
|
-
function loadSynonyms() {
|
|
2534
|
-
if (cachedSynonyms) return cachedSynonyms;
|
|
2535
|
-
const path = join7(dataDir(), "synonyms.json");
|
|
2536
|
-
cachedSynonyms = JSON.parse(readFileSync6(path, "utf8"));
|
|
2537
|
-
return cachedSynonyms;
|
|
2538
|
-
}
|
|
2539
|
-
var MODIFIER_RE = /\b(primarily|mostly|slightly|very|not)\s+([a-z][a-z-]*)/gi;
|
|
2540
|
-
function tokenize(description) {
|
|
2541
|
-
return description.toLowerCase().split(/[^a-z-]+/).filter(Boolean);
|
|
2542
|
-
}
|
|
2543
|
-
function extractKeywords(description) {
|
|
2544
|
-
const vocab = loadVocabulary();
|
|
2545
|
-
const { map: synonyms } = loadSynonyms();
|
|
2546
|
-
const lower = description.toLowerCase();
|
|
2547
|
-
const modifierOverrides = /* @__PURE__ */ new Map();
|
|
2548
|
-
let match;
|
|
2549
|
-
const modRe = new RegExp(MODIFIER_RE.source, MODIFIER_RE.flags);
|
|
2550
|
-
while ((match = modRe.exec(lower)) !== null) {
|
|
2551
|
-
const modName = match[1];
|
|
2552
|
-
const target = match[2];
|
|
2553
|
-
const value = vocab.modifiers[modName];
|
|
2554
|
-
if (value !== void 0) modifierOverrides.set(target, value);
|
|
2555
|
-
}
|
|
2556
|
-
const tokens = tokenize(description);
|
|
2557
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2558
|
-
const results = [];
|
|
2559
|
-
for (const token of tokens) {
|
|
2560
|
-
if (seen.has(token)) continue;
|
|
2561
|
-
seen.add(token);
|
|
2562
|
-
if (vocab.keywords[token]) {
|
|
2563
|
-
results.push({
|
|
2564
|
-
keyword: token,
|
|
2565
|
-
modifier: modifierOverrides.get(token) ?? 1,
|
|
2566
|
-
matchedAs: "direct"
|
|
2567
|
-
});
|
|
2568
|
-
continue;
|
|
2569
|
-
}
|
|
2570
|
-
const syn = synonyms[token];
|
|
2571
|
-
if (syn && vocab.keywords[syn]) {
|
|
2572
|
-
results.push({
|
|
2573
|
-
keyword: syn,
|
|
2574
|
-
modifier: modifierOverrides.get(token) ?? 1,
|
|
2575
|
-
matchedAs: "synonym",
|
|
2576
|
-
synonymSource: token
|
|
2577
|
-
});
|
|
2578
|
-
}
|
|
2579
|
-
}
|
|
2580
|
-
return results;
|
|
2581
|
-
}
|
|
2582
|
-
function resolveConflicts(keywords) {
|
|
2583
|
-
const vocab = loadVocabulary();
|
|
2584
|
-
const kept = [];
|
|
2585
|
-
const dropped = [];
|
|
2586
|
-
for (const kw of keywords) {
|
|
2587
|
-
const spec = vocab.keywords[kw.keyword];
|
|
2588
|
-
if (!spec) continue;
|
|
2589
|
-
const conflictingHere = keywords.filter(
|
|
2590
|
-
(other) => other.keyword !== kw.keyword && spec.conflicts_with.includes(other.keyword)
|
|
2591
|
-
);
|
|
2592
|
-
if (conflictingHere.length === 0) {
|
|
2593
|
-
kept.push(kw);
|
|
2594
|
-
continue;
|
|
2595
|
-
}
|
|
2596
|
-
const strictlyHigherThanAll = conflictingHere.every(
|
|
2597
|
-
(c) => kw.modifier > c.modifier + 1e-9
|
|
2598
|
-
);
|
|
2599
|
-
if (strictlyHigherThanAll) {
|
|
2600
|
-
kept.push(kw);
|
|
2601
|
-
continue;
|
|
2602
|
-
}
|
|
2603
|
-
dropped.push({
|
|
2604
|
-
keyword: kw.keyword,
|
|
2605
|
-
reason: `conflicts with ${conflictingHere.map((c) => c.keyword).join(",")}`
|
|
2606
|
-
});
|
|
2607
|
-
}
|
|
2608
|
-
const warnings = [];
|
|
2609
|
-
const keptSet = new Set(kept.map((k) => k.keyword));
|
|
2610
|
-
const warned = /* @__PURE__ */ new Set();
|
|
2611
|
-
for (const kw of keywords) {
|
|
2612
|
-
const spec = vocab.keywords[kw.keyword];
|
|
2613
|
-
if (!spec) continue;
|
|
2614
|
-
const conflictingHere = keywords.filter(
|
|
2615
|
-
(other) => other.keyword !== kw.keyword && spec.conflicts_with.includes(other.keyword)
|
|
2616
|
-
);
|
|
2617
|
-
if (conflictingHere.length === 0) continue;
|
|
2618
|
-
const groupKeys = [kw.keyword, ...conflictingHere.map((c) => c.keyword)];
|
|
2619
|
-
const groupHasWinner = groupKeys.some((k) => keptSet.has(k));
|
|
2620
|
-
if (groupHasWinner) continue;
|
|
2621
|
-
const pairKey = [...new Set(groupKeys)].sort().join(",");
|
|
2622
|
-
if (warned.has(pairKey)) continue;
|
|
2623
|
-
warned.add(pairKey);
|
|
2624
|
-
warnings.push(
|
|
2625
|
-
`${kw.keyword} \u2194 ${conflictingHere.map((c) => c.keyword).join(",")}: use "primarily <kw>" or "very <kw>" to pick a winner`
|
|
2626
|
-
);
|
|
2627
|
-
}
|
|
2628
|
-
return { kept, dropped, warnings };
|
|
2629
|
-
}
|
|
2630
|
-
function clamp(value, lo, hi) {
|
|
2631
|
-
return Math.max(lo, Math.min(hi, value));
|
|
2632
|
-
}
|
|
2633
|
-
function snap(value, spec) {
|
|
2634
|
-
if (spec.type === "int") return Math.round(value);
|
|
2635
|
-
if (spec.type === "enum") {
|
|
2636
|
-
const [lo, hi] = spec.domain;
|
|
2637
|
-
return Math.abs(value - lo) < Math.abs(value - hi) ? lo : hi;
|
|
2638
|
-
}
|
|
2639
|
-
return Number(value.toFixed(3));
|
|
2640
|
-
}
|
|
2641
|
-
function buildDeltaSet(description) {
|
|
2642
|
-
const vocab = loadVocabulary();
|
|
2643
|
-
const matched = extractKeywords(description);
|
|
2644
|
-
const { kept, dropped, warnings } = resolveConflicts(matched);
|
|
2645
|
-
const axes = {};
|
|
2646
|
-
const voiceHintSet = /* @__PURE__ */ new Set();
|
|
2647
|
-
for (const kw of kept) {
|
|
2648
|
-
const spec = vocab.keywords[kw.keyword];
|
|
2649
|
-
const multiplier = kw.modifier;
|
|
2650
|
-
for (const hint of spec.voice_hints) voiceHintSet.add(hint);
|
|
2651
|
-
for (const [axisName, axisDelta] of Object.entries(spec.axes)) {
|
|
2652
|
-
const bucket = axes[axisName] ?? {
|
|
2653
|
-
value: 0,
|
|
2654
|
-
rangeUnion: [axisDelta.range[0], axisDelta.range[1]],
|
|
2655
|
-
sources: []
|
|
2656
|
-
};
|
|
2657
|
-
bucket.value += axisDelta.delta * multiplier;
|
|
2658
|
-
bucket.rangeUnion = [
|
|
2659
|
-
Math.min(bucket.rangeUnion[0], axisDelta.range[0]),
|
|
2660
|
-
Math.max(bucket.rangeUnion[1], axisDelta.range[1])
|
|
2661
|
-
];
|
|
2662
|
-
bucket.sources.push(kw.keyword);
|
|
2663
|
-
axes[axisName] = bucket;
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2666
|
-
for (const [axisName, bucket] of Object.entries(axes)) {
|
|
2667
|
-
const registry = vocab.axis_registry[axisName];
|
|
2668
|
-
if (!registry) continue;
|
|
2669
|
-
let v = clamp(bucket.value, bucket.rangeUnion[0], bucket.rangeUnion[1]);
|
|
2670
|
-
v = clamp(v, registry.domain[0], registry.domain[1]);
|
|
2671
|
-
bucket.value = snap(v, registry);
|
|
2672
|
-
bucket.sources.sort();
|
|
2673
|
-
}
|
|
2674
|
-
return {
|
|
2675
|
-
axes,
|
|
2676
|
-
voiceHints: [...voiceHintSet],
|
|
2677
|
-
unresolved: [],
|
|
2678
|
-
warnings,
|
|
2679
|
-
droppedKeywords: dropped,
|
|
2680
|
-
matchedKeywords: kept
|
|
2681
|
-
};
|
|
2682
|
-
}
|
|
2683
|
-
function listKeywords() {
|
|
2684
|
-
return Object.keys(loadVocabulary().keywords).sort();
|
|
2685
|
-
}
|
|
2686
|
-
|
|
2687
|
-
// src/core/recommend.ts
|
|
2688
|
-
import { readFileSync as readFileSync7 } from "fs";
|
|
2689
|
-
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
2690
|
-
import { dirname as dirname7, join as join8 } from "path";
|
|
2691
|
-
var CATEGORY_HINTS = {
|
|
2692
|
-
Consumer: [
|
|
2693
|
-
"marketplace",
|
|
2694
|
-
"shopping",
|
|
2695
|
-
"ecommerce",
|
|
2696
|
-
"consumer",
|
|
2697
|
-
"b2c",
|
|
2698
|
-
"retail",
|
|
2699
|
-
"subscription",
|
|
2700
|
-
"family",
|
|
2701
|
-
"families",
|
|
2702
|
-
"meal",
|
|
2703
|
-
"meals",
|
|
2704
|
-
"meal-kit",
|
|
2705
|
-
"food",
|
|
2706
|
-
"travel",
|
|
2707
|
-
"social",
|
|
2708
|
-
"community",
|
|
2709
|
-
"buyer",
|
|
2710
|
-
"seller",
|
|
2711
|
-
"parents",
|
|
2712
|
-
"kids",
|
|
2713
|
-
"lifestyle",
|
|
2714
|
-
"recipe",
|
|
2715
|
-
"recipes"
|
|
2716
|
-
],
|
|
2717
|
-
Fintech: [
|
|
2718
|
-
"fintech",
|
|
2719
|
-
"banking",
|
|
2720
|
-
"bank",
|
|
2721
|
-
"payment",
|
|
2722
|
-
"payments",
|
|
2723
|
-
"crypto",
|
|
2724
|
-
"trading",
|
|
2725
|
-
"wallet",
|
|
2726
|
-
"invest",
|
|
2727
|
-
"investing",
|
|
2728
|
-
"money",
|
|
2729
|
-
"finance",
|
|
2730
|
-
"financial",
|
|
2731
|
-
"lending",
|
|
2732
|
-
"remittance",
|
|
2733
|
-
"tax"
|
|
2734
|
-
],
|
|
2735
|
-
"Developer Tools": [
|
|
2736
|
-
"developer",
|
|
2737
|
-
"devtool",
|
|
2738
|
-
"devtools",
|
|
2739
|
-
"dev-tool",
|
|
2740
|
-
"deploy",
|
|
2741
|
-
"deployment",
|
|
2742
|
-
"build",
|
|
2743
|
-
"ci",
|
|
2744
|
-
"cd",
|
|
2745
|
-
"cli",
|
|
2746
|
-
"sdk",
|
|
2747
|
-
"editor",
|
|
2748
|
-
"ide",
|
|
2749
|
-
"engineering",
|
|
2750
|
-
"compiler",
|
|
2751
|
-
"runtime"
|
|
2752
|
-
],
|
|
2753
|
-
AI: [
|
|
2754
|
-
"ai",
|
|
2755
|
-
"ml",
|
|
2756
|
-
"llm",
|
|
2757
|
-
"agent",
|
|
2758
|
-
"agents",
|
|
2759
|
-
"model",
|
|
2760
|
-
"models",
|
|
2761
|
-
"inference",
|
|
2762
|
-
"gpt",
|
|
2763
|
-
"chatbot",
|
|
2764
|
-
"rag",
|
|
2765
|
-
"embedding",
|
|
2766
|
-
"embeddings",
|
|
2767
|
-
"mcp"
|
|
2768
|
-
],
|
|
2769
|
-
"Design Tools": [
|
|
2770
|
-
"design",
|
|
2771
|
-
"design-tool",
|
|
2772
|
-
"whiteboard",
|
|
2773
|
-
"prototype",
|
|
2774
|
-
"prototyping",
|
|
2775
|
-
"wireframe",
|
|
2776
|
-
"wireframes",
|
|
2777
|
-
"mockup",
|
|
2778
|
-
"mockups",
|
|
2779
|
-
"figma-like",
|
|
2780
|
-
"illustration",
|
|
2781
|
-
"canvas"
|
|
2782
|
-
],
|
|
2783
|
-
Productivity: [
|
|
2784
|
-
"saas",
|
|
2785
|
-
"workspace",
|
|
2786
|
-
"team",
|
|
2787
|
-
"teams",
|
|
2788
|
-
"project-management",
|
|
2789
|
-
"enterprise",
|
|
2790
|
-
"b2b",
|
|
2791
|
-
"crm",
|
|
2792
|
-
"docs",
|
|
2793
|
-
"wiki",
|
|
2794
|
-
"collaboration",
|
|
2795
|
-
"kanban",
|
|
2796
|
-
"scheduling",
|
|
2797
|
-
"meetings"
|
|
2798
|
-
],
|
|
2799
|
-
Backend: [
|
|
2800
|
-
"backend",
|
|
2801
|
-
"database",
|
|
2802
|
-
"db",
|
|
2803
|
-
"api",
|
|
2804
|
-
"apis",
|
|
2805
|
-
"observability",
|
|
2806
|
-
"monitoring",
|
|
2807
|
-
"logging",
|
|
2808
|
-
"analytics",
|
|
2809
|
-
"pipeline",
|
|
2810
|
-
"data-pipeline",
|
|
2811
|
-
"streaming",
|
|
2812
|
-
"queue",
|
|
2813
|
-
"cache"
|
|
2814
|
-
],
|
|
2815
|
-
Automotive: [
|
|
2816
|
-
"car",
|
|
2817
|
-
"cars",
|
|
2818
|
-
"vehicle",
|
|
2819
|
-
"vehicles",
|
|
2820
|
-
"auto",
|
|
2821
|
-
"automotive",
|
|
2822
|
-
"driving",
|
|
2823
|
-
"ev",
|
|
2824
|
-
"electric-vehicle"
|
|
2825
|
-
],
|
|
2826
|
-
Marketing: [
|
|
2827
|
-
"marketing",
|
|
2828
|
-
"seo",
|
|
2829
|
-
"campaign",
|
|
2830
|
-
"campaigns",
|
|
2831
|
-
"newsletter",
|
|
2832
|
-
"email-marketing",
|
|
2833
|
-
"attribution"
|
|
2834
|
-
]
|
|
2835
|
-
};
|
|
2836
|
-
function matchedCategoriesFor(queryTokens, queryStems) {
|
|
2837
|
-
const out = /* @__PURE__ */ new Set();
|
|
2838
|
-
for (const [category, hints] of Object.entries(CATEGORY_HINTS)) {
|
|
2839
|
-
for (const hint of hints) {
|
|
2840
|
-
if (queryTokens.has(hint) || queryStems.has(stem(hint))) {
|
|
2841
|
-
out.add(category);
|
|
2842
|
-
break;
|
|
2843
|
-
}
|
|
2844
|
-
}
|
|
2845
|
-
}
|
|
2846
|
-
return out;
|
|
2847
|
-
}
|
|
2848
|
-
var cachedTags = null;
|
|
2849
|
-
function stem(s) {
|
|
2850
|
-
let out = s;
|
|
2851
|
-
for (const suffix of ["ing", "ed", "ly", "es", "s"]) {
|
|
2852
|
-
if (out.length - suffix.length >= 3 && out.endsWith(suffix)) {
|
|
2853
|
-
out = out.slice(0, -suffix.length);
|
|
2854
|
-
break;
|
|
2855
|
-
}
|
|
2856
|
-
}
|
|
2857
|
-
return out;
|
|
2858
|
-
}
|
|
2859
|
-
function tagsFilePath() {
|
|
2860
|
-
const here = dirname7(fileURLToPath4(import.meta.url));
|
|
2861
|
-
const candidates = [
|
|
2862
|
-
join8(here, "..", "data", "reference-tags.md"),
|
|
2863
|
-
join8(here, "..", "..", "data", "reference-tags.md")
|
|
2864
|
-
];
|
|
2865
|
-
for (const c of candidates) {
|
|
2866
|
-
try {
|
|
2867
|
-
readFileSync7(c, "utf8");
|
|
2868
|
-
return c;
|
|
2869
|
-
} catch {
|
|
2870
|
-
}
|
|
2871
|
-
}
|
|
2872
|
-
throw new Error("data/reference-tags.md not found");
|
|
2873
|
-
}
|
|
2874
|
-
var ROW_RE = /^\|\s*([a-z0-9._-]+)\s*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|$/i;
|
|
2875
|
-
function loadReferenceTags() {
|
|
2876
|
-
if (cachedTags) return cachedTags;
|
|
2877
|
-
const raw = readFileSync7(tagsFilePath(), "utf8");
|
|
2878
|
-
const rows = [];
|
|
2879
|
-
for (const line of raw.split("\n")) {
|
|
2880
|
-
const m = ROW_RE.exec(line);
|
|
2881
|
-
if (!m) continue;
|
|
2882
|
-
const [, id, color, category, keywordsRaw] = m;
|
|
2883
|
-
if (id === "id") continue;
|
|
2884
|
-
if (id.startsWith("---")) continue;
|
|
2885
|
-
rows.push({
|
|
2886
|
-
id: id.trim(),
|
|
2887
|
-
color: color.trim(),
|
|
2888
|
-
category: category.trim(),
|
|
2889
|
-
keywords: keywordsRaw.split(",").map((k) => k.trim().toLowerCase()).filter(Boolean)
|
|
2890
|
-
});
|
|
2891
|
-
}
|
|
2892
|
-
cachedTags = rows;
|
|
2893
|
-
return rows;
|
|
2894
|
-
}
|
|
2895
|
-
function recommend(description, opts = {}) {
|
|
2896
|
-
const topK = opts.topK ?? 5;
|
|
2897
|
-
const diversityByCategory = opts.diversityByCategory ?? true;
|
|
2898
|
-
const tags = loadReferenceTags();
|
|
2899
|
-
const rawTokens = [
|
|
2900
|
-
...tokenize(description),
|
|
2901
|
-
...description.toLowerCase().split(/[^a-z0-9-]+/).filter(Boolean)
|
|
2902
|
-
];
|
|
2903
|
-
const queryTokens = new Set(rawTokens);
|
|
2904
|
-
const queryStems = new Set(rawTokens.map(stem));
|
|
2905
|
-
const matchedCategories = matchedCategoriesFor(queryTokens, queryStems);
|
|
2906
|
-
const tagMatchByRef = tags.map(
|
|
2907
|
-
(t) => t.keywords.filter(
|
|
2908
|
-
(kw) => queryTokens.has(kw) || queryStems.has(stem(kw))
|
|
2909
|
-
)
|
|
2910
|
-
);
|
|
2911
|
-
const totalTagMatches = tagMatchByRef.reduce((a, m) => a + m.length, 0);
|
|
2912
|
-
const scored = tags.map((t, i) => {
|
|
2913
|
-
const matched = tagMatchByRef[i];
|
|
2914
|
-
const tagScore = matched.length;
|
|
2915
|
-
const categoryHit = matchedCategories.has(t.category);
|
|
2916
|
-
const categoryBonus = categoryHit && (tagScore > 0 || totalTagMatches === 0) ? 0.5 : 0;
|
|
2917
|
-
const score = tagScore + categoryBonus;
|
|
2918
|
-
const ratio = matched.length / Math.max(1, t.keywords.length);
|
|
2919
|
-
return {
|
|
2920
|
-
id: t.id,
|
|
2921
|
-
category: t.category,
|
|
2922
|
-
color: t.color,
|
|
2923
|
-
keywords: t.keywords,
|
|
2924
|
-
score,
|
|
2925
|
-
matchedKeywords: matched,
|
|
2926
|
-
matchedCategories: categoryHit ? [t.category] : [],
|
|
2927
|
-
_ratio: ratio
|
|
2928
|
-
};
|
|
2929
|
-
});
|
|
2930
|
-
scored.sort(
|
|
2931
|
-
(a, b) => b.score - a.score || b._ratio - a._ratio || a.id.localeCompare(b.id)
|
|
2932
|
-
);
|
|
2933
|
-
const stripRatio = (s) => s.map(({ _ratio, ...rest }) => {
|
|
2934
|
-
void _ratio;
|
|
2935
|
-
return rest;
|
|
2936
|
-
});
|
|
2937
|
-
if (!diversityByCategory) return stripRatio(scored.slice(0, topK));
|
|
2938
|
-
const picked = stripRatio(scored).slice(0, 0);
|
|
2939
|
-
const pickedSet = /* @__PURE__ */ new Set();
|
|
2940
|
-
const usedCategories = /* @__PURE__ */ new Set();
|
|
2941
|
-
const allHits = stripRatio(scored);
|
|
2942
|
-
for (const hit of allHits) {
|
|
2943
|
-
if (picked.length >= topK) break;
|
|
2944
|
-
if (usedCategories.has(hit.category)) continue;
|
|
2945
|
-
picked.push(hit);
|
|
2946
|
-
pickedSet.add(hit.id);
|
|
2947
|
-
usedCategories.add(hit.category);
|
|
2948
|
-
}
|
|
2949
|
-
for (const hit of allHits) {
|
|
2950
|
-
if (picked.length >= topK) break;
|
|
2951
|
-
if (pickedSet.has(hit.id)) continue;
|
|
2952
|
-
picked.push(hit);
|
|
2953
|
-
pickedSet.add(hit.id);
|
|
2954
|
-
}
|
|
2955
|
-
return picked;
|
|
2956
|
-
}
|
|
2957
|
-
|
|
2958
|
-
// src/core/apply-delta-stub.ts
|
|
2959
|
-
var HEX_RE = /#[0-9a-fA-F]{6}\b/g;
|
|
2960
|
-
var NEUTRAL_RE = /^#([0-9a-fA-F])\1([0-9a-fA-F])\2([0-9a-fA-F])\3$/;
|
|
2961
|
-
var STUB_HEADER = `<!-- omd:stub v=1 \u2014 color-only deterministic shift baseline. Limitations: only color hex codes are shifted (hue/saturation/lightness from delta_set); radius_px, typography.letter_spacing_em, spacing.scale_ratio, and all narrative are NOT shifted by the stub. Run the omd:init skill in an agent session to apply the full delta_set + produce a voice-preserved Hybrid variation. -->`;
|
|
2962
|
-
function isNeutral(hex) {
|
|
2963
|
-
if (NEUTRAL_RE.test(hex)) return true;
|
|
2964
|
-
const [, s] = hexToHsl(hex);
|
|
2965
|
-
return s < 5;
|
|
2966
|
-
}
|
|
2967
|
-
function shiftHex(hex, dh, ds, dl) {
|
|
2968
|
-
const [h, s, l] = hexToHsl(hex);
|
|
2969
|
-
const newH = ((h + dh) % 360 + 360) % 360;
|
|
2970
|
-
const newS = Math.max(0, Math.min(100, s + ds));
|
|
2971
|
-
const newL = Math.max(0, Math.min(100, l + dl));
|
|
2972
|
-
return hslToHex(newH, newS, newL);
|
|
2973
|
-
}
|
|
2974
|
-
function applyDeltaStub(referenceMd, delta) {
|
|
2975
|
-
const dh = delta.axes["color.hue_deg"]?.value ?? 0;
|
|
2976
|
-
const ds = delta.axes["color.saturation_pct"]?.value ?? 0;
|
|
2977
|
-
const dl = delta.axes["color.lightness_pct"]?.value ?? 0;
|
|
2978
|
-
const sourceSet = /* @__PURE__ */ new Set();
|
|
2979
|
-
for (const axis of ["color.hue_deg", "color.saturation_pct", "color.lightness_pct"]) {
|
|
2980
|
-
for (const src of delta.axes[axis]?.sources ?? []) sourceSet.add(src);
|
|
2981
|
-
}
|
|
2982
|
-
const sources = [...sourceSet].sort();
|
|
2983
|
-
let hexMatches = 0;
|
|
2984
|
-
let hexChanged = 0;
|
|
2985
|
-
const shifted = referenceMd.replace(HEX_RE, (match) => {
|
|
2986
|
-
hexMatches++;
|
|
2987
|
-
if (dh === 0 && ds === 0 && dl === 0) return match;
|
|
2988
|
-
if (isNeutral(match)) return match;
|
|
2989
|
-
const out = shiftHex(match, dh, ds, dl);
|
|
2990
|
-
if (out.toLowerCase() !== match.toLowerCase()) hexChanged++;
|
|
2991
|
-
return out;
|
|
2992
|
-
});
|
|
2993
|
-
const header = STUB_HEADER + "\n\n";
|
|
2994
|
-
return {
|
|
2995
|
-
designMd: header + shifted,
|
|
2996
|
-
stats: {
|
|
2997
|
-
hexMatches,
|
|
2998
|
-
hexChanged,
|
|
2999
|
-
hueShift: dh,
|
|
3000
|
-
satShift: ds,
|
|
3001
|
-
lightShift: dl,
|
|
3002
|
-
sourcesApplied: sources
|
|
3003
|
-
}
|
|
3004
|
-
};
|
|
3005
|
-
}
|
|
3006
|
-
|
|
3007
|
-
// src/core/init-deprecate.ts
|
|
3008
|
-
import {
|
|
3009
|
-
existsSync as existsSync6,
|
|
3010
|
-
readFileSync as readFileSync8,
|
|
3011
|
-
writeFileSync as writeFileSync4,
|
|
3012
|
-
unlinkSync,
|
|
3013
|
-
mkdirSync as mkdirSync4
|
|
3014
|
-
} from "fs";
|
|
3015
|
-
import { dirname as dirname8, join as join9 } from "path";
|
|
3016
|
-
function deprecationHeader(opts) {
|
|
3017
|
-
const now = (opts.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
3018
|
-
const lines = [
|
|
3019
|
-
"<!--",
|
|
3020
|
-
"omd:deprecated",
|
|
3021
|
-
` deprecated_at: ${now}`
|
|
3022
|
-
];
|
|
3023
|
-
if (opts.previousReference)
|
|
3024
|
-
lines.push(` previous_reference: ${opts.previousReference}`);
|
|
3025
|
-
lines.push(` new_reference: ${opts.newReference}`);
|
|
3026
|
-
if (opts.preferencesReplayed !== void 0)
|
|
3027
|
-
lines.push(` preferences_replayed: ${opts.preferencesReplayed}`);
|
|
3028
|
-
if (opts.preferencesOrphaned !== void 0)
|
|
3029
|
-
lines.push(` preferences_orphaned: ${opts.preferencesOrphaned}`);
|
|
3030
|
-
if (opts.orphanFile) lines.push(` orphan_file: ${opts.orphanFile}`);
|
|
3031
|
-
lines.push(` reason: ${opts.reason}`);
|
|
3032
|
-
lines.push("-->", "", "");
|
|
3033
|
-
return lines.join("\n");
|
|
3034
|
-
}
|
|
3035
|
-
function deprecateDesignMd(opts) {
|
|
3036
|
-
const from = join9(opts.projectRoot, "DESIGN.md");
|
|
3037
|
-
const baseTo = join9(opts.projectRoot, "DESIGN_DEPRECATED.md");
|
|
3038
|
-
if (!existsSync6(from)) {
|
|
3039
|
-
return { renamed: false, from, to: baseTo };
|
|
3040
|
-
}
|
|
3041
|
-
let target = baseTo;
|
|
3042
|
-
if (existsSync6(baseTo)) {
|
|
3043
|
-
const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3044
|
-
target = join9(opts.projectRoot, `DESIGN_DEPRECATED.${ts}.md`);
|
|
3045
|
-
}
|
|
3046
|
-
mkdirSync4(dirname8(target), { recursive: true });
|
|
3047
|
-
const prior = readFileSync8(from, "utf8");
|
|
3048
|
-
writeFileSync4(target, deprecationHeader(opts) + prior, "utf8");
|
|
3049
|
-
unlinkSync(from);
|
|
3050
|
-
return { renamed: true, from, to: target };
|
|
3051
|
-
}
|
|
3052
|
-
export {
|
|
3053
|
-
AGENTS_SHIM,
|
|
3054
|
-
ALL_SHIMS,
|
|
3055
|
-
CLAUDE_SHIM,
|
|
3056
|
-
CURSOR_SHIM,
|
|
3057
|
-
MANAGED_BLOCK_VERSION,
|
|
3058
|
-
PREFERENCES_PATH,
|
|
3059
|
-
PREFERENCES_SCHEMA,
|
|
3060
|
-
SYNC_LOCK_PATH,
|
|
3061
|
-
SYNC_LOCK_VERSION,
|
|
3062
|
-
appendEntry,
|
|
3063
|
-
applyDeltaStub,
|
|
3064
|
-
applyOverrides,
|
|
3065
|
-
buildDeltaSet,
|
|
3066
|
-
buildEntry,
|
|
3067
|
-
createLock,
|
|
3068
|
-
deprecateDesignMd,
|
|
3069
|
-
detectCallingAgent,
|
|
3070
|
-
detectInstalledAgents,
|
|
3071
|
-
extractKeywords,
|
|
3072
|
-
filterByStatus,
|
|
3073
|
-
generateComponentTokens,
|
|
3074
|
-
generateId as generatePreferenceId,
|
|
3075
|
-
generatePreviewHtml,
|
|
3076
|
-
getAllPresets,
|
|
3077
|
-
getPreset,
|
|
3078
|
-
groupByScope,
|
|
3079
|
-
hasDrift,
|
|
3080
|
-
hashContent,
|
|
3081
|
-
inferScope,
|
|
3082
|
-
inspectAllShims,
|
|
3083
|
-
inspectShim,
|
|
3084
|
-
listKeywords,
|
|
3085
|
-
listReferences,
|
|
3086
|
-
loadReference,
|
|
3087
|
-
loadReferenceTags,
|
|
3088
|
-
loadSynonyms,
|
|
3089
|
-
loadVocabulary,
|
|
3090
|
-
mapToShadcn,
|
|
3091
|
-
mergeWithBase,
|
|
3092
|
-
parseBlock,
|
|
3093
|
-
parseFile as parsePreferencesFile,
|
|
3094
|
-
readLock,
|
|
3095
|
-
readFile as readPreferencesFile,
|
|
3096
|
-
recommend,
|
|
3097
|
-
refreshDesignMdHash,
|
|
3098
|
-
renderDesignMd,
|
|
3099
|
-
renderFile as renderPreferencesFile,
|
|
3100
|
-
resolveConflicts,
|
|
3101
|
-
resolveTokens,
|
|
3102
|
-
shadcnToCss,
|
|
3103
|
-
slugify,
|
|
3104
|
-
updateDesignMdHash,
|
|
3105
|
-
updateEntryStatus,
|
|
3106
|
-
updateTarget,
|
|
3107
|
-
writeAllShims,
|
|
3108
|
-
writeBlock,
|
|
3109
|
-
writeLock,
|
|
3110
|
-
writeFile as writePreferencesFile,
|
|
3111
|
-
writeShim
|
|
3112
|
-
};
|
|
3113
|
-
//# sourceMappingURL=index.js.map
|