getfilepress 0.0.0 → 0.1.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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +178 -4
  3. package/package.json +89 -7
  4. package/packages/app/package.json +31 -0
  5. package/packages/app/src/app.d.ts +12 -0
  6. package/packages/app/src/app.html +12 -0
  7. package/packages/app/src/critical-theme.d.ts +4 -0
  8. package/packages/app/src/lib/content.server.ts +8 -0
  9. package/packages/app/src/lib/empty-theme.css +1 -0
  10. package/packages/app/src/lib/genie/GenieHost.svelte +16 -0
  11. package/packages/app/src/lib/genie/GeniePanel.svelte +429 -0
  12. package/packages/app/src/lib/genie/ops.ts +237 -0
  13. package/packages/app/src/lib/genie/store.ts +217 -0
  14. package/packages/app/src/lib/genie/types.ts +61 -0
  15. package/packages/app/src/lib/pages.server.ts +7 -0
  16. package/packages/app/src/lib/site.server.ts +52 -0
  17. package/packages/app/src/lib/theme-entry.ts +7 -0
  18. package/packages/app/src/routes/+layout.svelte +29 -0
  19. package/packages/app/src/routes/+layout.ts +10 -0
  20. package/packages/app/src/routes/+page.server.ts +28 -0
  21. package/packages/app/src/routes/+page.svelte +67 -0
  22. package/packages/app/src/routes/[slug]/+page.server.ts +20 -0
  23. package/packages/app/src/routes/[slug]/+page.svelte +47 -0
  24. package/packages/app/src/routes/page/[n]/+page.server.ts +21 -0
  25. package/packages/app/src/routes/page/[n]/+page.svelte +36 -0
  26. package/packages/app/src/routes/posts/[slug]/+page.server.ts +24 -0
  27. package/packages/app/src/routes/posts/[slug]/+page.svelte +95 -0
  28. package/packages/app/src/routes/robots.txt/+server.ts +11 -0
  29. package/packages/app/src/routes/rss.xml/+server.ts +13 -0
  30. package/packages/app/src/routes/sitemap.xml/+server.ts +19 -0
  31. package/packages/app/src/routes/tags/+page.server.ts +4 -0
  32. package/packages/app/src/routes/tags/+page.svelte +26 -0
  33. package/packages/app/src/routes/tags/[tag]/+page.server.ts +15 -0
  34. package/packages/app/src/routes/tags/[tag]/+page.svelte +23 -0
  35. package/packages/app/src/routes/topics/+page.server.ts +28 -0
  36. package/packages/app/src/routes/topics/+page.svelte +47 -0
  37. package/packages/app/src/routes/writing/+page.server.ts +12 -0
  38. package/packages/app/src/routes/writing/+page.svelte +38 -0
  39. package/packages/app/src/site-theme.d.ts +2 -0
  40. package/packages/app/static/.gitkeep +0 -0
  41. package/packages/app/tsconfig.json +15 -0
  42. package/packages/app/vite-plugin-critical-theme.ts +70 -0
  43. package/packages/app/vite-plugin-genie.ts +114 -0
  44. package/packages/app/vite.config.ts +141 -0
  45. package/packages/core/package.json +51 -0
  46. package/packages/core/src/lib/assets/favicon.svg +1 -0
  47. package/packages/core/src/lib/components/Newsletter.svelte +13 -0
  48. package/packages/core/src/lib/components/PostCard.svelte +34 -0
  49. package/packages/core/src/lib/components/PostIndex.svelte +86 -0
  50. package/packages/core/src/lib/components/SiteFooter.svelte +15 -0
  51. package/packages/core/src/lib/components/SiteHeader.svelte +28 -0
  52. package/packages/core/src/lib/config.ts +154 -0
  53. package/packages/core/src/lib/content/content.ts +193 -0
  54. package/packages/core/src/lib/content/feeds.ts +102 -0
  55. package/packages/core/src/lib/content/markdown.ts +78 -0
  56. package/packages/core/src/lib/content/pages.ts +103 -0
  57. package/packages/core/src/lib/content/parse.ts +214 -0
  58. package/packages/core/src/lib/content/rehype-figure.ts +79 -0
  59. package/packages/core/src/lib/content/types.ts +88 -0
  60. package/packages/core/src/lib/format.ts +25 -0
  61. package/packages/core/src/lib/index.ts +27 -0
  62. package/packages/core/src/lib/server.ts +33 -0
  63. package/packages/core/src/lib/styles/fonts.css +72 -0
  64. package/packages/core/src/lib/styles/theme.css +762 -0
  65. package/packages/core/src/lib/theme.ts +5 -0
  66. package/packages/import/package.json +27 -0
  67. package/packages/import/src/cli.ts +346 -0
  68. package/packages/import/src/discover.ts +190 -0
  69. package/packages/import/src/extract.ts +378 -0
  70. package/packages/import/src/fetch.ts +63 -0
  71. package/packages/import/src/html-to-md.ts +21 -0
  72. package/packages/import/src/images.ts +247 -0
  73. package/packages/import/src/inspire.ts +417 -0
  74. package/packages/import/src/ir.ts +109 -0
  75. package/packages/import/src/ollama.ts +145 -0
  76. package/packages/import/src/stock.ts +218 -0
  77. package/packages/import/src/theme.ts +478 -0
  78. package/packages/import/src/write-site.ts +371 -0
  79. package/packages/import/tsconfig.json +13 -0
  80. package/pnpm-workspace.yaml +2 -0
  81. package/scripts/create-site.mjs +260 -0
  82. package/scripts/filepress.mjs +273 -0
  83. package/scripts/link-embedded-packages.mjs +40 -0
  84. package/scripts/postinstall.mjs +56 -0
@@ -0,0 +1,478 @@
1
+ import type { DesignBrief } from './ir.ts';
2
+
3
+ const DEFAULT_BRIEF: DesignBrief = {
4
+ mood: 'Quiet editorial personal site with a modern accent.',
5
+ do: ['Keep reading measure comfortable', 'Use a single clear accent', 'Prefer whitespace over cards'],
6
+ dont: ['Do not add marketing section grids', 'Do not introduce pill-stat strips'],
7
+ tokens: {
8
+ accent: '#1e4d6b',
9
+ accentStrong: '#163a52',
10
+ bg: '#f7f5f1',
11
+ ink: '#1a1917'
12
+ },
13
+ density: 'sparse',
14
+ paletteMode: 'light',
15
+ hero: 'editorial',
16
+ atmosphere: 'none',
17
+ navStyle: 'soft',
18
+ elevatedCards: false,
19
+ cssNotes: []
20
+ };
21
+
22
+ function lighten(hex: string): string {
23
+ if (!/^#[0-9a-fA-F]{6}$/.test(hex)) return '#8eb6d4';
24
+ const n = parseInt(hex.slice(1), 16);
25
+ const r = Math.min(255, ((n >> 16) & 255) + 60);
26
+ const g = Math.min(255, ((n >> 8) & 255) + 60);
27
+ const b = Math.min(255, (n & 255) + 60);
28
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
29
+ }
30
+
31
+ function darken(hex: string): string {
32
+ if (!/^#[0-9a-fA-F]{6}$/.test(hex)) return '#0a0a0c';
33
+ const n = parseInt(hex.slice(1), 16);
34
+ const r = Math.max(0, ((n >> 16) & 255) - 40);
35
+ const g = Math.max(0, ((n >> 8) & 255) - 40);
36
+ const b = Math.max(0, (n & 255) - 40);
37
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
38
+ }
39
+
40
+ function fontStack(name: string, fallback: string): string {
41
+ const safe = name.replace(/"/g, '');
42
+ return `"${safe}", ${fallback}`;
43
+ }
44
+
45
+ /**
46
+ * Code-owned theme template. The brief fills slots; we never paste freeform LLM CSS.
47
+ * When inspiration signals are present, this intentionally diverges hard from stock Essay.
48
+ */
49
+ export function themeCssFromBrief(brief: DesignBrief = DEFAULT_BRIEF): string {
50
+ const t = { ...DEFAULT_BRIEF.tokens, ...brief.tokens };
51
+ const mode = brief.paletteMode || (t.bg && /^#0/.test(t.bg) ? 'dark' : 'light');
52
+ const fonts = brief.fonts;
53
+ const hero = brief.hero || 'editorial';
54
+ const atmosphere = brief.atmosphere || 'none';
55
+ const navStyle = brief.navStyle || 'soft';
56
+ const cards = Boolean(brief.elevatedCards);
57
+ const imgs = brief.images || {};
58
+ const punchy = mode === 'dark' || hero === 'bold' || cards;
59
+ const wash =
60
+ t.accentWash ||
61
+ (mode === 'dark'
62
+ ? 'color-mix(in srgb, var(--accent) 12%, transparent)'
63
+ : 'color-mix(in srgb, var(--accent) 7%, transparent)');
64
+ const safeImg = (path: string | null | undefined) =>
65
+ path && /^\/[\w./%-]+$/.test(path) ? path : null;
66
+
67
+ const importBlock =
68
+ fonts?.googleHref && /^https:\/\/fonts\.googleapis\.com\//.test(fonts.googleHref)
69
+ ? `@import url("${fonts.googleHref.replace(/"/g, '')}");\n\n`
70
+ : '';
71
+
72
+ const fontSerif = fonts?.serif
73
+ ? fontStack(fonts.serif, 'Georgia, "Times New Roman", serif')
74
+ : undefined;
75
+ const fontSans = fonts?.sans
76
+ ? fontStack(fonts.sans, '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif')
77
+ : undefined;
78
+ const fontMono = fonts?.mono ? fontStack(fonts.mono, 'Menlo, Consolas, monospace') : undefined;
79
+
80
+ const density =
81
+ brief.density === 'dense'
82
+ ? `body { font-size: 1.05rem; line-height: 1.65; }`
83
+ : brief.density === 'balanced'
84
+ ? `body { font-size: 1.0625rem; line-height: 1.7; }`
85
+ : `body { font-size: 1.15rem; line-height: 1.75; }`;
86
+
87
+ const noise =
88
+ atmosphere === 'noise'
89
+ ? `
90
+ body::before {
91
+ content: "";
92
+ position: fixed;
93
+ inset: 0;
94
+ pointer-events: none;
95
+ z-index: 9999;
96
+ opacity: ${mode === 'dark' ? '0.035' : '0.03'};
97
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
98
+ }
99
+ `
100
+ : '';
101
+
102
+ const heroImg = safeImg(imgs.hero ?? null);
103
+ const headerImg = safeImg(imgs.header ?? null);
104
+ const bgImg = safeImg(imgs.background ?? null);
105
+
106
+ const heroBlock =
107
+ hero === 'bold'
108
+ ? `
109
+ .hero {
110
+ margin-bottom: clamp(2rem, 5vw, 3rem);
111
+ padding: clamp(1.75rem, 4vw, 2.75rem) 0 clamp(1.5rem, 3vw, 2.25rem);
112
+ border-bottom: 1px solid var(--rule);
113
+ background:
114
+ radial-gradient(ellipse 80% 60% at 10% 0%, var(--accent-wash), transparent 55%)${
115
+ heroImg
116
+ ? `,
117
+ linear-gradient(180deg, color-mix(in srgb, var(--bg) 35%, transparent), color-mix(in srgb, var(--bg) 75%, transparent)),
118
+ url("${heroImg}") center / cover no-repeat`
119
+ : ''
120
+ };
121
+ }
122
+
123
+ .hero-lede {
124
+ font-family: var(--font-serif);
125
+ font-style: italic;
126
+ font-size: clamp(1.35rem, 3.2vw, 1.85rem);
127
+ line-height: 1.35;
128
+ letter-spacing: -0.02em;
129
+ color: var(--ink);
130
+ max-width: none;
131
+ }
132
+ `
133
+ : `
134
+ .hero-lede {
135
+ font-size: 1.28rem;
136
+ }
137
+ `;
138
+
139
+ /* Emitted after darkChrome so shorthand `background:` rules cannot wipe these. */
140
+ const imageChrome = `
141
+ ${
142
+ bgImg
143
+ ? `body {
144
+ background-color: var(--bg);
145
+ background-image:
146
+ linear-gradient(180deg, color-mix(in srgb, var(--bg) 72%, transparent), color-mix(in srgb, var(--bg) 88%, transparent)),
147
+ url("${bgImg}");
148
+ background-size: cover;
149
+ background-attachment: fixed;
150
+ background-position: center;
151
+ }
152
+ `
153
+ : ''
154
+ }
155
+ ${
156
+ headerImg
157
+ ? `.site-header {
158
+ background-color: transparent;
159
+ background-image:
160
+ linear-gradient(180deg, color-mix(in srgb, var(--bg) 55%, transparent), color-mix(in srgb, var(--bg) 82%, transparent)),
161
+ url("${headerImg}");
162
+ background-size: cover;
163
+ background-position: center;
164
+ }
165
+ `
166
+ : ''
167
+ }`;
168
+
169
+ const navBlock =
170
+ navStyle === 'uppercase-tracked'
171
+ ? `
172
+ .site-nav {
173
+ gap: 1.35rem;
174
+ font-size: 0.78rem;
175
+ font-weight: 600;
176
+ letter-spacing: 0.08em;
177
+ }
178
+
179
+ .site-nav a {
180
+ position: relative;
181
+ color: var(--ink-soft);
182
+ text-decoration: none;
183
+ padding: 0.35rem 0;
184
+ }
185
+
186
+ .site-nav a::after {
187
+ content: "";
188
+ position: absolute;
189
+ left: 0;
190
+ right: 0;
191
+ bottom: 0;
192
+ height: 1px;
193
+ background: var(--accent);
194
+ transform: scaleX(0);
195
+ transition: transform 0.2s ease;
196
+ }
197
+
198
+ .site-nav a:hover {
199
+ color: var(--ink);
200
+ }
201
+
202
+ .site-nav a:hover::after {
203
+ transform: scaleX(1);
204
+ }
205
+
206
+ .site-title {
207
+ font-size: clamp(1.45rem, 2.5vw, 1.75rem);
208
+ letter-spacing: -0.02em;
209
+ color: var(--accent);
210
+ font-weight: 500;
211
+ }
212
+ `
213
+ : `
214
+ .site-title {
215
+ letter-spacing: -0.02em;
216
+ }
217
+ `;
218
+
219
+ const cardBlock = cards
220
+ ? `
221
+ .featured,
222
+ .post-card {
223
+ background: var(--surface);
224
+ border: 1px solid var(--rule);
225
+ border-radius: calc(var(--radius) + 2px);
226
+ padding: 1.35rem 1.5rem;
227
+ box-shadow: 0 8px 28px color-mix(in srgb, #000 35%, transparent);
228
+ }
229
+
230
+ .featured {
231
+ border-bottom: 1px solid var(--rule);
232
+ }
233
+
234
+ .post-list {
235
+ gap: 1rem;
236
+ }
237
+
238
+ .post-card + .post-card {
239
+ margin-top: 0;
240
+ }
241
+ `
242
+ : '';
243
+
244
+ const darkChrome =
245
+ mode === 'dark'
246
+ ? `
247
+ .site-header {
248
+ border-bottom-color: var(--rule);
249
+ ${
250
+ headerImg
251
+ ? `/* Header photo set in imageChrome — avoid background shorthand wipe */`
252
+ : `background: linear-gradient(180deg, color-mix(in srgb, var(--bg) 92%, #000), transparent);`
253
+ }
254
+ backdrop-filter: blur(12px);
255
+ margin-bottom: clamp(1.5rem, 4vw, 2.5rem);
256
+ }
257
+
258
+ .site-footer {
259
+ border-top-color: var(--rule);
260
+ }
261
+
262
+ ::selection {
263
+ background: var(--accent);
264
+ color: var(--bg);
265
+ }
266
+
267
+ a {
268
+ text-decoration-color: color-mix(in srgb, var(--accent) 55%, transparent);
269
+ }
270
+
271
+ a:hover {
272
+ color: color-mix(in srgb, var(--accent) 70%, #fff);
273
+ }
274
+
275
+ .tag-list a,
276
+ .tag-list .tag {
277
+ border-radius: 6px;
278
+ border-color: var(--rule-strong);
279
+ }
280
+
281
+ .button,
282
+ .newsletter .button {
283
+ background: var(--accent);
284
+ color: var(--bg);
285
+ border: none;
286
+ font-weight: 600;
287
+ }
288
+
289
+ .button:hover,
290
+ .newsletter .button:hover {
291
+ background: color-mix(in srgb, var(--accent) 85%, #fff);
292
+ color: var(--bg);
293
+ }
294
+
295
+ .prose blockquote {
296
+ border-left-color: var(--accent);
297
+ background: var(--accent-wash);
298
+ padding: 0.75rem 1rem;
299
+ border-radius: 0 var(--radius) var(--radius) 0;
300
+ }
301
+ `
302
+ : `
303
+ .site-header {
304
+ border-bottom-color: color-mix(in srgb, var(--accent) 22%, var(--rule));
305
+ }
306
+ `;
307
+
308
+ return `/* Generated by filepress import — edit freely.
309
+ * Mood: ${brief.mood.replace(/\*\//g, '')}
310
+ * Mode: ${mode} · hero: ${hero} · atmosphere: ${atmosphere}
311
+ */
312
+ ${importBlock}:root {
313
+ ${fontSerif ? `--font-serif: ${fontSerif};` : ''}
314
+ ${fontSans ? `--font-sans: ${fontSans};` : ''}
315
+ ${fontMono ? `--font-mono: ${fontMono};` : ''}
316
+ --accent: ${t.accent};
317
+ --accent-strong: ${t.accentStrong};
318
+ --accent-wash: ${wash};
319
+ --bg: ${t.bg || (mode === 'dark' ? '#0a0a0c' : '#f7f5f1')};
320
+ --surface: ${t.surface || (mode === 'dark' ? '#16161a' : '#ffffff')};
321
+ --ink: ${t.ink || (mode === 'dark' ? '#e8e6e3' : '#1a1917')};
322
+ --ink-soft: ${t.inkSoft || (mode === 'dark' ? '#9a9898' : '#4d4a44')};
323
+ --ink-faint: ${mode === 'dark' ? '#6b6969' : '#837f77'};
324
+ --rule: ${t.rule || (mode === 'dark' ? '#2a2a33' : '#e7e2d8')};
325
+ --rule-strong: ${t.ruleStrong || (mode === 'dark' ? '#3d3d4a' : '#d6cfc1')};
326
+ --radius: ${mode === 'dark' ? '8px' : '6px'};
327
+ /* Essay defaults are editorial-narrow (46rem). Punchy import themes open up. */
328
+ ${punchy ? `--measure: 48rem;\n\t--measure-wide: 72rem;` : ''}
329
+ }
330
+
331
+ ${
332
+ mode === 'light'
333
+ ? `@media (prefers-color-scheme: dark) {
334
+ :root {
335
+ --accent: ${lighten(t.accent)};
336
+ --accent-strong: ${lighten(t.accentStrong)};
337
+ }
338
+ }`
339
+ : `/* Dark inspiration palette — force dark even when OS is light */
340
+ @media (prefers-color-scheme: light) {
341
+ :root {
342
+ --bg: ${t.bg || '#0a0a0c'};
343
+ --surface: ${t.surface || '#16161a'};
344
+ --ink: ${t.ink || '#e8e6e3'};
345
+ }
346
+ }`
347
+ }
348
+
349
+ ${density}
350
+
351
+ body {
352
+ ${mode === 'dark' ? `-webkit-font-smoothing: antialiased;` : ''}
353
+ ${mode === 'dark' && !bgImg ? `background-color: var(--bg);` : ''}
354
+ }
355
+
356
+ ${noise}
357
+
358
+ .post-title,
359
+ .page-header h1,
360
+ .post-header h1,
361
+ .featured .post-title {
362
+ letter-spacing: -0.03em;
363
+ font-weight: ${mode === 'dark' ? '500' : '600'};
364
+ ${hero === 'bold' ? `font-size: clamp(2.1rem, 5vw, 3.15rem);` : ''}
365
+ }
366
+
367
+ .featured .post-title {
368
+ font-size: clamp(1.85rem, 4vw, 2.6rem);
369
+ }
370
+
371
+ .eyebrow {
372
+ font-family: var(--font-mono), var(--font-sans);
373
+ letter-spacing: 0.12em;
374
+ }
375
+
376
+ ${navBlock}
377
+ ${heroBlock}
378
+ ${cardBlock}
379
+ ${darkChrome}
380
+ ${imageChrome}
381
+
382
+ .site-tagline {
383
+ color: var(--ink-soft);
384
+ ${mode === 'dark' ? 'font-style: normal; font-family: var(--font-sans); font-size: 0.9rem;' : ''}
385
+ }
386
+
387
+ .read-more {
388
+ font-family: var(--font-sans);
389
+ font-weight: 600;
390
+ letter-spacing: 0.02em;
391
+ }
392
+ `;
393
+ }
394
+
395
+ export function parseBriefJson(raw: string): DesignBrief {
396
+ const start = raw.indexOf('{');
397
+ const end = raw.lastIndexOf('}');
398
+ if (start === -1 || end === -1) throw new Error('No JSON object in model response');
399
+ const data = JSON.parse(raw.slice(start, end + 1)) as Partial<DesignBrief>;
400
+ if (!data.tokens?.accent || !data.tokens?.accentStrong) {
401
+ throw new Error('Design brief missing tokens.accent / tokens.accentStrong');
402
+ }
403
+ const fonts = data.fonts
404
+ ? {
405
+ serif: String(data.fonts.serif || 'Instrument Serif'),
406
+ sans: String(data.fonts.sans || 'DM Sans'),
407
+ mono: String(data.fonts.mono || 'JetBrains Mono'),
408
+ googleHref: data.fonts.googleHref ? String(data.fonts.googleHref) : null
409
+ }
410
+ : undefined;
411
+ return {
412
+ mood: String(data.mood ?? DEFAULT_BRIEF.mood),
413
+ do: Array.isArray(data.do) ? data.do.map(String) : DEFAULT_BRIEF.do,
414
+ dont: Array.isArray(data.dont) ? data.dont.map(String) : DEFAULT_BRIEF.dont,
415
+ tokens: {
416
+ accent: String(data.tokens.accent),
417
+ accentStrong: String(data.tokens.accentStrong),
418
+ bg: data.tokens.bg ? String(data.tokens.bg) : undefined,
419
+ ink: data.tokens.ink ? String(data.tokens.ink) : undefined,
420
+ inkSoft: data.tokens.inkSoft ? String(data.tokens.inkSoft) : undefined,
421
+ surface: data.tokens.surface ? String(data.tokens.surface) : undefined,
422
+ rule: data.tokens.rule ? String(data.tokens.rule) : undefined,
423
+ ruleStrong: data.tokens.ruleStrong ? String(data.tokens.ruleStrong) : undefined,
424
+ accentWash: data.tokens.accentWash ? String(data.tokens.accentWash) : undefined
425
+ },
426
+ density:
427
+ data.density === 'dense' || data.density === 'balanced' || data.density === 'sparse'
428
+ ? data.density
429
+ : 'sparse',
430
+ paletteMode: data.paletteMode === 'dark' || data.paletteMode === 'light' ? data.paletteMode : undefined,
431
+ fonts,
432
+ hero: data.hero === 'bold' || data.hero === 'editorial' ? data.hero : undefined,
433
+ atmosphere: data.atmosphere === 'noise' || data.atmosphere === 'none' ? data.atmosphere : undefined,
434
+ navStyle:
435
+ data.navStyle === 'uppercase-tracked' || data.navStyle === 'soft' ? data.navStyle : undefined,
436
+ elevatedCards: Boolean(data.elevatedCards),
437
+ cssNotes: Array.isArray(data.cssNotes) ? data.cssNotes.map(String) : []
438
+ };
439
+ }
440
+
441
+ /** Extract CSS custom properties from inline :root blocks (source site). */
442
+ export function tokensFromSourceCss(html: string): Partial<DesignBrief['tokens']> {
443
+ const m = html.match(/:root\s*\{([^}]+)\}/);
444
+ if (!m) return {};
445
+ const block = m[1];
446
+ const vars = new Map<string, string>();
447
+ for (const hit of block.matchAll(/--([a-zA-Z0-9-_]+)\s*:\s*([^;}]+)/g)) {
448
+ vars.set(hit[1], hit[2].trim());
449
+ }
450
+ const resolve = (name: string, depth = 0): string | undefined => {
451
+ if (depth > 5) return undefined;
452
+ const raw = vars.get(name);
453
+ if (!raw) return undefined;
454
+ const ref = raw.match(/^var\(\s*--([a-zA-Z0-9-_]+)\s*\)$/);
455
+ if (ref) return resolve(ref[1], depth + 1);
456
+ return raw;
457
+ };
458
+ const firstHex = (...names: string[]) => {
459
+ for (const n of names) {
460
+ const v = resolve(n);
461
+ if (v && /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(v)) return v;
462
+ }
463
+ return undefined;
464
+ };
465
+ const accent = firstHex('accent-color', 'accent', 'color-gold', 'color-augment');
466
+ const bg = firstHex('bg-color', 'bg', 'color-bg', 'color-cream');
467
+ const ink = firstHex('text-color', 'ink', 'color-espresso');
468
+ const out: Partial<DesignBrief['tokens']> = {};
469
+ if (accent) {
470
+ out.accent = accent;
471
+ out.accentStrong = firstHex('accent-strong', 'color-understand') || accent;
472
+ }
473
+ if (bg) out.bg = bg;
474
+ if (ink) out.ink = ink;
475
+ return out;
476
+ }
477
+
478
+ export { DEFAULT_BRIEF, darken };