@saastro/studio 0.8.0 → 0.10.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 (41) hide show
  1. package/dist/{chunk-FGYTDBZU.js → chunk-3VK5GQVH.js} +1323 -508
  2. package/dist/chunk-3VK5GQVH.js.map +1 -0
  3. package/dist/{chunk-PKDMKMD6.js → chunk-7V3SGN3T.js} +19 -2
  4. package/dist/chunk-7V3SGN3T.js.map +1 -0
  5. package/dist/{chunk-7U2GMTVW.js → chunk-IKMPDZYE.js} +17 -4
  6. package/dist/chunk-IKMPDZYE.js.map +1 -0
  7. package/dist/{chunk-JMSDBWL2.js → chunk-N2PHBHNZ.js} +2 -2
  8. package/dist/chunk-OI6N7H4Y.js +24 -0
  9. package/dist/chunk-OI6N7H4Y.js.map +1 -0
  10. package/dist/chunk-TCKD2I2B.js +443 -0
  11. package/dist/chunk-TCKD2I2B.js.map +1 -0
  12. package/dist/doctor.d.ts +35 -3
  13. package/dist/doctor.js +96 -10
  14. package/dist/doctor.js.map +1 -1
  15. package/dist/editable-array-item-Cko68alJ.d.ts +33 -0
  16. package/dist/index.d.ts +4 -32
  17. package/dist/index.js +9 -22
  18. package/dist/index.js.map +1 -1
  19. package/dist/integration/index.js +4 -4
  20. package/dist/markers.d.ts +20 -0
  21. package/dist/markers.js +23 -0
  22. package/dist/markers.js.map +1 -0
  23. package/dist/middleware/index.js +3 -1
  24. package/dist/middleware/index.js.map +1 -1
  25. package/dist/richtext/index.d.ts +96 -17
  26. package/dist/richtext/index.js +23 -1
  27. package/dist/vite/index.js +3 -3
  28. package/package.json +9 -5
  29. package/src/RichText.astro +45 -22
  30. package/dist/chunk-4L5C226B.js +0 -71
  31. package/dist/chunk-4L5C226B.js.map +0 -1
  32. package/dist/chunk-5QZN4IGF.js +0 -71
  33. package/dist/chunk-5QZN4IGF.js.map +0 -1
  34. package/dist/chunk-7U2GMTVW.js.map +0 -1
  35. package/dist/chunk-FGYTDBZU.js.map +0 -1
  36. package/dist/chunk-K7DTYPRE.js +0 -1348
  37. package/dist/chunk-K7DTYPRE.js.map +0 -1
  38. package/dist/chunk-PKDMKMD6.js.map +0 -1
  39. package/dist/chunk-S2IKJ3QM.js +0 -28
  40. package/dist/chunk-S2IKJ3QM.js.map +0 -1
  41. /package/dist/{chunk-JMSDBWL2.js.map → chunk-N2PHBHNZ.js.map} +0 -0
@@ -0,0 +1,443 @@
1
+ // src/vite/meta-tag.ts
2
+ var STUDIO_PROTOCOL_VERSION = "2";
3
+ var PACKAGE_VERSION = "0.10.0".length > 0 ? "0.10.0" : "0.0.0+unknown";
4
+ var META_TAGS = [
5
+ `<meta name="saastro:studio-version" content="${STUDIO_PROTOCOL_VERSION}">`,
6
+ `<meta name="saastro:studio-package" content="${PACKAGE_VERSION}">`
7
+ ].join("\n ");
8
+ function injectVersionMetaTag(html) {
9
+ if (html.includes("saastro:studio-version")) return html;
10
+ return html.replace(/<head([^>]*)>/i, `<head$1>
11
+ ${META_TAGS}`);
12
+ }
13
+
14
+ // src/visibility-css.ts
15
+ var CONFIG_FILENAME = "studio.config.json";
16
+ var STYLE_ID = "saastro-studio-hidden";
17
+ function readVisibilityConfigSync(projectRoot = ".") {
18
+ try {
19
+ const req = globalThis.require;
20
+ if (typeof req !== "function") return {};
21
+ const fs = req("node:fs");
22
+ const path = req("node:path");
23
+ const raw = fs.readFileSync(path.resolve(projectRoot, CONFIG_FILENAME), "utf8");
24
+ if (!raw.trim()) return {};
25
+ const parsed = JSON.parse(raw);
26
+ if (parsed && typeof parsed === "object") return parsed;
27
+ return {};
28
+ } catch {
29
+ return {};
30
+ }
31
+ }
32
+ function collectHiddenFieldPrefixes(config) {
33
+ const set = /* @__PURE__ */ new Set();
34
+ for (const prefixes of Object.values(config.hidden ?? {})) {
35
+ if (!Array.isArray(prefixes)) continue;
36
+ for (const p of prefixes) {
37
+ if (typeof p === "string" && p.length > 0) set.add(p);
38
+ }
39
+ }
40
+ return Array.from(set).sort();
41
+ }
42
+ function buildVisibilityStyleTag(config) {
43
+ const prefixes = collectHiddenFieldPrefixes(config);
44
+ if (prefixes.length === 0) return "";
45
+ const rules = prefixes.map((fp) => {
46
+ const safe = `sec:${fp}`.replace(/["\\]/g, "\\$&");
47
+ return `[data-saastro="${safe}"]{display:none !important}`;
48
+ }).join("");
49
+ return `<style id="${STYLE_ID}">${rules}</style>`;
50
+ }
51
+ function injectVisibilityStyle(html, _projectRoot = ".") {
52
+ if (html.includes(`id="${STYLE_ID}"`)) return html;
53
+ let tag = "";
54
+ try {
55
+ if (typeof __SAASTRO_STUDIO_HIDDEN_TAG__ === "string") {
56
+ tag = __SAASTRO_STUDIO_HIDDEN_TAG__;
57
+ }
58
+ } catch {
59
+ }
60
+ if (!tag) return html;
61
+ return html.replace(/<head([^>]*)>/i, `<head$1>
62
+ ${tag}`);
63
+ }
64
+
65
+ // ../studio-core/dist/chunk-7ZY4HGRC.js
66
+ var clamp = (x, lo = 0, hi = 1) => Math.min(hi, Math.max(lo, x));
67
+ function srgbToLinear(c) {
68
+ return c >= 0.04045 ? Math.pow((c + 0.055) / 1.055, 2.4) : c / 12.92;
69
+ }
70
+ function linearRgbToOklab(r, g, b) {
71
+ const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
72
+ const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
73
+ const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
74
+ const l_ = Math.cbrt(l);
75
+ const m_ = Math.cbrt(m);
76
+ const s_ = Math.cbrt(s);
77
+ return [
78
+ 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
79
+ 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
80
+ 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_
81
+ ];
82
+ }
83
+ function rgbToOklch(r, g, b, alpha) {
84
+ const [L, a, bb] = linearRgbToOklab(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b));
85
+ const C = Math.sqrt(a * a + bb * bb);
86
+ let H = Math.atan2(bb, a) * 180 / Math.PI;
87
+ if (H < 0) H += 360;
88
+ return { L, C, H, alpha };
89
+ }
90
+ function parseHex(str) {
91
+ let h = str.trim().replace(/^#/, "");
92
+ if (h.length === 3 || h.length === 4) {
93
+ h = h.split("").map((ch) => ch + ch).join("");
94
+ }
95
+ if (h.length !== 6 && h.length !== 8) return null;
96
+ if (!/^[0-9a-fA-F]+$/.test(h)) return null;
97
+ const r = parseInt(h.slice(0, 2), 16) / 255;
98
+ const g = parseInt(h.slice(2, 4), 16) / 255;
99
+ const b = parseInt(h.slice(4, 6), 16) / 255;
100
+ const alpha = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;
101
+ return rgbToOklch(r, g, b, alpha);
102
+ }
103
+ function parseNumberOrPercent(tok) {
104
+ if (tok === "none") return 0;
105
+ if (tok.endsWith("%")) {
106
+ const n2 = parseFloat(tok);
107
+ return Number.isFinite(n2) ? n2 / 100 : null;
108
+ }
109
+ const n = parseFloat(tok);
110
+ return Number.isFinite(n) ? n : null;
111
+ }
112
+ function parseOklchString(str) {
113
+ const m = str.trim().match(/^oklch\(\s*([^)]+)\)$/i);
114
+ if (!m || !m[1]) return null;
115
+ const [coords, alphaPart] = m[1].split("/");
116
+ if (!coords) return null;
117
+ const parts = coords.trim().split(/\s+/);
118
+ if (parts.length < 3) return null;
119
+ const hTok = parts[2];
120
+ const L = parseNumberOrPercent(parts[0]);
121
+ const C = parseNumberOrPercent(parts[1]);
122
+ let H = hTok === "none" ? 0 : parseFloat(hTok);
123
+ if (L == null || C == null || !Number.isFinite(H)) return null;
124
+ if (H < 0) H = (H % 360 + 360) % 360;
125
+ const alpha = alphaPart != null ? parseNumberOrPercent(alphaPart.trim()) ?? 1 : 1;
126
+ return { L, C, H, alpha };
127
+ }
128
+ function parseColor(str) {
129
+ if (typeof str !== "string") return null;
130
+ const s = str.trim();
131
+ if (s.startsWith("#")) return parseHex(s);
132
+ if (/^oklch\(/i.test(s)) return parseOklchString(s);
133
+ return null;
134
+ }
135
+ var round = (n, dp = 4) => {
136
+ const f = 10 ** dp;
137
+ return Math.round(n * f) / f;
138
+ };
139
+ function formatOklch(c) {
140
+ const L = round(clamp(c.L), 4);
141
+ const C = round(Math.max(0, c.C), 4);
142
+ const H = round((c.H % 360 + 360) % 360, 2);
143
+ const base = `${L} ${C} ${H}`;
144
+ return c.alpha >= 1 ? `oklch(${base})` : `oklch(${base} / ${round(clamp(c.alpha), 3)})`;
145
+ }
146
+ function withLightness(c, L) {
147
+ return { ...c, L: clamp(L) };
148
+ }
149
+ function mixOklch(a, b, t) {
150
+ const k = clamp(t);
151
+ let dh = b.H - a.H;
152
+ if (dh > 180) dh -= 360;
153
+ if (dh < -180) dh += 360;
154
+ return {
155
+ L: a.L + (b.L - a.L) * k,
156
+ C: a.C + (b.C - a.C) * k,
157
+ H: ((a.H + dh * k) % 360 + 360) % 360,
158
+ alpha: a.alpha + (b.alpha - a.alpha) * k
159
+ };
160
+ }
161
+ function withAlpha(c, alpha) {
162
+ return { ...c, alpha: clamp(alpha) };
163
+ }
164
+ var NEAR_WHITE = "oklch(0.985 0 0)";
165
+ var NEAR_BLACK = "oklch(0.205 0 0)";
166
+ var FG_FLIP = 0.62;
167
+ function deriveForeground(bg) {
168
+ const c = parseColor(bg);
169
+ if (!c) return NEAR_BLACK;
170
+ return c.L >= FG_FLIP ? NEAR_BLACK : NEAR_WHITE;
171
+ }
172
+ function deriveMutedForeground(foreground, muted) {
173
+ return formatOklch(mixOklch(foreground, muted, 0.45));
174
+ }
175
+ function expandColors(base) {
176
+ const fg = parseColor(base.foreground);
177
+ const muted = parseColor(base.muted);
178
+ const mutedForeground = fg && muted ? deriveMutedForeground(fg, muted) : base.foreground;
179
+ return {
180
+ background: base.background,
181
+ foreground: base.foreground,
182
+ card: base.background,
183
+ "card-foreground": base.foreground,
184
+ popover: base.background,
185
+ "popover-foreground": base.foreground,
186
+ primary: base.primary,
187
+ "primary-foreground": deriveForeground(base.primary),
188
+ secondary: base.muted,
189
+ "secondary-foreground": base.foreground,
190
+ muted: base.muted,
191
+ "muted-foreground": mutedForeground,
192
+ accent: base.accent,
193
+ "accent-foreground": deriveForeground(base.accent),
194
+ destructive: base.destructive,
195
+ border: base.border,
196
+ input: base.border,
197
+ ring: base.primary
198
+ };
199
+ }
200
+ function darkenSurface(color, targetL, fallback) {
201
+ const c = parseColor(color);
202
+ if (!c) return fallback;
203
+ return formatOklch(withLightness(c, targetL));
204
+ }
205
+ function brandForDark(color) {
206
+ const c = parseColor(color);
207
+ if (!c) return color;
208
+ if (c.L >= 0.6) return formatOklch(c);
209
+ return formatOklch(withLightness(c, Math.min(0.78, c.L + 0.28)));
210
+ }
211
+ function deriveDarkTokens(base) {
212
+ const fg = parseColor(base.foreground);
213
+ const bg = parseColor(base.background);
214
+ return {
215
+ background: bg ? formatOklch(withLightness(bg, 0.145)) : "oklch(0.145 0 0)",
216
+ foreground: fg ? formatOklch(withLightness(fg, 0.985)) : "oklch(0.985 0 0)",
217
+ muted: darkenSurface(base.muted, 0.269, "oklch(0.269 0 0)"),
218
+ border: borderForDark(base.border),
219
+ primary: brandForDark(base.primary),
220
+ accent: brandForDark(base.accent),
221
+ destructive: base.destructive
222
+ };
223
+ }
224
+ function borderForDark(color) {
225
+ const c = parseColor(color);
226
+ if (!c) return "oklch(1 0 0 / 0.1)";
227
+ return formatOklch(withAlpha({ L: 1, C: 0, H: 0, alpha: 0.1 }, 0.1));
228
+ }
229
+ var THEME_PRESETS = [
230
+ {
231
+ slug: "neutral",
232
+ label: "Neutral",
233
+ description: "The clean monochrome base \u2014 Geist, square-ish corners.",
234
+ colors: {
235
+ background: "#ffffff",
236
+ foreground: "#0a0a0a",
237
+ primary: "#1a1a1a",
238
+ accent: "#f4f4f5",
239
+ muted: "#f4f4f5",
240
+ border: "#e5e5e5",
241
+ destructive: "#dc2626"
242
+ },
243
+ fonts: { body: "Geist", display: "Geist" },
244
+ radius: "0.625rem"
245
+ },
246
+ {
247
+ slug: "editorial",
248
+ label: "Editorial",
249
+ description: "Warm paper, serif display headings \u2014 magazine feel.",
250
+ colors: {
251
+ background: "#fbfaf7",
252
+ foreground: "#1c1917",
253
+ primary: "#b45309",
254
+ accent: "#e8b04b",
255
+ muted: "#f0ece3",
256
+ border: "#e3ddd0",
257
+ destructive: "#b91c1c"
258
+ },
259
+ fonts: { body: "Work Sans", display: "DM Serif Display" },
260
+ radius: "0.25rem"
261
+ },
262
+ {
263
+ slug: "ocean",
264
+ label: "Ocean",
265
+ description: "Cool blues, rounded \u2014 calm and techy.",
266
+ colors: {
267
+ background: "#ffffff",
268
+ foreground: "#0f172a",
269
+ primary: "#1d4ed8",
270
+ accent: "#0ea5e9",
271
+ muted: "#eff6ff",
272
+ border: "#dbeafe",
273
+ destructive: "#dc2626"
274
+ },
275
+ fonts: { body: "Inter", display: "Inter" },
276
+ radius: "1rem"
277
+ },
278
+ {
279
+ slug: "bold",
280
+ label: "Bold",
281
+ description: "High-contrast, vivid primary, geometric sans.",
282
+ colors: {
283
+ background: "#ffffff",
284
+ foreground: "#09090b",
285
+ primary: "#7c3aed",
286
+ accent: "#ec4899",
287
+ muted: "#f5f3ff",
288
+ border: "#e9d5ff",
289
+ destructive: "#e11d48"
290
+ },
291
+ fonts: { body: "Plus Jakarta Sans", display: "Sora" },
292
+ radius: "0.75rem"
293
+ },
294
+ {
295
+ slug: "forest",
296
+ label: "Forest",
297
+ description: "Earthy greens with a refined serif body.",
298
+ colors: {
299
+ background: "#fcfdfb",
300
+ foreground: "#14201a",
301
+ primary: "#15803d",
302
+ accent: "#65a30d",
303
+ muted: "#ecf5ee",
304
+ border: "#d6e6da",
305
+ destructive: "#b91c1c"
306
+ },
307
+ fonts: { body: "Lora", display: "Fraunces" },
308
+ radius: "0.5rem"
309
+ }
310
+ ];
311
+ var BY_SLUG = new Map(THEME_PRESETS.map((p) => [p.slug, p]));
312
+ var DEFAULT_COLORS = THEME_PRESETS[0].colors;
313
+
314
+ // ../studio-core/dist/theme/index.js
315
+ var FONT_CATALOG = [
316
+ // Bundled (saastro-theme ships Geist via @fontsource-variable/geist)
317
+ { name: "Geist", source: "bundled", role: "sans", stack: "'Geist Variable', system-ui, sans-serif" },
318
+ // Google — sans
319
+ { name: "Inter", source: "google", role: "sans", stack: "'Inter', system-ui, sans-serif", googleSpec: "Inter:wght@400;500;600;700" },
320
+ { name: "Manrope", source: "google", role: "sans", stack: "'Manrope', system-ui, sans-serif", googleSpec: "Manrope:wght@400;500;600;700" },
321
+ { name: "Work Sans", source: "google", role: "sans", stack: "'Work Sans', system-ui, sans-serif", googleSpec: "Work+Sans:wght@400;500;600;700" },
322
+ { name: "Plus Jakarta Sans", source: "google", role: "sans", stack: "'Plus Jakarta Sans', system-ui, sans-serif", googleSpec: "Plus+Jakarta+Sans:wght@400;500;600;700" },
323
+ { name: "Sora", source: "google", role: "sans", stack: "'Sora', system-ui, sans-serif", googleSpec: "Sora:wght@400;500;600;700" },
324
+ // Google — serif / display
325
+ { name: "DM Serif Display", source: "google", role: "display", stack: "'DM Serif Display', Georgia, serif", googleSpec: "DM+Serif+Display:ital@0;1" },
326
+ { name: "Playfair Display", source: "google", role: "display", stack: "'Playfair Display', Georgia, serif", googleSpec: "Playfair+Display:wght@500;600;700" },
327
+ { name: "Fraunces", source: "google", role: "serif", stack: "'Fraunces', Georgia, serif", googleSpec: "Fraunces:wght@400;500;600;700" },
328
+ { name: "Lora", source: "google", role: "serif", stack: "'Lora', Georgia, serif", googleSpec: "Lora:wght@400;500;600;700" },
329
+ { name: "Source Serif 4", source: "google", role: "serif", stack: "'Source Serif 4', Georgia, serif", googleSpec: "Source+Serif+4:wght@400;500;600;700" },
330
+ // Google — mono
331
+ { name: "JetBrains Mono", source: "google", role: "mono", stack: "'JetBrains Mono', ui-monospace, monospace", googleSpec: "JetBrains+Mono:wght@400;500;700" }
332
+ ];
333
+ var BY_NAME = new Map(FONT_CATALOG.map((f) => [f.name, f]));
334
+ function resolveFontStack(name) {
335
+ if (!name) return void 0;
336
+ const entry = BY_NAME.get(name);
337
+ if (entry) return entry.stack;
338
+ return /[\s'"]/.test(name) ? name : `'${name}', sans-serif`;
339
+ }
340
+ function buildGoogleFontsHref(names) {
341
+ const specs = [];
342
+ const seen = /* @__PURE__ */ new Set();
343
+ for (const name of names) {
344
+ const entry = name ? BY_NAME.get(name) : void 0;
345
+ if (!entry || entry.source !== "google" || !entry.googleSpec) continue;
346
+ if (seen.has(entry.googleSpec)) continue;
347
+ seen.add(entry.googleSpec);
348
+ specs.push(entry.googleSpec);
349
+ }
350
+ if (specs.length === 0) return null;
351
+ const families = specs.map((s) => `family=${s}`).join("&");
352
+ return `https://fonts.googleapis.com/css2?${families}&display=swap`;
353
+ }
354
+ var THEME_STYLE_ID = "saastro-studio-theme";
355
+ var THEME_FONT_LINK_ID = "saastro-studio-theme-fonts";
356
+ function sanitizeValue(v) {
357
+ return String(v).replace(/[<>;{}]/g, "").trim();
358
+ }
359
+ function colorValue(v) {
360
+ const c = parseColor(v);
361
+ return c ? formatOklch(c) : sanitizeValue(v);
362
+ }
363
+ function colorVarsBlock(vars) {
364
+ return Object.keys(vars).map((k) => `--${k}:${colorValue(vars[k])}`).join(";");
365
+ }
366
+ function modeDeclarations(base, opts) {
367
+ const parts = [colorVarsBlock(expandColors(base))];
368
+ if (opts.includeNonColor) {
369
+ if (opts.radius) {
370
+ const r = sanitizeValue(opts.radius);
371
+ if (r) parts.push(`--radius:${r}`);
372
+ }
373
+ const body = resolveFontStack(opts.fonts?.body);
374
+ if (body) parts.push(`--font-body:${sanitizeValue(body)}`);
375
+ const display = resolveFontStack(opts.fonts?.display);
376
+ if (display) parts.push(`--font-display:${sanitizeValue(display)}`);
377
+ }
378
+ return parts.filter(Boolean).join(";");
379
+ }
380
+ function buildThemeCss(theme) {
381
+ if (!theme || !theme.colors) return "";
382
+ const dark = theme.colorsDark ?? deriveDarkTokens(theme.colors);
383
+ const light = modeDeclarations(theme.colors, {
384
+ radius: theme.radius,
385
+ fonts: theme.fonts,
386
+ includeNonColor: true
387
+ });
388
+ const darkBlock = modeDeclarations(dark, { includeNonColor: false });
389
+ const rules = [];
390
+ if (light) rules.push(`:root{${light}}`);
391
+ if (darkBlock) rules.push(`.dark{${darkBlock}}`);
392
+ return rules.join("");
393
+ }
394
+ function buildThemeStyleTag(theme) {
395
+ const css = buildThemeCss(theme);
396
+ if (!css) return "";
397
+ return `<style id="${THEME_STYLE_ID}">${css}</style>`;
398
+ }
399
+ function buildThemeFontLink(theme) {
400
+ const href = buildGoogleFontsHref([theme?.fonts?.body, theme?.fonts?.display]);
401
+ if (!href) return "";
402
+ return `<link id="${THEME_FONT_LINK_ID}" rel="stylesheet" href="${href}">`;
403
+ }
404
+ function buildThemeHeadTags(theme) {
405
+ const style = buildThemeStyleTag(theme);
406
+ if (!style) return "";
407
+ const link = buildThemeFontLink(theme);
408
+ const preconnect = link ? '<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>' : "";
409
+ return `${preconnect}${link}${style}`;
410
+ }
411
+
412
+ // src/theme-css.ts
413
+ var STYLE_ID2 = "saastro-studio-theme";
414
+ function buildThemeHeadFragment(projectRoot = ".") {
415
+ const config = readVisibilityConfigSync(projectRoot);
416
+ return buildThemeHeadTags(config.theme);
417
+ }
418
+ function injectThemeStyle(html) {
419
+ let tag = "";
420
+ try {
421
+ if (typeof __SAASTRO_STUDIO_THEME_TAG__ === "string") tag = __SAASTRO_STUDIO_THEME_TAG__;
422
+ } catch {
423
+ }
424
+ return placeThemeTag(html, tag);
425
+ }
426
+ function placeThemeTag(html, tag) {
427
+ if (!tag) return html;
428
+ if (html.includes(`id="${STYLE_ID2}"`)) return html;
429
+ if (/<\/head>/i.test(html)) return html.replace(/<\/head>/i, `${tag}
430
+ </head>`);
431
+ return html.replace(/<head([^>]*)>/i, `<head$1>
432
+ ${tag}`);
433
+ }
434
+
435
+ export {
436
+ injectVersionMetaTag,
437
+ readVisibilityConfigSync,
438
+ buildVisibilityStyleTag,
439
+ injectVisibilityStyle,
440
+ buildThemeHeadFragment,
441
+ injectThemeStyle
442
+ };
443
+ //# sourceMappingURL=chunk-TCKD2I2B.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/vite/meta-tag.ts","../src/visibility-css.ts","../../studio-core/src/theme/color.ts","../../studio-core/src/theme/derive.ts","../../studio-core/src/theme/presets.ts","../../studio-core/src/theme/tokens.ts","../../studio-core/src/theme/merge.ts","../../studio-core/src/theme/fonts.ts","../../studio-core/src/theme/css.ts","../src/theme-css.ts"],"sourcesContent":["/**\n * Injects two meta tags into the HTML <head>:\n * - <meta name=\"saastro:studio-version\" content=\"2\"> — protocol version\n * - <meta name=\"saastro:studio-package\" content=\"X.Y.Z\"> — npm package version\n *\n * B8 — spec v5.3 §2 bumps the postMessage protocol number to \"2\". The\n * `studio-version` field reads as the PROTOCOL version (the contract Hub\n * relies on when wiring overlay <-> host messages), not the npm package\n * version of `@saastro/studio`. The package version still ships in a\n * separate `saastro:studio-package` tag for telemetry.\n *\n * The package version comes from a tsup `define` substitution\n * (`__SAASTRO_STUDIO_VERSION__`) injected at build time. The previous\n * implementation used `createRequire(import.meta.url)` to read\n * `package.json` at runtime, but tsup's chunked output places dist\n * files at `dist/chunk-XXX.js` and `../../package.json` resolves\n * OUTSIDE the package — the require always threw and the catch fell\n * back to a hardcoded `'0.1.0'`. Build-time substitution is bullet-\n * proof regardless of how tsup chunks the bundle.\n *\n * This is separate from the auto-wrap transform hook —\n * transformIndexHtml runs on rendered HTML output, not on .astro\n * source files.\n */\n\n/**\n * postMessage protocol version. Bumped to \"2\" in spec v5.3 to add\n * `studio:sections:enumerate` + `studio:select:section` exchanges.\n * Increment when adding/changing the iframe⇄host wire format.\n */\nconst STUDIO_PROTOCOL_VERSION = '2'\n\ndeclare const __SAASTRO_STUDIO_VERSION__: string | undefined\n\nconst PACKAGE_VERSION =\n typeof __SAASTRO_STUDIO_VERSION__ === 'string' && __SAASTRO_STUDIO_VERSION__.length > 0\n ? __SAASTRO_STUDIO_VERSION__\n : '0.0.0+unknown'\n\nconst META_TAGS = [\n `<meta name=\"saastro:studio-version\" content=\"${STUDIO_PROTOCOL_VERSION}\">`,\n `<meta name=\"saastro:studio-package\" content=\"${PACKAGE_VERSION}\">`,\n].join('\\n ')\n\n/**\n * Inject the two studio meta tags into the HTML head. Idempotent —\n * skips injection if the protocol-version tag is already present\n * (matches both the new shape and the legacy single-tag shape, so\n * customer sites with old metadata aren't double-tagged on rebuild).\n */\nexport function injectVersionMetaTag(html: string): string {\n if (html.includes('saastro:studio-version')) return html\n return html.replace(/<head([^>]*)>/i, `<head$1>\\n ${META_TAGS}`)\n}\n","/**\n * Read `studio.config.json` from a project root and build the\n * `<style>` block that hides sections flagged as `hidden`.\n *\n * The config shape is the one written by the Studio Hub at publish time:\n *\n * {\n * \"hidden\": {\n * \"<pageFile>\": [\"<fieldPrefix>\", ...]\n * }\n * }\n *\n * The CSS targets the `data-saastro=\"sec:<fieldPrefix>\"` markers the\n * auto-wrap plugin emits — so hiding is \"exactly the section root,\n * nothing more\". Selectors are flattened across all `pageFile` entries:\n * Studio's `fieldPrefix` naming convention is `<sectionName>.<pageName>`\n * which keeps them unique site-wide, so this loses no precision.\n *\n * Both the Vite `transformIndexHtml` hook (static-build sites) and the\n * Astro middleware (SSR routes) call into the same helpers so the\n * injected CSS is identical regardless of render mode.\n */\n\n// **CRITICAL — DO NOT add static node:fs / node:path imports here.**\n// This module is loaded by `@saastro/studio/middleware`, which runs\n// inside Cloudflare Workers at SSR boot. CF Workers does not support\n// `node:fs` even with the `nodejs_compat` flag (sync fs I/O is\n// fundamentally incompatible with the worker model) — a top-level\n// `import { readFileSync } from 'node:fs'` causes the module to fail\n// to load with `Error: No such module \"node:fs\"`, which breaks the\n// entire middleware chain and makes Astro return 500 on every SSR\n// route.\n//\n// The `node:fs` fallback (used by `astro dev` in Node) loads via\n// dynamic import inside `readVisibilityConfig`, wrapped in try/catch.\n// Workers never reaches that path because the build-time `define`\n// (`__SAASTRO_STUDIO_HIDDEN_TAG__`) short-circuits the readFileSync\n// call entirely.\n\nconst CONFIG_FILENAME = 'studio.config.json'\nconst STYLE_ID = 'saastro-studio-hidden'\n\n/**\n * Compile-time constant injected by the `@saastro/studio` Vite plugin\n * (see `config()` hook in `vite/index.ts`). esbuild substitutes the\n * identifier at customer-bundle time with a JSON string of the\n * full `<style>` tag (or an empty string when nothing is hidden), so\n * the middleware doesn't need filesystem access at runtime — a\n * hard requirement on Cloudflare Workers, where `node:fs` is a no-op.\n *\n * Declared as a free variable on the global scope so esbuild's\n * `define` substitution recognises it as a literal-replacement\n * target. When the plugin isn't installed, the identifier remains\n * undefined and the middleware falls back to the readFileSync path\n * (works in node-based deployments).\n */\ndeclare const __SAASTRO_STUDIO_HIDDEN_TAG__: string | undefined\n\ninterface StudioConfig {\n hidden?: Record<string, string[]>\n // Type-only — erased at build, so this stays free of the node:fs taboo\n // above. The theme block is read by `theme-css.ts`, not this module.\n theme?: import('@saastro/studio-core/theme').ThemeTokensStored\n}\n\n/**\n * Read studio.config.json from `projectRoot`. Returns an empty config\n * if the file is missing or malformed — a publish that hasn't written\n * the config yet behaves like \"nothing hidden\".\n *\n * Defaulting `projectRoot` to `'.'` lets callers omit it when they\n * cannot easily determine the resolved Vite root — `node:path.resolve`\n * resolves relative to the current working directory, which is the\n * project root in every realistic deployment of the middleware.\n */\n/**\n * Build-time-only helper. Imports `node:fs` lazily so this module\n * stays loadable on CF Workers (where `node:fs` is not available).\n * The Vite plugin (`vite/index.ts`, runs in Node) calls this during\n * `config()` to inline the visibility tag as a `define` constant.\n * The runtime middleware (`middleware/index.ts`, runs in Workers)\n * never calls it — it reads the inlined define directly.\n *\n * Returns `{}` when the file isn't there, isn't readable, or when\n * `node:fs` simply isn't available (e.g. bundled into a non-Node\n * runtime that somehow reached this branch).\n */\nexport async function readVisibilityConfig(\n projectRoot: string = '.',\n): Promise<StudioConfig> {\n try {\n const [{ readFileSync }, { resolve }] = await Promise.all([\n import('node:fs'),\n import('node:path'),\n ])\n const raw = readFileSync(resolve(projectRoot, CONFIG_FILENAME), 'utf8')\n if (!raw.trim()) return {}\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === 'object') return parsed as StudioConfig\n return {}\n } catch {\n return {}\n }\n}\n\n/**\n * Synchronous variant kept for back-compat with callers that can't\n * await (e.g. test fixtures, legacy plugin hooks). Same lazy node:fs\n * import via `require` — works in Node CJS contexts, returns `{}`\n * on CF Workers / pure-ESM environments where `require` is undefined.\n *\n * The dynamic `globalThis.require` lookup avoids esbuild bundling the\n * module reference, which means CF Workers never throws\n * \"No such module 'node:fs'\" at import time.\n */\nexport function readVisibilityConfigSync(projectRoot: string = '.'): StudioConfig {\n try {\n const req = (globalThis as { require?: (id: string) => unknown }).require\n if (typeof req !== 'function') return {}\n const fs = req('node:fs') as { readFileSync: (p: string, e: string) => string }\n const path = req('node:path') as { resolve: (...parts: string[]) => string }\n const raw = fs.readFileSync(path.resolve(projectRoot, CONFIG_FILENAME), 'utf8')\n if (!raw.trim()) return {}\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === 'object') return parsed as StudioConfig\n return {}\n } catch {\n return {}\n }\n}\n\n/**\n * Collect every fieldPrefix flagged as hidden across all pageFile\n * entries. Deduplicates so a prefix that appears under multiple\n * pageFiles only emits one selector.\n */\nexport function collectHiddenFieldPrefixes(config: StudioConfig): string[] {\n const set = new Set<string>()\n for (const prefixes of Object.values(config.hidden ?? {})) {\n if (!Array.isArray(prefixes)) continue\n for (const p of prefixes) {\n if (typeof p === 'string' && p.length > 0) set.add(p)\n }\n }\n return Array.from(set).sort()\n}\n\n/**\n * Build the `<style>` block. Returns an empty string when there's\n * nothing to hide so callers can no-op without conditional branching.\n */\nexport function buildVisibilityStyleTag(config: StudioConfig): string {\n const prefixes = collectHiddenFieldPrefixes(config)\n if (prefixes.length === 0) return ''\n const rules = prefixes\n .map(fp => {\n // CSS attribute-selector grammar: only `\\` and `\"` need escaping\n // inside `[attr=\"...\"]`. fieldPrefix is conventionally\n // `[a-zA-Z0-9._-]+` so this is belt-and-braces.\n const safe = `sec:${fp}`.replace(/[\"\\\\]/g, '\\\\$&')\n return `[data-saastro=\"${safe}\"]{display:none !important}`\n })\n .join('')\n return `<style id=\"${STYLE_ID}\">${rules}</style>`\n}\n\n/**\n * Inject the visibility `<style>` into an HTML document's `<head>`.\n * Idempotent: a page that already carries a `${STYLE_ID}` style is\n * returned unchanged.\n */\nexport function injectVisibilityStyle(html: string, _projectRoot: string = '.'): string {\n if (html.includes(`id=\"${STYLE_ID}\"`)) return html\n // Prefer the build-time-injected tag when the customer's Vite build\n // populated `__SAASTRO_STUDIO_HIDDEN_TAG__` via the plugin's `config`\n // hook. This is the only path that works on Cloudflare Workers, where\n // `node:fs` is a no-op. The readFileSync fallback handles node-based\n // deployments (or local `astro dev` before the plugin's define lands).\n let tag = ''\n try {\n if (typeof __SAASTRO_STUDIO_HIDDEN_TAG__ === 'string') {\n tag = __SAASTRO_STUDIO_HIDDEN_TAG__\n }\n } catch {\n // ReferenceError swallowed — identifier wasn't substituted.\n }\n // No `readFileSync` fallback in this runtime path — that branch\n // only worked on Node and breaks CF Workers (`node:fs` import\n // throws \"No such module\"). When the build-time define isn't\n // populated (e.g. someone uses the middleware without the Vite\n // plugin), the visibility CSS just isn't injected — graceful\n // degradation, the meta tag still works.\n if (!tag) return html\n return html.replace(/<head([^>]*)>/i, `<head$1>\\n ${tag}`)\n}\n","/**\n * Minimal, dependency-free color math for the Studio theme editor.\n *\n * The customer themes (saastro-theme) express their semantic tokens in\n * the OKLCH color space (`oklch(0.205 0 0)`); the Studio color pickers\n * hand us hex (`#1d4ed8`). To derive dark-mode variants, readable\n * foregrounds and softened \"muted\" surfaces we need a perceptual space —\n * OKLCH is the right one and keeps this package `zod`-only (no culori et\n * al). Conversions follow Björn Ottosson's reference OKLab matrices.\n *\n * Internally a color is `{ L, C, H, alpha }` (OKLCH). We parse hex +\n * `oklch(...)` (the only two forms used by themes/pickers) and always\n * EMIT `oklch(...)` so derived and verbatim tokens read consistently in\n * the injected `<style>`. Any unparseable string is passed through\n * untouched by the higher-level helpers (graceful degradation — a weird\n * value still renders, it just can't be derived from).\n */\n\nexport interface Oklch {\n /** Perceptual lightness, 0..1 */\n L: number\n /** Chroma, 0..~0.4 */\n C: number\n /** Hue in degrees, 0..360 */\n H: number\n /** Alpha 0..1 */\n alpha: number\n}\n\nconst clamp = (x: number, lo = 0, hi = 1): number => Math.min(hi, Math.max(lo, x))\n\n// --- sRGB gamma ---------------------------------------------------------\n\nfunction srgbToLinear(c: number): number {\n return c >= 0.04045 ? Math.pow((c + 0.055) / 1.055, 2.4) : c / 12.92\n}\n\nfunction linearToSrgb(c: number): number {\n return c >= 0.0031308 ? 1.055 * Math.pow(c, 1 / 2.4) - 0.055 : 12.92 * c\n}\n\n// --- linear sRGB <-> OKLCH ---------------------------------------------\n\nfunction linearRgbToOklab(r: number, g: number, b: number): [number, number, number] {\n const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b\n const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b\n const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b\n const l_ = Math.cbrt(l)\n const m_ = Math.cbrt(m)\n const s_ = Math.cbrt(s)\n return [\n 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,\n 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,\n 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_,\n ]\n}\n\nfunction oklabToLinearRgb(L: number, a: number, b: number): [number, number, number] {\n const l_ = L + 0.3963377774 * a + 0.2158037573 * b\n const m_ = L - 0.1055613458 * a - 0.0638541728 * b\n const s_ = L - 0.0894841775 * a - 1.291485548 * b\n const l = l_ * l_ * l_\n const m = m_ * m_ * m_\n const s = s_ * s_ * s_\n return [\n 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,\n -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,\n -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,\n ]\n}\n\nfunction rgbToOklch(r: number, g: number, b: number, alpha: number): Oklch {\n const [L, a, bb] = linearRgbToOklab(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b))\n const C = Math.sqrt(a * a + bb * bb)\n let H = (Math.atan2(bb, a) * 180) / Math.PI\n if (H < 0) H += 360\n return { L, C, H, alpha }\n}\n\nfunction oklchToRgb(c: Oklch): [number, number, number] {\n const hr = (c.H * Math.PI) / 180\n const a = c.C * Math.cos(hr)\n const b = c.C * Math.sin(hr)\n const [lr, lg, lb] = oklabToLinearRgb(c.L, a, b)\n return [clamp(linearToSrgb(lr)), clamp(linearToSrgb(lg)), clamp(linearToSrgb(lb))]\n}\n\n// --- parsing ------------------------------------------------------------\n\nfunction parseHex(str: string): Oklch | null {\n let h = str.trim().replace(/^#/, '')\n if (h.length === 3 || h.length === 4) {\n h = h\n .split('')\n .map(ch => ch + ch)\n .join('')\n }\n if (h.length !== 6 && h.length !== 8) return null\n if (!/^[0-9a-fA-F]+$/.test(h)) return null\n const r = parseInt(h.slice(0, 2), 16) / 255\n const g = parseInt(h.slice(2, 4), 16) / 255\n const b = parseInt(h.slice(4, 6), 16) / 255\n const alpha = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1\n return rgbToOklch(r, g, b, alpha)\n}\n\nfunction parseNumberOrPercent(tok: string): number | null {\n if (tok === 'none') return 0\n if (tok.endsWith('%')) {\n const n = parseFloat(tok)\n return Number.isFinite(n) ? n / 100 : null\n }\n const n = parseFloat(tok)\n return Number.isFinite(n) ? n : null\n}\n\nfunction parseOklchString(str: string): Oklch | null {\n const m = str.trim().match(/^oklch\\(\\s*([^)]+)\\)$/i)\n if (!m || !m[1]) return null\n // `L C H` optionally followed by `/ A`\n const [coords, alphaPart] = m[1].split('/')\n if (!coords) return null\n const parts = coords.trim().split(/\\s+/)\n if (parts.length < 3) return null\n const hTok = parts[2]!\n const L = parseNumberOrPercent(parts[0]!)\n const C = parseNumberOrPercent(parts[1]!)\n let H = hTok === 'none' ? 0 : parseFloat(hTok)\n if (L == null || C == null || !Number.isFinite(H)) return null\n if (H < 0) H = ((H % 360) + 360) % 360\n const alpha = alphaPart != null ? (parseNumberOrPercent(alphaPart.trim()) ?? 1) : 1\n return { L, C, H, alpha }\n}\n\n/** Parse hex or `oklch(...)` into OKLCH. Returns null if unrecognized. */\nexport function parseColor(str: string): Oklch | null {\n if (typeof str !== 'string') return null\n const s = str.trim()\n if (s.startsWith('#')) return parseHex(s)\n if (/^oklch\\(/i.test(s)) return parseOklchString(s)\n return null\n}\n\n// --- formatting ---------------------------------------------------------\n\nconst round = (n: number, dp = 4): number => {\n const f = 10 ** dp\n return Math.round(n * f) / f\n}\n\n/** Serialize OKLCH to a CSS `oklch(...)` string. */\nexport function formatOklch(c: Oklch): string {\n const L = round(clamp(c.L), 4)\n const C = round(Math.max(0, c.C), 4)\n const H = round(((c.H % 360) + 360) % 360, 2)\n const base = `${L} ${C} ${H}`\n return c.alpha >= 1 ? `oklch(${base})` : `oklch(${base} / ${round(clamp(c.alpha), 3)})`\n}\n\n// --- manipulation -------------------------------------------------------\n\n/** Replace the lightness channel (0..1). */\nexport function withLightness(c: Oklch, L: number): Oklch {\n return { ...c, L: clamp(L) }\n}\n\n/** Linear interpolation between two OKLCH colors (hue via shortest arc). */\nexport function mixOklch(a: Oklch, b: Oklch, t: number): Oklch {\n const k = clamp(t)\n let dh = b.H - a.H\n if (dh > 180) dh -= 360\n if (dh < -180) dh += 360\n return {\n L: a.L + (b.L - a.L) * k,\n C: a.C + (b.C - a.C) * k,\n H: ((a.H + dh * k) % 360 + 360) % 360,\n alpha: a.alpha + (b.alpha - a.alpha) * k,\n }\n}\n\n/** Perceptual lightness 0..1 (OKLab L). Returns null if unparseable. */\nexport function lightnessOf(color: string): number | null {\n const c = parseColor(color)\n return c ? c.L : null\n}\n\n/**\n * Convert any parseable color to a `#rrggbb` hex string — for binding a\n * stored token (hex or oklch) to a native `<input type=\"color\">`, which\n * only accepts hex. Falls back to `fallback` for unparseable input.\n */\nexport function toHex(color: string, fallback = '#000000'): string {\n const c = parseColor(color)\n if (!c) return fallback\n const [r, g, b] = oklchToRgb(c)\n const h = (n: number) =>\n Math.round(clamp(n) * 255)\n .toString(16)\n .padStart(2, '0')\n return `#${h(r)}${h(g)}${h(b)}`\n}\n\n/**\n * Force a color to a flat alpha overlay value (used for dark borders that\n * the theme expresses as `oklch(1 0 0 / 10%)`).\n */\nexport function withAlpha(c: Oklch, alpha: number): Oklch {\n return { ...c, alpha: clamp(alpha) }\n}\n\nexport { clamp as _clampForTest, oklchToRgb as _oklchToRgbForTest }\n","/**\n * Derivations for the theme editor. The user edits ~7 light-mode base\n * colors; everything below is computed so the rendered result is\n * coherent:\n *\n * - `deriveForeground(c)` — readable text color (near-black/near-white)\n * on top of `c`, chosen by perceptual lightness.\n * - `expandColors(base)` — the full `:root` semantic var set (surfaces,\n * foregrounds, input, ring) from the 7 base colors.\n * - `deriveDarkTokens(base)` — a dark-mode BASE-color set from the light\n * one (heuristic OKLCH lightness flip). Fed back through\n * `expandColors` to emit the `.dark { … }` block.\n *\n * All heuristic — the plan ships \"edit light, derive dark\" for v1; a\n * manual dark override is a v2 escape hatch.\n */\n\nimport {\n formatOklch,\n mixOklch,\n parseColor,\n withAlpha,\n withLightness,\n type Oklch,\n} from './color.js'\nimport type { ThemeBaseColors, ResolvedColorVars } from './tokens.js'\n\nconst NEAR_WHITE = 'oklch(0.985 0 0)'\nconst NEAR_BLACK = 'oklch(0.205 0 0)'\n\n/** Lightness threshold above which black text is more readable. */\nconst FG_FLIP = 0.62\n\n/**\n * Pick a readable foreground (text/icon) color to sit on `bg`. Falls\n * back to near-black when `bg` is unparseable (safe default on light\n * themes).\n */\nexport function deriveForeground(bg: string): string {\n const c = parseColor(bg)\n if (!c) return NEAR_BLACK\n return c.L >= FG_FLIP ? NEAR_BLACK : NEAR_WHITE\n}\n\n/** A softened mid-contrast text color for muted surfaces. */\nfunction deriveMutedForeground(foreground: Oklch, muted: Oklch): string {\n // Blend the body text toward the muted surface so it reads as\n // \"secondary text\" rather than full-strength body copy.\n return formatOklch(mixOklch(foreground, muted, 0.45))\n}\n\n/**\n * Expand the 7 editable base colors into the full `:root` semantic var\n * set saastro-theme exposes. Surfaces track `background`; their\n * foregrounds track `foreground`; strong colors get a contrast-derived\n * foreground.\n */\nexport function expandColors(base: ThemeBaseColors): ResolvedColorVars {\n const fg = parseColor(base.foreground)\n const muted = parseColor(base.muted)\n const mutedForeground =\n fg && muted ? deriveMutedForeground(fg, muted) : base.foreground\n\n return {\n background: base.background,\n foreground: base.foreground,\n card: base.background,\n 'card-foreground': base.foreground,\n popover: base.background,\n 'popover-foreground': base.foreground,\n primary: base.primary,\n 'primary-foreground': deriveForeground(base.primary),\n secondary: base.muted,\n 'secondary-foreground': base.foreground,\n muted: base.muted,\n 'muted-foreground': mutedForeground,\n accent: base.accent,\n 'accent-foreground': deriveForeground(base.accent),\n destructive: base.destructive,\n border: base.border,\n input: base.border,\n ring: base.primary,\n }\n}\n\n/** Flip a surface color's lightness into the dark range. */\nfunction darkenSurface(color: string, targetL: number, fallback: string): string {\n const c = parseColor(color)\n if (!c) return fallback\n return formatOklch(withLightness(c, targetL))\n}\n\n/** Lighten a brand color so it stays vivid on a dark background. */\nfunction brandForDark(color: string): string {\n const c = parseColor(color)\n if (!c) return color\n // Keep hue/chroma; ensure enough lightness to pop on dark surfaces.\n if (c.L >= 0.6) return formatOklch(c)\n return formatOklch(withLightness(c, Math.min(0.78, c.L + 0.28)))\n}\n\n/**\n * Derive a dark-mode BASE-color set from the light one. Heuristic:\n * - background → very dark, foreground → near-white\n * - muted → dark surface\n * - border → low-alpha white\n * - primary/accent → lightened to stay legible\n * - destructive → kept (its own foreground handles contrast)\n */\nexport function deriveDarkTokens(base: ThemeBaseColors): ThemeBaseColors {\n const fg = parseColor(base.foreground)\n const bg = parseColor(base.background)\n return {\n background: bg ? formatOklch(withLightness(bg, 0.145)) : 'oklch(0.145 0 0)',\n foreground: fg ? formatOklch(withLightness(fg, 0.985)) : 'oklch(0.985 0 0)',\n muted: darkenSurface(base.muted, 0.269, 'oklch(0.269 0 0)'),\n border: borderForDark(base.border),\n primary: brandForDark(base.primary),\n accent: brandForDark(base.accent),\n destructive: base.destructive,\n }\n}\n\nfunction borderForDark(color: string): string {\n const c = parseColor(color)\n // Themes express dark borders as translucent white. Mirror that.\n if (!c) return 'oklch(1 0 0 / 0.1)'\n return formatOklch(withAlpha({ L: 1, C: 0, H: 0, alpha: 0.1 }, 0.1))\n}\n","/**\n * Theme presets — the \"variations\" surface. Each preset is a complete\n * set of light-mode base colors + fonts + radius. Applying one stages a\n * single `theme_update` carrying these concrete tokens (the Hub expands\n * the preset before staging so `studio.config.json` stays self-contained\n * and doesn't depend on this catalog at the customer build).\n *\n * Colors are hex for authoring convenience; the CSS builder emits OKLCH.\n * `slug` is stable (stored as `theme.preset`); `colors` keys match\n * `ThemeBaseColors`.\n */\n\nimport type { ThemeBaseColors, ThemeFonts } from './tokens.js'\n\nexport interface ThemePreset {\n slug: string\n label: string\n description: string\n colors: ThemeBaseColors\n fonts: ThemeFonts\n radius: string\n}\n\nexport const THEME_PRESETS: ReadonlyArray<ThemePreset> = [\n {\n slug: 'neutral',\n label: 'Neutral',\n description: 'The clean monochrome base — Geist, square-ish corners.',\n colors: {\n background: '#ffffff',\n foreground: '#0a0a0a',\n primary: '#1a1a1a',\n accent: '#f4f4f5',\n muted: '#f4f4f5',\n border: '#e5e5e5',\n destructive: '#dc2626',\n },\n fonts: { body: 'Geist', display: 'Geist' },\n radius: '0.625rem',\n },\n {\n slug: 'editorial',\n label: 'Editorial',\n description: 'Warm paper, serif display headings — magazine feel.',\n colors: {\n background: '#fbfaf7',\n foreground: '#1c1917',\n primary: '#b45309',\n accent: '#e8b04b',\n muted: '#f0ece3',\n border: '#e3ddd0',\n destructive: '#b91c1c',\n },\n fonts: { body: 'Work Sans', display: 'DM Serif Display' },\n radius: '0.25rem',\n },\n {\n slug: 'ocean',\n label: 'Ocean',\n description: 'Cool blues, rounded — calm and techy.',\n colors: {\n background: '#ffffff',\n foreground: '#0f172a',\n primary: '#1d4ed8',\n accent: '#0ea5e9',\n muted: '#eff6ff',\n border: '#dbeafe',\n destructive: '#dc2626',\n },\n fonts: { body: 'Inter', display: 'Inter' },\n radius: '1rem',\n },\n {\n slug: 'bold',\n label: 'Bold',\n description: 'High-contrast, vivid primary, geometric sans.',\n colors: {\n background: '#ffffff',\n foreground: '#09090b',\n primary: '#7c3aed',\n accent: '#ec4899',\n muted: '#f5f3ff',\n border: '#e9d5ff',\n destructive: '#e11d48',\n },\n fonts: { body: 'Plus Jakarta Sans', display: 'Sora' },\n radius: '0.75rem',\n },\n {\n slug: 'forest',\n label: 'Forest',\n description: 'Earthy greens with a refined serif body.',\n colors: {\n background: '#fcfdfb',\n foreground: '#14201a',\n primary: '#15803d',\n accent: '#65a30d',\n muted: '#ecf5ee',\n border: '#d6e6da',\n destructive: '#b91c1c',\n },\n fonts: { body: 'Lora', display: 'Fraunces' },\n radius: '0.5rem',\n },\n]\n\nconst BY_SLUG = new Map(THEME_PRESETS.map(p => [p.slug, p]))\n\nexport function findPreset(slug: string | undefined): ThemePreset | undefined {\n return slug ? BY_SLUG.get(slug) : undefined\n}\n","/**\n * Theme token model for the Studio theme editor.\n *\n * The customer edits a small set of SEMANTIC base colors (light mode\n * only) + two font families + a radius. Everything else — dark mode,\n * `*-foreground` text colors, surfaces (card/popover/secondary), input,\n * ring — is DERIVED so the result stays internally coherent (see\n * `derive.ts`). The derived values map onto the `:root` custom\n * properties that saastro-theme already exposes and that its Tailwind 4\n * `@theme inline` block references via `var(--token)` indirection — that\n * indirection is what makes a runtime `<style>:root{…}</style>` override\n * win without a rebuild.\n */\n\n/** The base colors the user actually edits (v1: 7 tokens, light mode). */\nexport interface ThemeBaseColors {\n background: string\n foreground: string\n primary: string\n accent: string\n muted: string\n border: string\n destructive: string\n}\n\nexport type ThemeColorKey = keyof ThemeBaseColors\n\n/** Ordered list of editable colors + UI metadata for the panel. */\nexport const EDITABLE_COLORS: ReadonlyArray<{\n key: ThemeColorKey\n label: string\n hint: string\n}> = [\n { key: 'primary', label: 'Primary', hint: 'Buttons, links, brand accents' },\n { key: 'accent', label: 'Accent', hint: 'Secondary highlight' },\n { key: 'background', label: 'Background', hint: 'Page background' },\n { key: 'foreground', label: 'Text', hint: 'Body text color' },\n { key: 'muted', label: 'Muted', hint: 'Subtle surfaces / dividers area' },\n { key: 'border', label: 'Border', hint: 'Hairlines and outlines' },\n { key: 'destructive', label: 'Destructive', hint: 'Errors / danger' },\n]\n\nexport interface ThemeFonts {\n /** Body / sans font family — by catalog name (e.g. \"Inter\"). */\n body?: string\n /** Display / heading font family — by catalog name (e.g. \"DM Serif Display\"). */\n display?: string\n}\n\n/**\n * The shape persisted under `studio.config.json` → `theme`. `colorsDark`\n * is computed from `colors` at publish/apply time and stored for\n * predictability (and a future manual dark override). Everything except\n * `colors` is optional so a minimal theme block is valid.\n */\nexport interface ThemeTokensStored {\n /** Name of the preset this theme was last seeded from (UI label only). */\n preset?: string\n colors: ThemeBaseColors\n colorsDark?: ThemeBaseColors\n fonts?: ThemeFonts\n /** CSS length for `--radius`, e.g. \"0.5rem\". */\n radius?: string\n}\n\n/** Partial patch carried by a `theme_update` change; deep-merged. */\nexport interface ThemeTokensPatch {\n preset?: string\n colors?: Partial<ThemeBaseColors>\n fonts?: ThemeFonts\n radius?: string\n}\n\n/**\n * The full set of `:root` semantic custom properties a theme override\n * emits. Keys are WITHOUT the leading `--`. Matches saastro-theme's\n * `:root` block so an override of any subset cascades cleanly.\n */\nexport interface ResolvedColorVars {\n background: string\n foreground: string\n card: string\n 'card-foreground': string\n popover: string\n 'popover-foreground': string\n primary: string\n 'primary-foreground': string\n secondary: string\n 'secondary-foreground': string\n muted: string\n 'muted-foreground': string\n accent: string\n 'accent-foreground': string\n destructive: string\n border: string\n input: string\n ring: string\n}\n\nexport const REQUIRED_COLOR_KEYS: ReadonlyArray<ThemeColorKey> = [\n 'background',\n 'foreground',\n 'primary',\n 'accent',\n 'muted',\n 'border',\n 'destructive',\n]\n\n/** Type guard / normalizer: is this a complete base-color set? */\nexport function isCompleteBaseColors(x: unknown): x is ThemeBaseColors {\n if (!x || typeof x !== 'object') return false\n const o = x as Record<string, unknown>\n return REQUIRED_COLOR_KEYS.every(k => typeof o[k] === 'string' && o[k] !== '')\n}\n","/**\n * Deep-merge a `theme_update` patch into the stored theme and recompute\n * the derived dark set. Used by the diff engine at publish time and by\n * the Hub when staging edits, so both produce byte-identical\n * `studio.config.json` content.\n */\n\nimport { deriveDarkTokens } from './derive.js'\nimport { THEME_PRESETS } from './presets.js'\nimport {\n REQUIRED_COLOR_KEYS,\n type ThemeBaseColors,\n type ThemeTokensPatch,\n type ThemeTokensStored,\n} from './tokens.js'\n\n/** Fallback base colors when nothing has been set yet (the Neutral preset). */\nconst DEFAULT_COLORS: ThemeBaseColors = THEME_PRESETS[0]!.colors\n\nfunction completeColors(\n partial: Partial<ThemeBaseColors> | undefined,\n base: ThemeBaseColors,\n): ThemeBaseColors {\n const out = { ...base }\n if (partial) {\n for (const k of REQUIRED_COLOR_KEYS) {\n const v = partial[k]\n if (typeof v === 'string' && v !== '') out[k] = v\n }\n }\n return out\n}\n\nexport function mergeThemePatch(\n current: ThemeTokensStored | undefined,\n patch: ThemeTokensPatch,\n): ThemeTokensStored {\n const baseColors = current?.colors ?? DEFAULT_COLORS\n const colors = completeColors(patch.colors, baseColors)\n\n const fonts = {\n ...(current?.fonts ?? {}),\n ...(patch.fonts ?? {}),\n }\n const hasFonts = fonts.body || fonts.display\n\n const next: ThemeTokensStored = {\n colors,\n colorsDark: deriveDarkTokens(colors),\n }\n const preset = patch.preset ?? current?.preset\n if (preset) next.preset = preset\n if (hasFonts) next.fonts = fonts\n const radius = patch.radius ?? current?.radius\n if (radius) next.radius = radius\n\n return next\n}\n","/**\n * Curated font catalog for the Studio theme editor.\n *\n * Font-family swaps can't go through a rebuild (the customer site is\n * already deployed), so each catalog entry is either:\n * - `bundled` — already shipped with the theme via @fontsource (no\n * network), or\n * - `google` — loaded at runtime by injecting a `<link>` to the\n * Google Fonts CSS API, which serves the @font-face + woff2 from\n * its CDN. No package install, no rebuild.\n *\n * `stack` is the CSS `font-family` value written into `--font-body` /\n * `--font-display`. `googleSpec` is the `family=` segment for the CSS2\n * API (already URL-encoded with `+`).\n */\n\nexport type FontSource = 'bundled' | 'google'\n\nexport interface FontCatalogEntry {\n /** Display name + the lookup key stored in `theme.fonts`. */\n name: string\n source: FontSource\n /** CSS font-family stack value. */\n stack: string\n /** Recommended use. */\n role: 'sans' | 'serif' | 'mono' | 'display'\n /** `family=` spec for the Google Fonts CSS2 API (google only). */\n googleSpec?: string\n}\n\nexport const FONT_CATALOG: ReadonlyArray<FontCatalogEntry> = [\n // Bundled (saastro-theme ships Geist via @fontsource-variable/geist)\n { name: 'Geist', source: 'bundled', role: 'sans', stack: \"'Geist Variable', system-ui, sans-serif\" },\n\n // Google — sans\n { name: 'Inter', source: 'google', role: 'sans', stack: \"'Inter', system-ui, sans-serif\", googleSpec: 'Inter:wght@400;500;600;700' },\n { name: 'Manrope', source: 'google', role: 'sans', stack: \"'Manrope', system-ui, sans-serif\", googleSpec: 'Manrope:wght@400;500;600;700' },\n { name: 'Work Sans', source: 'google', role: 'sans', stack: \"'Work Sans', system-ui, sans-serif\", googleSpec: 'Work+Sans:wght@400;500;600;700' },\n { name: 'Plus Jakarta Sans', source: 'google', role: 'sans', stack: \"'Plus Jakarta Sans', system-ui, sans-serif\", googleSpec: 'Plus+Jakarta+Sans:wght@400;500;600;700' },\n { name: 'Sora', source: 'google', role: 'sans', stack: \"'Sora', system-ui, sans-serif\", googleSpec: 'Sora:wght@400;500;600;700' },\n\n // Google — serif / display\n { name: 'DM Serif Display', source: 'google', role: 'display', stack: \"'DM Serif Display', Georgia, serif\", googleSpec: 'DM+Serif+Display:ital@0;1' },\n { name: 'Playfair Display', source: 'google', role: 'display', stack: \"'Playfair Display', Georgia, serif\", googleSpec: 'Playfair+Display:wght@500;600;700' },\n { name: 'Fraunces', source: 'google', role: 'serif', stack: \"'Fraunces', Georgia, serif\", googleSpec: 'Fraunces:wght@400;500;600;700' },\n { name: 'Lora', source: 'google', role: 'serif', stack: \"'Lora', Georgia, serif\", googleSpec: 'Lora:wght@400;500;600;700' },\n { name: 'Source Serif 4', source: 'google', role: 'serif', stack: \"'Source Serif 4', Georgia, serif\", googleSpec: 'Source+Serif+4:wght@400;500;600;700' },\n\n // Google — mono\n { name: 'JetBrains Mono', source: 'google', role: 'mono', stack: \"'JetBrains Mono', ui-monospace, monospace\", googleSpec: 'JetBrains+Mono:wght@400;500;700' },\n]\n\nconst BY_NAME = new Map(FONT_CATALOG.map(f => [f.name, f]))\n\nexport function findFont(name: string | undefined): FontCatalogEntry | undefined {\n if (!name) return undefined\n return BY_NAME.get(name)\n}\n\n/**\n * Resolve a stored font name to its CSS stack. Unknown names are passed\n * through as a quoted family (graceful: a custom family still applies if\n * the customer has it available locally).\n */\nexport function resolveFontStack(name: string | undefined): string | undefined {\n if (!name) return undefined\n const entry = BY_NAME.get(name)\n if (entry) return entry.stack\n // Unknown — treat as a literal family name.\n return /[\\s'\"]/.test(name) ? name : `'${name}', sans-serif`\n}\n\n/**\n * Build the Google Fonts CSS2 `<link href>` for the given font names.\n * Skips bundled/unknown fonts. Returns null when nothing needs loading.\n */\nexport function buildGoogleFontsHref(names: ReadonlyArray<string | undefined>): string | null {\n const specs: string[] = []\n const seen = new Set<string>()\n for (const name of names) {\n const entry = name ? BY_NAME.get(name) : undefined\n if (!entry || entry.source !== 'google' || !entry.googleSpec) continue\n if (seen.has(entry.googleSpec)) continue\n seen.add(entry.googleSpec)\n specs.push(entry.googleSpec)\n }\n if (specs.length === 0) return null\n const families = specs.map(s => `family=${s}`).join('&')\n return `https://fonts.googleapis.com/css2?${families}&display=swap`\n}\n","/**\n * CSS builder for the theme override — the single source of truth shared\n * by:\n * - `@saastro/studio` (customer runtime): `buildThemeHeadTags` injects a\n * `<style>` + Google-Fonts `<link>` just before `</head>` so it wins\n * the cascade over the bundled Tailwind CSS (no `!important` needed).\n * - the Hub overlay (live preview): `buildThemeCss` feeds a managed\n * `<style id=\"saastro-studio-theme\">` element in the iframe, updated\n * on every edit before publish.\n *\n * Output targets the `:root` SEMANTIC custom properties saastro-theme\n * exposes (`--primary`, `--background`, …) — NOT the Tailwind `--color-*`\n * names — because the theme's `@theme inline` block resolves utilities to\n * `var(--primary)` etc. Overriding the semantic var is what cascades.\n */\n\nimport { formatOklch, parseColor } from './color.js'\nimport { deriveDarkTokens, expandColors } from './derive.js'\nimport { resolveFontStack, buildGoogleFontsHref } from './fonts.js'\nimport type { ResolvedColorVars, ThemeBaseColors, ThemeTokensStored } from './tokens.js'\n\nexport const THEME_STYLE_ID = 'saastro-studio-theme'\nexport const THEME_PREVIEW_STYLE_ID = 'studio-theme-preview'\nexport const THEME_FONT_LINK_ID = 'saastro-studio-theme-fonts'\n\n/** Strip anything that could break out of a `<style>` / declaration. */\nfunction sanitizeValue(v: string): string {\n return String(v).replace(/[<>;{}]/g, '').trim()\n}\n\n/** Emit a color as OKLCH when parseable, else the sanitized passthrough. */\nfunction colorValue(v: string): string {\n const c = parseColor(v)\n return c ? formatOklch(c) : sanitizeValue(v)\n}\n\nfunction colorVarsBlock(vars: ResolvedColorVars): string {\n return (Object.keys(vars) as Array<keyof ResolvedColorVars>)\n .map(k => `--${k}:${colorValue(vars[k])}`)\n .join(';')\n}\n\n/** Build the inner declarations for one mode (light or dark). */\nfunction modeDeclarations(\n base: ThemeBaseColors,\n opts: { radius?: string; fonts?: { body?: string; display?: string }; includeNonColor: boolean },\n): string {\n const parts: string[] = [colorVarsBlock(expandColors(base))]\n if (opts.includeNonColor) {\n if (opts.radius) {\n const r = sanitizeValue(opts.radius)\n if (r) parts.push(`--radius:${r}`)\n }\n const body = resolveFontStack(opts.fonts?.body)\n if (body) parts.push(`--font-body:${sanitizeValue(body)}`)\n const display = resolveFontStack(opts.fonts?.display)\n if (display) parts.push(`--font-display:${sanitizeValue(display)}`)\n }\n return parts.filter(Boolean).join(';')\n}\n\n/**\n * Build the raw CSS (`:root{…}` + `.dark{…}`) for a theme. Radius/fonts\n * live only on `:root` (they're mode-agnostic). Returns '' when there's\n * no usable color set.\n */\nexport function buildThemeCss(theme: ThemeTokensStored | undefined): string {\n if (!theme || !theme.colors) return ''\n const dark = theme.colorsDark ?? deriveDarkTokens(theme.colors)\n const light = modeDeclarations(theme.colors, {\n radius: theme.radius,\n fonts: theme.fonts,\n includeNonColor: true,\n })\n const darkBlock = modeDeclarations(dark, { includeNonColor: false })\n const rules: string[] = []\n if (light) rules.push(`:root{${light}}`)\n if (darkBlock) rules.push(`.dark{${darkBlock}}`)\n return rules.join('')\n}\n\n/** `<style>` tag for the customer runtime (id = THEME_STYLE_ID). */\nexport function buildThemeStyleTag(theme: ThemeTokensStored | undefined): string {\n const css = buildThemeCss(theme)\n if (!css) return ''\n return `<style id=\"${THEME_STYLE_ID}\">${css}</style>`\n}\n\n/** Google Fonts `<link>` for the selected families, or ''. */\nexport function buildThemeFontLink(theme: ThemeTokensStored | undefined): string {\n const href = buildGoogleFontsHref([theme?.fonts?.body, theme?.fonts?.display])\n if (!href) return ''\n return `<link id=\"${THEME_FONT_LINK_ID}\" rel=\"stylesheet\" href=\"${href}\">`\n}\n\n/**\n * The full set of head tags the customer-side @saastro/studio injects\n * before `</head>`: font preconnect + link, then the override style.\n */\nexport function buildThemeHeadTags(theme: ThemeTokensStored | undefined): string {\n const style = buildThemeStyleTag(theme)\n if (!style) return ''\n const link = buildThemeFontLink(theme)\n const preconnect = link\n ? '<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\"><link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>'\n : ''\n return `${preconnect}${link}${style}`\n}\n","/**\n * Read `studio.config.json` → `theme` and build the `<style>` (+ Google\n * Fonts `<link>`) that override the site's design tokens at runtime.\n *\n * Companion to `visibility-css.ts` — same `studio.config.json`, same\n * build-time `define` + CF-Workers-safe runtime read pattern. The actual\n * CSS is generated by `@saastro/studio-core/theme` (shared with the Hub\n * preview so both emit identical output).\n *\n * **Cascade note:** unlike the visibility CSS (which uses\n * `display:none !important`, so injection order is irrelevant), a theme\n * override sets `:root` custom properties at normal specificity. It must\n * therefore be injected as LATE as possible in `<head>` — *after* the\n * bundled Tailwind stylesheet — so it wins by source order. Hence\n * `injectThemeStyle` targets `</head>`, not `<head>`.\n *\n * **CF Workers:** do NOT add a static `node:fs` import here — see the big\n * comment in `visibility-css.ts`. The runtime path uses the build-time\n * `__SAASTRO_STUDIO_THEME_TAG__` define; the `node:fs` read only runs in\n * the Node build via `readVisibilityConfigSync`.\n */\n\nimport { buildThemeHeadTags } from '@saastro/studio-core/theme'\nimport { readVisibilityConfigSync } from './visibility-css.js'\n\nconst STYLE_ID = 'saastro-studio-theme'\n\n/**\n * Compile-time constant injected by the Vite plugin's `config()` hook\n * (mirrors `__SAASTRO_STUDIO_HIDDEN_TAG__`). esbuild substitutes it with\n * the JSON string of the full head fragment (preconnect + font link +\n * `<style>`), or `''` when no theme is set.\n */\ndeclare const __SAASTRO_STUDIO_THEME_TAG__: string | undefined\n\n/**\n * Build-time helper (Node): read `studio.config.json.theme` from\n * `projectRoot` and return the head fragment to inline as a `define`.\n * Returns '' when there's no theme block.\n */\nexport function buildThemeHeadFragment(projectRoot: string = '.'): string {\n const config = readVisibilityConfigSync(projectRoot)\n return buildThemeHeadTags(config.theme)\n}\n\n/**\n * Inject the theme override fragment into an HTML document, just before\n * `</head>` so it cascades over the bundled CSS. Idempotent: a document\n * that already carries the `${STYLE_ID}` style is returned unchanged.\n * Falls back to `''` (no-op) when the build-time define wasn't populated\n * (e.g. middleware used without the Vite plugin) — graceful degradation.\n */\nexport function injectThemeStyle(html: string): string {\n let tag = ''\n try {\n if (typeof __SAASTRO_STUDIO_THEME_TAG__ === 'string') tag = __SAASTRO_STUDIO_THEME_TAG__\n } catch {\n // ReferenceError swallowed — identifier wasn't substituted.\n }\n return placeThemeTag(html, tag)\n}\n\n/**\n * Pure placement logic (separated from the `define` read so it's unit\n * testable). Inserts `tag` just before `</head>`; idempotent on the\n * `${STYLE_ID}` marker; no-ops on an empty tag.\n */\nexport function placeThemeTag(html: string, tag: string): string {\n if (!tag) return html\n if (html.includes(`id=\"${STYLE_ID}\"`)) return html\n if (/<\\/head>/i.test(html)) return html.replace(/<\\/head>/i, `${tag}\\n</head>`)\n // No closing tag found (fragment / malformed) — fall back to after <head>.\n return html.replace(/<head([^>]*)>/i, `<head$1>\\n${tag}`)\n}\n"],"mappings":"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","names":["n","STYLE_ID"]}
package/dist/doctor.d.ts CHANGED
@@ -21,7 +21,7 @@
21
21
  * The route (`doctor-route.ts`) does the I/O (list repo + read i18n keys) and
22
22
  * feeds this module; everything here is pure.
23
23
  */
24
- type DoctorStatus = 'editable_props' | 'editable_i18n' | 'no_schema_no_i18n' | 'missing_marker' | 'autowrap_gap';
24
+ type DoctorStatus = 'editable_props' | 'editable_i18n' | 'no_schema_no_i18n' | 'missing_marker' | 'autowrap_gap' | 'unreachable_root';
25
25
  interface DoctorSection {
26
26
  /** Repo-relative file the finding is anchored to. */
27
27
  file: string;
@@ -145,7 +145,7 @@ export { type AutoWrapConfigState, detectAutoWrapPagesInConfig, stripCommentsAnd
145
145
  * Each finding carries a severity: `error` fails the gate (counts toward
146
146
  * `needsAttention`), `warn` is advisory. `runDoctorChecks` aggregates them.
147
147
  */
148
- type CheckStatus = 'i18n_parity' | 'hardcoded_hex' | 'raw_form' | 'allowlist_violation' | 'spans_malformed' | 'spans_shape_mismatch';
148
+ type CheckStatus = 'i18n_parity' | 'hardcoded_hex' | 'raw_form' | 'allowlist_violation' | 'spans_malformed' | 'spans_shape_mismatch' | 'theme_font_not_overridable' | 'island_raw_markers';
149
149
  type CheckSeverity = 'error' | 'warn';
150
150
  interface CheckFinding {
151
151
  file: string;
@@ -215,6 +215,38 @@ declare function checkAllowlistCompliance(files: ReadonlyArray<{
215
215
  * default locale's array into locales missing the key).
216
216
  */
217
217
  declare function checkSpansIntegrity(locales: ReadonlyArray<LocaleTranslations>): CheckFinding[];
218
+ /**
219
+ * Tailwind 4 gotcha: a font family written as a LITERAL inside `@theme
220
+ * inline` (`--font-sans: 'Geist', sans-serif`) gets baked into the
221
+ * `.font-sans` utility at build time, so a runtime `:root` override of
222
+ * that var is a no-op — the Studio theme editor can't swap the font.
223
+ * The fix is indirection: keep the family in a `:root` var
224
+ * (`--font-body`/`--font-display`) and reference it from `@theme`
225
+ * (`--font-sans: var(--font-body)`).
226
+ *
227
+ * Advisory (`warn`): colors already use this indirection in saastro-theme,
228
+ * so color edits work regardless; only font swaps are affected.
229
+ */
230
+ declare function checkThemeFontOverridable(files: ReadonlyArray<{
231
+ path: string;
232
+ content: string;
233
+ }>): CheckFinding[];
234
+ /**
235
+ * React islands are never auto-instrumented (the Vite plugin only injects
236
+ * markers in `.astro`), so their markers are written by hand — and a redesign
237
+ * that rewrites an island silently drops them (13 hand-typed attributes in a
238
+ * typical cookie banner). Requiring the `@saastro/studio/markers` helpers
239
+ * makes the markers greppable and centralizes the attribute shape.
240
+ *
241
+ * Severity is WARN (not error) on purpose for now: existing client copies
242
+ * predate the markers subpath and would flip their CI red on the next doctor
243
+ * update with no code change of their own. Escalate to error once the fleet
244
+ * has migrated.
245
+ */
246
+ declare function checkIslandMarkerHygiene(files: ReadonlyArray<{
247
+ path: string;
248
+ content: string;
249
+ }>): CheckFinding[];
218
250
  interface DoctorChecksInput {
219
251
  files: ReadonlyArray<{
220
252
  path: string;
@@ -225,4 +257,4 @@ interface DoctorChecksInput {
225
257
  /** Run all five checks and tally them (error count = needsAttention). */
226
258
  declare function runDoctorChecks(input: DoctorChecksInput): ChecksReport;
227
259
 
228
- export { type CheckFinding, type CheckSeverity, type CheckStatus, type ChecksReport, type DoctorChecksInput, type LocaleTranslations, checkAllowlistCompliance, checkFormCompliance, checkI18nParity, checkSpansIntegrity, checkTokenCompliance, runDoctorChecks };
260
+ export { type CheckFinding, type CheckSeverity, type CheckStatus, type ChecksReport, type DoctorChecksInput, type LocaleTranslations, checkAllowlistCompliance, checkFormCompliance, checkI18nParity, checkIslandMarkerHygiene, checkSpansIntegrity, checkThemeFontOverridable, checkTokenCompliance, runDoctorChecks };