@bison-lab/payload-core 3.8.0 → 3.9.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/admin.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  "use client";
2
- import { FieldDescription, FieldError, FieldLabel, useField, useFormFields } from "@payloadcms/ui";
3
- import { auditTheme, contrastLevelLabel, contrastVerdict, isThemeHex } from "@bison-lab/tokens";
2
+ import { r as stateFromColorDoc, t as colorDocFromState } from "./map-BUDkX8g1.mjs";
3
+ import { FieldDescription, FieldError, FieldLabel, useField, useForm, useFormFields } from "@payloadcms/ui";
4
+ import { GREY_SCALES, SHADE_STEPS, contrastForeground, contrastRatio, createColorScale, hexToHSL, hslToString, includedShadeSteps, isThemeHex, presetHints, regenerateColorScale, setColorScaleInclude, updateColorScale } from "@bison-lab/tokens";
4
5
  import { useEffect, useId, useMemo, useRef, useState } from "react";
5
- import { jsx, jsxs } from "react/jsx-runtime";
6
- import { findFont, fontFaceCss } from "@bison-lab/fonts";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+ import { findFont, fontFaceCss, isFontId } from "@bison-lab/fonts";
7
8
  //#region src/admin/color-field.tsx
8
9
  const HEX_ERROR = "Enter a six-digit hex colour like #1e3a5f";
9
10
  /**
@@ -39,7 +40,7 @@ const ColorField = ({ field, path, readOnly }) => {
39
40
  /* @__PURE__ */ jsx("style", {
40
41
  href: "bl-color-field",
41
42
  precedence: "default",
42
- children: CSS$2
43
+ children: CSS$3
43
44
  }),
44
45
  /* @__PURE__ */ jsx(FieldLabel, {
45
46
  htmlFor: hexId,
@@ -98,7 +99,7 @@ const ColorField = ({ field, path, readOnly }) => {
98
99
  ]
99
100
  });
100
101
  };
101
- const CSS$2 = `
102
+ const CSS$3 = `
102
103
  .bl-color-field__control{display:flex;align-items:center;gap:calc(var(--base) * .4)}
103
104
  .bl-color-field__swatch{flex:none;width:calc(var(--base) * 1.6);height:calc(var(--base) * 1.6);border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:var(--theme-elevation-50)}
104
105
  .bl-color-field__picker{flex:none;width:calc(var(--base) * 2);height:calc(var(--base) * 2);padding:0;border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:transparent;cursor:pointer}
@@ -108,11 +109,1007 @@ const CSS$2 = `
108
109
  .bl-color-field__hex:disabled{color:var(--theme-elevation-400)}
109
110
  `;
110
111
  //#endregion
112
+ //#region src/theme/appearance-labels.ts
113
+ /**
114
+ * Named choices the Appearance child shows. Values stay the token presets;
115
+ * labels match the mock workflow (Soft → pill, Gentle → minimal, Balanced
116
+ * → default, Follow device → system). No rem, ms, or CSS variable tables.
117
+ */
118
+ const APPEARANCE_CHOICES = {
119
+ defaultTheme: {
120
+ light: {
121
+ label: "Light",
122
+ hint: "Always opens in light mode"
123
+ },
124
+ dark: {
125
+ label: "Dark",
126
+ hint: "Always opens in dark mode"
127
+ },
128
+ system: {
129
+ label: "Follow device",
130
+ hint: "Matches the visitor’s system setting"
131
+ }
132
+ },
133
+ radius: {
134
+ sharp: {
135
+ label: "Sharp",
136
+ hint: "Crisp and structured"
137
+ },
138
+ subtle: {
139
+ label: "Subtle",
140
+ hint: "Clean with a little softness"
141
+ },
142
+ rounded: {
143
+ label: "Rounded",
144
+ hint: "Friendly and contemporary"
145
+ },
146
+ pill: {
147
+ label: "Soft",
148
+ hint: "Very rounded and expressive"
149
+ }
150
+ },
151
+ shadow: {
152
+ flat: {
153
+ label: "Flat",
154
+ hint: "Minimal visual depth"
155
+ },
156
+ subtle: {
157
+ label: "Subtle",
158
+ hint: "Light separation between surfaces"
159
+ },
160
+ elevated: {
161
+ label: "Elevated",
162
+ hint: "More noticeable layering and depth"
163
+ }
164
+ },
165
+ motion: {
166
+ snappy: {
167
+ label: "Quick",
168
+ hint: "Fast and responsive"
169
+ },
170
+ smooth: {
171
+ label: "Smooth",
172
+ hint: "Balanced and natural"
173
+ },
174
+ minimal: {
175
+ label: "Gentle",
176
+ hint: "Slower and more relaxed"
177
+ }
178
+ },
179
+ density: {
180
+ compact: {
181
+ label: "Compact",
182
+ hint: "Fits more content on screen"
183
+ },
184
+ default: {
185
+ label: "Balanced",
186
+ hint: "Comfortable for most sites"
187
+ },
188
+ spacious: {
189
+ label: "Spacious",
190
+ hint: "More breathing room and larger controls"
191
+ }
192
+ }
193
+ };
194
+ const APPEARANCE_FAMILY_COPY = {
195
+ defaultTheme: {
196
+ title: "Default theme",
197
+ hint: "Choose how the public site opens for visitors."
198
+ },
199
+ radius: {
200
+ title: "Corner style",
201
+ hint: "Controls the shape of buttons, inputs, cards, and panels across the site."
202
+ },
203
+ shadow: {
204
+ title: "Depth",
205
+ hint: "Choose how much cards and floating elements stand out from the page."
206
+ },
207
+ motion: {
208
+ title: "Motion",
209
+ hint: "Controls how quickly menus, cards, and interface transitions move."
210
+ },
211
+ density: {
212
+ title: "Spacing",
213
+ hint: "Choose how compact or roomy forms, buttons, cards, and page sections feel."
214
+ }
215
+ };
216
+ const FONT_ROLE_COPY = {
217
+ heading: {
218
+ label: "Heading font",
219
+ hint: "Used for page titles, section headings, and cards"
220
+ },
221
+ body: {
222
+ label: "Body font",
223
+ hint: "Used for paragraphs, navigation, forms, and buttons"
224
+ }
225
+ };
226
+ const SYSTEM_COLOR_COPY = {
227
+ primary: {
228
+ label: "Primary",
229
+ hint: "Primary actions and key links",
230
+ group: "brand"
231
+ },
232
+ secondary: {
233
+ label: "Secondary",
234
+ hint: "Supporting accents and eyebrows",
235
+ group: "brand"
236
+ },
237
+ accent: {
238
+ label: "Accent",
239
+ hint: "Dark brand surfaces",
240
+ group: "brand"
241
+ },
242
+ highlight: {
243
+ label: "Highlight",
244
+ hint: "Promotional and emphasis fills",
245
+ group: "brand"
246
+ },
247
+ success: {
248
+ label: "Success",
249
+ hint: "Theme chrome for confirmations. Not offered as a page-editor fill.",
250
+ group: "status"
251
+ },
252
+ destructive: {
253
+ label: "Destructive",
254
+ hint: "Theme chrome for errors and dangerous actions. Not offered as a page-editor fill.",
255
+ group: "status"
256
+ }
257
+ };
258
+ //#endregion
259
+ //#region src/theme/readability.ts
260
+ /** Same threshold `deriveDarkPalette` uses for brand fills. */
261
+ const DARK_FILL_LIGHTEN_BELOW = 40;
262
+ const DARK_FILL_LIGHTEN_BY = 25;
263
+ function clamp(value, min, max) {
264
+ return Math.min(max, Math.max(min, value));
265
+ }
266
+ function darkFill(color) {
267
+ if (color.l < DARK_FILL_LIGHTEN_BELOW) return {
268
+ ...color,
269
+ l: clamp(color.l + DARK_FILL_LIGHTEN_BY, 0, 100)
270
+ };
271
+ return color;
272
+ }
273
+ function sample(fill, target) {
274
+ const label = contrastForeground(fill);
275
+ const fillCss = hslToString(fill);
276
+ const ratio = contrastRatio(fillCss, label);
277
+ return {
278
+ ratio,
279
+ shortfall: Math.max(0, Math.round((target - ratio) * 100) / 100),
280
+ fill: fillCss,
281
+ label
282
+ };
283
+ }
284
+ /**
285
+ * Editable fill + the automatic label, Light and Dark. Target 7:1.
286
+ * Meets only when both pass. Warnings do not block save.
287
+ */
288
+ function fillLabelWarning(id, name, hex, target = 7) {
289
+ if (!isThemeHex(hex)) return null;
290
+ const lightFill = hexToHSL(hex);
291
+ const light = sample(lightFill, target);
292
+ const dark = sample(darkFill(lightFill), target);
293
+ return {
294
+ id,
295
+ name,
296
+ light,
297
+ dark,
298
+ meets: light.ratio >= target && dark.ratio >= target
299
+ };
300
+ }
301
+ /** Green/teal family. A warning at admin time, not a schema block. */
302
+ function successHueWarning(hex) {
303
+ if (!isThemeHex(hex)) return null;
304
+ const { h } = hexToHSL(hex);
305
+ if (h >= 80 && h <= 180) return null;
306
+ return "Keep success colors recognizable — stay in a green or teal family.";
307
+ }
308
+ /** Red family. A warning at admin time, not a schema block. */
309
+ function destructiveHueWarning(hex) {
310
+ if (!isThemeHex(hex)) return null;
311
+ const { h } = hexToHSL(hex);
312
+ if (h <= 30 || h >= 330) return null;
313
+ return "Keep destructive colors recognizable — stay in a red family.";
314
+ }
315
+ function automaticLabel(hex) {
316
+ if (!isThemeHex(hex)) return null;
317
+ return contrastForeground(hexToHSL(hex));
318
+ }
319
+ function hslCss(value) {
320
+ return `hsl(${value})`;
321
+ }
322
+ /** Which appearances fail, in words. Empty when both pass. */
323
+ function hardToReadModes(item) {
324
+ const light = item.light.shortfall > 0;
325
+ const dark = item.dark.shortfall > 0;
326
+ if (light && dark) return "in Light and Dark";
327
+ if (light) return "in Light";
328
+ if (dark) return "in Dark";
329
+ return "";
330
+ }
331
+ function readabilityHeadline(item) {
332
+ return item.meets ? `Text on ${item.name} looks readable` : `Text on ${item.name} may be hard to read`;
333
+ }
334
+ function readabilityModesLine(item) {
335
+ const modes = hardToReadModes(item);
336
+ return modes ? `Hard to read ${modes}.` : "Looks good in Light and Dark.";
337
+ }
338
+ function readabilityGapPercent(sample, target = 7) {
339
+ if (sample.shortfall <= 0) return 0;
340
+ return Math.round(sample.shortfall / target * 100);
341
+ }
342
+ function readabilityRatioLine(sample, target = 7) {
343
+ const percent = readabilityGapPercent(sample, target);
344
+ if (percent <= 0) return "Meets the contrast we want";
345
+ if (percent < 10) return "A little short of the contrast we want";
346
+ return `About ${percent}% short of the contrast we want`;
347
+ }
348
+ const READABILITY_FIX = "Darken or lighten the hex, then regenerate the scale. The site picks light or dark type for you.";
349
+ const READABILITY_PUBLISH_NOTE = "You can still publish. To improve a color, darken or lighten it and regenerate.";
350
+ //#endregion
351
+ //#region src/admin/preview-mode.tsx
352
+ const ATTR = "data-bl-theme-preview";
353
+ const EVENT = "bl-theme-preview";
354
+ function readMode() {
355
+ if (typeof document === "undefined") return "light";
356
+ return document.documentElement.getAttribute(ATTR) === "dark" ? "dark" : "light";
357
+ }
358
+ function setThemePreviewMode(mode) {
359
+ if (typeof document === "undefined") return;
360
+ document.documentElement.setAttribute(ATTR, mode);
361
+ window.dispatchEvent(new Event(EVENT));
362
+ }
363
+ function useThemePreviewMode() {
364
+ const [mode, setMode] = useState(readMode);
365
+ useEffect(() => {
366
+ const sync = () => setMode(readMode());
367
+ window.addEventListener(EVENT, sync);
368
+ return () => window.removeEventListener(EVENT, sync);
369
+ }, []);
370
+ return mode;
371
+ }
372
+ function ThemePreviewToggle() {
373
+ const mode = useThemePreviewMode();
374
+ return /* @__PURE__ */ jsxs("div", {
375
+ className: "bl-preview-toggle",
376
+ role: "group",
377
+ "aria-label": "Preview colors in",
378
+ children: [/* @__PURE__ */ jsx("button", {
379
+ type: "button",
380
+ "data-on": mode === "light" ? "" : void 0,
381
+ onClick: () => setThemePreviewMode("light"),
382
+ children: "Light"
383
+ }), /* @__PURE__ */ jsx("button", {
384
+ type: "button",
385
+ "data-on": mode === "dark" ? "" : void 0,
386
+ onClick: () => setThemePreviewMode("dark"),
387
+ children: "Dark"
388
+ })]
389
+ });
390
+ }
391
+ const THEME_PREVIEW_TOGGLE_CSS = `
392
+ .bl-preview-toggle{display:inline-flex;padding:3px;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:10px;background:var(--theme-elevation-0,#fff)}
393
+ .bl-preview-toggle button{min-height:2rem;padding:.25rem .75rem;border:0;border-radius:7px;background:transparent;color:var(--theme-elevation-600,#475569);font:inherit;font-size:13px;font-weight:600;cursor:pointer}
394
+ .bl-preview-toggle button[data-on]{background:var(--theme-elevation-800,#0f172a);color:#fff}
395
+ `;
396
+ //#endregion
397
+ //#region src/admin/readability-list.tsx
398
+ function ReadabilityDetail({ item, target = 7 }) {
399
+ return /* @__PURE__ */ jsxs("div", {
400
+ className: "bl-readability-detail",
401
+ children: [
402
+ /* @__PURE__ */ jsx("p", {
403
+ className: "bl-readability-detail__lead",
404
+ children: readabilityModesLine(item)
405
+ }),
406
+ /* @__PURE__ */ jsx("div", {
407
+ className: "bl-readability-detail__modes",
408
+ children: [["Light", item.light], ["Dark", item.dark]].map(([mode, sample]) => /* @__PURE__ */ jsxs("figure", {
409
+ "data-failing": sample.shortfall > 0 ? "" : void 0,
410
+ children: [/* @__PURE__ */ jsx("span", {
411
+ "aria-hidden": "true",
412
+ className: "bl-readability__swatch",
413
+ style: {
414
+ background: hslCss(sample.fill),
415
+ color: hslCss(sample.label)
416
+ },
417
+ children: "Aa"
418
+ }), /* @__PURE__ */ jsxs("figcaption", { children: [
419
+ /* @__PURE__ */ jsx("strong", { children: mode }),
420
+ /* @__PURE__ */ jsx("span", { children: sample.shortfall > 0 ? "Hard to read" : "Looks good" }),
421
+ /* @__PURE__ */ jsx("span", { children: readabilityRatioLine(sample, target) })
422
+ ] })]
423
+ }, mode))
424
+ }),
425
+ item.meets ? null : /* @__PURE__ */ jsx("p", {
426
+ className: "bl-readability-detail__fix",
427
+ children: READABILITY_FIX
428
+ })
429
+ ]
430
+ });
431
+ }
432
+ function ReadabilityList({ warnings, target = 7 }) {
433
+ const failing = warnings.filter((item) => !item.meets).length;
434
+ return /* @__PURE__ */ jsxs("aside", {
435
+ className: "bl-readability",
436
+ children: [/* @__PURE__ */ jsxs("div", {
437
+ className: "bl-readability__head",
438
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", { children: "Readability checks" }), /* @__PURE__ */ jsx("p", { children: "Text the site puts on each fill. Warnings do not block save." })] }), /* @__PURE__ */ jsx("span", {
439
+ "data-attention": failing ? "" : void 0,
440
+ children: failing === 0 ? "All look readable" : `${failing} may be hard to read`
441
+ })]
442
+ }), /* @__PURE__ */ jsx("ul", { children: warnings.map((item) => /* @__PURE__ */ jsxs("li", {
443
+ "data-failing": item.meets ? void 0 : "",
444
+ children: [/* @__PURE__ */ jsx("strong", { children: readabilityHeadline(item) }), /* @__PURE__ */ jsx(ReadabilityDetail, {
445
+ item,
446
+ target
447
+ })]
448
+ }, item.id)) })]
449
+ });
450
+ }
451
+ const READABILITY_LIST_CSS = `
452
+ .bl-readability{padding:calc(var(--base,16px) * .8);border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:var(--style-radius-m,12px);background:var(--theme-elevation-0,#fff)}
453
+ .bl-readability__head{display:flex;justify-content:space-between;gap:1rem;align-items:flex-start}
454
+ .bl-readability__head h2{margin:0;font-size:1rem}
455
+ .bl-readability__head p{margin:.35rem 0 0;font-size:12px;color:var(--theme-elevation-500,#64748b)}
456
+ .bl-readability__head span{flex:none;font-size:12px;font-weight:600;padding:.2rem .55rem;border-radius:999px;background:var(--theme-elevation-100,#f1f5f9)}
457
+ .bl-readability__head span[data-attention]{background:#fffbeb;color:#92400e}
458
+ .bl-readability ul{margin:.85rem 0 0;padding:0;list-style:none}
459
+ .bl-readability li{padding:.65rem 0;border-top:1px solid var(--theme-elevation-100,#f1f5f9)}
460
+ .bl-readability li:first-child{border-top:0}
461
+ .bl-readability li[data-failing]{background:#fff7ed;margin:0 -.4rem;padding:.65rem .4rem;border-radius:8px}
462
+ .bl-readability__swatch{flex:none;display:inline-flex;align-items:center;justify-content:center;width:2.75rem;height:2rem;border-radius:6px;font-weight:700;font-size:13px}
463
+ .bl-readability strong{display:block;font-size:13px}
464
+ .bl-readability-detail__lead{margin:0 0 .65rem;font-size:13px;color:var(--theme-elevation-600,#475569)}
465
+ .bl-readability-detail__modes{display:grid;grid-template-columns:1fr 1fr;gap:.65rem}
466
+ .bl-readability-detail__modes figure{display:flex;gap:.6rem;align-items:center;margin:0;padding:.55rem .6rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:10px;background:#fff}
467
+ .bl-readability-detail__modes figure[data-failing]{border-color:#fdba74;background:#fff7ed}
468
+ .bl-readability-detail__modes figcaption{display:flex;flex-direction:column;gap:.1rem;min-width:0}
469
+ .bl-readability-detail__modes figcaption strong{font-size:12px}
470
+ .bl-readability-detail__modes figcaption span{font-size:11px;color:var(--theme-elevation-500,#64748b)}
471
+ .bl-readability-detail__fix{margin:.7rem 0 0;font-size:13px;color:var(--theme-elevation-600,#475569)}
472
+ `;
473
+ //#endregion
474
+ //#region src/admin/color-scale-card.tsx
475
+ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onDelete, readOnly, role }) {
476
+ const label = automaticLabel(state.hex);
477
+ const warning = fillLabelWarning(id ?? name.toLowerCase(), name, state.hex);
478
+ const previewMode = useThemePreviewMode();
479
+ const preview = warning ? previewMode === "dark" ? warning.dark : warning.light : null;
480
+ const [open, setOpen] = useState(false);
481
+ const titleId = useId();
482
+ const hueWarning = role === "success" ? successHueWarning(state.hex) : role === "destructive" ? destructiveHueWarning(state.hex) : null;
483
+ const included = new Set(includedShadeSteps(state));
484
+ function setHex(hex) {
485
+ onChange(updateColorScale(state, { hex }));
486
+ }
487
+ function setStep(step) {
488
+ onChange(updateColorScale(state, { sourceStep: step }));
489
+ }
490
+ function toggleStep(step) {
491
+ if (step === state.sourceStep) return;
492
+ const next = new Set(included);
493
+ if (next.has(step)) next.delete(step);
494
+ else next.add(step);
495
+ onChange(setColorScaleInclude(state, SHADE_STEPS.filter((item) => next.has(item))));
496
+ }
497
+ return /* @__PURE__ */ jsxs("article", {
498
+ className: "bl-scale-card",
499
+ "data-preview": previewMode,
500
+ children: [
501
+ /* @__PURE__ */ jsxs("header", {
502
+ className: "bl-scale-card__head",
503
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
504
+ className: "bl-scale-card__title-row",
505
+ children: [
506
+ /* @__PURE__ */ jsx("h3", {
507
+ className: "bl-scale-card__title",
508
+ children: name
509
+ }),
510
+ required ? /* @__PURE__ */ jsx("span", {
511
+ className: "bl-scale-card__badge",
512
+ children: "Required"
513
+ }) : null,
514
+ custom ? /* @__PURE__ */ jsx("span", {
515
+ className: "bl-scale-card__badge",
516
+ children: "Optional"
517
+ }) : null,
518
+ warning ? warning.meets ? /* @__PURE__ */ jsxs("span", {
519
+ className: "bl-scale-card__status",
520
+ "data-ok": "",
521
+ title: `Text on ${name} looks readable`,
522
+ children: [/* @__PURE__ */ jsx("span", {
523
+ "aria-hidden": "true",
524
+ children: "✓"
525
+ }), /* @__PURE__ */ jsxs("span", {
526
+ className: "bl-scale-card__sr",
527
+ children: [
528
+ "Text on ",
529
+ name,
530
+ " looks readable"
531
+ ]
532
+ })]
533
+ }) : /* @__PURE__ */ jsx("button", {
534
+ type: "button",
535
+ className: "bl-scale-card__badge bl-scale-card__badge--attention",
536
+ "aria-haspopup": "dialog",
537
+ onClick: () => setOpen(true),
538
+ children: "Needs attention"
539
+ }) : null
540
+ ]
541
+ }), hint ? /* @__PURE__ */ jsx("p", {
542
+ className: "bl-scale-card__hint",
543
+ children: hint
544
+ }) : null] }), onDelete ? /* @__PURE__ */ jsx("button", {
545
+ type: "button",
546
+ className: "bl-scale-card__delete",
547
+ disabled: readOnly,
548
+ onClick: onDelete,
549
+ children: "Delete"
550
+ }) : null]
551
+ }),
552
+ /* @__PURE__ */ jsxs("div", {
553
+ className: "bl-scale-card__row",
554
+ children: [/* @__PURE__ */ jsxs("label", {
555
+ className: "bl-scale-card__field",
556
+ children: [/* @__PURE__ */ jsx("span", { children: "Brand hex" }), /* @__PURE__ */ jsxs("span", {
557
+ className: "bl-scale-card__hex-wrap",
558
+ children: [/* @__PURE__ */ jsx("input", {
559
+ type: "color",
560
+ "aria-label": `${name} picker`,
561
+ disabled: readOnly,
562
+ value: state.hex,
563
+ onChange: (event) => setHex(event.target.value)
564
+ }), /* @__PURE__ */ jsx("input", {
565
+ type: "text",
566
+ spellCheck: false,
567
+ autoComplete: "off",
568
+ disabled: readOnly,
569
+ value: state.hex,
570
+ onChange: (event) => {
571
+ const next = event.target.value.trim();
572
+ if (next) setHex(next);
573
+ }
574
+ })]
575
+ })]
576
+ }), /* @__PURE__ */ jsxs("label", {
577
+ className: "bl-scale-card__field",
578
+ children: [/* @__PURE__ */ jsx("span", { children: "This color is" }), /* @__PURE__ */ jsx("select", {
579
+ disabled: readOnly,
580
+ value: String(state.sourceStep),
581
+ onChange: (event) => setStep(Number(event.target.value)),
582
+ children: SHADE_STEPS.map((step) => /* @__PURE__ */ jsx("option", {
583
+ value: step,
584
+ children: step
585
+ }, step))
586
+ })]
587
+ })]
588
+ }),
589
+ /* @__PURE__ */ jsxs("div", {
590
+ className: "bl-scale-card__label-sample",
591
+ children: [/* @__PURE__ */ jsx("span", {
592
+ className: "bl-scale-card__swatch",
593
+ style: {
594
+ background: preview ? hslCss(preview.fill) : state.hex,
595
+ color: preview ? hslCss(preview.label) : label ? hslCss(label) : void 0
596
+ },
597
+ children: "Aa"
598
+ }), /* @__PURE__ */ jsx("p", { children: previewMode === "dark" ? "Previewing this fill in Dark. A dark-enough hex lightens; otherwise it stays the typed color." : "The site chooses light or dark type on this fill. Darken or lighten the hex and regenerate to flip it. There is no ink picker." })]
599
+ }),
600
+ hueWarning ? /* @__PURE__ */ jsx("p", {
601
+ className: "bl-scale-card__warn",
602
+ children: hueWarning
603
+ }) : null,
604
+ state.stale ? /* @__PURE__ */ jsx("p", {
605
+ className: "bl-scale-card__stale",
606
+ children: "This scale is out of date. Regenerate to rebuild 50–950 around the hex."
607
+ }) : null,
608
+ /* @__PURE__ */ jsx("button", {
609
+ type: "button",
610
+ className: "bl-scale-card__regen",
611
+ disabled: readOnly || !state.stale,
612
+ onClick: () => onChange(regenerateColorScale(state)),
613
+ children: "Regenerate scale"
614
+ }),
615
+ /* @__PURE__ */ jsxs("div", {
616
+ className: "bl-scale-card__scale-head",
617
+ children: [/* @__PURE__ */ jsx("span", { children: "Generated scale" }), /* @__PURE__ */ jsxs("span", { children: [
618
+ /* @__PURE__ */ jsx("button", {
619
+ type: "button",
620
+ disabled: readOnly,
621
+ onClick: () => onChange(setColorScaleInclude(state, "all")),
622
+ children: "Include all"
623
+ }),
624
+ " · ",
625
+ /* @__PURE__ */ jsx("button", {
626
+ type: "button",
627
+ disabled: readOnly,
628
+ onClick: () => onChange(setColorScaleInclude(state, "source")),
629
+ children: "Source only"
630
+ })
631
+ ] })]
632
+ }),
633
+ /* @__PURE__ */ jsx("div", {
634
+ className: "bl-scale-card__steps",
635
+ role: "group",
636
+ "aria-label": `${name} scale steps`,
637
+ children: SHADE_STEPS.map((step) => {
638
+ const source = step === state.sourceStep;
639
+ const on = included.has(step);
640
+ return /* @__PURE__ */ jsxs("button", {
641
+ type: "button",
642
+ className: "bl-scale-card__step",
643
+ "data-source": source ? "" : void 0,
644
+ "data-excluded": on ? void 0 : "",
645
+ disabled: readOnly || source,
646
+ title: source ? `Source step ${step} cannot be excluded` : on ? `Exclude ${step} from page editors` : `Include ${step} for page editors`,
647
+ onClick: () => toggleStep(step),
648
+ children: [/* @__PURE__ */ jsx("span", {
649
+ className: "bl-scale-card__chip",
650
+ style: { background: hslCss(state.scale[step]) }
651
+ }), /* @__PURE__ */ jsx("span", {
652
+ className: "bl-scale-card__step-label",
653
+ children: step
654
+ })]
655
+ }, step);
656
+ })
657
+ }),
658
+ open && warning ? /* @__PURE__ */ jsx("div", {
659
+ className: "bl-scale-card__modal",
660
+ role: "dialog",
661
+ "aria-modal": "true",
662
+ "aria-labelledby": titleId,
663
+ onClick: (event) => {
664
+ if (event.target === event.currentTarget) setOpen(false);
665
+ },
666
+ children: /* @__PURE__ */ jsxs("div", {
667
+ className: "bl-scale-card__panel",
668
+ children: [
669
+ /* @__PURE__ */ jsxs("div", {
670
+ className: "bl-scale-card__panel-head",
671
+ children: [/* @__PURE__ */ jsx("h4", {
672
+ id: titleId,
673
+ children: readabilityHeadline(warning)
674
+ }), /* @__PURE__ */ jsx("button", {
675
+ type: "button",
676
+ onClick: () => setOpen(false),
677
+ "aria-label": "Close",
678
+ children: "×"
679
+ })]
680
+ }),
681
+ /* @__PURE__ */ jsx(ReadabilityDetail, { item: warning }),
682
+ /* @__PURE__ */ jsx("button", {
683
+ type: "button",
684
+ className: "bl-scale-card__panel-done",
685
+ onClick: () => setOpen(false),
686
+ children: "Keep editing this color"
687
+ })
688
+ ]
689
+ })
690
+ }) : null
691
+ ]
692
+ });
693
+ }
694
+ const COLOR_SCALE_CARD_CSS = `
695
+ .bl-scale-card{padding:calc(var(--base,16px) * .8);border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:var(--style-radius-m,12px);background:var(--theme-elevation-0,#fff)}
696
+ .bl-scale-card__head{display:flex;justify-content:space-between;gap:1rem;align-items:flex-start}
697
+ .bl-scale-card__title-row{position:relative;display:flex;align-items:center;flex-wrap:wrap;gap:.5rem}
698
+ .bl-scale-card__title{margin:0;font-size:1rem}
699
+ .bl-scale-card__badge{font:inherit;font-size:10px;text-transform:uppercase;letter-spacing:.04em;padding:.15rem .45rem;border-radius:999px;border:0;background:var(--theme-elevation-100,#f1f5f9);color:var(--theme-elevation-500,#64748b)}
700
+ .bl-scale-card__badge--attention{background:#fff1f2;color:#be123c;cursor:pointer}
701
+ .bl-scale-card__status{display:inline-grid;place-items:center;width:1.4rem;height:1.4rem;border-radius:999px;border:0;font:inherit;font-weight:700;line-height:1}
702
+ .bl-scale-card__status[data-ok]{background:#ecfdf5;color:#047857}
703
+ .bl-scale-card__sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
704
+ .bl-scale-card__hint{margin:.35rem 0 0;color:var(--theme-elevation-500,#64748b);font-size:12px}
705
+ .bl-scale-card__delete{border:0;background:none;color:#be123c;font:inherit;font-size:13px;font-weight:600;cursor:pointer}
706
+ .bl-scale-card__row{display:grid;grid-template-columns:1fr 8rem;gap:.75rem;margin-top:1rem}
707
+ .bl-scale-card__field{display:flex;flex-direction:column;gap:.35rem;font-size:12px;font-weight:600;color:var(--theme-elevation-600,#475569)}
708
+ .bl-scale-card__hex-wrap{display:flex;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:8px;overflow:hidden;background:var(--theme-input-bg,#fff)}
709
+ .bl-scale-card__hex-wrap input[type=color]{width:2.75rem;height:2.25rem;padding:0;border:0;border-right:1px solid var(--theme-elevation-150,#e2e8f0);background:transparent;cursor:pointer}
710
+ .bl-scale-card__hex-wrap input[type=text]{flex:1;min-width:0;border:0;padding:.4rem .7rem;font-family:var(--font-mono,ui-monospace,monospace);font-size:13px;background:transparent;color:inherit}
711
+ .bl-scale-card__field select{min-height:2.25rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:8px;padding:.35rem .5rem;background:var(--theme-input-bg,#fff);color:inherit}
712
+ .bl-scale-card__label-sample{display:flex;gap:.75rem;align-items:center;margin-top:1rem}
713
+ .bl-scale-card__swatch{flex:none;display:inline-flex;align-items:center;justify-content:center;width:3rem;height:2.25rem;border-radius:8px;border:1px solid var(--theme-elevation-150,#e2e8f0);font-weight:700}
714
+ .bl-scale-card__label-sample p{margin:0;font-size:12px;color:var(--theme-elevation-500,#64748b)}
715
+ .bl-scale-card__warn,.bl-scale-card__stale{margin:.75rem 0 0;padding:.65rem .75rem;border-radius:8px;font-size:12px}
716
+ .bl-scale-card__warn{border:1px solid #a7f3d0;background:#ecfdf5;color:#065f46}
717
+ .bl-scale-card__stale{border:1px solid #fde68a;background:#fffbeb;color:#92400e}
718
+ .bl-scale-card__regen{width:100%;margin-top:.85rem;min-height:2.25rem;border:0;border-radius:8px;background:var(--theme-elevation-800,#0f172a);color:#fff;font:inherit;font-weight:600;cursor:pointer}
719
+ .bl-scale-card__regen:disabled{opacity:.45;cursor:not-allowed}
720
+ .bl-scale-card__scale-head{display:flex;justify-content:space-between;margin:1rem 0 .4rem;font-size:12px;color:var(--theme-elevation-500,#64748b)}
721
+ .bl-scale-card__scale-head button{border:0;background:none;padding:0;font:inherit;color:inherit;text-decoration:underline;cursor:pointer}
722
+ .bl-scale-card__steps{display:grid;grid-template-columns:repeat(11,minmax(0,1fr));gap:.45rem 3px}
723
+ .bl-scale-card__step{display:flex;flex-direction:column;align-items:center;gap:.3rem;border:0;padding:0;background:transparent;cursor:pointer}
724
+ .bl-scale-card__chip{display:block;width:100%;height:2.15rem;border-radius:6px}
725
+ .bl-scale-card__step-label{font-size:10px;line-height:1;color:var(--theme-elevation-600,#475569)}
726
+ .bl-scale-card__step[data-source] .bl-scale-card__chip{box-shadow:0 0 0 2px #0f172a}
727
+ .bl-scale-card__step[data-excluded]{opacity:.35}
728
+ .bl-scale-card__step:disabled{cursor:default}
729
+ .bl-scale-card__modal{position:fixed;inset:0;z-index:80;display:grid;place-items:center;padding:1rem;background:rgba(15,23,42,.45)}
730
+ .bl-scale-card__panel{width:min(32rem,100%);padding:1.15rem 1.2rem;border-radius:16px;background:#fff;color:#0f172a}
731
+ .bl-scale-card__panel-head{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem}
732
+ .bl-scale-card__panel-head h4{margin:0;font-size:1rem}
733
+ .bl-scale-card__panel-head button{border:0;background:none;font:inherit;font-size:1.25rem;line-height:1;color:#64748b;cursor:pointer}
734
+ .bl-scale-card__panel-done{margin-top:1rem;min-height:2.1rem;padding:.35rem .85rem;border:0;border-radius:8px;background:#0f172a;color:#fff;font:inherit;font-weight:600;cursor:pointer}
735
+ [data-bl-theme-preview=dark] .bl-scale-card{background:#0f172a;border-color:#334155;color:#f8fafc}
736
+ [data-bl-theme-preview=dark] .bl-scale-card__hint,[data-bl-theme-preview=dark] .bl-scale-card__label-sample p,[data-bl-theme-preview=dark] .bl-scale-card__scale-head,[data-bl-theme-preview=dark] .bl-scale-card__step-label,[data-bl-theme-preview=dark] .bl-scale-card__field{color:#94a3b8}
737
+ [data-bl-theme-preview=dark] .bl-scale-card__hex-wrap,[data-bl-theme-preview=dark] .bl-scale-card__field select{background:#1e293b;border-color:#334155;color:#f8fafc}
738
+ [data-bl-theme-preview=dark] .bl-scale-card__step[data-source] .bl-scale-card__chip{box-shadow:0 0 0 2px #f8fafc}
739
+ ` + READABILITY_LIST_CSS;
740
+ //#endregion
741
+ //#region src/admin/color-scale-field.tsx
742
+ const SYSTEM_ROLES = new Set(Object.keys(SYSTEM_COLOR_COPY));
743
+ /**
744
+ * One system (or library row) color: hex, source step, regenerate, stale,
745
+ * include/exclude, automatic label. Writes the group's stored fields.
746
+ */
747
+ const ColorScaleField = ({ field, path, readOnly }) => {
748
+ const hex = useField({ path: `${path}.hex` });
749
+ const sourceStep = useField({ path: `${path}.sourceStep` });
750
+ const scale = useField({ path: `${path}.scale` });
751
+ const stale = useField({ path: `${path}.stale` });
752
+ const include = useField({ path: `${path}.include` });
753
+ const includedSteps = useField({ path: `${path}.includedSteps` });
754
+ const key = path.split(".").pop() ?? "";
755
+ const copy = SYSTEM_ROLES.has(key) ? SYSTEM_COLOR_COPY[key] : void 0;
756
+ const state = stateFromColorDoc({
757
+ hex: hex.value,
758
+ sourceStep: sourceStep.value,
759
+ scale: scale.value,
760
+ stale: stale.value,
761
+ include: include.value === "source" || include.value === "custom" || include.value === "all" ? include.value : "all",
762
+ includedSteps: includedSteps.value
763
+ });
764
+ function write(next) {
765
+ const doc = colorDocFromState(next);
766
+ hex.setValue(doc.hex);
767
+ sourceStep.setValue(String(doc.sourceStep));
768
+ scale.setValue(doc.scale);
769
+ stale.setValue(doc.stale);
770
+ include.setValue(doc.include);
771
+ includedSteps.setValue(doc.includedSteps ?? (doc.include === "source" ? [doc.sourceStep] : [...SHADE_STEPS]));
772
+ }
773
+ const label = copy?.label ?? (typeof field.label === "string" ? field.label : key);
774
+ return /* @__PURE__ */ jsxs("div", {
775
+ className: "field-type bl-scale-field",
776
+ children: [/* @__PURE__ */ jsx("style", {
777
+ href: "bl-scale-card",
778
+ precedence: "default",
779
+ children: COLOR_SCALE_CARD_CSS
780
+ }), /* @__PURE__ */ jsx(ColorScaleCard, {
781
+ id: key,
782
+ name: label,
783
+ hint: copy?.hint,
784
+ required: Boolean(copy) || Boolean("required" in field && field.required),
785
+ state: isThemeHex(state.hex) ? state : {
786
+ ...state,
787
+ hex: "#000000"
788
+ },
789
+ onChange: write,
790
+ readOnly,
791
+ role: key === "success" || key === "destructive" ? key : null
792
+ })]
793
+ });
794
+ };
795
+ //#endregion
796
+ //#region src/theme/types.ts
797
+ const SYSTEM_COLOR_KEYS = [
798
+ "primary",
799
+ "secondary",
800
+ "accent",
801
+ "highlight",
802
+ "success",
803
+ "destructive"
804
+ ];
805
+ function themeColorKeys(doc) {
806
+ const custom = (doc?.colors?.library ?? []).map((row) => row.key?.trim()).filter((key) => Boolean(key));
807
+ return [...SYSTEM_COLOR_KEYS, ...custom];
808
+ }
809
+ /**
810
+ * Remove a custom color. Unused: omit `replacement` and the row is gone.
811
+ * In use: pass another system or custom key; the caller rewrites page
812
+ * tokens with `rewriteColorToken`. After this, no library row still
813
+ * has `key`.
814
+ */
815
+ function deleteLibraryColor(doc, key, replacement) {
816
+ const library = [...doc.colors?.library ?? []];
817
+ const index = library.findIndex((row) => row.key === key);
818
+ if (index === -1) throw new Error(`No custom color named ${JSON.stringify(key)}`);
819
+ library.splice(index, 1);
820
+ const next = {
821
+ ...doc,
822
+ colors: {
823
+ ...doc.colors,
824
+ library
825
+ }
826
+ };
827
+ if (replacement !== void 0) {
828
+ if (replacement === key) throw new Error("Replacement must be a different color");
829
+ if (!themeColorKeys(next).includes(replacement)) throw new Error(`Replacement ${JSON.stringify(replacement)} is not a system or remaining custom color`);
830
+ }
831
+ return next;
832
+ }
833
+ //#endregion
834
+ //#region src/admin/library-field.tsx
835
+ function slugify(value) {
836
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
837
+ }
838
+ /**
839
+ * Custom colors: the same scale controls as system colors, plus add and
840
+ * in-use delete (one replacement scale; unused rows delete immediately).
841
+ */
842
+ const LibraryField = ({ path, readOnly }) => {
843
+ const { value, setValue } = useField({ path });
844
+ const rows = value ?? [];
845
+ const [adding, setAdding] = useState(false);
846
+ const [name, setName] = useState("");
847
+ const [hex, setHex] = useState("#E07A5F");
848
+ const [pendingDelete, setPendingDelete] = useState(null);
849
+ const [replacement, setReplacement] = useState("");
850
+ function write(next) {
851
+ setValue(next);
852
+ }
853
+ function add() {
854
+ const key = slugify(name);
855
+ if (!key || rows.some((row) => row.key === key)) return;
856
+ write([...rows, {
857
+ key,
858
+ label: name.trim(),
859
+ ...colorDocFromState(createColorScale(hex, 500))
860
+ }]);
861
+ setName("");
862
+ setAdding(false);
863
+ }
864
+ function remove(row, replaceWith) {
865
+ write(deleteLibraryColor({ colors: { library: rows } }, row.key ?? "", replaceWith).colors?.library ?? []);
866
+ setPendingDelete(null);
867
+ setReplacement("");
868
+ }
869
+ const replacements = [...SYSTEM_COLOR_KEYS.map((key) => ({
870
+ value: key,
871
+ label: key[0].toUpperCase() + key.slice(1)
872
+ })), ...rows.filter((row) => row.key && row.key !== pendingDelete?.key).map((row) => ({
873
+ value: row.key ?? "",
874
+ label: row.label ?? row.key ?? ""
875
+ }))];
876
+ return /* @__PURE__ */ jsxs("div", {
877
+ className: "field-type bl-library",
878
+ children: [
879
+ /* @__PURE__ */ jsx("style", {
880
+ href: "bl-scale-card",
881
+ precedence: "default",
882
+ children: COLOR_SCALE_CARD_CSS
883
+ }),
884
+ /* @__PURE__ */ jsx("style", {
885
+ href: "bl-library",
886
+ precedence: "default",
887
+ children: CSS$2
888
+ }),
889
+ /* @__PURE__ */ jsxs("div", {
890
+ className: "bl-library__head",
891
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", { children: "Custom colors" }), /* @__PURE__ */ jsx("p", { children: "Named scales that sit with the house colors. Page editors pick them as keys (`coral-400`)." })] }), /* @__PURE__ */ jsx("button", {
892
+ type: "button",
893
+ disabled: readOnly,
894
+ onClick: () => setAdding(true),
895
+ children: "Add color"
896
+ })]
897
+ }),
898
+ adding ? /* @__PURE__ */ jsxs("div", {
899
+ className: "bl-library__add",
900
+ children: [
901
+ /* @__PURE__ */ jsxs("label", { children: ["Name", /* @__PURE__ */ jsx("input", {
902
+ value: name,
903
+ onChange: (event) => setName(event.target.value)
904
+ })] }),
905
+ /* @__PURE__ */ jsxs("label", { children: ["Hex", /* @__PURE__ */ jsx("input", {
906
+ value: hex,
907
+ onChange: (event) => setHex(event.target.value)
908
+ })] }),
909
+ /* @__PURE__ */ jsx("button", {
910
+ type: "button",
911
+ onClick: () => setAdding(false),
912
+ children: "Cancel"
913
+ }),
914
+ /* @__PURE__ */ jsx("button", {
915
+ type: "button",
916
+ disabled: !slugify(name),
917
+ onClick: add,
918
+ children: "Create color scale"
919
+ })
920
+ ]
921
+ }) : null,
922
+ /* @__PURE__ */ jsx("div", {
923
+ className: "bl-library__list",
924
+ children: rows.map((row, index) => /* @__PURE__ */ jsx(ColorScaleCard, {
925
+ id: row.key ?? `custom-${index}`,
926
+ name: row.label ?? row.key ?? "Color",
927
+ hint: row.key ? `Key: ${row.key}` : void 0,
928
+ custom: true,
929
+ state: stateFromColorDoc(row),
930
+ onChange: (next) => {
931
+ const copy = [...rows];
932
+ copy[index] = {
933
+ ...row,
934
+ ...colorDocFromState(next)
935
+ };
936
+ write(copy);
937
+ },
938
+ onDelete: readOnly ? void 0 : () => setPendingDelete(row),
939
+ readOnly
940
+ }, row.key ?? index))
941
+ }),
942
+ pendingDelete ? /* @__PURE__ */ jsx("div", {
943
+ className: "bl-library__modal",
944
+ role: "dialog",
945
+ "aria-modal": "true",
946
+ "aria-labelledby": "bl-library-delete-title",
947
+ children: /* @__PURE__ */ jsxs("div", {
948
+ className: "bl-library__panel",
949
+ children: [
950
+ /* @__PURE__ */ jsxs("h2", {
951
+ id: "bl-library-delete-title",
952
+ children: ["Delete ", pendingDelete.label ?? pendingDelete.key]
953
+ }),
954
+ /* @__PURE__ */ jsxs("p", { children: [
955
+ "Unused custom colors delete immediately. If this scale is in use, pick one replacement — every",
956
+ " ",
957
+ /* @__PURE__ */ jsxs("code", { children: [pendingDelete.key, "-400"] }),
958
+ " becomes ",
959
+ /* @__PURE__ */ jsxs("code", { children: [replacement || "…", "-400"] }),
960
+ "."
961
+ ] }),
962
+ /* @__PURE__ */ jsxs("label", { children: ["Replacement scale", /* @__PURE__ */ jsxs("select", {
963
+ value: replacement,
964
+ onChange: (event) => setReplacement(event.target.value),
965
+ children: [/* @__PURE__ */ jsx("option", {
966
+ value: "",
967
+ children: "Choose a scale…"
968
+ }), replacements.map((option) => /* @__PURE__ */ jsx("option", {
969
+ value: option.value,
970
+ children: option.label
971
+ }, option.value))]
972
+ })] }),
973
+ /* @__PURE__ */ jsxs("div", {
974
+ className: "bl-library__actions",
975
+ children: [
976
+ /* @__PURE__ */ jsx("button", {
977
+ type: "button",
978
+ onClick: () => setPendingDelete(null),
979
+ children: "Cancel"
980
+ }),
981
+ /* @__PURE__ */ jsx("button", {
982
+ type: "button",
983
+ onClick: () => remove(pendingDelete),
984
+ children: "Delete immediately"
985
+ }),
986
+ /* @__PURE__ */ jsx("button", {
987
+ type: "button",
988
+ disabled: !replacement,
989
+ onClick: () => remove(pendingDelete, replacement),
990
+ children: "Replace & delete"
991
+ })
992
+ ]
993
+ })
994
+ ]
995
+ })
996
+ }) : null
997
+ ]
998
+ });
999
+ };
1000
+ const CSS$2 = `
1001
+ .bl-library__head{display:flex;justify-content:space-between;gap:1rem;align-items:flex-start;margin-bottom:.85rem}
1002
+ .bl-library__head h2{margin:0;font-size:1.1rem}
1003
+ .bl-library__head p{margin:.35rem 0 0;font-size:13px;color:var(--theme-elevation-500,#64748b)}
1004
+ .bl-library__head button,.bl-library__add button,.bl-library__actions button{min-height:2.1rem;padding:.35rem .75rem;border-radius:8px;border:1px solid var(--theme-elevation-150,#e2e8f0);background:var(--theme-elevation-0,#fff);font:inherit;cursor:pointer}
1005
+ .bl-library__list{display:grid;gap:.85rem}
1006
+ .bl-library__add{display:flex;flex-wrap:wrap;gap:.6rem;align-items:end;margin-bottom:.85rem;padding:.85rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:12px}
1007
+ .bl-library__add label{display:flex;flex-direction:column;gap:.3rem;font-size:12px;font-weight:600}
1008
+ .bl-library__add input{min-height:2.1rem;padding:.3rem .55rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:8px}
1009
+ .bl-library__modal{position:fixed;inset:0;z-index:80;display:grid;place-items:center;padding:1rem;background:rgba(15,23,42,.45)}
1010
+ .bl-library__panel{width:min(28rem,100%);padding:1.2rem;border-radius:16px;background:#fff;color:#0f172a}
1011
+ .bl-library__panel h2{margin:0}
1012
+ .bl-library__panel p{margin:.5rem 0 0;color:#475569;font-size:14px}
1013
+ .bl-library__panel label{display:flex;flex-direction:column;gap:.4rem;margin-top:1rem;font-size:13px;font-weight:600}
1014
+ .bl-library__panel select{min-height:2.25rem;border:1px solid #cbd5e1;border-radius:8px;padding:.35rem .5rem}
1015
+ .bl-library__actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:.5rem;margin-top:1rem}
1016
+ `;
1017
+ //#endregion
1018
+ //#region src/admin/typography-pairing.tsx
1019
+ function FontRoleCard({ title, hint, required, family, preview, children }) {
1020
+ return /* @__PURE__ */ jsxs("article", {
1021
+ className: "bl-font-role",
1022
+ children: [
1023
+ /* @__PURE__ */ jsxs("header", {
1024
+ className: "bl-font-role__head",
1025
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", { children: title }), /* @__PURE__ */ jsx("p", { children: hint })] }), required ? /* @__PURE__ */ jsx("span", {
1026
+ className: "bl-font-role__badge",
1027
+ children: "Required"
1028
+ }) : null]
1029
+ }),
1030
+ children,
1031
+ /* @__PURE__ */ jsxs("div", {
1032
+ className: "bl-font-role__preview",
1033
+ children: [/* @__PURE__ */ jsx("span", { children: "Preview" }), preview === "heading" ? /* @__PURE__ */ jsx("p", {
1034
+ className: "bl-font-role__heading",
1035
+ style: { fontFamily: `"${family}"` },
1036
+ children: "Advancing the work with clarity."
1037
+ }) : /* @__PURE__ */ jsx("p", {
1038
+ className: "bl-font-role__body",
1039
+ style: { fontFamily: `"${family}"` },
1040
+ children: "Clear, comfortable text for longer reading across the site."
1041
+ })]
1042
+ })
1043
+ ]
1044
+ });
1045
+ }
1046
+ function TypographyPairing({ headingFamily, bodyFamily }) {
1047
+ return /* @__PURE__ */ jsxs("section", {
1048
+ className: "bl-pairing",
1049
+ "aria-label": "Font pairing sample",
1050
+ children: [/* @__PURE__ */ jsxs("header", { children: [/* @__PURE__ */ jsx("h2", { children: "Pairing sample" }), /* @__PURE__ */ jsx("p", { children: "Heading and body from the approved catalogue. Type scale stays with the library." })] }), /* @__PURE__ */ jsxs("div", {
1051
+ className: "bl-pairing__page",
1052
+ children: [
1053
+ /* @__PURE__ */ jsx("p", {
1054
+ className: "bl-pairing__eyebrow",
1055
+ style: { fontFamily: `"${bodyFamily}"` },
1056
+ children: "For visitors"
1057
+ }),
1058
+ /* @__PURE__ */ jsx("p", {
1059
+ className: "bl-pairing__title",
1060
+ style: { fontFamily: `"${headingFamily}"` },
1061
+ children: "Stay in motion."
1062
+ }),
1063
+ /* @__PURE__ */ jsx("p", {
1064
+ className: "bl-pairing__copy",
1065
+ style: { fontFamily: `"${bodyFamily}"` },
1066
+ children: "Heading and body come from the catalogue. This is the pairing, not a full site preview."
1067
+ }),
1068
+ /* @__PURE__ */ jsxs("div", {
1069
+ className: "bl-pairing__actions",
1070
+ children: [/* @__PURE__ */ jsx("span", {
1071
+ className: "bl-pairing__btn",
1072
+ style: { fontFamily: `"${bodyFamily}"` },
1073
+ children: "Primary action"
1074
+ }), /* @__PURE__ */ jsx("span", {
1075
+ className: "bl-pairing__btn bl-pairing__btn--ghost",
1076
+ style: { fontFamily: `"${bodyFamily}"` },
1077
+ children: "Learn more"
1078
+ })]
1079
+ })
1080
+ ]
1081
+ })]
1082
+ });
1083
+ }
1084
+ const TYPOGRAPHY_PAIRING_CSS = `
1085
+ .bl-font-role{padding:1.1rem 1.15rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:12px;background:var(--theme-elevation-0,#fff)}
1086
+ .bl-font-role__head{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem}
1087
+ .bl-font-role__head h3{margin:0;font-size:1rem}
1088
+ .bl-font-role__head p{margin:.3rem 0 0;font-size:12px;color:var(--theme-elevation-500,#64748b)}
1089
+ .bl-font-role__badge{font-size:10px;text-transform:uppercase;letter-spacing:.04em;padding:.15rem .45rem;border-radius:999px;background:var(--theme-elevation-100,#f1f5f9);color:var(--theme-elevation-500,#64748b)}
1090
+ .bl-font-role__preview{margin-top:1rem;padding:.85rem .9rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:12px;background:var(--theme-elevation-50,#f8fafc)}
1091
+ .bl-font-role__preview > span{display:block;margin-bottom:.4rem;font-size:11px;color:var(--theme-elevation-500,#64748b)}
1092
+ .bl-font-role__heading{margin:0;font-size:1.5rem;line-height:1.2;font-weight:600}
1093
+ .bl-font-role__body{margin:0;font-size:1rem;line-height:1.6;color:var(--theme-elevation-600,#475569)}
1094
+ .bl-pairing{padding:0;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:12px;background:var(--theme-elevation-0,#fff);overflow:hidden}
1095
+ .bl-pairing > header{padding:1rem 1.15rem;border-bottom:1px solid var(--theme-elevation-150,#e2e8f0)}
1096
+ .bl-pairing > header h2{margin:0;font-size:1rem}
1097
+ .bl-pairing > header p{margin:.3rem 0 0;font-size:12px;color:var(--theme-elevation-500,#64748b)}
1098
+ .bl-pairing__page{padding:1.35rem 1.4rem 1.5rem}
1099
+ .bl-pairing__eyebrow{margin:0;font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:#7c1fa2}
1100
+ .bl-pairing__title{margin:.5rem 0 0;font-size:2rem;line-height:1.1;font-weight:600}
1101
+ .bl-pairing__copy{margin:.75rem 0 0;max-width:36rem;font-size:1rem;line-height:1.6;color:var(--theme-elevation-600,#475569)}
1102
+ .bl-pairing__actions{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:1.1rem}
1103
+ .bl-pairing__btn{display:inline-flex;align-items:center;min-height:2.15rem;padding:.4rem .85rem;border-radius:8px;background:#db0082;color:#fff;font-size:14px;font-weight:600}
1104
+ .bl-pairing__btn--ghost{background:#fff;color:#0f172a;border:1px solid #cbd5e1}
1105
+ `;
1106
+ //#endregion
111
1107
  //#region src/admin/font-field.tsx
112
1108
  /**
113
1109
  * Listbox over the catalogue ids in `admin.custom.ids`. Each option is the
114
1110
  * family name in that face, with the catalogue hint beneath. Faces load
115
- * from the site's font route the first time the picker opens.
1111
+ * from the site's font route for the selected family, and for every
1112
+ * catalogue id once the picker opens.
116
1113
  */
117
1114
  const FontField = ({ field, path, readOnly }) => {
118
1115
  const { value, setValue, showError, errorMessage } = useField({ path });
@@ -142,6 +1139,8 @@ const FontField = ({ field, path, readOnly }) => {
142
1139
  const [open, setOpen] = useState(false);
143
1140
  const [faces, setFaces] = useState(false);
144
1141
  const [active, setActive] = useState(-1);
1142
+ const [query, setQuery] = useState("");
1143
+ const [category, setCategory] = useState("all");
145
1144
  const typed = useRef("");
146
1145
  const typedAt = useRef(0);
147
1146
  const id = useId();
@@ -152,10 +1151,18 @@ const FontField = ({ field, path, readOnly }) => {
152
1151
  if (open) setFaces(true);
153
1152
  }, [open]);
154
1153
  const selected = options.find((option) => option.value === (value ?? "")) ?? options[0];
1154
+ const visible = options.filter((option) => {
1155
+ if (query && !option.family.toLowerCase().includes(query.trim().toLowerCase())) return false;
1156
+ if (category === "all" || !option.specimen) return true;
1157
+ return findFont(option.value)?.category === category;
1158
+ });
1159
+ const role = sameAsBody ? FONT_ROLE_COPY.heading : FONT_ROLE_COPY.body;
155
1160
  function choose(option) {
156
1161
  setValue(option.value);
157
1162
  setOpen(false);
158
1163
  setActive(-1);
1164
+ setQuery("");
1165
+ setCategory("all");
159
1166
  }
160
1167
  function onKeyDown(event) {
161
1168
  if (event.key === "ArrowDown") {
@@ -198,26 +1205,35 @@ const FontField = ({ field, path, readOnly }) => {
198
1205
  }
199
1206
  }
200
1207
  const activeId = active >= 0 && open ? `${id}-option-${active}` : void 0;
1208
+ const faceIds = faces ? ids : isFontId(value) ? [value] : [];
201
1209
  return /* @__PURE__ */ jsxs("div", {
202
1210
  className: "field-type bl-font-field",
203
1211
  children: [
1212
+ /* @__PURE__ */ jsx("style", {
1213
+ href: "bl-pairing",
1214
+ precedence: "default",
1215
+ children: TYPOGRAPHY_PAIRING_CSS
1216
+ }),
204
1217
  /* @__PURE__ */ jsx("style", {
205
1218
  href: "bl-font-field",
206
1219
  precedence: "default",
207
1220
  children: CSS$1
208
1221
  }),
209
- faces ? /* @__PURE__ */ jsx("style", {
1222
+ faceIds.length > 0 ? /* @__PURE__ */ jsx("style", {
210
1223
  "data-bl-font-faces": "",
211
- children: fontFaceCss(ids, fontsBaseUrl)
1224
+ children: fontFaceCss(faceIds, fontsBaseUrl)
212
1225
  }) : null,
213
- /* @__PURE__ */ jsx(FieldLabel, {
214
- htmlFor: buttonId,
215
- label: field.label,
216
- required
217
- }),
218
- /* @__PURE__ */ jsxs("div", {
219
- className: "bl-font-field__control",
220
- children: [/* @__PURE__ */ jsx("button", {
1226
+ /* @__PURE__ */ jsxs(FontRoleCard, {
1227
+ title: typeof field.label === "string" ? field.label : role.label,
1228
+ hint: role.hint,
1229
+ required,
1230
+ family: selected?.family ?? "Inter",
1231
+ preview: sameAsBody ? "heading" : "body",
1232
+ children: [/* @__PURE__ */ jsx(FieldLabel, {
1233
+ htmlFor: buttonId,
1234
+ label: field.label,
1235
+ required
1236
+ }), /* @__PURE__ */ jsxs("button", {
221
1237
  id: buttonId,
222
1238
  type: "button",
223
1239
  className: "bl-font-field__trigger",
@@ -227,34 +1243,94 @@ const FontField = ({ field, path, readOnly }) => {
227
1243
  "aria-controls": listId,
228
1244
  "aria-activedescendant": activeId,
229
1245
  onClick: () => setOpen((was) => !was),
230
- onBlur: () => setOpen(false),
231
1246
  onKeyDown,
232
- children: selected?.family ?? "Choose a font"
233
- }), open ? /* @__PURE__ */ jsx("ul", {
234
- id: listId,
235
- role: "listbox",
236
- className: "bl-font-field__menu",
237
- children: options.map((option, i) => /* @__PURE__ */ jsxs("li", {
238
- id: `${id}-option-${i}`,
239
- role: "option",
240
- "aria-selected": option.value === (value ?? ""),
241
- className: "bl-font-field__option",
242
- onMouseDown: (event) => {
243
- event.preventDefault();
244
- choose(option);
245
- },
246
- onMouseEnter: () => setActive(i),
247
- children: [/* @__PURE__ */ jsx("span", {
248
- className: "bl-font-field__family",
249
- style: option.specimen ? { fontFamily: `"${option.family}"` } : void 0,
250
- children: option.family
251
- }), /* @__PURE__ */ jsx("span", {
252
- className: "bl-font-field__hint",
253
- children: option.hint
254
- })]
255
- }, option.value || "same"))
256
- }) : null]
1247
+ children: [/* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("span", {
1248
+ className: "bl-font-field__selected-label",
1249
+ children: "Selected"
1250
+ }), /* @__PURE__ */ jsx("span", {
1251
+ className: "bl-font-field__selected-name",
1252
+ style: selected?.specimen ? { fontFamily: `"${selected.family}"` } : void 0,
1253
+ children: selected?.family ?? "Choose a font"
1254
+ })] }), /* @__PURE__ */ jsx("span", {
1255
+ "aria-hidden": "true",
1256
+ children: ""
1257
+ })]
1258
+ })]
257
1259
  }),
1260
+ open ? /* @__PURE__ */ jsx("div", {
1261
+ className: "bl-font-field__modal",
1262
+ onClick: (event) => {
1263
+ if (event.target === event.currentTarget) setOpen(false);
1264
+ },
1265
+ children: /* @__PURE__ */ jsxs("div", {
1266
+ className: "bl-font-field__panel",
1267
+ children: [
1268
+ /* @__PURE__ */ jsxs("div", {
1269
+ className: "bl-font-field__panel-head",
1270
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", { children: "Choose a font" }), /* @__PURE__ */ jsx("h2", { children: role.label })] }), /* @__PURE__ */ jsx("button", {
1271
+ type: "button",
1272
+ onClick: () => setOpen(false),
1273
+ "aria-label": "Close",
1274
+ children: "×"
1275
+ })]
1276
+ }),
1277
+ /* @__PURE__ */ jsx("input", {
1278
+ type: "search",
1279
+ className: "bl-font-field__search",
1280
+ placeholder: "Search fonts…",
1281
+ value: query,
1282
+ onChange: (event) => setQuery(event.target.value)
1283
+ }),
1284
+ /* @__PURE__ */ jsx("div", {
1285
+ className: "bl-font-field__filters",
1286
+ children: [
1287
+ "all",
1288
+ "sans",
1289
+ "serif",
1290
+ "display",
1291
+ "mono"
1292
+ ].map((item) => /* @__PURE__ */ jsx("button", {
1293
+ type: "button",
1294
+ "data-on": category === item ? "" : void 0,
1295
+ onClick: () => setCategory(item),
1296
+ children: item === "all" ? "All" : item === "sans" ? "Sans serif" : item === "serif" ? "Serif" : item === "display" ? "Display" : "Mono"
1297
+ }, item))
1298
+ }),
1299
+ /* @__PURE__ */ jsx("ul", {
1300
+ id: listId,
1301
+ role: "listbox",
1302
+ className: "bl-font-field__menu",
1303
+ children: visible.map((option, i) => /* @__PURE__ */ jsxs("li", {
1304
+ id: `${id}-option-${i}`,
1305
+ role: "option",
1306
+ "aria-selected": option.value === (value ?? ""),
1307
+ className: "bl-font-field__option",
1308
+ onMouseDown: (event) => {
1309
+ event.preventDefault();
1310
+ choose(option);
1311
+ },
1312
+ onMouseEnter: () => setActive(i),
1313
+ children: [
1314
+ /* @__PURE__ */ jsx("span", {
1315
+ className: "bl-font-field__family",
1316
+ style: option.specimen ? { fontFamily: `"${option.family}"` } : void 0,
1317
+ children: option.family
1318
+ }),
1319
+ /* @__PURE__ */ jsx("span", {
1320
+ className: "bl-font-field__hint",
1321
+ children: option.hint
1322
+ }),
1323
+ /* @__PURE__ */ jsx("span", {
1324
+ className: "bl-font-field__specimen",
1325
+ style: option.specimen ? { fontFamily: `"${option.family}"` } : void 0,
1326
+ children: "The quick brown fox jumps over the lazy dog."
1327
+ })
1328
+ ]
1329
+ }, option.value || "same"))
1330
+ })
1331
+ ]
1332
+ })
1333
+ }) : null,
258
1334
  /* @__PURE__ */ jsx(FieldDescription, {
259
1335
  description: field.admin?.description,
260
1336
  path
@@ -268,166 +1344,713 @@ const FontField = ({ field, path, readOnly }) => {
268
1344
  });
269
1345
  };
270
1346
  const CSS$1 = `
271
- .bl-font-field__control{position:relative}
272
- .bl-font-field__trigger{display:flex;align-items:center;width:100%;min-height:calc(var(--base) * 2);padding:calc(var(--base) * .25) calc(var(--base) * .6);border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:var(--theme-input-bg);color:var(--theme-elevation-800);font:inherit;text-align:left;cursor:pointer}
273
- .bl-font-field__trigger:focus{border-color:var(--theme-elevation-400);outline:none;box-shadow:0 0 0 1px var(--theme-elevation-400)}
274
- .bl-font-field__trigger:disabled{color:var(--theme-elevation-400);cursor:not-allowed}
275
- .bl-font-field__menu{position:absolute;left:0;right:0;top:calc(100% + 4px);z-index:10;margin:0;padding:calc(var(--base) * .2) 0;list-style:none;max-height:18rem;overflow:auto;border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:var(--theme-elevation-0);box-shadow:0 8px 24px var(--theme-overlay)}
276
- .bl-font-field__option{display:flex;flex-direction:column;gap:2px;padding:calc(var(--base) * .35) calc(var(--base) * .6);cursor:pointer}
277
- .bl-font-field__option[aria-selected="true"],.bl-font-field__option:hover{background:var(--theme-elevation-100)}
278
- .bl-font-field__family{font-weight:500}
279
- .bl-font-field__hint{color:var(--theme-elevation-500);font-size:12px}
1347
+ .bl-font-field{margin-bottom:1rem}
1348
+ .bl-font-field .field-label{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
1349
+ .bl-font-field__trigger{display:flex;align-items:center;justify-content:space-between;width:100%;margin-top:1rem;min-height:3.25rem;padding:.7rem 1rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:12px;background:var(--theme-input-bg,#fff);color:inherit;font:inherit;text-align:left;cursor:pointer}
1350
+ .bl-font-field__trigger:focus{border-color:var(--theme-elevation-400,#94a3b8);outline:none;box-shadow:0 0 0 1px var(--theme-elevation-400,#94a3b8)}
1351
+ .bl-font-field__trigger:disabled{color:var(--theme-elevation-400,#94a3b8);cursor:not-allowed}
1352
+ .bl-font-field__selected-label{display:block;font-size:11px;color:var(--theme-elevation-500,#64748b)}
1353
+ .bl-font-field__selected-name{display:block;margin-top:.15rem;font-weight:600}
1354
+ .bl-font-field__modal{position:fixed;inset:0;z-index:80;display:grid;place-items:center;padding:1rem;background:rgba(15,23,42,.45)}
1355
+ .bl-font-field__panel{width:min(36rem,100%);max-height:min(36rem,90vh);display:flex;flex-direction:column;padding:1.15rem 1.2rem;border-radius:16px;background:#fff;color:#0f172a}
1356
+ .bl-font-field__panel-head{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem}
1357
+ .bl-font-field__panel-head p{margin:0;font-size:12px;color:#64748b}
1358
+ .bl-font-field__panel-head h2{margin:.2rem 0 0;font-size:1.15rem}
1359
+ .bl-font-field__panel-head button{border:0;background:none;font:inherit;font-size:1.25rem;color:#64748b;cursor:pointer}
1360
+ .bl-font-field__search{margin-top:.9rem;min-height:2.5rem;padding:.5rem .75rem;border:1px solid #cbd5e1;border-radius:10px;font:inherit}
1361
+ .bl-font-field__filters{display:flex;flex-wrap:wrap;gap:.4rem;margin-top:.75rem}
1362
+ .bl-font-field__filters button{min-height:1.8rem;padding:.2rem .7rem;border:0;border-radius:999px;background:#f1f5f9;color:#475569;font:inherit;font-size:12px;cursor:pointer}
1363
+ .bl-font-field__filters button[data-on]{background:#0f172a;color:#fff}
1364
+ .bl-font-field__menu{margin:.75rem 0 0;padding:0;list-style:none;overflow:auto;max-height:18rem}
1365
+ .bl-font-field__option{display:flex;flex-direction:column;gap:2px;margin-bottom:.5rem;padding:.75rem .85rem;border:1px solid #e2e8f0;border-radius:12px;cursor:pointer}
1366
+ .bl-font-field__option[aria-selected="true"]{border-color:#7c1fa2;background:#f5f3ff}
1367
+ .bl-font-field__family{font-weight:600}
1368
+ .bl-font-field__hint{color:#64748b;font-size:12px}
1369
+ .bl-font-field__specimen{margin-top:.45rem;font-size:1.25rem;line-height:1.3}
280
1370
  `;
281
1371
  //#endregion
282
- //#region src/admin/contrast-report.tsx
283
- function fieldValue(fields, path) {
284
- return fields[path]?.value;
1372
+ //#region src/admin/pairing-field.tsx
1373
+ /**
1374
+ * Pairing sample only — no weight, letter-spacing, line-height, or health
1375
+ * scores. Faces load from the site's font route.
1376
+ */
1377
+ const PairingField = ({ field }) => {
1378
+ const fontsBaseUrl = (field.admin?.custom ?? {}).fontsBaseUrl ?? "/fonts";
1379
+ const { body, heading } = useFormFields(([fields]) => {
1380
+ const record = fields;
1381
+ return {
1382
+ body: String(record["typography.body"]?.value ?? ""),
1383
+ heading: String(record["typography.heading"]?.value ?? "")
1384
+ };
1385
+ });
1386
+ const headingId = heading || body;
1387
+ const bodyFont = findFont(body);
1388
+ const headingFont = findFont(headingId);
1389
+ const ids = [body, headingId].filter(Boolean);
1390
+ return /* @__PURE__ */ jsxs("div", {
1391
+ className: "field-type bl-pairing-field",
1392
+ children: [
1393
+ /* @__PURE__ */ jsx("style", {
1394
+ href: "bl-pairing",
1395
+ precedence: "default",
1396
+ children: TYPOGRAPHY_PAIRING_CSS
1397
+ }),
1398
+ ids.length > 0 ? /* @__PURE__ */ jsx("style", {
1399
+ "data-bl-font-faces": "",
1400
+ children: fontFaceCss(ids, fontsBaseUrl)
1401
+ }) : null,
1402
+ /* @__PURE__ */ jsx(TypographyPairing, {
1403
+ headingFamily: headingFont?.family ?? "Georgia",
1404
+ bodyFamily: bodyFont?.family ?? "system-ui"
1405
+ })
1406
+ ]
1407
+ });
1408
+ };
1409
+ //#endregion
1410
+ //#region src/admin/appearance-choices.tsx
1411
+ function ChoicePreview({ family, value }) {
1412
+ return /* @__PURE__ */ jsx("div", {
1413
+ className: "bl-appearance__preview",
1414
+ "data-family": family,
1415
+ "data-value": value,
1416
+ "aria-hidden": "true",
1417
+ children: family === "defaultTheme" && value === "system" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
1418
+ className: "bl-appearance__pane bl-appearance__pane--light",
1419
+ children: [/* @__PURE__ */ jsx("i", {}), /* @__PURE__ */ jsx("b", {})]
1420
+ }), /* @__PURE__ */ jsxs("div", {
1421
+ className: "bl-appearance__pane bl-appearance__pane--dark",
1422
+ children: [/* @__PURE__ */ jsx("i", {}), /* @__PURE__ */ jsx("b", {})]
1423
+ })] }) : family === "motion" ? /* @__PURE__ */ jsx("span", { className: "bl-appearance__dot" }) : family === "shadow" ? /* @__PURE__ */ jsx("div", { className: "bl-appearance__lift" }) : family === "radius" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1424
+ /* @__PURE__ */ jsx("b", { children: "Button" }),
1425
+ /* @__PURE__ */ jsx("i", { className: "bl-appearance__input" }),
1426
+ /* @__PURE__ */ jsx("i", { className: "bl-appearance__card" })
1427
+ ] }) : family === "density" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1428
+ /* @__PURE__ */ jsx("b", {}),
1429
+ /* @__PURE__ */ jsx("i", { className: "bl-appearance__input" }),
1430
+ /* @__PURE__ */ jsx("i", { className: "bl-appearance__card" })
1431
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1432
+ /* @__PURE__ */ jsx("i", {}),
1433
+ /* @__PURE__ */ jsx("i", {}),
1434
+ /* @__PURE__ */ jsx("b", {})
1435
+ ] })
1436
+ });
285
1437
  }
286
- function isCompleteBrand(values) {
287
- return isThemeHex(values.primary) && isThemeHex(values.secondary) && isThemeHex(values.accent) && isThemeHex(values.highlight) && isThemeHex(values.success) && typeof values.greyScale === "string";
1438
+ function AppearanceChoices({ values, onChange, readOnly }) {
1439
+ return /* @__PURE__ */ jsxs("div", {
1440
+ className: "bl-appearance",
1441
+ children: [/* @__PURE__ */ jsxs("header", {
1442
+ className: "bl-appearance__intro",
1443
+ children: [/* @__PURE__ */ jsx("h2", { children: "Set the overall feel of your site" }), /* @__PURE__ */ jsx("p", { children: "Choose how rounded, elevated, animated, and spacious the interface feels. You don’t manage the technical values — the system handles those." })]
1444
+ }), Object.keys(APPEARANCE_CHOICES).map((family) => {
1445
+ const choices = APPEARANCE_CHOICES[family];
1446
+ const copy = APPEARANCE_FAMILY_COPY[family];
1447
+ return /* @__PURE__ */ jsxs("fieldset", {
1448
+ className: "bl-appearance__family",
1449
+ children: [
1450
+ /* @__PURE__ */ jsxs("legend", { children: [/* @__PURE__ */ jsx("strong", { children: copy.title }), /* @__PURE__ */ jsx("span", { children: copy.hint })] }),
1451
+ /* @__PURE__ */ jsx("div", {
1452
+ className: "bl-appearance__choices",
1453
+ "data-family": family,
1454
+ children: Object.entries(choices).map(([value, item]) => {
1455
+ const selected = values[family] === value;
1456
+ return /* @__PURE__ */ jsxs("label", {
1457
+ className: "bl-appearance__choice",
1458
+ "data-selected": selected ? "" : void 0,
1459
+ children: [
1460
+ /* @__PURE__ */ jsx("input", {
1461
+ type: "radio",
1462
+ name: family,
1463
+ value,
1464
+ checked: selected,
1465
+ disabled: readOnly,
1466
+ onChange: () => onChange(family, value)
1467
+ }),
1468
+ /* @__PURE__ */ jsx(ChoicePreview, {
1469
+ family,
1470
+ value
1471
+ }),
1472
+ /* @__PURE__ */ jsxs("span", {
1473
+ className: "bl-appearance__copy",
1474
+ children: [/* @__PURE__ */ jsxs("strong", { children: [item.label, selected ? /* @__PURE__ */ jsx("em", {
1475
+ "aria-hidden": "true",
1476
+ children: "✓"
1477
+ }) : null] }), /* @__PURE__ */ jsx("span", { children: item.hint })]
1478
+ })
1479
+ ]
1480
+ }, value);
1481
+ })
1482
+ }),
1483
+ family === "motion" ? /* @__PURE__ */ jsx("p", {
1484
+ className: "bl-appearance__note",
1485
+ children: "Visitors who prefer reduced motion will still see fewer animations automatically."
1486
+ }) : null
1487
+ ]
1488
+ }, family);
1489
+ })]
1490
+ });
288
1491
  }
289
- function LevelIcon({ level }) {
290
- if (level === "fail") return /* @__PURE__ */ jsx("svg", {
291
- viewBox: "0 0 16 16",
292
- width: "14",
293
- height: "14",
294
- "aria-hidden": "true",
295
- children: /* @__PURE__ */ jsx("path", {
296
- fill: "currentColor",
297
- d: "M8 1.5 1.5 14h13L8 1.5zm0 4.2c.4 0 .7.3.7.8v3.2c0 .4-.3.8-.7.8s-.8-.4-.8-.8V6.5c0-.5.4-.8.8-.8zm0 6.3a.8.8 0 1 1 0 1.6.8.8 0 0 1 0-1.6z"
298
- })
1492
+ const APPEARANCE_CHOICES_CSS = `
1493
+ .bl-appearance{display:flex;flex-direction:column;gap:1.25rem}
1494
+ .bl-appearance__intro h2{margin:0;font-size:1.1rem}
1495
+ .bl-appearance__intro p{margin:.35rem 0 0;font-size:13px;color:var(--theme-elevation-500,#64748b);max-width:40rem}
1496
+ .bl-appearance__family{margin:0;padding:1rem 1.1rem 1.15rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:12px;background:var(--theme-elevation-0,#fff)}
1497
+ .bl-appearance__family legend{padding:0;margin-bottom:.85rem}
1498
+ .bl-appearance__family legend strong{display:block;font-size:14px}
1499
+ .bl-appearance__family legend span{display:block;margin-top:.25rem;font-size:12px;color:var(--theme-elevation-500,#64748b);font-weight:400}
1500
+ .bl-appearance__choices{display:grid;gap:.6rem}
1501
+ .bl-appearance__choices[data-family=defaultTheme],.bl-appearance__choices[data-family=shadow],.bl-appearance__choices[data-family=motion],.bl-appearance__choices[data-family=density]{grid-template-columns:repeat(3,minmax(0,1fr))}
1502
+ .bl-appearance__choices[data-family=radius]{grid-template-columns:repeat(4,minmax(0,1fr))}
1503
+ .bl-appearance__choice{display:flex;flex-direction:column;gap:.65rem;padding:.75rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:12px;background:var(--theme-elevation-0,#fff);cursor:pointer}
1504
+ .bl-appearance__choice[data-selected]{box-shadow:inset 0 0 0 2px #0f172a;border-color:#0f172a}
1505
+ .bl-appearance__choice input{position:absolute;opacity:0;pointer-events:none}
1506
+ .bl-appearance__copy{display:flex;flex-direction:column;gap:.2rem}
1507
+ .bl-appearance__copy strong{display:flex;justify-content:space-between;align-items:center;gap:.4rem;font-size:14px}
1508
+ .bl-appearance__copy em{font-style:normal}
1509
+ .bl-appearance__copy span{font-size:12px;color:var(--theme-elevation-500,#64748b)}
1510
+ .bl-appearance__note{margin:.75rem 0 0;font-size:12px;color:var(--theme-elevation-500,#64748b)}
1511
+ .bl-appearance__preview{--bl-preview-brand:#db0082;--bl-preview-radius:8px;border-radius:10px;background:#f8fafc;overflow:hidden}
1512
+ .bl-appearance__preview[data-family=defaultTheme]{display:grid;min-height:5.5rem;padding:.65rem}
1513
+ .bl-appearance__preview[data-family=defaultTheme][data-value=system]{grid-template-columns:1fr 1fr;padding:0}
1514
+ .bl-appearance__preview[data-family=defaultTheme][data-value=dark]{background:#020617}
1515
+ .bl-appearance__pane{display:flex;flex-direction:column;gap:.45rem;padding:.65rem}
1516
+ .bl-appearance__pane--dark{background:#020617}
1517
+ .bl-appearance__pane i,.bl-appearance__preview[data-family=defaultTheme] > i{display:block;height:.4rem;width:42%;border-radius:99px;background:#0f172a}
1518
+ .bl-appearance__pane i + i,.bl-appearance__preview[data-family=defaultTheme] > i + i{width:64%;background:#cbd5e1}
1519
+ .bl-appearance__pane--dark i{background:#fff}
1520
+ .bl-appearance__pane--dark i + i{background:#334155}
1521
+ .bl-appearance__preview[data-family=defaultTheme][data-value=dark] > i{background:#fff}
1522
+ .bl-appearance__preview[data-family=defaultTheme][data-value=dark] > i + i{background:#334155}
1523
+ .bl-appearance__pane b,.bl-appearance__preview[data-family=defaultTheme] > b{display:block;height:1.6rem;margin-top:auto;border-radius:6px;background:var(--bl-preview-brand)}
1524
+ .bl-appearance__pane--dark b{background:var(--bl-preview-brand)}
1525
+ .bl-appearance__preview[data-family=radius]{display:flex;flex-direction:column;gap:.45rem;padding:.65rem;background:#fff}
1526
+ .bl-appearance__preview[data-family=radius] b{display:grid;place-items:center;height:1.85rem;background:var(--bl-preview-brand);color:#fff;font-size:10px;font-weight:600;border-radius:var(--bl-preview-radius)}
1527
+ .bl-appearance__input{display:block;height:1.85rem;border:1px solid #cbd5e1;background:#f8fafc;border-radius:var(--bl-preview-radius)}
1528
+ .bl-appearance__card{display:block;height:2.4rem;border:1px solid #e2e8f0;background:#fff;border-radius:var(--bl-preview-radius)}
1529
+ .bl-appearance__preview[data-value=sharp]{--bl-preview-radius:0}
1530
+ .bl-appearance__preview[data-value=subtle]{--bl-preview-radius:6px}
1531
+ .bl-appearance__preview[data-value=rounded]{--bl-preview-radius:12px}
1532
+ .bl-appearance__preview[data-value=pill]{--bl-preview-radius:999px}
1533
+ .bl-appearance__preview[data-value=pill] .bl-appearance__card{--bl-preview-radius:1.25rem}
1534
+ .bl-appearance__preview[data-family=shadow]{display:grid;place-items:center;min-height:6.5rem;padding:.85rem}
1535
+ .bl-appearance__lift{width:100%;height:3.5rem;border-radius:10px;background:#fff}
1536
+ .bl-appearance__preview[data-value=flat] .bl-appearance__lift{border:1px solid #e2e8f0}
1537
+ .bl-appearance__preview[data-value=subtle] .bl-appearance__lift{box-shadow:0 1px 2px rgba(15,23,42,.06),0 4px 12px rgba(15,23,42,.06)}
1538
+ .bl-appearance__preview[data-value=elevated] .bl-appearance__lift{box-shadow:0 10px 30px rgba(15,23,42,.12)}
1539
+ .bl-appearance__preview[data-family=motion]{display:flex;align-items:center;min-height:4.5rem;padding:0 .85rem}
1540
+ .bl-appearance__dot{width:1.15rem;height:1.15rem;border-radius:999px;background:var(--bl-preview-brand);animation:bl-appear-dot 1.8s ease-in-out infinite}
1541
+ .bl-appearance__preview[data-value=snappy] .bl-appearance__dot{animation-duration:1s}
1542
+ .bl-appearance__preview[data-value=minimal] .bl-appearance__dot{animation-duration:2.5s}
1543
+ @keyframes bl-appear-dot{0%,100%{transform:translateX(0)}50%{transform:translateX(4.5rem)}}
1544
+ .bl-appearance__preview[data-family=density]{display:flex;flex-direction:column;background:#f8fafc}
1545
+ .bl-appearance__preview[data-value=compact]{gap:.4rem;padding:.55rem}
1546
+ .bl-appearance__preview[data-value=default]{gap:.65rem;padding:.8rem}
1547
+ .bl-appearance__preview[data-value=spacious]{gap:.9rem;padding:1.05rem}
1548
+ .bl-appearance__preview[data-family=density] b{display:block;height:1.7rem;background:var(--bl-preview-brand);border-radius:6px}
1549
+ .bl-appearance__preview[data-value=compact] b,.bl-appearance__preview[data-value=compact] .bl-appearance__input{height:1.45rem}
1550
+ .bl-appearance__preview[data-value=spacious] b,.bl-appearance__preview[data-value=spacious] .bl-appearance__input{height:2.1rem}
1551
+ @media (max-width:720px){
1552
+ .bl-appearance__choices[data-family=defaultTheme],.bl-appearance__choices[data-family=shadow],.bl-appearance__choices[data-family=motion],.bl-appearance__choices[data-family=density],.bl-appearance__choices[data-family=radius]{grid-template-columns:1fr}
1553
+ }
1554
+ @media (prefers-reduced-motion:reduce){
1555
+ .bl-appearance__dot{animation:none}
1556
+ }
1557
+ `;
1558
+ //#endregion
1559
+ //#region src/admin/appearance-field.tsx
1560
+ /**
1561
+ * Named appearance choices. Soft → pill, Gentle → minimal, Balanced →
1562
+ * default, Follow device → system. No rem or ms.
1563
+ */
1564
+ const AppearanceField = ({ path, readOnly }) => {
1565
+ const defaultTheme = useField({ path: `${path}.defaultTheme` });
1566
+ const radius = useField({ path: `${path}.radius` });
1567
+ const shadow = useField({ path: `${path}.shadow` });
1568
+ const motion = useField({ path: `${path}.motion` });
1569
+ const density = useField({ path: `${path}.density` });
1570
+ const fields = {
1571
+ defaultTheme,
1572
+ radius,
1573
+ shadow,
1574
+ motion,
1575
+ density
1576
+ };
1577
+ function onChange(name, value) {
1578
+ fields[name].setValue(value);
1579
+ }
1580
+ return /* @__PURE__ */ jsxs("div", {
1581
+ className: "field-type bl-appearance-field",
1582
+ children: [/* @__PURE__ */ jsx("style", {
1583
+ href: "bl-appearance",
1584
+ precedence: "default",
1585
+ children: APPEARANCE_CHOICES_CSS
1586
+ }), /* @__PURE__ */ jsx(AppearanceChoices, {
1587
+ values: {
1588
+ defaultTheme: defaultTheme.value ?? "system",
1589
+ radius: radius.value ?? "rounded",
1590
+ shadow: shadow.value ?? "subtle",
1591
+ motion: motion.value ?? "smooth",
1592
+ density: density.value ?? "default"
1593
+ },
1594
+ onChange,
1595
+ readOnly
1596
+ })]
299
1597
  });
300
- if (level === "aa-large") return /* @__PURE__ */ jsx("svg", {
301
- viewBox: "0 0 16 16",
302
- width: "14",
303
- height: "14",
304
- "aria-hidden": "true",
305
- children: /* @__PURE__ */ jsx("path", {
306
- fill: "currentColor",
307
- d: "M3 8h10v1.5H3z"
308
- })
1598
+ };
1599
+ //#endregion
1600
+ //#region src/admin/grey-scale-field.tsx
1601
+ const FAMILIES = Object.keys(presetHints.greyScale);
1602
+ const SWATCHES = [
1603
+ "50",
1604
+ "200",
1605
+ "400",
1606
+ "700",
1607
+ "950"
1608
+ ];
1609
+ function GreyScaleChoices({ value, onChange, readOnly }) {
1610
+ return /* @__PURE__ */ jsxs("section", {
1611
+ className: "bl-grey",
1612
+ children: [/* @__PURE__ */ jsxs("header", {
1613
+ className: "bl-grey__head",
1614
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", { children: "Gray family" }), /* @__PURE__ */ jsx("p", { children: "Choose the neutral family used for page backgrounds, cards, borders, inputs, and muted text." })] }), /* @__PURE__ */ jsx("span", {
1615
+ className: "bl-grey__badge",
1616
+ children: "Required"
1617
+ })]
1618
+ }), /* @__PURE__ */ jsx("div", {
1619
+ className: "bl-grey__choices",
1620
+ children: FAMILIES.map((id) => {
1621
+ const copy = presetHints.greyScale[id];
1622
+ const selected = value === id;
1623
+ return /* @__PURE__ */ jsxs("label", {
1624
+ className: "bl-grey__choice",
1625
+ "data-selected": selected ? "" : void 0,
1626
+ children: [
1627
+ /* @__PURE__ */ jsx("input", {
1628
+ type: "radio",
1629
+ name: "greyScale",
1630
+ value: id,
1631
+ checked: selected,
1632
+ disabled: readOnly,
1633
+ onChange: () => onChange(id)
1634
+ }),
1635
+ /* @__PURE__ */ jsx("span", {
1636
+ className: "bl-grey__ramp",
1637
+ "aria-hidden": "true",
1638
+ children: SWATCHES.map((step) => /* @__PURE__ */ jsx("i", { style: { background: `hsl(${GREY_SCALES[id][step]})` } }, step))
1639
+ }),
1640
+ /* @__PURE__ */ jsxs("strong", { children: [copy.label, selected ? /* @__PURE__ */ jsx("em", {
1641
+ "aria-hidden": "true",
1642
+ children: "✓"
1643
+ }) : null] }),
1644
+ /* @__PURE__ */ jsx("span", { children: copy.hint })
1645
+ ]
1646
+ }, id);
1647
+ })
1648
+ })]
309
1649
  });
310
- return /* @__PURE__ */ jsx("svg", {
311
- viewBox: "0 0 16 16",
312
- width: "14",
313
- height: "14",
314
- "aria-hidden": "true",
315
- children: /* @__PURE__ */ jsx("path", {
316
- fill: "currentColor",
317
- d: "M8 1.2a6.8 6.8 0 1 1 0 13.6A6.8 6.8 0 0 1 8 1.2zm3.1 4.3L7.2 9.4 5 7.2l-.9.9 3.1 3.1 4.8-4.8-.9-.9z"
318
- })
1650
+ }
1651
+ /**
1652
+ * Visual picker over the five grey-scale presets. Replaces the stock select
1653
+ * on Colors; the stored value is still `colors.greyScale`.
1654
+ */
1655
+ const GreyScaleField = ({ path, readOnly }) => {
1656
+ const { value, setValue } = useField({ path });
1657
+ return /* @__PURE__ */ jsxs("div", {
1658
+ className: "field-type bl-grey-field",
1659
+ children: [/* @__PURE__ */ jsx("style", {
1660
+ href: "bl-grey",
1661
+ precedence: "default",
1662
+ children: GREY_SCALE_FIELD_CSS
1663
+ }), /* @__PURE__ */ jsx(GreyScaleChoices, {
1664
+ value: value ?? "slate",
1665
+ onChange: setValue,
1666
+ readOnly
1667
+ })]
319
1668
  });
1669
+ };
1670
+ const GREY_SCALE_FIELD_CSS = `
1671
+ .bl-grey{margin:0 0 1.75rem}
1672
+ .bl-grey__head{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;margin-bottom:.85rem}
1673
+ .bl-grey__head h2{margin:0;font-size:1.1rem}
1674
+ .bl-grey__head p{margin:.35rem 0 0;max-width:40rem;font-size:13px;color:var(--theme-elevation-500,#64748b)}
1675
+ .bl-grey__badge{font:inherit;font-size:10px;text-transform:uppercase;letter-spacing:.04em;padding:.15rem .45rem;border-radius:999px;background:var(--theme-elevation-100,#f1f5f9);color:var(--theme-elevation-500,#64748b)}
1676
+ .bl-grey__choices{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:.6rem}
1677
+ .bl-grey__choice{display:flex;flex-direction:column;gap:.45rem;padding:.75rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:12px;background:var(--theme-elevation-0,#fff);cursor:pointer}
1678
+ .bl-grey__choice[data-selected]{box-shadow:inset 0 0 0 2px #0f172a;border-color:#0f172a}
1679
+ .bl-grey__choice input{position:absolute;opacity:0;pointer-events:none}
1680
+ .bl-grey__ramp{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:3px}
1681
+ .bl-grey__ramp i{display:block;height:2rem;border-radius:6px}
1682
+ .bl-grey__choice strong{display:flex;justify-content:space-between;align-items:center;gap:.4rem;font-size:14px}
1683
+ .bl-grey__choice em{font-style:normal}
1684
+ .bl-grey__choice > span{font-size:12px;color:var(--theme-elevation-500,#64748b)}
1685
+ [data-bl-theme-preview=dark] .bl-grey__head p,[data-bl-theme-preview=dark] .bl-grey__choice > span{color:#94a3b8}
1686
+ [data-bl-theme-preview=dark] .bl-grey__choice{background:#0f172a;border-color:#334155;color:#f8fafc}
1687
+ [data-bl-theme-preview=dark] .bl-grey__choice[data-selected]{box-shadow:inset 0 0 0 2px #f8fafc;border-color:#f8fafc}
1688
+ [data-bl-theme-preview=dark] .bl-grey__badge{background:#1e293b;color:#94a3b8}
1689
+ @media (max-width:720px){.bl-grey__choices{grid-template-columns:1fr}}
1690
+ `;
1691
+ //#endregion
1692
+ //#region src/admin/contrast-report.tsx
1693
+ function hexAt(fields, path) {
1694
+ const value = fields[path]?.value;
1695
+ return typeof value === "string" ? value : "";
320
1696
  }
321
1697
  /**
322
- * Reads the five brand colours and grey scale from the form, runs
323
- * `auditTheme` client-side, and paints its own rows. Warn only: nothing
324
- * here touches validity.
1698
+ * Fill + automatic label only. Target 7:1, Light and Dark. No gray
1699
+ * ContrastStrip. Warnings do not block save.
325
1700
  */
326
1701
  const ContrastReport = ({ field }) => {
327
- const target = Number((field.admin?.custom)?.target) || 4.5;
328
- const values = useFormFields(([fields]) => {
1702
+ const target = Number((field.admin?.custom)?.target) || 7;
1703
+ const hexes = useFormFields(([fields]) => {
329
1704
  const record = fields;
1705
+ const system = Object.fromEntries(SYSTEM_COLOR_KEYS.map((key) => [key, hexAt(record, `colors.brand.${key}.hex`)]));
1706
+ const library = record["colors.library"]?.value;
330
1707
  return {
331
- primary: fieldValue(record, "brand.primary"),
332
- secondary: fieldValue(record, "brand.secondary"),
333
- accent: fieldValue(record, "brand.accent"),
334
- highlight: fieldValue(record, "brand.highlight"),
335
- success: fieldValue(record, "brand.success"),
336
- greyScale: fieldValue(record, "greyScale")
1708
+ system,
1709
+ custom: Array.isArray(library) ? library.flatMap((row, index) => {
1710
+ const hex = typeof row === "object" && row && "hex" in row ? String(row.hex ?? "") : "";
1711
+ const name = typeof row === "object" && row && "label" in row ? String(row.label ?? row.key ?? "Custom") : "Custom";
1712
+ return hex ? [{
1713
+ id: `library-${index}`,
1714
+ name,
1715
+ hex
1716
+ }] : [];
1717
+ }) : []
337
1718
  };
338
1719
  });
339
1720
  const last = useRef(null);
340
- const complete = isCompleteBrand(values);
341
- if (complete) last.current = auditTheme({
342
- brandPrimary: values.primary,
343
- brandSecondary: values.secondary,
344
- brandAccent: values.accent,
345
- brandHighlight: values.highlight,
346
- brandSuccess: values.success,
347
- greyScale: values.greyScale
348
- }, { target });
349
- const checks = last.current;
350
- const stale = Boolean(checks) && !complete;
351
- const failing = checks?.filter((check) => !check.meetsTarget).length ?? 0;
352
- const summary = !checks ? "Enter five hex colours to measure contrast." : failing === 0 ? `Every pairing meets ${target}:1` : `${failing} of ${checks.length} pairings below ${target}:1`;
1721
+ const complete = SYSTEM_COLOR_KEYS.every((key) => isThemeHex(hexes.system[key]));
1722
+ if (complete) last.current = [...SYSTEM_COLOR_KEYS.map((key) => fillLabelWarning(key, SYSTEM_COLOR_COPY[key].label, hexes.system[key], target)), ...hexes.custom.map((row) => fillLabelWarning(row.id, row.name, row.hex, target))].filter((item) => item !== null);
1723
+ const warnings = last.current ?? [];
1724
+ const stale = Boolean(last.current) && !complete;
353
1725
  return /* @__PURE__ */ jsxs("div", {
354
1726
  className: "field-type bl-contrast-report",
355
1727
  children: [
356
1728
  /* @__PURE__ */ jsx("style", {
357
- href: "bl-contrast-report",
1729
+ href: "bl-readability",
358
1730
  precedence: "default",
359
- children: CSS
360
- }),
361
- /* @__PURE__ */ jsx("p", {
362
- className: "bl-contrast-report__summary",
363
- children: summary
1731
+ children: READABILITY_LIST_CSS
364
1732
  }),
365
1733
  stale ? /* @__PURE__ */ jsx("p", {
366
1734
  className: "bl-contrast-report__stale",
367
1735
  children: "Showing the last complete audit while a colour is being typed."
368
1736
  }) : null,
369
- checks ? /* @__PURE__ */ jsx("ul", {
370
- className: "bl-contrast-report__list",
371
- children: checks.flatMap((check) => ["light", "dark"].map((mode) => {
372
- const sample = check[mode];
373
- const failingRow = sample.ratio < check.target;
374
- const verdict = contrastVerdict(sample, check.target);
375
- return /* @__PURE__ */ jsxs("li", {
376
- className: "bl-contrast-report__row",
377
- "data-failing": failingRow ? "" : void 0,
378
- children: [
379
- /* @__PURE__ */ jsx("span", {
380
- "aria-hidden": "true",
381
- className: "bl-contrast-report__swatch",
382
- style: {
383
- color: `hsl(${sample.foreground})`,
384
- backgroundColor: `hsl(${sample.background})`
385
- },
386
- children: "Aa"
387
- }),
388
- /* @__PURE__ */ jsxs("span", {
389
- className: "bl-contrast-report__ratio",
390
- children: [sample.ratio.toFixed(2), ":1"]
391
- }),
392
- /* @__PURE__ */ jsxs("span", {
393
- className: "bl-contrast-report__level",
394
- children: [/* @__PURE__ */ jsx(LevelIcon, { level: sample.level }), contrastLevelLabel(sample.level)]
395
- }),
396
- /* @__PURE__ */ jsx("span", {
397
- className: "bl-contrast-report__verdict",
398
- children: verdict.text
399
- }),
400
- /* @__PURE__ */ jsxs("span", {
401
- className: "bl-contrast-report__label",
402
- children: [
403
- check.label,
404
- " (",
405
- mode === "light" ? "Light" : "Dark",
406
- ")"
407
- ]
408
- })
409
- ]
410
- }, `${check.id}-${mode}`);
411
- }))
412
- }) : null
1737
+ warnings.length > 0 ? /* @__PURE__ */ jsx(ReadabilityList, {
1738
+ warnings,
1739
+ target
1740
+ }) : /* @__PURE__ */ jsx("p", { children: "Enter hex colours to measure the automatic label on each fill." })
1741
+ ]
1742
+ });
1743
+ };
1744
+ //#endregion
1745
+ //#region src/admin/section-heading.tsx
1746
+ /**
1747
+ * Unnamed UI heading between Theme fields. No document path.
1748
+ */
1749
+ const SectionHeading = ({ field }) => {
1750
+ const custom = field.admin?.custom ?? {};
1751
+ if (!custom.title) return null;
1752
+ return /* @__PURE__ */ jsxs("header", {
1753
+ className: "bl-section-heading",
1754
+ children: [
1755
+ /* @__PURE__ */ jsx("style", {
1756
+ href: "bl-section-heading",
1757
+ precedence: "default",
1758
+ children: CSS
1759
+ }),
1760
+ /* @__PURE__ */ jsx("h2", { children: custom.title }),
1761
+ custom.hint ? /* @__PURE__ */ jsx("p", { children: custom.hint }) : null
413
1762
  ]
414
1763
  });
415
1764
  };
416
1765
  const CSS = `
417
- .bl-contrast-report{margin:calc(var(--base) * .6) 0}
418
- .bl-contrast-report__summary{margin:0 0 calc(var(--base) * .3);font-weight:600}
419
- .bl-contrast-report__stale{margin:0 0 calc(var(--base) * .3);color:var(--theme-elevation-500);font-size:12px}
420
- .bl-contrast-report__list{margin:0;padding:0;list-style:none;border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:var(--theme-elevation-0)}
421
- .bl-contrast-report__row{display:flex;flex-wrap:wrap;align-items:center;gap:calc(var(--base) * .4);padding:calc(var(--base) * .35) calc(var(--base) * .6);border-top:1px solid var(--theme-elevation-100)}
422
- .bl-contrast-report__row:first-child{border-top:0}
423
- .bl-contrast-report__row[data-failing]{background:var(--theme-error-50)}
424
- .bl-contrast-report__swatch{flex:none;display:inline-flex;align-items:center;justify-content:center;width:2.5rem;height:1.75rem;border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);font-weight:600}
425
- .bl-contrast-report__ratio{width:4.5rem;font-variant-numeric:tabular-nums}
426
- .bl-contrast-report__level{display:inline-flex;align-items:center;gap:4px;width:5.5rem}
427
- .bl-contrast-report__verdict{width:6rem;font-weight:600}
428
- .bl-contrast-report__label{flex:1;min-width:12rem;color:var(--theme-elevation-500)}
1766
+ .bl-section-heading{margin:1.25rem 0 .65rem}
1767
+ .bl-section-heading:first-child{margin-top:0}
1768
+ .bl-section-heading h2{margin:0;font-size:1.1rem}
1769
+ .bl-section-heading p{margin:.35rem 0 0;font-size:13px;color:var(--theme-elevation-500,#64748b);max-width:40rem}
1770
+ `;
1771
+ //#endregion
1772
+ //#region src/theme/changes.ts
1773
+ function colorLine(name, before, after) {
1774
+ const a = before && typeof before === "object" ? before : { hex: before };
1775
+ const b = after && typeof after === "object" ? after : { hex: after };
1776
+ const left = a;
1777
+ const right = b;
1778
+ if (left.hex === right.hex && String(left.sourceStep) === String(right.sourceStep)) return null;
1779
+ return `${name}: ${String(left.hex ?? "—")} → ${String(right.hex ?? "—")}`;
1780
+ }
1781
+ /**
1782
+ * Human lines for the Publish confirmation. Only this child's
1783
+ * differences; other responsibilities are not listed.
1784
+ */
1785
+ function themeChildChanges(child, published, incoming) {
1786
+ if (child === "colors") {
1787
+ const lines = [];
1788
+ for (const key of SYSTEM_COLOR_KEYS) {
1789
+ const line = colorLine(key[0].toUpperCase() + key.slice(1), published.colors?.brand?.[key], incoming.colors?.brand?.[key]);
1790
+ if (line) lines.push(line);
1791
+ }
1792
+ const beforeKeys = (published.colors?.library ?? []).map((row) => row.key).join(", ");
1793
+ const afterKeys = (incoming.colors?.library ?? []).map((row) => row.key).join(", ");
1794
+ if (beforeKeys !== afterKeys) lines.push(`Custom colors: ${beforeKeys || "none"} → ${afterKeys || "none"}`);
1795
+ if ((published.colors?.greyScale ?? "") !== (incoming.colors?.greyScale ?? "")) lines.push(`Gray family: ${published.colors?.greyScale ?? "—"} → ${incoming.colors?.greyScale ?? "—"}`);
1796
+ return lines;
1797
+ }
1798
+ if (child === "typography") {
1799
+ const lines = [];
1800
+ if ((published.typography?.heading ?? "") !== (incoming.typography?.heading ?? "")) lines.push(`Heading: ${published.typography?.heading || "same as body"} → ${incoming.typography?.heading || "same as body"}`);
1801
+ if ((published.typography?.body ?? "") !== (incoming.typography?.body ?? "")) lines.push(`Body: ${published.typography?.body ?? "—"} → ${incoming.typography?.body ?? "—"}`);
1802
+ return lines;
1803
+ }
1804
+ if (child === "appearance") return [
1805
+ "defaultTheme",
1806
+ "radius",
1807
+ "shadow",
1808
+ "motion",
1809
+ "density"
1810
+ ].flatMap((key) => {
1811
+ const left = published.appearance?.[key] ?? "";
1812
+ const right = incoming.appearance?.[key] ?? "";
1813
+ return left === right ? [] : [`${key}: ${left || "—"} → ${right || "—"}`];
1814
+ });
1815
+ const lines = [];
1816
+ if (uploadId(published.logo) !== uploadId(incoming.logo)) lines.push("Logo updated");
1817
+ if (uploadId(published.favicon) !== uploadId(incoming.favicon)) lines.push("Favicon updated");
1818
+ if (uploadId(published.logoMark) !== uploadId(incoming.logoMark)) lines.push("Mobile menu logo updated");
1819
+ return lines;
1820
+ }
1821
+ function uploadId(value) {
1822
+ if (value && typeof value === "object" && value.id != null) return String(value.id);
1823
+ if (typeof value === "string" || typeof value === "number") return String(value);
1824
+ return "";
1825
+ }
1826
+ //#endregion
1827
+ //#region src/theme/publish.ts
1828
+ const THEME_CHILDREN = [
1829
+ "colors",
1830
+ "typography",
1831
+ "appearance",
1832
+ "identity"
1833
+ ];
1834
+ function isThemeChild(value) {
1835
+ return typeof value === "string" && THEME_CHILDREN.includes(value);
1836
+ }
1837
+ //#endregion
1838
+ //#region src/admin/publish-confirm.tsx
1839
+ const OTHER_CHILDREN = {
1840
+ Colors: "Typography, Appearance, and Identity stay as they are.",
1841
+ Typography: "Colors, Appearance, and Identity stay as they are.",
1842
+ Appearance: "Colors, Typography, and Identity stay as they are.",
1843
+ Identity: "Colors, Typography, and Appearance stay as they are."
1844
+ };
1845
+ function PublishConfirm({ childLabel, changes, shorts, onKeepEditing, onPublish, busy }) {
1846
+ const remaining = shorts?.filter((item) => !item.meets) ?? [];
1847
+ return /* @__PURE__ */ jsx("div", {
1848
+ className: "bl-publish",
1849
+ role: "dialog",
1850
+ "aria-modal": "true",
1851
+ "aria-labelledby": "bl-publish-title",
1852
+ children: /* @__PURE__ */ jsxs("div", {
1853
+ className: "bl-publish__panel",
1854
+ children: [
1855
+ /* @__PURE__ */ jsxs("h2", {
1856
+ id: "bl-publish-title",
1857
+ children: ["Publish ", childLabel]
1858
+ }),
1859
+ /* @__PURE__ */ jsxs("p", { children: [
1860
+ "These ",
1861
+ childLabel.toLowerCase(),
1862
+ " changes go live now.",
1863
+ " ",
1864
+ OTHER_CHILDREN[childLabel] ?? "The other Theme sections stay as they are."
1865
+ ] }),
1866
+ changes.length === 0 ? /* @__PURE__ */ jsx("p", {
1867
+ className: "bl-publish__empty",
1868
+ children: "No changes to publish."
1869
+ }) : /* @__PURE__ */ jsx("ul", { children: changes.map((change) => /* @__PURE__ */ jsx("li", { children: change }, change)) }),
1870
+ remaining.length > 0 ? /* @__PURE__ */ jsxs("div", {
1871
+ className: "bl-publish__shorts",
1872
+ children: [
1873
+ /* @__PURE__ */ jsx("strong", { children: "Some colors may be hard to read" }),
1874
+ /* @__PURE__ */ jsx("ul", { children: remaining.map((item) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("span", {
1875
+ "aria-hidden": "true",
1876
+ className: "bl-publish__swatch",
1877
+ style: {
1878
+ background: hslCss(item.light.fill),
1879
+ color: hslCss(item.light.label)
1880
+ },
1881
+ children: "Aa"
1882
+ }), /* @__PURE__ */ jsxs("span", { children: [
1883
+ readabilityHeadline(item).replace(/^Text on /, ""),
1884
+ " ",
1885
+ hardToReadModes(item),
1886
+ "."
1887
+ ] })] }, item.id)) }),
1888
+ /* @__PURE__ */ jsx("p", { children: READABILITY_PUBLISH_NOTE })
1889
+ ]
1890
+ }) : null,
1891
+ /* @__PURE__ */ jsxs("div", {
1892
+ className: "bl-publish__actions",
1893
+ children: [/* @__PURE__ */ jsx("button", {
1894
+ type: "button",
1895
+ onClick: onKeepEditing,
1896
+ children: "Keep editing"
1897
+ }), /* @__PURE__ */ jsx("button", {
1898
+ type: "button",
1899
+ disabled: busy || changes.length === 0,
1900
+ onClick: onPublish,
1901
+ children: remaining.length > 0 ? "Publish anyway" : "Publish"
1902
+ })]
1903
+ })
1904
+ ]
1905
+ })
1906
+ });
1907
+ }
1908
+ const PUBLISH_CONFIRM_CSS = `
1909
+ .bl-publish{position:fixed;inset:0;z-index:80;display:grid;place-items:center;padding:1rem;background:rgba(15,23,42,.45)}
1910
+ .bl-publish__panel{width:min(36rem,100%);padding:1.25rem 1.35rem;border-radius:16px;background:#fff;color:#0f172a;box-shadow:0 24px 60px rgba(15,23,42,.2)}
1911
+ .bl-publish__panel h2{margin:0}
1912
+ .bl-publish__panel p{margin:.5rem 0 0;color:#475569}
1913
+ .bl-publish__empty{font-style:italic}
1914
+ .bl-publish__panel > ul{margin:.75rem 0 0;padding-left:1.1rem}
1915
+ .bl-publish__shorts{margin-top:1rem;padding:.85rem .9rem;border-radius:10px;background:#fffbeb}
1916
+ .bl-publish__shorts strong{display:block}
1917
+ .bl-publish__shorts ul{margin:.65rem 0 0;padding:0;list-style:none}
1918
+ .bl-publish__shorts li{display:flex;gap:.65rem;align-items:center;margin-top:.45rem;font-size:13px}
1919
+ .bl-publish__swatch{flex:none;display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:1.6rem;border-radius:6px;font-weight:700;font-size:12px}
1920
+ .bl-publish__shorts p{margin:.65rem 0 0;font-size:13px}
1921
+ .bl-publish__actions{display:flex;justify-content:flex-end;gap:.6rem;margin-top:1.1rem}
1922
+ .bl-publish__actions button{min-height:2.25rem;padding:.4rem .9rem;border-radius:8px;border:1px solid #cbd5e1;background:#fff;font:inherit;cursor:pointer}
1923
+ .bl-publish__actions button:last-child{border:0;background:#0f172a;color:#fff;font-weight:600}
1924
+ .bl-publish__actions button:disabled{opacity:.45;cursor:not-allowed}
429
1925
  `;
430
1926
  //#endregion
431
- export { ColorField, ContrastReport, FontField };
1927
+ //#region src/admin/publish-child.tsx
1928
+ const LABELS = {
1929
+ colors: "Colors",
1930
+ typography: "Typography",
1931
+ appearance: "Appearance",
1932
+ identity: "Identity"
1933
+ };
1934
+ function asColor(value) {
1935
+ if (value && typeof value === "object") return value;
1936
+ if (typeof value === "string") return { hex: value };
1937
+ }
1938
+ function formDoc(fields) {
1939
+ return {
1940
+ colors: {
1941
+ brand: Object.fromEntries(SYSTEM_COLOR_KEYS.map((key) => [key, {
1942
+ hex: fields[`colors.brand.${key}.hex`]?.value,
1943
+ sourceStep: fields[`colors.brand.${key}.sourceStep`]?.value
1944
+ }])),
1945
+ library: fields["colors.library"]?.value ?? [],
1946
+ greyScale: fields["colors.greyScale"]?.value
1947
+ },
1948
+ typography: {
1949
+ body: fields["typography.body"]?.value,
1950
+ heading: fields["typography.heading"]?.value
1951
+ },
1952
+ appearance: {
1953
+ defaultTheme: fields["appearance.defaultTheme"]?.value,
1954
+ radius: fields["appearance.radius"]?.value,
1955
+ shadow: fields["appearance.shadow"]?.value,
1956
+ motion: fields["appearance.motion"]?.value,
1957
+ density: fields["appearance.density"]?.value
1958
+ },
1959
+ logo: fields.logo?.value,
1960
+ favicon: fields.favicon?.value,
1961
+ logoMark: fields.logoMark?.value
1962
+ };
1963
+ }
1964
+ function publishedDoc(fields) {
1965
+ return formDoc(Object.fromEntries(Object.entries(fields).map(([path, field]) => [path, { value: field?.initialValue }])));
1966
+ }
1967
+ function shortsFrom(doc) {
1968
+ const brand = doc.colors?.brand ?? {};
1969
+ const custom = doc.colors?.library ?? [];
1970
+ return [...SYSTEM_COLOR_KEYS.flatMap((key) => {
1971
+ const hex = asColor(brand[key])?.hex;
1972
+ const warning = typeof hex === "string" ? fillLabelWarning(key, SYSTEM_COLOR_COPY[key].label, hex) : null;
1973
+ return warning ? [warning] : [];
1974
+ }), ...custom.flatMap((row, index) => {
1975
+ const warning = row.hex ? fillLabelWarning(`library-${index}`, row.label ?? row.key ?? "Custom", row.hex) : null;
1976
+ return warning ? [warning] : [];
1977
+ })];
1978
+ }
1979
+ /**
1980
+ * Per-child Publish. Sets `publishChild` then submits; `beforeChange`
1981
+ * writes only that slice. Confirmation lists this child's changes and,
1982
+ * on Colors, remaining automatic-label shorts.
1983
+ */
1984
+ const PublishChild = ({ field }) => {
1985
+ const child = (field.admin?.custom)?.child;
1986
+ const label = isThemeChild(child) ? LABELS[child] : "Theme";
1987
+ const { submit } = useForm();
1988
+ const publishChild = useField({ path: "publishChild" });
1989
+ const snapshot = useFormFields(([fields]) => {
1990
+ const record = fields;
1991
+ return {
1992
+ incoming: formDoc(record),
1993
+ published: publishedDoc(record)
1994
+ };
1995
+ });
1996
+ const [open, setOpen] = useState(false);
1997
+ const [busy, setBusy] = useState(false);
1998
+ const changes = useMemo(() => isThemeChild(child) ? themeChildChanges(child, snapshot.published, snapshot.incoming) : [], [child, snapshot]);
1999
+ const shorts = child === "colors" ? shortsFrom(snapshot.incoming) : void 0;
2000
+ async function publish() {
2001
+ if (!isThemeChild(child)) return;
2002
+ setBusy(true);
2003
+ publishChild.setValue(child);
2004
+ try {
2005
+ await submit();
2006
+ setOpen(false);
2007
+ } finally {
2008
+ setBusy(false);
2009
+ }
2010
+ }
2011
+ return /* @__PURE__ */ jsxs("div", {
2012
+ className: "field-type bl-publish-child",
2013
+ children: [
2014
+ /* @__PURE__ */ jsx("style", {
2015
+ href: "bl-publish",
2016
+ precedence: "default",
2017
+ children: PUBLISH_CONFIRM_CSS
2018
+ }),
2019
+ /* @__PURE__ */ jsx("style", {
2020
+ href: "bl-publish-child",
2021
+ precedence: "default",
2022
+ children: `${THEME_PREVIEW_TOGGLE_CSS}.bl-publish-child{position:sticky;top:0;z-index:15;display:flex;justify-content:flex-end;align-items:center;gap:.75rem;padding:.65rem 0;background:var(--theme-bg,#fff)}.bl-publish-child__button{min-height:2.25rem;padding:.4rem 1rem;border:0;border-radius:8px;background:var(--theme-elevation-800,#0f172a);color:#fff;font:inherit;font-weight:600;cursor:pointer}`
2023
+ }),
2024
+ child === "colors" ? /* @__PURE__ */ jsx(ThemePreviewToggle, {}) : null,
2025
+ /* @__PURE__ */ jsxs("button", {
2026
+ type: "button",
2027
+ className: "bl-publish-child__button",
2028
+ onClick: () => setOpen(true),
2029
+ children: ["Publish ", label]
2030
+ }),
2031
+ open ? /* @__PURE__ */ jsx(PublishConfirm, {
2032
+ childLabel: label,
2033
+ changes,
2034
+ shorts,
2035
+ busy,
2036
+ onKeepEditing: () => setOpen(false),
2037
+ onPublish: () => {
2038
+ publish();
2039
+ }
2040
+ }) : null
2041
+ ]
2042
+ });
2043
+ };
2044
+ //#endregion
2045
+ //#region src/admin/save-button.tsx
2046
+ /**
2047
+ * Theme children publish themselves. The stock Save would write every
2048
+ * dirty tab at once, so it is hidden.
2049
+ */
2050
+ function HiddenSaveButton() {
2051
+ return null;
2052
+ }
2053
+ //#endregion
2054
+ export { AppearanceField, ColorField, ColorScaleField, ContrastReport, FontField, GreyScaleField, HiddenSaveButton, LibraryField, PairingField, PublishChild, SectionHeading };
432
2055
 
433
2056
  //# sourceMappingURL=admin.mjs.map