riverbankcms 0.7.0 → 0.7.1

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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { D as parseFooterCompositionV1, E as footerNavigationLayouts, Rt as err, U as parseUtcIsoDateTimeString, b as stableJsonStringify, j as isPresent, k as isCanonicalRichTextDoc, l as EVENT_PRESET_VALUES, ot as assertNever, q as parseBookingTimeZone, s as resolveSiteVerticalBookingConfig, t as NAVIGATION_LOCATIONS, u as ORDINARY_EVENT_PRESET_VALUES, zt as ok } from "./src-DT7DBZtQ.mjs";
2
+ import { A as parseFooterCompositionV1, Ht as ok, K as parseUtcIsoDateTimeString, M as isCanonicalRichTextDoc, P as isPresent, Vt as err, X as parseBookingTimeZone, b as stableJsonStringify, k as footerNavigationLayouts, l as EVENT_PRESET_VALUES, lt as assertNever, s as resolveSiteVerticalBookingConfig, t as NAVIGATION_LOCATIONS, u as ORDINARY_EVENT_PRESET_VALUES } from "./src-B8fOSKJ6.mjs";
3
3
  import "node:module";
4
4
  import { z } from "zod";
5
5
 
@@ -1805,90 +1805,6 @@ function resolveCardRole(theme, cardSystem, role) {
1805
1805
  }
1806
1806
  }
1807
1807
 
1808
- //#endregion
1809
- //#region ../theme-core/src/tokens/resolver.ts
1810
- /**
1811
- * Resolves theme tokens to CSS variables
1812
- *
1813
- * This resolver ensures all button styles use theme tokens (not hardcoded colors)
1814
- * so that changing the palette automatically updates all buttons.
1815
- */
1816
- var TokenResolver = class {
1817
- constructor(theme) {
1818
- this.theme = theme;
1819
- }
1820
- /**
1821
- * Resolve a color token to a CSS variable reference wrapped in rgb()
1822
- *
1823
- * @param token - Token name (e.g., 'primary-500', 'white', 'text-900')
1824
- * @returns CSS rgb() function with var reference (e.g., 'rgb(var(--tb-primary-500))')
1825
- *
1826
- * Note: CSS variables are stored as RGB channels (e.g., "37 99 235") following Tailwind convention,
1827
- * so we must wrap them with rgb() to create a valid color value.
1828
- *
1829
- * @example
1830
- * resolver.getColor('primary-500') // => 'rgb(var(--tb-primary-500))'
1831
- * resolver.getColor('white') // => 'rgb(var(--tb-white))'
1832
- */
1833
- getColor(token) {
1834
- if (!this.tokenExists(token)) {
1835
- console.warn(`[TokenResolver] Color token "${token}" not found in theme palette. Button may render incorrectly.`);
1836
- }
1837
- return `rgb(var(--tb-${token}))`;
1838
- }
1839
- /**
1840
- * Adjust a color token's shade by a given amount
1841
- *
1842
- * @param token - Base token (e.g., 'primary-500')
1843
- * @param amount - Shade adjustment (+100 = darker, -100 = lighter)
1844
- * @returns Adjusted token name (e.g., 'primary-600')
1845
- *
1846
- * @example
1847
- * resolver.adjustShade('primary-500', 100) // => 'primary-600' (darker)
1848
- * resolver.adjustShade('primary-500', -100) // => 'primary-400' (lighter)
1849
- */
1850
- adjustShade(token, amount) {
1851
- const parts = token.split("-");
1852
- if (parts.length < 2) {
1853
- const newShade$1 = Math.max(100, Math.min(900, 500 + amount));
1854
- return `${token}-${newShade$1}`;
1855
- }
1856
- const base = parts.slice(0, -1).join("-");
1857
- const shade = parseInt(parts[parts.length - 1] ?? "", 10);
1858
- if (isNaN(shade)) {
1859
- console.warn(`[TokenResolver] Cannot adjust shade for non-shade token "${token}". Returning original token.`);
1860
- return token;
1861
- }
1862
- const newShade = Math.max(100, Math.min(900, shade + amount));
1863
- return `${base}-${newShade}`;
1864
- }
1865
- /**
1866
- * Check if a token exists in the theme palette
1867
- *
1868
- * @param token - Token name to check
1869
- * @returns True if token exists in theme
1870
- */
1871
- tokenExists(token) {
1872
- const commonTokens = [
1873
- "white",
1874
- "black",
1875
- "transparent"
1876
- ];
1877
- if (commonTokens.includes(token)) {
1878
- return true;
1879
- }
1880
- const parts = token.split("-");
1881
- if (parts.length >= 2) {
1882
- const lastPart = parts[parts.length - 1] ?? "";
1883
- const shade = parseInt(lastPart, 10);
1884
- if (!isNaN(shade) && shade >= 100 && shade <= 900 && shade % 100 === 0) {
1885
- return true;
1886
- }
1887
- }
1888
- return true;
1889
- }
1890
- };
1891
-
1892
1808
  //#endregion
1893
1809
  //#region ../theme-core/src/surfaces/foregroundRoles.ts
1894
1810
  const SURFACE_FOREGROUND_ROLES = [
@@ -2161,6 +2077,302 @@ function raiseContrastText(bgHex, preferred) {
2161
2077
  return text;
2162
2078
  }
2163
2079
 
2080
+ //#endregion
2081
+ //#region ../theme-core/src/tokens/tokens.ts
2082
+ const colorTokens = [
2083
+ "background/base",
2084
+ "background/elevated",
2085
+ "surface/base",
2086
+ "surface/subtle",
2087
+ "surface/inverted",
2088
+ "content/primary",
2089
+ "content/secondary",
2090
+ "accent/primary",
2091
+ "accent/secondary",
2092
+ "border/base"
2093
+ ];
2094
+ const typographyTokens = [
2095
+ "display/hero",
2096
+ "display/section",
2097
+ "heading/large",
2098
+ "heading/medium",
2099
+ "heading/small",
2100
+ "body/normal",
2101
+ "body/small",
2102
+ "caption"
2103
+ ];
2104
+ const spacingTokens = [
2105
+ "none",
2106
+ "3xs",
2107
+ "2xs",
2108
+ "xs",
2109
+ "sm",
2110
+ "md",
2111
+ "lg",
2112
+ "xl",
2113
+ "2xl",
2114
+ "3xl"
2115
+ ];
2116
+ const radiusTokens = [
2117
+ "none",
2118
+ "xs",
2119
+ "sm",
2120
+ "md",
2121
+ "lg",
2122
+ "pill",
2123
+ "full"
2124
+ ];
2125
+ const shadowTokens = [
2126
+ "none",
2127
+ "soft",
2128
+ "medium",
2129
+ "strong"
2130
+ ];
2131
+ const defaultThemeTokens = {
2132
+ color: {
2133
+ "background/base": "#ffffff",
2134
+ "background/elevated": "#f5f5f5",
2135
+ "surface/base": "#ffffff",
2136
+ "surface/subtle": "#f9f9fb",
2137
+ "surface/inverted": "#111111",
2138
+ "content/primary": "#111111",
2139
+ "content/secondary": "#3f3f46",
2140
+ "accent/primary": "#2563eb",
2141
+ "accent/secondary": "#1d4ed8",
2142
+ "border/base": "#e5e7eb"
2143
+ },
2144
+ typography: {
2145
+ "display/hero": {
2146
+ fontSize: "clamp(48px, 8vw, 96px)",
2147
+ lineHeight: "1.05",
2148
+ fontWeight: "700"
2149
+ },
2150
+ "display/section": {
2151
+ fontSize: "clamp(40px, 6vw, 72px)",
2152
+ lineHeight: "1.1",
2153
+ fontWeight: "700"
2154
+ },
2155
+ "heading/large": {
2156
+ fontSize: "clamp(32px, 4vw, 48px)",
2157
+ lineHeight: "1.2",
2158
+ fontWeight: "600"
2159
+ },
2160
+ "heading/medium": {
2161
+ fontSize: "clamp(24px, 3vw, 32px)",
2162
+ lineHeight: "1.25",
2163
+ fontWeight: "600"
2164
+ },
2165
+ "heading/small": {
2166
+ fontSize: "20px",
2167
+ lineHeight: "1.3",
2168
+ fontWeight: "600"
2169
+ },
2170
+ "body/normal": {
2171
+ fontSize: "16px",
2172
+ lineHeight: "1.6",
2173
+ fontWeight: "400"
2174
+ },
2175
+ "body/small": {
2176
+ fontSize: "14px",
2177
+ lineHeight: "1.5",
2178
+ fontWeight: "400"
2179
+ },
2180
+ caption: {
2181
+ fontSize: "12px",
2182
+ lineHeight: "1.4",
2183
+ fontWeight: "500"
2184
+ }
2185
+ },
2186
+ spacing: {
2187
+ none: "0px",
2188
+ "3xs": "4px",
2189
+ "2xs": "8px",
2190
+ xs: "12px",
2191
+ sm: "16px",
2192
+ md: "24px",
2193
+ lg: "32px",
2194
+ xl: "40px",
2195
+ "2xl": "56px",
2196
+ "3xl": "72px"
2197
+ },
2198
+ radius: {
2199
+ none: "0px",
2200
+ xs: "2px",
2201
+ sm: "4px",
2202
+ md: "8px",
2203
+ lg: "12px",
2204
+ pill: "999px",
2205
+ full: "9999px"
2206
+ },
2207
+ shadow: {
2208
+ none: "none",
2209
+ soft: "0px 2px 8px rgba(15, 23, 42, 0.08)",
2210
+ medium: "0px 6px 18px rgba(15, 23, 42, 0.12)",
2211
+ strong: "0px 12px 36px rgba(15, 23, 42, 0.16)"
2212
+ },
2213
+ palette: {
2214
+ surface: "#ffffff",
2215
+ "neutral-50": "#f9fafb",
2216
+ "neutral-900": "#111827",
2217
+ primary: "#2563eb",
2218
+ "primary-50": "#eff6ff",
2219
+ "primary-500": "#2563eb",
2220
+ "primary-900": "#1d4ed8",
2221
+ secondary: "#9333ea",
2222
+ "secondary-50": "#f3e8ff",
2223
+ "secondary-500": "#9333ea",
2224
+ "secondary-900": "#4c1d95",
2225
+ accent: "#f97316",
2226
+ "accent-50": "#fff7ed",
2227
+ "accent-500": "#f97316",
2228
+ "accent-900": "#7c2d12"
2229
+ }
2230
+ };
2231
+ /**
2232
+ * Project the expanded semantic palette into the token buckets consumed by
2233
+ * runtime transforms. Keep this mapping shared by full themes and the
2234
+ * tokens-only hosted-booking runtime so the two paths cannot drift.
2235
+ */
2236
+ function buildThemeTokens(palette) {
2237
+ const tokens = cloneThemeTokens(defaultThemeTokens);
2238
+ const paletteStrings = {};
2239
+ for (const [key, value] of Object.entries(palette)) {
2240
+ if (typeof value === "string" && value.trim().length > 0) {
2241
+ paletteStrings[key] = value;
2242
+ }
2243
+ }
2244
+ tokens.palette = paletteStrings;
2245
+ const resolveColor = (...keys) => {
2246
+ for (const key of keys) {
2247
+ const value = paletteStrings[key];
2248
+ if (value) return value;
2249
+ }
2250
+ return undefined;
2251
+ };
2252
+ const assignColor = (token, ...keys) => {
2253
+ const value = resolveColor(...keys);
2254
+ if (value) tokens.color[token] = value;
2255
+ };
2256
+ assignColor("background/base", "background", "surface", "primary-50");
2257
+ assignColor("background/elevated", "surface", "background");
2258
+ assignColor("surface/base", "surface", "background");
2259
+ assignColor("surface/subtle", "neutral-100", "surface", "background");
2260
+ assignColor("surface/inverted", "neutral-900", "text", "primary-900");
2261
+ assignColor("content/primary", "text", "body", "neutral-900");
2262
+ assignColor("content/secondary", "mutedText", "secondary", "neutral-600");
2263
+ assignColor("accent/primary", "primary", "accent");
2264
+ assignColor("accent/secondary", "accent", "secondary", "primary-400");
2265
+ assignColor("border/base", "border", "neutral-300");
2266
+ return tokens;
2267
+ }
2268
+ function cloneThemeTokens(source) {
2269
+ return JSON.parse(JSON.stringify(source));
2270
+ }
2271
+
2272
+ //#endregion
2273
+ //#region ../theme-core/src/surfaces/foregroundInteraction.ts
2274
+ /**
2275
+ * How far a hover state softens away from its resting foreground, as a
2276
+ * percentage of full opacity.
2277
+ *
2278
+ * Softening — rather than brightening — is forced by the token vocabulary
2279
+ * rather than chosen for taste. A resolved foreground is frequently an
2280
+ * `on*` token, which `expandPalette` emits as pure `#FFFFFF` or `#000000`
2281
+ * with no shade ramp: over a primary-painted section the resolved text role
2282
+ * is `onPrimary`, and white has no headroom left to move further from its
2283
+ * background. Moving *toward* the mid-range always has headroom, whatever
2284
+ * the resting colour, and it keeps the hue exactly.
2285
+ *
2286
+ * 85% is the measured floor, not a round number. Softening costs contrast, and
2287
+ * the binding case is a scrolled overlay header: `contrast-glass` paints 60%
2288
+ * black or white, which composites the backdrop into a known band whatever the
2289
+ * page content underneath happens to be. The lightest backdrop a dark glass can
2290
+ * present is 60% black over white, `#666666`; a white foreground softened to
2291
+ * 85% reads 4.69:1 there, against 4.07:1 at 75%, which fails. Nav links are
2292
+ * 18px regular (`textSize: 'lg'`), so the bar is WCAG AA's 4.5:1 rather than
2293
+ * the large-text 3:1.
2294
+ *
2295
+ * The mirror case — a light glass over dark content, `#999999` — cannot be met
2296
+ * by any softening, because the *resting* colour is already only about 4.6:1
2297
+ * there. That is a property of the glass rather than of this derivation, and
2298
+ * it wants fixing where the glass tone is chosen.
2299
+ */
2300
+ const FOREGROUND_HOVER_OPACITY_PERCENT = 85;
2301
+ /**
2302
+ * Derive the hover foreground that belongs with a resolved resting foreground.
2303
+ *
2304
+ * A resting foreground is chosen to be legible against a known surface. A hover
2305
+ * state derived from it stays within a bounded distance of that colour, so it
2306
+ * inherits most of the resting colour's contrast margin instead of asserting
2307
+ * an unrelated brand colour that no one checked against the surface.
2308
+ *
2309
+ * That bounded-distance property is what lets this be used on a transparent
2310
+ * overlay header, where the backdrop is only partly knowable: the scrolled
2311
+ * state composites a 60% glass tint over arbitrary page content, so no
2312
+ * compile-time contrast proof is available there. Staying near a colour that
2313
+ * is already trusted in both states is a weaker claim than a contrast proof,
2314
+ * and it is one this function can actually keep.
2315
+ *
2316
+ * Returns `null` when the resting token is not a plain palette token — an
2317
+ * already-faded or raw value has no well-defined softening, and the caller
2318
+ * should leave hover alone rather than guess.
2319
+ */
2320
+ function deriveForegroundHoverToken(restingToken) {
2321
+ const parts = parseColorTokenParts(restingToken);
2322
+ if (parts.kind !== "token") return null;
2323
+ if (parts.opacityPercent !== null) return null;
2324
+ return parseThemeColorToken(`${parts.token}/${FOREGROUND_HOVER_OPACITY_PERCENT}`);
2325
+ }
2326
+ /**
2327
+ * The opacity `contrast-glass` paints at. Mirrors the value in
2328
+ * `generateHeaderScrollTreatmentStyles`; the two must agree, because the whole
2329
+ * point of the bound below is that it describes what that treatment can produce.
2330
+ */
2331
+ const CONTRAST_GLASS_ALPHA = .6;
2332
+ /**
2333
+ * The extreme backdrop a glass tone can present, whatever page content happens
2334
+ * to be underneath it.
2335
+ *
2336
+ * A 60% tint clamps the composite into a known band, which is what makes a
2337
+ * conservative check possible at compile time even though the content passing
2338
+ * under a sticky header is not knowable. For a dark glass the composite is
2339
+ * lightest over white; for a light glass it is darkest over black. Those are
2340
+ * the two ends that a foreground has to survive.
2341
+ */
2342
+ function worstCaseGlassBackdrop(tone) {
2343
+ const [over, tint] = tone === "dark" ? ["#ffffff", "#000000"] : ["#000000", "#ffffff"];
2344
+ return compositeHex$1(tint, over, CONTRAST_GLASS_ALPHA);
2345
+ }
2346
+ /** Flatten `foreground` at `alpha` over `background` into an opaque hex. */
2347
+ function compositeHex$1(foreground, background, alpha) {
2348
+ const f = hexToRgb(foreground);
2349
+ const b = hexToRgb(background);
2350
+ return rgbToHex(Math.round(mix$2(b.r, f.r, alpha)), Math.round(mix$2(b.g, f.g, alpha)), Math.round(mix$2(b.b, f.b, alpha)));
2351
+ }
2352
+ /**
2353
+ * Decide whether the resting foreground can safely be softened for hover.
2354
+ *
2355
+ * `glassTone` is the scroll treatment's compiled tone, or `null` when the style
2356
+ * has no glass. Without one there is no bound on what the header can end up
2357
+ * over, so this declines to soften rather than guess.
2358
+ *
2359
+ * The light-glass end cannot be satisfied by any softening: against `#999999`
2360
+ * a dark resting foreground is already close to the 4.5:1 line, so every step
2361
+ * away from it fails. That is why this returns a plan rather than a colour —
2362
+ * the caller has to have a non-colour affordance to fall back on.
2363
+ */
2364
+ function planForegroundHover(restingToken, restingHex, glassTone) {
2365
+ const softened = deriveForegroundHoverToken(restingToken);
2366
+ if (!softened) return { kind: "resting" };
2367
+ if (!restingHex || !glassTone) return { kind: "resting" };
2368
+ const backdrop = worstCaseGlassBackdrop(glassTone);
2369
+ const softenedHex = compositeHex$1(restingHex, backdrop, FOREGROUND_HOVER_OPACITY_PERCENT / 100);
2370
+ return contrastRatio(softenedHex, backdrop) >= ORDINARY_TEXT_MIN_CONTRAST_RATIO ? {
2371
+ kind: "softened",
2372
+ token: softened
2373
+ } : { kind: "resting" };
2374
+ }
2375
+
2164
2376
  //#endregion
2165
2377
  //#region ../theme-core/src/palette/utils/onColor.ts
2166
2378
  const BACKGROUND_ROLE_NAMES = new Set([
@@ -2328,63 +2540,288 @@ function resolveDeterministicForeground(backgroundReference, candidates, palette
2328
2540
  link: safeTokenOr(candidates.link, text, backgroundHex, palette),
2329
2541
  supporting: safeTokenOr(candidates.supporting, text, backgroundHex, palette)
2330
2542
  }
2331
- };
2332
- }
2333
- function resolveTrustedTreatmentForeground(candidates, palette) {
2334
- const text = canonicalToken(candidates.text, palette);
2335
- if (!text) return { kind: "unknown" };
2336
- return {
2337
- kind: "known",
2338
- roles: {
2339
- text,
2340
- heading: canonicalToken(candidates.heading, palette) ?? text,
2341
- link: canonicalToken(candidates.link, palette) ?? text,
2342
- supporting: canonicalToken(candidates.supporting, palette) ?? text
2543
+ };
2544
+ }
2545
+ function resolveTrustedTreatmentForeground(candidates, palette) {
2546
+ const text = canonicalToken(candidates.text, palette);
2547
+ if (!text) return { kind: "unknown" };
2548
+ return {
2549
+ kind: "known",
2550
+ roles: {
2551
+ text,
2552
+ heading: canonicalToken(candidates.heading, palette) ?? text,
2553
+ link: canonicalToken(candidates.link, palette) ?? text,
2554
+ supporting: canonicalToken(candidates.supporting, palette) ?? text
2555
+ }
2556
+ };
2557
+ }
2558
+ function safeTokenOr(candidate$1, fallback, backgroundHex, palette) {
2559
+ return firstSafeToken([candidate$1], backgroundHex, palette) ?? fallback;
2560
+ }
2561
+ function firstSafeToken(candidates, backgroundHex, palette, preferredEquivalentTokens = []) {
2562
+ const seen = new Set();
2563
+ for (const candidate$1 of candidates) {
2564
+ const token = canonicalToken(candidate$1, palette, preferredEquivalentTokens);
2565
+ if (!token || seen.has(token)) continue;
2566
+ seen.add(token);
2567
+ const foregroundHex = resolveConcreteColorHex(token, palette);
2568
+ if (foregroundHex && contrastRatio(backgroundHex, foregroundHex) >= ORDINARY_TEXT_MIN_CONTRAST_RATIO) {
2569
+ return token;
2570
+ }
2571
+ }
2572
+ return null;
2573
+ }
2574
+ function canonicalToken(reference, palette, preferredEquivalentTokens = []) {
2575
+ const normalized = normalizedReference(reference);
2576
+ if (!normalized || normalized === "transparent") return null;
2577
+ const direct = parseThemeColorToken(normalized);
2578
+ if (direct && resolveConcreteColorHex(direct, palette)) return direct;
2579
+ const referenceHex = resolveConcreteColorHex(normalized, palette);
2580
+ if (!referenceHex) return null;
2581
+ const equivalent = [
2582
+ ...preferredEquivalentTokens,
2583
+ ...CANONICAL_EQUIVALENT_TOKEN_ORDER,
2584
+ ...Object.keys(palette).sort()
2585
+ ].find((token) => {
2586
+ const parsed = parseThemeColorToken(token);
2587
+ return parsed !== null && resolveConcreteColorHex(parsed, palette)?.toLowerCase() === referenceHex.toLowerCase();
2588
+ });
2589
+ return equivalent ? parseThemeColorToken(equivalent) : null;
2590
+ }
2591
+ function paletteToken(reference) {
2592
+ const parts = parseColorTokenParts(reference);
2593
+ return parts.kind === "token" && parts.opacityPercent === null ? parts.token : null;
2594
+ }
2595
+ function normalizedReference(value) {
2596
+ if (typeof value !== "string") return null;
2597
+ const normalized = value.trim();
2598
+ return normalized.length > 0 ? normalized : null;
2599
+ }
2600
+
2601
+ //#endregion
2602
+ //#region ../theme-core/src/utils/colorStyles.ts
2603
+ const COLOR_VAR_PREFIX = "--tb-";
2604
+ function parseToken(source) {
2605
+ const parts = parseColorTokenParts(source);
2606
+ if (parts.kind === "raw") {
2607
+ return {
2608
+ token: source,
2609
+ raw: parts.value
2610
+ };
2611
+ }
2612
+ if (parts.kind === "invalid") {
2613
+ return { token: source };
2614
+ }
2615
+ return {
2616
+ token: parts.token,
2617
+ opacityPercent: parts.opacityPercent === null ? undefined : parts.opacityPercent
2618
+ };
2619
+ }
2620
+ const SURFACE_TEXT_ROLE_BY_TOKEN = {
2621
+ text: "text",
2622
+ heading: "heading",
2623
+ mutedText: "supporting"
2624
+ };
2625
+ function surfaceRoleForTextToken(token) {
2626
+ return Object.prototype.hasOwnProperty.call(SURFACE_TEXT_ROLE_BY_TOKEN, token) ? SURFACE_TEXT_ROLE_BY_TOKEN[token] : null;
2627
+ }
2628
+ /**
2629
+ * Converts a theme color token to a CSS rgb value.
2630
+ * This is the primary function for applying theme colors to CSS properties.
2631
+ *
2632
+ * @param token - Theme color token (semantic, with opacity, or raw)
2633
+ * @returns CSS color value (rgb with optional alpha, or raw CSS value)
2634
+ *
2635
+ * @example
2636
+ * rgbColorValue('primary') // => 'rgb(var(--tb-primary))'
2637
+ * rgbColorValue('surface/95') // => 'rgb(var(--tb-surface) / 0.95)'
2638
+ * rgbColorValue('raw:transparent') // => 'transparent'
2639
+ */
2640
+ function rgbColorValue(token) {
2641
+ const { token: baseToken, opacityPercent, raw } = parseToken(token);
2642
+ if (raw) {
2643
+ return raw;
2644
+ }
2645
+ const cssVar = `${COLOR_VAR_PREFIX}${baseToken}`;
2646
+ if (opacityPercent === undefined) {
2647
+ return `rgb(var(${cssVar}))`;
2648
+ }
2649
+ return `rgb(var(${cssVar}) / ${opacityPercent / 100})`;
2650
+ }
2651
+ /**
2652
+ * Converts a theme color token to a CSS variable reference.
2653
+ * Note: This returns the base variable without rgb() wrapper, ignoring opacity.
2654
+ *
2655
+ * @param token - Theme color token
2656
+ * @returns CSS variable reference
2657
+ *
2658
+ * @example
2659
+ * cssVarValue('primary') // => 'var(--tb-primary)'
2660
+ * cssVarValue('surface/95') // => 'var(--tb-surface)' (opacity ignored)
2661
+ */
2662
+ function cssVarValue(token) {
2663
+ const { token: baseToken } = parseToken(token);
2664
+ return `var(${COLOR_VAR_PREFIX}${baseToken})`;
2665
+ }
2666
+ /**
2667
+ * Creates a backgroundColor style object from a theme token.
2668
+ *
2669
+ * @param token - Theme color token
2670
+ * @returns CSSProperties with backgroundColor
2671
+ */
2672
+ function backgroundColorStyle(token) {
2673
+ return { backgroundColor: rgbColorValue(token) };
2674
+ }
2675
+ /**
2676
+ * Creates a text color style object from a theme token.
2677
+ *
2678
+ * @param token - Theme color token
2679
+ * @returns CSSProperties with color
2680
+ */
2681
+ function textColorStyle(token) {
2682
+ const parsed = parseToken(token);
2683
+ const surfaceRole = parsed.raw ? null : surfaceRoleForTextToken(parsed.token);
2684
+ if (surfaceRole) {
2685
+ return { color: parsed.opacityPercent === undefined ? surfaceForegroundValue(surfaceRole) : surfaceForegroundValueWithOpacity(surfaceRole, parsed.opacityPercent) };
2686
+ }
2687
+ return { color: rgbColorValue(token) };
2688
+ }
2689
+ /**
2690
+ * Creates a border color style object from a theme token.
2691
+ *
2692
+ * @param token - Theme color token
2693
+ * @returns CSSProperties with borderColor
2694
+ */
2695
+ function borderColorStyle(token) {
2696
+ return { borderColor: rgbColorValue(token) };
2697
+ }
2698
+ /**
2699
+ * Creates a linear gradient background from theme tokens.
2700
+ *
2701
+ * @param tokens - Array of theme color tokens for gradient stops
2702
+ * @param direction - CSS gradient direction (default: 'to bottom')
2703
+ * @returns CSSProperties with backgroundImage
2704
+ *
2705
+ * @example
2706
+ * gradientBackgroundStyle(['primary', 'secondary'])
2707
+ * // => { backgroundImage: 'linear-gradient(to bottom, rgb(var(--tb-primary)), rgb(var(--tb-secondary)))' }
2708
+ *
2709
+ * @example
2710
+ * gradientBackgroundStyle(['primary/80', 'secondary/60'], '45deg')
2711
+ * // => { backgroundImage: 'linear-gradient(45deg, rgb(var(--tb-primary) / 0.8), rgb(var(--tb-secondary) / 0.6))' }
2712
+ */
2713
+ function gradientBackgroundStyle(tokens, direction = "to bottom") {
2714
+ const stops = tokens.map((stop) => rgbColorValue(stop)).join(", ");
2715
+ return { backgroundImage: `linear-gradient(${direction}, ${stops})` };
2716
+ }
2717
+ /**
2718
+ * Merges multiple CSSProperties objects into a single object.
2719
+ * Later styles override earlier ones. Undefined values are filtered out.
2720
+ * Returns undefined if the merged result is empty.
2721
+ *
2722
+ * @param styles - Variable number of CSSProperties objects (can be undefined)
2723
+ * @returns Merged CSSProperties object, or undefined if empty
2724
+ *
2725
+ * @example
2726
+ * mergeStyles(
2727
+ * backgroundColorStyle('surface'),
2728
+ * textColorStyle('text'),
2729
+ * borderColorStyle('border')
2730
+ * )
2731
+ * // => { backgroundColor: '...', color: '...', borderColor: '...' }
2732
+ */
2733
+ function mergeStyles(...styles) {
2734
+ const merged = {};
2735
+ for (const style of styles) {
2736
+ if (!style) continue;
2737
+ Object.assign(merged, style);
2738
+ }
2739
+ return Object.keys(merged).length ? merged : undefined;
2740
+ }
2741
+
2742
+ //#endregion
2743
+ //#region ../theme-core/src/tokens/resolver.ts
2744
+ /**
2745
+ * Resolves theme tokens to CSS variables
2746
+ *
2747
+ * This resolver ensures all button styles use theme tokens (not hardcoded colors)
2748
+ * so that changing the palette automatically updates all buttons.
2749
+ */
2750
+ var TokenResolver = class {
2751
+ constructor(theme) {
2752
+ this.theme = theme;
2753
+ }
2754
+ /**
2755
+ * Resolve a color token to a CSS variable reference wrapped in rgb()
2756
+ *
2757
+ * @param token - Token name (e.g., 'primary-500', 'white', 'text-900')
2758
+ * @returns CSS rgb() function with var reference (e.g., 'rgb(var(--tb-primary-500))')
2759
+ *
2760
+ * Note: CSS variables are stored as RGB channels (e.g., "37 99 235") following Tailwind convention,
2761
+ * so we must wrap them with rgb() to create a valid color value.
2762
+ *
2763
+ * @example
2764
+ * resolver.getColor('primary-500') // => 'rgb(var(--tb-primary-500))'
2765
+ * resolver.getColor('white') // => 'rgb(var(--tb-white))'
2766
+ */
2767
+ getColor(token) {
2768
+ if (!this.tokenExists(token)) {
2769
+ console.warn(`[TokenResolver] Color token "${token}" not found in theme palette. Button may render incorrectly.`);
2770
+ }
2771
+ return rgbColorValue(token);
2772
+ }
2773
+ /**
2774
+ * Adjust a color token's shade by a given amount
2775
+ *
2776
+ * @param token - Base token (e.g., 'primary-500')
2777
+ * @param amount - Shade adjustment (+100 = darker, -100 = lighter)
2778
+ * @returns Adjusted token name (e.g., 'primary-600')
2779
+ *
2780
+ * @example
2781
+ * resolver.adjustShade('primary-500', 100) // => 'primary-600' (darker)
2782
+ * resolver.adjustShade('primary-500', -100) // => 'primary-400' (lighter)
2783
+ */
2784
+ adjustShade(token, amount) {
2785
+ const parts = token.split("-");
2786
+ if (parts.length < 2) {
2787
+ const newShade$1 = Math.max(100, Math.min(900, 500 + amount));
2788
+ return `${token}-${newShade$1}`;
2343
2789
  }
2344
- };
2345
- }
2346
- function safeTokenOr(candidate$1, fallback, backgroundHex, palette) {
2347
- return firstSafeToken([candidate$1], backgroundHex, palette) ?? fallback;
2348
- }
2349
- function firstSafeToken(candidates, backgroundHex, palette, preferredEquivalentTokens = []) {
2350
- const seen = new Set();
2351
- for (const candidate$1 of candidates) {
2352
- const token = canonicalToken(candidate$1, palette, preferredEquivalentTokens);
2353
- if (!token || seen.has(token)) continue;
2354
- seen.add(token);
2355
- const foregroundHex = resolveConcreteColorHex(token, palette);
2356
- if (foregroundHex && contrastRatio(backgroundHex, foregroundHex) >= ORDINARY_TEXT_MIN_CONTRAST_RATIO) {
2790
+ const base = parts.slice(0, -1).join("-");
2791
+ const shade = parseInt(parts[parts.length - 1] ?? "", 10);
2792
+ if (isNaN(shade)) {
2793
+ console.warn(`[TokenResolver] Cannot adjust shade for non-shade token "${token}". Returning original token.`);
2357
2794
  return token;
2358
2795
  }
2796
+ const newShade = Math.max(100, Math.min(900, shade + amount));
2797
+ return `${base}-${newShade}`;
2359
2798
  }
2360
- return null;
2361
- }
2362
- function canonicalToken(reference, palette, preferredEquivalentTokens = []) {
2363
- const normalized = normalizedReference(reference);
2364
- if (!normalized || normalized === "transparent") return null;
2365
- const direct = parseThemeColorToken(normalized);
2366
- if (direct && resolveConcreteColorHex(direct, palette)) return direct;
2367
- const referenceHex = resolveConcreteColorHex(normalized, palette);
2368
- if (!referenceHex) return null;
2369
- const equivalent = [
2370
- ...preferredEquivalentTokens,
2371
- ...CANONICAL_EQUIVALENT_TOKEN_ORDER,
2372
- ...Object.keys(palette).sort()
2373
- ].find((token) => {
2374
- const parsed = parseThemeColorToken(token);
2375
- return parsed !== null && resolveConcreteColorHex(parsed, palette)?.toLowerCase() === referenceHex.toLowerCase();
2376
- });
2377
- return equivalent ? parseThemeColorToken(equivalent) : null;
2378
- }
2379
- function paletteToken(reference) {
2380
- const parts = parseColorTokenParts(reference);
2381
- return parts.kind === "token" && parts.opacityPercent === null ? parts.token : null;
2382
- }
2383
- function normalizedReference(value) {
2384
- if (typeof value !== "string") return null;
2385
- const normalized = value.trim();
2386
- return normalized.length > 0 ? normalized : null;
2387
- }
2799
+ /**
2800
+ * Check if a token exists in the theme palette
2801
+ *
2802
+ * @param token - Token name to check
2803
+ * @returns True if token exists in theme
2804
+ */
2805
+ tokenExists(token) {
2806
+ const commonTokens = [
2807
+ "white",
2808
+ "black",
2809
+ "transparent"
2810
+ ];
2811
+ if (commonTokens.includes(token)) {
2812
+ return true;
2813
+ }
2814
+ const parts = token.split("-");
2815
+ if (parts.length >= 2) {
2816
+ const lastPart = parts[parts.length - 1] ?? "";
2817
+ const shade = parseInt(lastPart, 10);
2818
+ if (!isNaN(shade) && shade >= 100 && shade <= 900 && shade % 100 === 0) {
2819
+ return true;
2820
+ }
2821
+ }
2822
+ return true;
2823
+ }
2824
+ };
2388
2825
 
2389
2826
  //#endregion
2390
2827
  //#region ../theme-core/src/cards/generateCardCss.ts
@@ -4087,158 +4524,17 @@ const themeV2BlockSpecificRawStyleFieldNames = [
4087
4524
  "_heroLayoutStyles",
4088
4525
  "_textCardStyles"
4089
4526
  ];
4090
- const themeV2NormalEditorRawStyleFieldNames = [...themeV2RuntimeStyleFieldNames, ...themeV2BlockSpecificRawStyleFieldNames];
4091
- const themeV2NormalEditorRawStyleFieldNameSet = new Set(themeV2NormalEditorRawStyleFieldNames);
4092
- const themeV2CompilerOwnedFieldNames = [...themeV2RuntimeStyleFieldNames, ...themeV2BlockSpecificRawStyleFieldNames];
4093
- const pageDesignEditorBackgroundOverrideOwner = "page-design-editor";
4094
- const siteChromeBackgroundOverrideOwner = "site-chrome-editor";
4095
- function isThemeV2NormalEditorRawStyleFieldName(fieldName) {
4096
- return themeV2NormalEditorRawStyleFieldNameSet.has(fieldName);
4097
- }
4098
- function isThemeV2NormalEditorRawStylePath(fieldPath) {
4099
- const rootField = fieldPath.split(".")[0] ?? "";
4100
- return isThemeV2NormalEditorRawStyleFieldName(rootField);
4101
- }
4102
-
4103
- //#endregion
4104
- //#region ../theme-core/src/utils/colorStyles.ts
4105
- const COLOR_VAR_PREFIX = "--tb-";
4106
- function parseToken(source) {
4107
- const parts = parseColorTokenParts(source);
4108
- if (parts.kind === "raw") {
4109
- return {
4110
- token: source,
4111
- raw: parts.value
4112
- };
4113
- }
4114
- if (parts.kind === "invalid") {
4115
- return { token: source };
4116
- }
4117
- return {
4118
- token: parts.token,
4119
- opacityPercent: parts.opacityPercent === null ? undefined : parts.opacityPercent
4120
- };
4121
- }
4122
- const SURFACE_TEXT_ROLE_BY_TOKEN = {
4123
- text: "text",
4124
- heading: "heading",
4125
- mutedText: "supporting"
4126
- };
4127
- function surfaceRoleForTextToken(token) {
4128
- return Object.prototype.hasOwnProperty.call(SURFACE_TEXT_ROLE_BY_TOKEN, token) ? SURFACE_TEXT_ROLE_BY_TOKEN[token] : null;
4129
- }
4130
- /**
4131
- * Converts a theme color token to a CSS rgb value.
4132
- * This is the primary function for applying theme colors to CSS properties.
4133
- *
4134
- * @param token - Theme color token (semantic, with opacity, or raw)
4135
- * @returns CSS color value (rgb with optional alpha, or raw CSS value)
4136
- *
4137
- * @example
4138
- * rgbColorValue('primary') // => 'rgb(var(--tb-primary))'
4139
- * rgbColorValue('surface/95') // => 'rgb(var(--tb-surface) / 0.95)'
4140
- * rgbColorValue('raw:transparent') // => 'transparent'
4141
- */
4142
- function rgbColorValue(token) {
4143
- const { token: baseToken, opacityPercent, raw } = parseToken(token);
4144
- if (raw) {
4145
- return raw;
4146
- }
4147
- const cssVar = `${COLOR_VAR_PREFIX}${baseToken}`;
4148
- if (opacityPercent === undefined) {
4149
- return `rgb(var(${cssVar}))`;
4150
- }
4151
- return `rgb(var(${cssVar}) / ${opacityPercent / 100})`;
4152
- }
4153
- /**
4154
- * Converts a theme color token to a CSS variable reference.
4155
- * Note: This returns the base variable without rgb() wrapper, ignoring opacity.
4156
- *
4157
- * @param token - Theme color token
4158
- * @returns CSS variable reference
4159
- *
4160
- * @example
4161
- * cssVarValue('primary') // => 'var(--tb-primary)'
4162
- * cssVarValue('surface/95') // => 'var(--tb-surface)' (opacity ignored)
4163
- */
4164
- function cssVarValue(token) {
4165
- const { token: baseToken } = parseToken(token);
4166
- return `var(${COLOR_VAR_PREFIX}${baseToken})`;
4167
- }
4168
- /**
4169
- * Creates a backgroundColor style object from a theme token.
4170
- *
4171
- * @param token - Theme color token
4172
- * @returns CSSProperties with backgroundColor
4173
- */
4174
- function backgroundColorStyle(token) {
4175
- return { backgroundColor: rgbColorValue(token) };
4176
- }
4177
- /**
4178
- * Creates a text color style object from a theme token.
4179
- *
4180
- * @param token - Theme color token
4181
- * @returns CSSProperties with color
4182
- */
4183
- function textColorStyle(token) {
4184
- const parsed = parseToken(token);
4185
- const surfaceRole = parsed.raw ? null : surfaceRoleForTextToken(parsed.token);
4186
- if (surfaceRole) {
4187
- return { color: parsed.opacityPercent === undefined ? surfaceForegroundValue(surfaceRole) : surfaceForegroundValueWithOpacity(surfaceRole, parsed.opacityPercent) };
4188
- }
4189
- return { color: rgbColorValue(token) };
4190
- }
4191
- /**
4192
- * Creates a border color style object from a theme token.
4193
- *
4194
- * @param token - Theme color token
4195
- * @returns CSSProperties with borderColor
4196
- */
4197
- function borderColorStyle(token) {
4198
- return { borderColor: rgbColorValue(token) };
4199
- }
4200
- /**
4201
- * Creates a linear gradient background from theme tokens.
4202
- *
4203
- * @param tokens - Array of theme color tokens for gradient stops
4204
- * @param direction - CSS gradient direction (default: 'to bottom')
4205
- * @returns CSSProperties with backgroundImage
4206
- *
4207
- * @example
4208
- * gradientBackgroundStyle(['primary', 'secondary'])
4209
- * // => { backgroundImage: 'linear-gradient(to bottom, rgb(var(--tb-primary)), rgb(var(--tb-secondary)))' }
4210
- *
4211
- * @example
4212
- * gradientBackgroundStyle(['primary/80', 'secondary/60'], '45deg')
4213
- * // => { backgroundImage: 'linear-gradient(45deg, rgb(var(--tb-primary) / 0.8), rgb(var(--tb-secondary) / 0.6))' }
4214
- */
4215
- function gradientBackgroundStyle(tokens, direction = "to bottom") {
4216
- const stops = tokens.map((stop) => rgbColorValue(stop)).join(", ");
4217
- return { backgroundImage: `linear-gradient(${direction}, ${stops})` };
4527
+ const themeV2NormalEditorRawStyleFieldNames = [...themeV2RuntimeStyleFieldNames, ...themeV2BlockSpecificRawStyleFieldNames];
4528
+ const themeV2NormalEditorRawStyleFieldNameSet = new Set(themeV2NormalEditorRawStyleFieldNames);
4529
+ const themeV2CompilerOwnedFieldNames = [...themeV2RuntimeStyleFieldNames, ...themeV2BlockSpecificRawStyleFieldNames];
4530
+ const pageDesignEditorBackgroundOverrideOwner = "page-design-editor";
4531
+ const siteChromeBackgroundOverrideOwner = "site-chrome-editor";
4532
+ function isThemeV2NormalEditorRawStyleFieldName(fieldName) {
4533
+ return themeV2NormalEditorRawStyleFieldNameSet.has(fieldName);
4218
4534
  }
4219
- /**
4220
- * Merges multiple CSSProperties objects into a single object.
4221
- * Later styles override earlier ones. Undefined values are filtered out.
4222
- * Returns undefined if the merged result is empty.
4223
- *
4224
- * @param styles - Variable number of CSSProperties objects (can be undefined)
4225
- * @returns Merged CSSProperties object, or undefined if empty
4226
- *
4227
- * @example
4228
- * mergeStyles(
4229
- * backgroundColorStyle('surface'),
4230
- * textColorStyle('text'),
4231
- * borderColorStyle('border')
4232
- * )
4233
- * // => { backgroundColor: '...', color: '...', borderColor: '...' }
4234
- */
4235
- function mergeStyles(...styles) {
4236
- const merged = {};
4237
- for (const style of styles) {
4238
- if (!style) continue;
4239
- Object.assign(merged, style);
4240
- }
4241
- return Object.keys(merged).length ? merged : undefined;
4535
+ function isThemeV2NormalEditorRawStylePath(fieldPath) {
4536
+ const rootField = fieldPath.split(".")[0] ?? "";
4537
+ return isThemeV2NormalEditorRawStyleFieldName(rootField);
4242
4538
  }
4243
4539
 
4244
4540
  //#endregion
@@ -4634,8 +4930,14 @@ const headerInteractionPillPaletteValues = [
4634
4930
  "accent"
4635
4931
  ];
4636
4932
  /**
4637
- * "No pill": the nav link keeps whatever hover treatment its recipe compiled,
4638
- * and the palette step writes nothing at all (#1318 Phase 4c decision 2).
4933
+ * "No pill": a colour change on hover rather than a filled background.
4934
+ *
4935
+ * The palette step writes nothing (#1318 Phase 4c decision 2), but on a
4936
+ * foreground-adaptive style that is not the end of the story: the hover colour
4937
+ * is derived from the resolved opening foreground at render time, paired with
4938
+ * the resting colour, so `quiet` resolves to a definite colour rather than to
4939
+ * whatever brand token the recipe happened to name. See
4940
+ * `applyOpeningForeground`.
4639
4941
  *
4640
4942
  * It exists because "a colour change on hover, no filled background" was not
4641
4943
  * expressible in the vocabulary, and it is what
@@ -15999,198 +16301,6 @@ function assertUniqueLayoutProfileKeys(profiles) {
15999
16301
  }
16000
16302
  }
16001
16303
 
16002
- //#endregion
16003
- //#region ../theme-core/src/tokens/tokens.ts
16004
- const colorTokens = [
16005
- "background/base",
16006
- "background/elevated",
16007
- "surface/base",
16008
- "surface/subtle",
16009
- "surface/inverted",
16010
- "content/primary",
16011
- "content/secondary",
16012
- "accent/primary",
16013
- "accent/secondary",
16014
- "border/base"
16015
- ];
16016
- const typographyTokens = [
16017
- "display/hero",
16018
- "display/section",
16019
- "heading/large",
16020
- "heading/medium",
16021
- "heading/small",
16022
- "body/normal",
16023
- "body/small",
16024
- "caption"
16025
- ];
16026
- const spacingTokens = [
16027
- "none",
16028
- "3xs",
16029
- "2xs",
16030
- "xs",
16031
- "sm",
16032
- "md",
16033
- "lg",
16034
- "xl",
16035
- "2xl",
16036
- "3xl"
16037
- ];
16038
- const radiusTokens = [
16039
- "none",
16040
- "xs",
16041
- "sm",
16042
- "md",
16043
- "lg",
16044
- "pill",
16045
- "full"
16046
- ];
16047
- const shadowTokens = [
16048
- "none",
16049
- "soft",
16050
- "medium",
16051
- "strong"
16052
- ];
16053
- const defaultThemeTokens = {
16054
- color: {
16055
- "background/base": "#ffffff",
16056
- "background/elevated": "#f5f5f5",
16057
- "surface/base": "#ffffff",
16058
- "surface/subtle": "#f9f9fb",
16059
- "surface/inverted": "#111111",
16060
- "content/primary": "#111111",
16061
- "content/secondary": "#3f3f46",
16062
- "accent/primary": "#2563eb",
16063
- "accent/secondary": "#1d4ed8",
16064
- "border/base": "#e5e7eb"
16065
- },
16066
- typography: {
16067
- "display/hero": {
16068
- fontSize: "clamp(48px, 8vw, 96px)",
16069
- lineHeight: "1.05",
16070
- fontWeight: "700"
16071
- },
16072
- "display/section": {
16073
- fontSize: "clamp(40px, 6vw, 72px)",
16074
- lineHeight: "1.1",
16075
- fontWeight: "700"
16076
- },
16077
- "heading/large": {
16078
- fontSize: "clamp(32px, 4vw, 48px)",
16079
- lineHeight: "1.2",
16080
- fontWeight: "600"
16081
- },
16082
- "heading/medium": {
16083
- fontSize: "clamp(24px, 3vw, 32px)",
16084
- lineHeight: "1.25",
16085
- fontWeight: "600"
16086
- },
16087
- "heading/small": {
16088
- fontSize: "20px",
16089
- lineHeight: "1.3",
16090
- fontWeight: "600"
16091
- },
16092
- "body/normal": {
16093
- fontSize: "16px",
16094
- lineHeight: "1.6",
16095
- fontWeight: "400"
16096
- },
16097
- "body/small": {
16098
- fontSize: "14px",
16099
- lineHeight: "1.5",
16100
- fontWeight: "400"
16101
- },
16102
- caption: {
16103
- fontSize: "12px",
16104
- lineHeight: "1.4",
16105
- fontWeight: "500"
16106
- }
16107
- },
16108
- spacing: {
16109
- none: "0px",
16110
- "3xs": "4px",
16111
- "2xs": "8px",
16112
- xs: "12px",
16113
- sm: "16px",
16114
- md: "24px",
16115
- lg: "32px",
16116
- xl: "40px",
16117
- "2xl": "56px",
16118
- "3xl": "72px"
16119
- },
16120
- radius: {
16121
- none: "0px",
16122
- xs: "2px",
16123
- sm: "4px",
16124
- md: "8px",
16125
- lg: "12px",
16126
- pill: "999px",
16127
- full: "9999px"
16128
- },
16129
- shadow: {
16130
- none: "none",
16131
- soft: "0px 2px 8px rgba(15, 23, 42, 0.08)",
16132
- medium: "0px 6px 18px rgba(15, 23, 42, 0.12)",
16133
- strong: "0px 12px 36px rgba(15, 23, 42, 0.16)"
16134
- },
16135
- palette: {
16136
- surface: "#ffffff",
16137
- "neutral-50": "#f9fafb",
16138
- "neutral-900": "#111827",
16139
- primary: "#2563eb",
16140
- "primary-50": "#eff6ff",
16141
- "primary-500": "#2563eb",
16142
- "primary-900": "#1d4ed8",
16143
- secondary: "#9333ea",
16144
- "secondary-50": "#f3e8ff",
16145
- "secondary-500": "#9333ea",
16146
- "secondary-900": "#4c1d95",
16147
- accent: "#f97316",
16148
- "accent-50": "#fff7ed",
16149
- "accent-500": "#f97316",
16150
- "accent-900": "#7c2d12"
16151
- }
16152
- };
16153
- /**
16154
- * Project the expanded semantic palette into the token buckets consumed by
16155
- * runtime transforms. Keep this mapping shared by full themes and the
16156
- * tokens-only hosted-booking runtime so the two paths cannot drift.
16157
- */
16158
- function buildThemeTokens(palette) {
16159
- const tokens = cloneThemeTokens(defaultThemeTokens);
16160
- const paletteStrings = {};
16161
- for (const [key, value] of Object.entries(palette)) {
16162
- if (typeof value === "string" && value.trim().length > 0) {
16163
- paletteStrings[key] = value;
16164
- }
16165
- }
16166
- tokens.palette = paletteStrings;
16167
- const resolveColor = (...keys) => {
16168
- for (const key of keys) {
16169
- const value = paletteStrings[key];
16170
- if (value) return value;
16171
- }
16172
- return undefined;
16173
- };
16174
- const assignColor = (token, ...keys) => {
16175
- const value = resolveColor(...keys);
16176
- if (value) tokens.color[token] = value;
16177
- };
16178
- assignColor("background/base", "background", "surface", "primary-50");
16179
- assignColor("background/elevated", "surface", "background");
16180
- assignColor("surface/base", "surface", "background");
16181
- assignColor("surface/subtle", "neutral-100", "surface", "background");
16182
- assignColor("surface/inverted", "neutral-900", "text", "primary-900");
16183
- assignColor("content/primary", "text", "body", "neutral-900");
16184
- assignColor("content/secondary", "mutedText", "secondary", "neutral-600");
16185
- assignColor("accent/primary", "primary", "accent");
16186
- assignColor("accent/secondary", "accent", "secondary", "primary-400");
16187
- assignColor("border/base", "border", "neutral-300");
16188
- return tokens;
16189
- }
16190
- function cloneThemeTokens(source) {
16191
- return JSON.parse(JSON.stringify(source));
16192
- }
16193
-
16194
16304
  //#endregion
16195
16305
  //#region ../theme-core/src/site-styles/backgroundAppearance.ts
16196
16306
  const backgroundAppearanceQuickColorTokenNames = [
@@ -28373,6 +28483,10 @@ function compareCompiledHeaders(left, right) {
28373
28483
 
28374
28484
  //#endregion
28375
28485
  //#region ../theme-core/src/site-styles/headerOpeningForeground.ts
28486
+ /** The nav-link hover effect that is a plain colour change. */
28487
+ const TEXT_COLOR_CHANGE_EFFECT_ID = "text-color-change";
28488
+ /** The nav-link hover affordance that does not depend on a colour delta. */
28489
+ const NAV_UNDERLINE_EFFECT_ID = "nav-underline";
28376
28490
  function headerStyleAvailability(recipe, context) {
28377
28491
  if (!recipe.openingForeground) return { kind: "available" };
28378
28492
  return context?.openingForeground?.kind === "known" ? { kind: "available" } : {
@@ -28399,10 +28513,7 @@ function applyOpeningForeground(theme, header, recipe, context) {
28399
28513
  return {
28400
28514
  ...withBrandForeground,
28401
28515
  navColor: foreground.roles.text,
28402
- navLinkStyle: header.navLinkStyle ? {
28403
- ...header.navLinkStyle,
28404
- colorToken: foreground.roles.link
28405
- } : header.navLinkStyle,
28516
+ navLinkStyle: header.navLinkStyle ? adaptNavLinkStyleToForeground(header.navLinkStyle, foreground.roles.link, planForegroundHover(foreground.roles.link, paletteHexForToken(theme, foreground.roles.link), scrollTreatment?.kind === "contrast-glass" ? scrollTreatment.backgroundTone : null)) : header.navLinkStyle,
28406
28517
  border: {
28407
28518
  style: "solid",
28408
28519
  width: "thin",
@@ -28412,6 +28523,79 @@ function applyOpeningForeground(theme, header, recipe, context) {
28412
28523
  ...scrollTreatment ? { scrollTreatment } : {}
28413
28524
  };
28414
28525
  }
28526
+ /**
28527
+ * Give the resolved foreground both the resting and the hover colour.
28528
+ *
28529
+ * Adapting the resting colour alone left hover as whatever the recipe wrote —
28530
+ * for `minimal-surface` a bare `primary`, which is a brand colour no one had
28531
+ * checked against the section the header floats over. On a primary-painted
28532
+ * opening section that resolves to the section's own colour, and the link
28533
+ * disappears on hover.
28534
+ *
28535
+ * The two colours are a pair: the scroll treatment picks its glass tone from
28536
+ * the resting foreground precisely so one colour reads both against the opening
28537
+ * section and against the glass. A hover colour outside that pairing is not
28538
+ * covered by either half of it.
28539
+ *
28540
+ * A pill palette is left alone. It owns hover through `background-on-hover`,
28541
+ * which carries its own paired background and foreground tokens, so hover
28542
+ * legibility there does not depend on the surface underneath at all.
28543
+ */
28544
+ function adaptNavLinkStyleToForeground(navLinkStyle, linkToken, hoverPlan) {
28545
+ const adapted = {
28546
+ ...navLinkStyle,
28547
+ colorToken: linkToken
28548
+ };
28549
+ const hoverEffects = navLinkStyle.effects?.hover ?? [];
28550
+ const hasColorChange = hoverEffects.some(isTextColorChangeEffect);
28551
+ if (!hasColorChange && hoverEffects.length > 0) return adapted;
28552
+ if (!hasColorChange && !navLinkStyle.hoverColorToken) return adapted;
28553
+ const underline = {
28554
+ effectId: NAV_UNDERLINE_EFFECT_ID,
28555
+ options: {
28556
+ style: "grow",
28557
+ colorToken: linkToken
28558
+ }
28559
+ };
28560
+ const withoutColorChange = hoverEffects.filter((effect) => !isTextColorChangeEffect(effect) && !isNavUnderlineEffect(effect));
28561
+ if (hoverPlan.kind === "resting") {
28562
+ return {
28563
+ ...adapted,
28564
+ hoverColorToken: null,
28565
+ effects: {
28566
+ ...adapted.effects,
28567
+ hover: [...withoutColorChange, underline]
28568
+ }
28569
+ };
28570
+ }
28571
+ const hoverColorToken = hoverPlan.token;
28572
+ return {
28573
+ ...adapted,
28574
+ hoverColorToken,
28575
+ effects: {
28576
+ ...adapted.effects,
28577
+ hover: [...hoverEffects.map((effect) => isTextColorChangeEffect(effect) ? {
28578
+ ...effect,
28579
+ options: {
28580
+ ...effect.options,
28581
+ hoverColorToken
28582
+ }
28583
+ } : effect).filter((effect) => !isNavUnderlineEffect(effect)), underline]
28584
+ }
28585
+ };
28586
+ }
28587
+ function isTextColorChangeEffect(effect) {
28588
+ return effect.effectId === TEXT_COLOR_CHANGE_EFFECT_ID;
28589
+ }
28590
+ function isNavUnderlineEffect(effect) {
28591
+ return effect.effectId === NAV_UNDERLINE_EFFECT_ID;
28592
+ }
28593
+ /** Concrete hex behind a plain palette token, or null when it is not one. */
28594
+ function paletteHexForToken(theme, token) {
28595
+ const parts = parseColorTokenParts(token);
28596
+ if (parts.kind !== "token" || parts.opacityPercent !== null) return null;
28597
+ return expandPalette(theme.palette)[parts.token] ?? null;
28598
+ }
28415
28599
  function compileHeaderScrollTreatment(theme, recipe, foregroundToken) {
28416
28600
  if (recipe.scrollTreatment !== "contrast-glass") return undefined;
28417
28601
  const tokenParts = parseColorTokenParts(foregroundToken);
@@ -32553,4 +32737,4 @@ function parseSdkEventRunUpsertInput(input, runIndex, options) {
32553
32737
  }
32554
32738
 
32555
32739
  //#endregion
32556
- export { defineBlockDesignMetadata as $, surfaceForegroundDeclarations as $n, borderColorStyle as $t, defaultPaletteForSiteStyle as A, NAV_LINK_TEXT_SIZE_MAP as An, __toESM as Ar, themeSchema as At, parsePersistedHeaderLayoutId as B, ICON_SIZE_PX as Bn, BREAKPOINT_SM as Bt, retargetCompiledBoundaryRuntimeForContentAdjacency as C, getComponentShadowVar as Cn, createFormRef as Cr, themeButtonClassName as Ct, curatedSiteStyles as D, NAV_LINK_LETTER_SPACING_MAP as Dn, createVenueRef as Dr, generateShadowValue as Dt, concretePaletteToTheme as E, NAV_LINK_BORDER_RADIUS_MAP as En, createSiteCommandSiteRef as Er, formatButtonColourContractError as Et, findFooterLook as F, ACCORDION_OPEN_ACCENT_THICKNESS as Fn, generateMobileOverlayBackdropFilterFix as Ft, parseGradientPresetId as G, isOnColorBackgroundCandidateToken as Gn, MQ_SM_UP as Gt, gradientToCss as H, getDefaultCardVariants as Hn, MQ_LG_UP as Ht, guaranteedFallbackFooterLookId as I, ACCORDION_SPACING_MAP as In, getHeaderPaintTargets as It, backgroundAppearanceColorTokenNames as J, contrastRatio as Jn, isCssColorLiteral as Jt, findCuratedPalette as K, onColorTokenName as Kn, MQ_TABLET_ONLY as Kt, parsePersistedFooterLookId as L, ACCORDION_TRIGGER_FONT_SCALE as Ln, renderHeaderPaintTargetRules as Lt, headerLayoutChoicesForSiteStyle as M, INPUT_FONT_SIZE_MAP as Mn, boxBackgroundOverlaySchema as Mt, headerStyleChoicesForLayout as N, INPUT_SPACING_MAP as Nn, boxBackgroundSchema as Nt, defaultButtonStyleSelectionForSiteStyle as O, NAV_LINK_PADDING_MAP as On, refSlug as Or, expandPalette as Ot, compileTransitionRuntime as P, LABEL_FONT_SIZE_MAP as Pn, generateHeaderScrollTreatmentStyles as Pt, defaultThemeTokens as Q, DEFAULT_SURFACE_TEXT_COLOR as Qn, backgroundColorStyle as Qt, getHeaderLayout as R, ACCORDION_TRIGGER_LETTER_SPACING as Rn, BREAKPOINT_LG as Rt, eventBookingOfferIntentSchema as S, getComponentBorderWidthPx as Sn, createFooterRef as Sr, parseThemeButtonVariantId as St, readHeaderNavFontSizePreference as T, parseCustomCssRules as Tn, createPageRef as Tr, compileButtonColourStates as Tt, GRADIENT_PRESET_IDS as U, generateCardCss as Un, MQ_MD_DOWN as Ut, parsePersistedHeaderStyleId as V, generateCardCoreCss as Vn, BREAKPOINT_XL as Vt, getGradientPreset as W, resolveSurfaceForeground as Wn, MQ_SM_DOWN as Wt, parseBackgroundAppearanceColorToken as X, hexToRgb as Xn, resolveThemeColorReferenceValue as Xt, backgroundAppearanceQuickColorTokenNames as Y, featuresFromHex as Yn, resolveOnColorForThemeToken as Yt, buildThemeTokens as Z, oklchToHexGamut as Zn, resolveTokenOrCssColorValue as Zt, entryIdentifierForCommand as _, makeBoundaryId as _n, createBlocksRef as _r, resolveContextualButtonRole as _t, classifySdkEventBookingOffer as a, asAppearancePresetId as an, cardVariantRoleValues as ar, buttonStyleSelectionSchema as at, pagePathForCommand as b, tileTreatmentValues as bn, createEventCategoryRef as br, generateBaseInteractiveCSS as bt, stableStringify as c, assertNever$1 as cn, mediaIdentifierSchema as cr, PUBLIC_BUTTON_ROLES as ct, mapEventCommandContent as d, defineNoBlockDesignCapability as dn, persistableMediaSchema as dr, RESOLVED_BUTTON_DISABLED_DECLARATIONS as dt, cssVarValue as en, surfaceForegroundStyle as er, require_jsx_runtime as et, evaluateSiteCommandBatchExposure as f, eventListTreatmentValues as fn, VARIANT_ROLES as fr, resolvedButtonColourDeclarations as ft, blockOrderIdentifiersForCommand as g, introCompositionTreatmentValues as gn, createBlockRef as gr, deriveButtonSurfaceContext as gt, blockIdentifierForCommand as h, headingDecorationColorTokenValues as hn, parseBlockColorToken as hr, contextualButtonSurfaceStyles as ht, classifySdkEventSchedule as i, pageDesignEditorBackgroundOverrideOwner as in, cardRoleSchema as ir, findButtonStyleRecipe as it, getCuratedSiteStyle as j, FONT_WEIGHT_MAP$1 as jn, typographySchema as jt, defaultHeaderSelectionForSiteStyle as k, NAV_LINK_PADDING_X_MAP as kn, slugPart as kr, getTextColorForBackground as kt, createDryRunReport as l, defineBlockDesignCapability as ln, mediaSchema as lr, AFFORDANCE_SUPPRESS_CLASS as lt, getSiteCommandMetadata as m, featureCardCompositionTreatmentValues as mn, unsafeAsOpaqueButtonColourToken as mr, contextualButtonSurfaceResetStyles as mt, sdkEventOccurrenceInputSchema as n, rgbColorValue as nn, TokenResolver as nr, pickShadeTowardBackground as nt, venueManagementFields as o, asDesignBlockId as on, normalizeCardRole as or, createDefaultButtonStyleSelection as ot, evaluateSiteCommandExposure as p, eyebrowTreatmentValues as pn, buttonVariantIdSchema as pr, resolvedButtonFocusDeclarations as pt, boundaryRuntimeMetricsForTransition as q, ORDINARY_TEXT_MIN_CONTRAST_RATIO as qn, headerVariantValues as qt, sdkEventUpsertOptionalFieldSchemas as r, textColorStyle as rn, generateShadowValue$1 as rr, generateCssVars as rt, sortRecord as s, asSiteStyleId as sn, paletteSchema as sr, isButtonStyleId as st, classifySdkEventTiming as t, mergeStyles as tn, surfaceForegroundValue as tr, require_react as tt, parseAndPlanCommandBatchForTypes as u, defineFrameCapability as un, mediaTypeDiscriminants as ur, generateButtonCss as ut, pageIdentifierForBlockCommand as v, offeringsPreviewCompositionTreatmentValues as vn, createCommandRef as vr, generateDefaultButtonSystem as vt, mergeHeaderNavTypographyPreference as w, parseBlockCustomCssRules as wn, createNavigationRef as wr, parseThemeForPersistence as wt, hasConfiguredBookingVertical as x, visibleHeadingDecorationFamilyValues as xn, createEventSeriesRef as xr, generateInteractiveChildInheritanceCSS as xt, pageIdentifierForCommand as y, testimonialsCompositionTreatmentValues as yn, createContentEntryRef as yr, generateEffectsCSS as yt, getHeaderStyle as z, ICON_CONFIG as zn, BREAKPOINT_MD as zt };
32740
+ export { require_react as $, DEFAULT_SURFACE_TEXT_COLOR as $n, asSiteStyleId as $t, defaultPaletteForSiteStyle as A, ACCORDION_TRIGGER_LETTER_SPACING as An, __toESM as Ar, boxBackgroundOverlaySchema as At, parsePersistedHeaderLayoutId as B, mergeStyles as Bn, MQ_LG_UP as Bt, retargetCompiledBoundaryRuntimeForContentAdjacency as C, FONT_WEIGHT_MAP$1 as Cn, createFormRef as Cr, compileButtonColourStates as Ct, curatedSiteStyles as D, ACCORDION_OPEN_ACCENT_THICKNESS as Dn, createVenueRef as Dr, getTextColorForBackground as Dt, concretePaletteToTheme as E, LABEL_FONT_SIZE_MAP as En, createSiteCommandSiteRef as Er, expandPalette as Et, findFooterLook as F, generateCardCss as Fn, renderHeaderPaintTargetRules as Ft, parseGradientPresetId as G, onColorTokenName as Gn, headerVariantValues as Gt, gradientToCss as H, textColorStyle as Hn, MQ_SM_DOWN as Ht, guaranteedFallbackFooterLookId as I, TokenResolver as In, BREAKPOINT_LG as It, backgroundAppearanceColorTokenNames as J, ORDINARY_TEXT_MIN_CONTRAST_RATIO as Jn, resolveThemeColorReferenceValue as Jt, findCuratedPalette as K, buildThemeTokens as Kn, isCssColorLiteral as Kt, parsePersistedFooterLookId as L, backgroundColorStyle as Ln, BREAKPOINT_MD as Lt, headerLayoutChoicesForSiteStyle as M, ICON_SIZE_PX as Mn, generateHeaderScrollTreatmentStyles as Mt, headerStyleChoicesForLayout as N, generateCardCoreCss as Nn, generateMobileOverlayBackdropFilterFix as Nt, defaultButtonStyleSelectionForSiteStyle as O, ACCORDION_SPACING_MAP as On, refSlug as Or, themeSchema as Ot, compileTransitionRuntime as P, getDefaultCardVariants as Pn, getHeaderPaintTargets as Pt, require_jsx_runtime as Q, oklchToHexGamut as Qn, asDesignBlockId as Qt, getHeaderLayout as R, borderColorStyle as Rn, BREAKPOINT_SM as Rt, eventBookingOfferIntentSchema as S, NAV_LINK_TEXT_SIZE_MAP as Sn, createFooterRef as Sr, parseThemeForPersistence as St, readHeaderNavFontSizePreference as T, INPUT_SPACING_MAP as Tn, createPageRef as Tr, generateShadowValue as Tt, GRADIENT_PRESET_IDS as U, resolveSurfaceForeground as Un, MQ_SM_UP as Ut, parsePersistedHeaderStyleId as V, rgbColorValue as Vn, MQ_MD_DOWN as Vt, getGradientPreset as W, isOnColorBackgroundCandidateToken as Wn, MQ_TABLET_ONLY as Wt, parseBackgroundAppearanceColorToken as X, featuresFromHex as Xn, pageDesignEditorBackgroundOverrideOwner as Xt, backgroundAppearanceQuickColorTokenNames as Y, contrastRatio as Yn, resolveTokenOrCssColorValue as Yt, defineBlockDesignMetadata as Z, hexToRgb as Zn, asAppearancePresetId as Zt, entryIdentifierForCommand as _, parseCustomCssRules as _n, createBlocksRef as _r, generateEffectsCSS as _t, classifySdkEventBookingOffer as a, eyebrowTreatmentValues as an, cardVariantRoleValues as ar, isButtonStyleId as at, pagePathForCommand as b, NAV_LINK_PADDING_MAP as bn, createEventCategoryRef as br, parseThemeButtonVariantId as bt, stableStringify as c, introCompositionTreatmentValues as cn, mediaIdentifierSchema as cr, generateButtonCss as ct, mapEventCommandContent as d, testimonialsCompositionTreatmentValues as dn, persistableMediaSchema as dr, resolvedButtonFocusDeclarations as dt, assertNever$1 as en, surfaceForegroundDeclarations as er, pickShadeTowardBackground as et, evaluateSiteCommandBatchExposure as f, tileTreatmentValues as fn, VARIANT_ROLES as fr, contextualButtonSurfaceResetStyles as ft, blockOrderIdentifiersForCommand as g, parseBlockCustomCssRules as gn, createBlockRef as gr, generateDefaultButtonSystem as gt, blockIdentifierForCommand as h, getComponentShadowVar as hn, parseBlockColorToken as hr, resolveContextualButtonRole as ht, classifySdkEventSchedule as i, eventListTreatmentValues as in, cardRoleSchema as ir, createDefaultButtonStyleSelection as it, getCuratedSiteStyle as j, ICON_CONFIG as jn, boxBackgroundSchema as jt, defaultHeaderSelectionForSiteStyle as k, ACCORDION_TRIGGER_FONT_SCALE as kn, slugPart as kr, typographySchema as kt, createDryRunReport as l, makeBoundaryId as ln, mediaSchema as lr, RESOLVED_BUTTON_DISABLED_DECLARATIONS as lt, getSiteCommandMetadata as m, getComponentBorderWidthPx as mn, unsafeAsOpaqueButtonColourToken as mr, deriveButtonSurfaceContext as mt, sdkEventOccurrenceInputSchema as n, defineFrameCapability as nn, surfaceForegroundValue as nr, findButtonStyleRecipe as nt, venueManagementFields as o, featureCardCompositionTreatmentValues as on, normalizeCardRole as or, PUBLIC_BUTTON_ROLES as ot, evaluateSiteCommandExposure as p, visibleHeadingDecorationFamilyValues as pn, buttonVariantIdSchema as pr, contextualButtonSurfaceStyles as pt, boundaryRuntimeMetricsForTransition as q, defaultThemeTokens as qn, resolveOnColorForThemeToken as qt, sdkEventUpsertOptionalFieldSchemas as r, defineNoBlockDesignCapability as rn, generateShadowValue$1 as rr, buttonStyleSelectionSchema as rt, sortRecord as s, headingDecorationColorTokenValues as sn, paletteSchema as sr, AFFORDANCE_SUPPRESS_CLASS as st, classifySdkEventTiming as t, defineBlockDesignCapability as tn, surfaceForegroundStyle as tr, generateCssVars as tt, parseAndPlanCommandBatchForTypes as u, offeringsPreviewCompositionTreatmentValues as un, mediaTypeDiscriminants as ur, resolvedButtonColourDeclarations as ut, pageIdentifierForBlockCommand as v, NAV_LINK_BORDER_RADIUS_MAP as vn, createCommandRef as vr, generateBaseInteractiveCSS as vt, mergeHeaderNavTypographyPreference as w, INPUT_FONT_SIZE_MAP as wn, createNavigationRef as wr, formatButtonColourContractError as wt, hasConfiguredBookingVertical as x, NAV_LINK_PADDING_X_MAP as xn, createEventSeriesRef as xr, themeButtonClassName as xt, pageIdentifierForCommand as y, NAV_LINK_LETTER_SPACING_MAP as yn, createContentEntryRef as yr, generateInteractiveChildInheritanceCSS as yt, getHeaderStyle as z, cssVarValue as zn, BREAKPOINT_XL as zt };