@saastro/studio 0.9.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.
- package/dist/{chunk-T3ZZAM65.js → chunk-3VK5GQVH.js} +1247 -501
- package/dist/chunk-3VK5GQVH.js.map +1 -0
- package/dist/{chunk-PKDMKMD6.js → chunk-7V3SGN3T.js} +19 -2
- package/dist/chunk-7V3SGN3T.js.map +1 -0
- package/dist/{chunk-G3MUVKWX.js → chunk-IKMPDZYE.js} +11 -3
- package/dist/chunk-IKMPDZYE.js.map +1 -0
- package/dist/{chunk-3MKWTNQA.js → chunk-N2PHBHNZ.js} +2 -2
- package/dist/chunk-OI6N7H4Y.js +24 -0
- package/dist/chunk-OI6N7H4Y.js.map +1 -0
- package/dist/{chunk-TU7VF6SY.js → chunk-TCKD2I2B.js} +2 -2
- package/dist/{chunk-TU7VF6SY.js.map → chunk-TCKD2I2B.js.map} +1 -1
- package/dist/doctor.d.ts +19 -3
- package/dist/doctor.js +57 -6
- package/dist/doctor.js.map +1 -1
- package/dist/editable-array-item-Cko68alJ.d.ts +33 -0
- package/dist/index.d.ts +4 -32
- package/dist/index.js +9 -22
- package/dist/index.js.map +1 -1
- package/dist/integration/index.js +4 -4
- package/dist/markers.d.ts +20 -0
- package/dist/markers.js +23 -0
- package/dist/markers.js.map +1 -0
- package/dist/middleware/index.js +1 -1
- package/dist/richtext/index.js +1 -1
- package/dist/vite/index.js +3 -3
- package/package.json +9 -5
- package/dist/chunk-G3MUVKWX.js.map +0 -1
- package/dist/chunk-PKDMKMD6.js.map +0 -1
- package/dist/chunk-T3ZZAM65.js.map +0 -1
- /package/dist/{chunk-3MKWTNQA.js.map → chunk-N2PHBHNZ.js.map} +0 -0
|
@@ -1 +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"]}
|
|
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' | 'theme_font_not_overridable';
|
|
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;
|
|
@@ -231,6 +231,22 @@ declare function checkThemeFontOverridable(files: ReadonlyArray<{
|
|
|
231
231
|
path: string;
|
|
232
232
|
content: string;
|
|
233
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[];
|
|
234
250
|
interface DoctorChecksInput {
|
|
235
251
|
files: ReadonlyArray<{
|
|
236
252
|
path: string;
|
|
@@ -241,4 +257,4 @@ interface DoctorChecksInput {
|
|
|
241
257
|
/** Run all five checks and tally them (error count = needsAttention). */
|
|
242
258
|
declare function runDoctorChecks(input: DoctorChecksInput): ChecksReport;
|
|
243
259
|
|
|
244
|
-
export { type CheckFinding, type CheckSeverity, type CheckStatus, type ChecksReport, type DoctorChecksInput, type LocaleTranslations, checkAllowlistCompliance, checkFormCompliance, checkI18nParity, checkSpansIntegrity, checkThemeFontOverridable, checkTokenCompliance, runDoctorChecks };
|
|
260
|
+
export { type CheckFinding, type CheckSeverity, type CheckStatus, type ChecksReport, type DoctorChecksInput, type LocaleTranslations, checkAllowlistCompliance, checkFormCompliance, checkI18nParity, checkIslandMarkerHygiene, checkSpansIntegrity, checkThemeFontOverridable, checkTokenCompliance, runDoctorChecks };
|
package/dist/doctor.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
+
diagnoseSectionRoot,
|
|
2
3
|
hasFrontmatterFieldPrefix,
|
|
3
4
|
hasPageContentAnchor,
|
|
4
5
|
hasPageMarker,
|
|
5
6
|
resolvePageKey,
|
|
6
7
|
routeBaseName
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-7V3SGN3T.js";
|
|
8
9
|
import {
|
|
9
10
|
SPAN_MARKS,
|
|
10
11
|
isSpansShape,
|
|
11
12
|
isSpansValue,
|
|
12
13
|
isValidMark
|
|
13
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-3VK5GQVH.js";
|
|
14
15
|
import "./chunk-PZ5AY32C.js";
|
|
15
16
|
|
|
16
17
|
// ../studio-core/dist/chunk-CW5U4CZJ.js
|
|
@@ -147,7 +148,34 @@ function analyzeSections(files, i18nKeys, autoWrapPagesEnabled = false) {
|
|
|
147
148
|
seen.add(id);
|
|
148
149
|
sections.push(s);
|
|
149
150
|
};
|
|
151
|
+
const unreachableKeys = /* @__PURE__ */ new Set();
|
|
152
|
+
for (const f of facts) {
|
|
153
|
+
if (!f.path.endsWith(".astro")) continue;
|
|
154
|
+
const d = diagnoseSectionRoot(f.content);
|
|
155
|
+
if (d.status !== "component-root") continue;
|
|
156
|
+
const fmMatch = /^\s*---\s*\n([\s\S]*?)\n---\s*\n/.exec(f.content);
|
|
157
|
+
const fmBody = fmMatch?.[1] ?? "";
|
|
158
|
+
const frontmatterKeys = [
|
|
159
|
+
...matchAll(FIELDPREFIX_ATTR_RE, fmBody),
|
|
160
|
+
...matchAll(FIELDPREFIX_NULLISH_RE, fmBody)
|
|
161
|
+
];
|
|
162
|
+
const keys = frontmatterKeys.length > 0 ? frontmatterKeys : [fileBaseCamel(f.path)];
|
|
163
|
+
for (const k of keys) {
|
|
164
|
+
const rescuedElsewhere = facts.some((o) => o.path !== f.path && o.markerKeys.includes(k));
|
|
165
|
+
if (rescuedElsewhere) continue;
|
|
166
|
+
unreachableKeys.add(k);
|
|
167
|
+
push({
|
|
168
|
+
file: f.path,
|
|
169
|
+
sectionPrefix: k,
|
|
170
|
+
status: "unreachable_root",
|
|
171
|
+
reason: `Section root is <${d.tagName}> (a component) and no data-saastro marker ships anywhere \u2014 the marker can only land on a real HTML element, so the Studio never sees this section. @saastro/studio \u22650.10 fails the build on this.`,
|
|
172
|
+
remedy: "Make the section root a lowercase HTML element (wrap it in <section>/<div>), or hand-place the marker on the element that renders it.",
|
|
173
|
+
snippet: "data-saastro={`sec:${fieldPrefix}`}"
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
150
177
|
for (const key of instrumentedKeys) {
|
|
178
|
+
if (unreachableKeys.has(key)) continue;
|
|
151
179
|
const file = keyToFile.get(key) ?? "";
|
|
152
180
|
if (propsEditableKeys.has(key)) {
|
|
153
181
|
push({
|
|
@@ -279,11 +307,12 @@ function analyzeSections(files, i18nKeys, autoWrapPagesEnabled = false) {
|
|
|
279
307
|
editable_i18n: 0,
|
|
280
308
|
no_schema_no_i18n: 0,
|
|
281
309
|
missing_marker: 0,
|
|
282
|
-
autowrap_gap: 0
|
|
310
|
+
autowrap_gap: 0,
|
|
311
|
+
unreachable_root: 0
|
|
283
312
|
};
|
|
284
313
|
for (const s of sections) byStatus[s.status]++;
|
|
285
314
|
const editable = byStatus.editable_props + byStatus.editable_i18n;
|
|
286
|
-
const needsAttention = byStatus.no_schema_no_i18n + byStatus.missing_marker + byStatus.autowrap_gap;
|
|
315
|
+
const needsAttention = byStatus.no_schema_no_i18n + byStatus.missing_marker + byStatus.autowrap_gap + byStatus.unreachable_root;
|
|
287
316
|
return {
|
|
288
317
|
sections,
|
|
289
318
|
stats: {
|
|
@@ -588,6 +617,25 @@ function checkThemeFontOverridable(files) {
|
|
|
588
617
|
}
|
|
589
618
|
return findings;
|
|
590
619
|
}
|
|
620
|
+
var RAW_ISLAND_MARKER_RE = /data-saastro(?:-field)?\s*=|["']data-saastro(?:-field)?["']\s*:/;
|
|
621
|
+
var MARKERS_IMPORT_RE = /from\s+["']@saastro\/studio\/markers["']/;
|
|
622
|
+
function checkIslandMarkerHygiene(files) {
|
|
623
|
+
const findings = [];
|
|
624
|
+
for (const f of files) {
|
|
625
|
+
if (!/\.(tsx|jsx)$/.test(f.path)) continue;
|
|
626
|
+
if (!RAW_ISLAND_MARKER_RE.test(f.content)) continue;
|
|
627
|
+
if (MARKERS_IMPORT_RE.test(f.content)) continue;
|
|
628
|
+
findings.push({
|
|
629
|
+
file: f.path,
|
|
630
|
+
status: "island_raw_markers",
|
|
631
|
+
severity: "warn",
|
|
632
|
+
reason: "React island carries hand-typed data-saastro markers \u2014 a redesign that rewrites this island will silently drop them.",
|
|
633
|
+
remedy: "Import the helpers from '@saastro/studio/markers' (SSR-safe subpath) and spread them: <div {...editableSection('key')}> / <p {...editableField('path')}>.",
|
|
634
|
+
detail: "data-saastro"
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
return findings;
|
|
638
|
+
}
|
|
591
639
|
function runDoctorChecks(input) {
|
|
592
640
|
const findings = [
|
|
593
641
|
...checkI18nParity(input.locales),
|
|
@@ -595,7 +643,8 @@ function runDoctorChecks(input) {
|
|
|
595
643
|
...checkFormCompliance(input.files),
|
|
596
644
|
...checkAllowlistCompliance(input.files),
|
|
597
645
|
...checkSpansIntegrity(input.locales),
|
|
598
|
-
...checkThemeFontOverridable(input.files)
|
|
646
|
+
...checkThemeFontOverridable(input.files),
|
|
647
|
+
...checkIslandMarkerHygiene(input.files)
|
|
599
648
|
];
|
|
600
649
|
const byStatus = {
|
|
601
650
|
i18n_parity: 0,
|
|
@@ -604,7 +653,8 @@ function runDoctorChecks(input) {
|
|
|
604
653
|
allowlist_violation: 0,
|
|
605
654
|
spans_malformed: 0,
|
|
606
655
|
spans_shape_mismatch: 0,
|
|
607
|
-
theme_font_not_overridable: 0
|
|
656
|
+
theme_font_not_overridable: 0,
|
|
657
|
+
island_raw_markers: 0
|
|
608
658
|
};
|
|
609
659
|
let needsAttention = 0;
|
|
610
660
|
let warnings = 0;
|
|
@@ -620,6 +670,7 @@ export {
|
|
|
620
670
|
checkAllowlistCompliance,
|
|
621
671
|
checkFormCompliance,
|
|
622
672
|
checkI18nParity,
|
|
673
|
+
checkIslandMarkerHygiene,
|
|
623
674
|
checkSpansIntegrity,
|
|
624
675
|
checkThemeFontOverridable,
|
|
625
676
|
checkTokenCompliance,
|