@bison-lab/payload-core 3.10.0 → 3.11.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,7 +1,7 @@
1
1
  "use client";
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";
2
+ import { a as colorDocFromState, i as SYSTEM_COLOR_KEYS, n as pageEditorLooks, r as pageEditorTokens, s as stateFromColorDoc } from "./looks-BbL309kO.mjs";
3
+ import { FieldDescription, FieldError, FieldLabel, useConfig, useField, useForm, useFormFields } from "@payloadcms/ui";
4
+ import { GREY_SCALES, SHADE_STEPS, contrastForeground, contrastRatio, createColorScale, deriveDarkPalette, deriveLightPalette, hexToHSL, hslToString, includedShadeSteps, isThemeHex, presetHints, setColorScaleInclude } from "@bison-lab/tokens";
5
5
  import { useEffect, useId, useMemo, useRef, useState } from "react";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  import { createPortal } from "react-dom";
@@ -247,12 +247,12 @@ const SYSTEM_COLOR_COPY = {
247
247
  },
248
248
  success: {
249
249
  label: "Success",
250
- hint: "Theme chrome for confirmations. Not offered as a page-editor fill.",
250
+ hint: "Success messages and confirmations",
251
251
  group: "status"
252
252
  },
253
253
  destructive: {
254
254
  label: "Destructive",
255
- hint: "Theme chrome for errors and dangerous actions. Not offered as a page-editor fill.",
255
+ hint: "Errors, dangerous actions, and destructive confirmations",
256
256
  group: "status"
257
257
  }
258
258
  };
@@ -313,10 +313,6 @@ function destructiveHueWarning(hex) {
313
313
  if (h <= 30 || h >= 330) return null;
314
314
  return "Keep destructive colors recognizable — stay in a red family.";
315
315
  }
316
- function automaticLabel(hex) {
317
- if (!isThemeHex(hex)) return null;
318
- return contrastForeground(hexToHSL(hex));
319
- }
320
316
  function hslCss(value) {
321
317
  return `hsl(${value})`;
322
318
  }
@@ -349,28 +345,90 @@ function readabilityRatioLine(sample, target = 7) {
349
345
  const READABILITY_FIX = "Darken or lighten the hex, then regenerate the scale. The site picks light or dark type for you.";
350
346
  const READABILITY_PUBLISH_NOTE = "You can still publish. To improve a color, darken or lighten it and regenerate.";
351
347
  //#endregion
348
+ //#region src/admin/colors-layout.ts
349
+ /**
350
+ * Theme Colors tab chrome. Payload group-in-group draws a left gutter;
351
+ * the mockup does not. Brand cards sit two-up. Light / Dark lives on the
352
+ * cards only — the field list stays Payload chrome.
353
+ */
354
+ const THEME_COLORS_LAYOUT_CSS = `
355
+ .group-field--gutter:has(.bl-scale-field),
356
+ .group-field--gutter:has(.bl-section-heading),
357
+ .group-field--gutter:has(.bl-grey),
358
+ .group-field--gutter:has(.bl-library){
359
+ border-left:none;
360
+ padding-left:0;
361
+ }
362
+ .render-fields:has(> .bl-scale-field),
363
+ .render-fields:has(> .field-type.bl-scale-field){
364
+ display:grid;
365
+ grid-template-columns:minmax(0,1fr) minmax(0,1fr);
366
+ gap:1rem;
367
+ align-items:start;
368
+ }
369
+ .render-fields:has(> .bl-scale-field) > .bl-section-heading,
370
+ .render-fields:has(> .field-type.bl-scale-field) > .bl-section-heading,
371
+ .render-fields:has(> .bl-scale-field) > :has(.bl-section-heading),
372
+ .render-fields:has(> .field-type.bl-scale-field) > :has(.bl-section-heading),
373
+ .render-fields:has(> .bl-scale-field) > .bl-library,
374
+ .render-fields:has(> .field-type.bl-scale-field) > .field-type.bl-library,
375
+ .render-fields:has(> .bl-scale-field) > :has(.bl-library),
376
+ .render-fields:has(> .field-type.bl-scale-field) > :has(.bl-library),
377
+ .render-fields:has(> .bl-scale-field) > :has(.bl-grey),
378
+ .render-fields:has(> .field-type.bl-scale-field) > :has(.bl-grey){
379
+ grid-column:1/-1;
380
+ }
381
+ @media (max-width:860px){
382
+ .render-fields:has(> .bl-scale-field),
383
+ .render-fields:has(> .field-type.bl-scale-field){
384
+ grid-template-columns:1fr;
385
+ }
386
+ }
387
+ `;
388
+ //#endregion
389
+ //#region src/admin/portal.tsx
390
+ /** Theme overlays must leave Payload's transformed/clipped admin tree. */
391
+ function ThemePortal({ children }) {
392
+ if (typeof document === "undefined") return null;
393
+ return createPortal(children, document.body);
394
+ }
395
+ //#endregion
352
396
  //#region src/admin/preview-mode.tsx
353
397
  const ATTR = "data-bl-theme-preview";
354
398
  const EVENT = "bl-theme-preview";
355
- function adminIsDark() {
356
- if (typeof document === "undefined") return false;
357
- const root = document.documentElement;
358
- return root.getAttribute("data-theme") === "dark" || root.getAttribute("data-theme-pref") === "dark" || root.classList.contains("dark");
359
- }
399
+ const PREVIEW_VARS = [
400
+ "--bl-preview-bg",
401
+ "--bl-preview-card",
402
+ "--bl-preview-border",
403
+ "--bl-preview-text",
404
+ "--bl-preview-muted",
405
+ "--bl-preview-input"
406
+ ];
407
+ /** Brand hexes do not affect grey-driven surfaces; derive still requires them. */
408
+ const PREVIEW_BRAND = {
409
+ primary: "#111111",
410
+ secondary: "#111111",
411
+ success: "#111111"
412
+ };
413
+ let currentMode = "light";
360
414
  function readMode() {
361
- if (typeof document === "undefined") return "light";
362
- const set = document.documentElement.getAttribute(ATTR);
363
- if (set === "dark" || set === "light") return set;
364
- return adminIsDark() ? "dark" : "light";
415
+ return currentMode;
365
416
  }
366
- function setThemePreviewMode(mode) {
417
+ /** Drop leftovers from the old html-wide preview so Payload chrome stays Payload. */
418
+ function clearDocumentPreview() {
367
419
  if (typeof document === "undefined") return;
368
- document.documentElement.setAttribute(ATTR, mode);
369
- window.dispatchEvent(new Event(EVENT));
420
+ document.documentElement.removeAttribute(ATTR);
421
+ for (const name of PREVIEW_VARS) document.documentElement.style.removeProperty(name);
422
+ }
423
+ function setThemePreviewMode(mode) {
424
+ currentMode = mode;
425
+ clearDocumentPreview();
426
+ if (typeof window !== "undefined") window.dispatchEvent(new Event(EVENT));
370
427
  }
371
428
  function useThemePreviewMode() {
372
- const [mode, setMode] = useState("light");
429
+ const [mode, setMode] = useState(readMode);
373
430
  useEffect(() => {
431
+ clearDocumentPreview();
374
432
  const sync = () => setMode(readMode());
375
433
  sync();
376
434
  window.addEventListener(EVENT, sync);
@@ -378,41 +436,64 @@ function useThemePreviewMode() {
378
436
  }, []);
379
437
  return mode;
380
438
  }
439
+ function previewVarsFor(grey, mode) {
440
+ const palette = mode === "dark" ? deriveDarkPalette({
441
+ ...PREVIEW_BRAND,
442
+ grey
443
+ }) : deriveLightPalette({
444
+ ...PREVIEW_BRAND,
445
+ grey
446
+ });
447
+ return {
448
+ "--bl-preview-bg": `hsl(${palette.background})`,
449
+ "--bl-preview-card": `hsl(${palette.card})`,
450
+ "--bl-preview-border": `hsl(${palette.border})`,
451
+ "--bl-preview-text": `hsl(${palette.foreground})`,
452
+ "--bl-preview-muted": `hsl(${palette["muted-foreground"]})`,
453
+ "--bl-preview-input": `hsl(${palette.input})`
454
+ };
455
+ }
456
+ function useGreyScale() {
457
+ return useFormFields(([fields]) => {
458
+ const value = fields["colors.greyScale"]?.value;
459
+ return value === "gray" || value === "zinc" || value === "neutral" || value === "stone" || value === "slate" ? value : "slate";
460
+ });
461
+ }
462
+ /**
463
+ * Light / Dark + the live grey family, applied on each Theme field canvas.
464
+ * Never writes to `documentElement` — nav, tabs, and Publish stay Payload.
465
+ */
466
+ function usePreviewCanvas() {
467
+ const mode = useThemePreviewMode();
468
+ return {
469
+ mode,
470
+ className: "bl-preview-canvas",
471
+ style: previewVarsFor(useGreyScale(), mode)
472
+ };
473
+ }
381
474
  function ThemePreviewToggle() {
382
475
  const mode = useThemePreviewMode();
383
476
  return /* @__PURE__ */ jsxs("div", {
384
477
  className: "bl-preview-toggle",
385
478
  role: "group",
386
- "aria-label": "Preview fills in",
387
- children: [/* @__PURE__ */ jsx("span", {
388
- className: "bl-preview-toggle__label",
389
- children: "Preview fills"
390
- }), /* @__PURE__ */ jsxs("div", {
391
- className: "bl-preview-toggle__pills",
392
- children: [/* @__PURE__ */ jsx("button", {
393
- type: "button",
394
- "aria-pressed": mode === "light",
395
- onClick: () => setThemePreviewMode("light"),
396
- children: "Light"
397
- }), /* @__PURE__ */ jsx("button", {
398
- type: "button",
399
- "aria-pressed": mode === "dark",
400
- onClick: () => setThemePreviewMode("dark"),
401
- children: "Dark"
402
- })]
479
+ "aria-label": "Preview theme in",
480
+ children: [/* @__PURE__ */ jsx("button", {
481
+ type: "button",
482
+ "data-on": mode === "light" ? "" : void 0,
483
+ onClick: () => setThemePreviewMode("light"),
484
+ children: "Light"
485
+ }), /* @__PURE__ */ jsx("button", {
486
+ type: "button",
487
+ "data-on": mode === "dark" ? "" : void 0,
488
+ onClick: () => setThemePreviewMode("dark"),
489
+ children: "Dark"
403
490
  })]
404
491
  });
405
492
  }
406
493
  const THEME_PREVIEW_TOGGLE_CSS = `
407
- .bl-preview-toggle{display:inline-flex;align-items:center;gap:.6rem}
408
- .bl-preview-toggle__label{font-size:12px;font-weight:600;color:#a1a1aa}
409
- .bl-preview-toggle__pills{display:inline-flex;padding:3px;border:1px solid #3f3f46;border-radius:10px;background:#18181b}
410
- .bl-preview-toggle__pills button{min-height:2rem;padding:.25rem .8rem;border:0;border-radius:7px;background:transparent;color:#d4d4d8;font:inherit;font-size:13px;font-weight:600;cursor:pointer}
411
- .bl-preview-toggle__pills button[aria-pressed=true]{background:#f4f4f5;color:#18181b;box-shadow:0 1px 2px rgba(0,0,0,.25)}
412
- html[data-theme=light] .bl-preview-toggle__label{color:#52525b}
413
- html[data-theme=light] .bl-preview-toggle__pills{border-color:#d4d4d8;background:#e4e4e7}
414
- html[data-theme=light] .bl-preview-toggle__pills button{color:#3f3f46}
415
- html[data-theme=light] .bl-preview-toggle__pills button[aria-pressed=true]{background:#fff;color:#18181b;box-shadow:0 1px 2px rgba(15,23,42,.12)}
494
+ .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)}
495
+ .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}
496
+ .bl-preview-toggle button[data-on]{background:var(--theme-text,#0f172a);color:var(--theme-bg,#fff)}
416
497
  `;
417
498
  //#endregion
418
499
  //#region src/admin/readability-list.tsx
@@ -450,6 +531,25 @@ function ReadabilityDetail({ item, target = 7 }) {
450
531
  ]
451
532
  });
452
533
  }
534
+ function ReadabilityList({ warnings, target = 7 }) {
535
+ const failing = warnings.filter((item) => !item.meets).length;
536
+ return /* @__PURE__ */ jsxs("section", {
537
+ className: "bl-readability",
538
+ children: [/* @__PURE__ */ jsxs("div", {
539
+ className: "bl-readability__head",
540
+ 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", {
541
+ "data-attention": failing ? "" : void 0,
542
+ children: failing === 0 ? "All look readable" : `${failing} may be hard to read`
543
+ })]
544
+ }), /* @__PURE__ */ jsx("ul", { children: warnings.map((item) => /* @__PURE__ */ jsxs("li", {
545
+ "data-failing": item.meets ? void 0 : "",
546
+ children: [/* @__PURE__ */ jsx("strong", { children: readabilityHeadline(item) }), /* @__PURE__ */ jsx(ReadabilityDetail, {
547
+ item,
548
+ target
549
+ })]
550
+ }, item.id)) })]
551
+ });
552
+ }
453
553
  const READABILITY_LIST_CSS = `
454
554
  .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)}
455
555
  .bl-readability__head{display:flex;justify-content:space-between;gap:1rem;align-items:flex-start}
@@ -474,31 +574,74 @@ const READABILITY_LIST_CSS = `
474
574
  `;
475
575
  //#endregion
476
576
  //#region src/admin/color-scale-card.tsx
577
+ const STATUS_COPY = {
578
+ success: {
579
+ title: "Keep success colors recognizable",
580
+ body: "Success states should stay in a green or teal family so users can quickly recognize positive outcomes and confirmations.",
581
+ drift: "This color may no longer read as a success state."
582
+ },
583
+ destructive: {
584
+ title: "Keep destructive colors recognizable",
585
+ body: "Destructive actions should stay in a red or crimson family so users can quickly recognize errors and dangerous actions.",
586
+ drift: "This color may no longer read as a destructive or error state."
587
+ }
588
+ };
589
+ function IncludeMark({ on }) {
590
+ return /* @__PURE__ */ jsx("span", {
591
+ className: "bl-scale-card__status",
592
+ "aria-hidden": "true",
593
+ children: /* @__PURE__ */ jsx("svg", {
594
+ viewBox: "0 0 16 16",
595
+ children: on ? /* @__PURE__ */ jsx("path", {
596
+ d: "M3.5 8.5 6.5 11.5 12.5 4.5",
597
+ fill: "none",
598
+ stroke: "currentColor",
599
+ strokeWidth: "2.4",
600
+ strokeLinecap: "round",
601
+ strokeLinejoin: "round"
602
+ }) : /* @__PURE__ */ jsx("path", {
603
+ d: "M4.5 4.5 11.5 11.5M11.5 4.5 4.5 11.5",
604
+ fill: "none",
605
+ stroke: "currentColor",
606
+ strokeWidth: "2.4",
607
+ strokeLinecap: "round"
608
+ })
609
+ })
610
+ });
611
+ }
477
612
  function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onDelete, readOnly, role }) {
478
- const label = automaticLabel(state.hex);
479
613
  const warning = fillLabelWarning(id ?? name.toLowerCase(), name, state.hex);
480
- const previewMode = useThemePreviewMode();
481
- const preview = warning ? previewMode === "dark" ? warning.dark : warning.light : null;
482
- const [open, setOpen] = useState(false);
614
+ const canvas = usePreviewCanvas();
615
+ const [attentionOpen, setAttentionOpen] = useState(false);
616
+ const [shakeStep, setShakeStep] = useState(null);
617
+ const [hexDraft, setHexDraft] = useState(state.hex);
483
618
  const titleId = useId();
484
- const hueWarning = role === "success" ? successHueWarning(state.hex) : role === "destructive" ? destructiveHueWarning(state.hex) : null;
485
619
  const included = new Set(includedShadeSteps(state));
486
- function setHex(hex) {
487
- onChange(updateColorScale(state, { hex }));
620
+ const hueDrift = role === "success" ? Boolean(successHueWarning(state.hex)) : role === "destructive" ? Boolean(destructiveHueWarning(state.hex)) : false;
621
+ useEffect(() => {
622
+ setHexDraft(state.hex);
623
+ }, [state.hex]);
624
+ function commitHex(hex) {
625
+ onChange(createColorScale(hex, state.sourceStep, state.include));
488
626
  }
489
- function setStep(step) {
490
- onChange(updateColorScale(state, { sourceStep: step }));
627
+ function commitStep(step) {
628
+ onChange(createColorScale(state.hex, step, state.include));
491
629
  }
492
630
  function toggleStep(step) {
493
- if (step === state.sourceStep) return;
631
+ if (step === state.sourceStep) {
632
+ setShakeStep(step);
633
+ window.setTimeout(() => setShakeStep((was) => was === step ? null : was), 180);
634
+ return;
635
+ }
494
636
  const next = new Set(included);
495
637
  if (next.has(step)) next.delete(step);
496
638
  else next.add(step);
497
639
  onChange(setColorScaleInclude(state, SHADE_STEPS.filter((item) => next.has(item))));
498
640
  }
499
641
  return /* @__PURE__ */ jsxs("article", {
500
- className: "bl-scale-card",
501
- "data-preview": previewMode,
642
+ className: `bl-scale-card ${canvas.className}`,
643
+ "data-preview": canvas.mode,
644
+ style: canvas.style,
502
645
  children: [
503
646
  /* @__PURE__ */ jsxs("header", {
504
647
  className: "bl-scale-card__head",
@@ -517,50 +660,28 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
517
660
  className: "bl-scale-card__badge",
518
661
  children: "Optional"
519
662
  }) : null,
520
- warning ? warning.meets ? /* @__PURE__ */ jsxs("span", {
521
- className: "bl-scale-card__status",
522
- "data-ok": "",
523
- title: `Text on ${name} looks readable`,
524
- children: [/* @__PURE__ */ jsx("span", {
525
- "aria-hidden": "true",
526
- children: "✓"
527
- }), /* @__PURE__ */ jsxs("span", {
528
- className: "bl-scale-card__sr",
529
- children: [
530
- "Text on ",
531
- name,
532
- " looks readable"
533
- ]
534
- })]
535
- }) : /* @__PURE__ */ jsx("button", {
663
+ warning && !warning.meets ? /* @__PURE__ */ jsx("button", {
536
664
  type: "button",
537
665
  className: "bl-scale-card__badge bl-scale-card__badge--attention",
538
666
  "aria-haspopup": "dialog",
539
- onClick: () => setOpen(true),
667
+ onClick: () => setAttentionOpen(true),
540
668
  children: "Needs attention"
541
669
  }) : null
542
670
  ]
543
671
  }), hint ? /* @__PURE__ */ jsx("p", {
544
672
  className: "bl-scale-card__hint",
545
673
  children: hint
546
- }) : null] }), /* @__PURE__ */ jsxs("div", {
547
- className: "bl-scale-card__head-actions",
548
- children: [/* @__PURE__ */ jsx("span", {
549
- className: "bl-scale-card__mark",
550
- style: { background: state.hex },
551
- "aria-hidden": "true"
552
- }), onDelete ? /* @__PURE__ */ jsx("button", {
553
- type: "button",
554
- className: "bl-scale-card__delete",
555
- disabled: readOnly,
556
- onClick: onDelete,
557
- children: "Delete"
558
- }) : null]
559
- })]
674
+ }) : null] }), onDelete ? /* @__PURE__ */ jsx("button", {
675
+ type: "button",
676
+ className: "bl-scale-card__delete",
677
+ disabled: readOnly,
678
+ onClick: onDelete,
679
+ children: "Delete"
680
+ }) : null]
560
681
  }),
561
682
  /* @__PURE__ */ jsxs("div", {
562
683
  className: "bl-scale-card__row",
563
- children: [/* @__PURE__ */ jsxs("label", {
684
+ children: [/* @__PURE__ */ jsxs("div", {
564
685
  className: "bl-scale-card__field",
565
686
  children: [/* @__PURE__ */ jsx("span", { children: "Brand hex" }), /* @__PURE__ */ jsxs("span", {
566
687
  className: "bl-scale-card__hex-wrap",
@@ -568,17 +689,32 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
568
689
  type: "color",
569
690
  "aria-label": `${name} picker`,
570
691
  disabled: readOnly,
571
- value: state.hex,
572
- onChange: (event) => setHex(event.target.value)
692
+ value: isThemeHex(state.hex) ? state.hex : "#000000",
693
+ onPointerDown: (event) => {
694
+ const input = event.currentTarget;
695
+ if (typeof input.showPicker !== "function") return;
696
+ event.preventDefault();
697
+ try {
698
+ input.showPicker();
699
+ } catch {
700
+ input.click();
701
+ }
702
+ },
703
+ onChange: (event) => commitHex(event.target.value)
573
704
  }), /* @__PURE__ */ jsx("input", {
574
705
  type: "text",
575
706
  spellCheck: false,
576
707
  autoComplete: "off",
577
708
  disabled: readOnly,
578
- value: state.hex,
709
+ "aria-label": `${name} hex`,
710
+ value: hexDraft,
579
711
  onChange: (event) => {
580
712
  const next = event.target.value.trim();
581
- if (next) setHex(next);
713
+ setHexDraft(next);
714
+ if (isThemeHex(next)) commitHex(next);
715
+ },
716
+ onBlur: () => {
717
+ if (!isThemeHex(hexDraft)) setHexDraft(state.hex);
582
718
  }
583
719
  })]
584
720
  })]
@@ -587,7 +723,7 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
587
723
  children: [/* @__PURE__ */ jsx("span", { children: "This color is" }), /* @__PURE__ */ jsx("select", {
588
724
  disabled: readOnly,
589
725
  value: String(state.sourceStep),
590
- onChange: (event) => setStep(Number(event.target.value)),
726
+ onChange: (event) => commitStep(Number(event.target.value)),
591
727
  children: SHADE_STEPS.map((step) => /* @__PURE__ */ jsx("option", {
592
728
  value: step,
593
729
  children: step
@@ -595,49 +731,29 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
595
731
  })]
596
732
  })]
597
733
  }),
598
- /* @__PURE__ */ jsxs("div", {
599
- className: "bl-scale-card__label-sample",
600
- children: [/* @__PURE__ */ jsx("span", {
601
- className: "bl-scale-card__swatch",
602
- style: {
603
- background: preview ? hslCss(preview.fill) : state.hex,
604
- color: preview ? hslCss(preview.label) : label ? hslCss(label) : void 0
605
- },
606
- children: "Aa"
607
- }), /* @__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." })]
608
- }),
609
- hueWarning ? /* @__PURE__ */ jsx("p", {
610
- className: "bl-scale-card__warn",
611
- children: hueWarning
612
- }) : null,
613
- state.stale ? /* @__PURE__ */ jsx("p", {
614
- className: "bl-scale-card__stale",
615
- children: "This scale is out of date. Regenerate to rebuild 50–950 around the hex."
734
+ role ? /* @__PURE__ */ jsxs("div", {
735
+ className: "bl-scale-card__family",
736
+ "data-role": role,
737
+ "data-drift": hueDrift ? "" : void 0,
738
+ children: [/* @__PURE__ */ jsx("div", {
739
+ className: "bl-scale-card__family-mark",
740
+ "aria-hidden": "true",
741
+ children: "!"
742
+ }), /* @__PURE__ */ jsxs("div", { children: [
743
+ /* @__PURE__ */ jsx("div", {
744
+ className: "bl-scale-card__family-title",
745
+ children: STATUS_COPY[role].title
746
+ }),
747
+ /* @__PURE__ */ jsx("p", { children: STATUS_COPY[role].body }),
748
+ hueDrift ? /* @__PURE__ */ jsx("p", {
749
+ className: "bl-scale-card__family-drift",
750
+ children: STATUS_COPY[role].drift
751
+ }) : null
752
+ ] })]
616
753
  }) : null,
617
- /* @__PURE__ */ jsx("button", {
618
- type: "button",
619
- className: "bl-scale-card__regen",
620
- disabled: readOnly || !state.stale,
621
- onClick: () => onChange(regenerateColorScale(state)),
622
- children: "Regenerate scale"
623
- }),
624
754
  /* @__PURE__ */ jsxs("div", {
625
755
  className: "bl-scale-card__scale-head",
626
- children: [/* @__PURE__ */ jsx("span", { children: "Generated scale" }), /* @__PURE__ */ jsxs("span", { children: [
627
- /* @__PURE__ */ jsx("button", {
628
- type: "button",
629
- disabled: readOnly,
630
- onClick: () => onChange(setColorScaleInclude(state, "all")),
631
- children: "Include all"
632
- }),
633
- " · ",
634
- /* @__PURE__ */ jsx("button", {
635
- type: "button",
636
- disabled: readOnly,
637
- onClick: () => onChange(setColorScaleInclude(state, "source")),
638
- children: "Source only"
639
- })
640
- ] })]
756
+ children: [/* @__PURE__ */ jsx("span", { children: "Generated scale" }), /* @__PURE__ */ jsxs("span", { children: [SHADE_STEPS.length, " shades"] })]
641
757
  }),
642
758
  /* @__PURE__ */ jsx("div", {
643
759
  className: "bl-scale-card__steps",
@@ -651,44 +767,22 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
651
767
  className: "bl-scale-card__step",
652
768
  "data-source": source ? "" : void 0,
653
769
  "data-excluded": on ? void 0 : "",
654
- disabled: readOnly || source,
655
- title: source ? `Source step ${step} cannot be excluded` : on ? `Exclude ${step} from page editors` : `Include ${step} for page editors`,
656
- onClick: () => toggleStep(step),
657
- children: [/* @__PURE__ */ jsx("span", {
658
- className: "bl-scale-card__chip",
659
- style: { background: hslCss(state.scale[step]) },
660
- children: source ? null : /* @__PURE__ */ jsx("span", {
661
- className: "bl-scale-card__intent",
662
- "data-intent": on ? "exclude" : "include",
663
- "aria-hidden": "true",
664
- children: on ? /* @__PURE__ */ jsxs("svg", {
665
- viewBox: "0 0 24 24",
666
- width: "16",
667
- height: "16",
668
- children: [/* @__PURE__ */ jsx("circle", {
669
- cx: "12",
670
- cy: "12",
671
- r: "8",
672
- fill: "none",
673
- stroke: "currentColor",
674
- strokeWidth: "2.4"
675
- }), /* @__PURE__ */ jsx("path", {
676
- d: "M7.2 16.8 16.8 7.2",
677
- stroke: "currentColor",
678
- strokeWidth: "2.4"
679
- })]
680
- }) : /* @__PURE__ */ jsx("svg", {
681
- viewBox: "0 0 24 24",
682
- width: "16",
683
- height: "16",
684
- children: /* @__PURE__ */ jsx("path", {
685
- d: "M12 6v12M6 12h12",
686
- fill: "none",
687
- stroke: "currentColor",
688
- strokeWidth: "2.6"
689
- })
690
- })
691
- })
770
+ "data-shake": shakeStep === step ? "" : void 0,
771
+ disabled: readOnly,
772
+ "aria-pressed": on,
773
+ "aria-label": source ? `Source step ${step} cannot be excluded` : on ? `Shade ${step}, available — click to exclude from the palette` : `Shade ${step}, excluded — click to include in the palette`,
774
+ title: source ? `Source step ${step} cannot be excluded` : on ? "Available to page editors — click to exclude" : "Excluded from page editors — click to include",
775
+ onClick: (event) => {
776
+ event.preventDefault();
777
+ event.stopPropagation();
778
+ toggleStep(step);
779
+ },
780
+ children: [/* @__PURE__ */ jsxs("span", {
781
+ className: "bl-scale-card__chip-wrap",
782
+ children: [/* @__PURE__ */ jsx("span", {
783
+ className: "bl-scale-card__chip",
784
+ style: { backgroundColor: hslCss(state.scale[step]) }
785
+ }), /* @__PURE__ */ jsx(IncludeMark, { on })]
692
786
  }), /* @__PURE__ */ jsx("span", {
693
787
  className: "bl-scale-card__step-label",
694
788
  children: step
@@ -696,13 +790,32 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
696
790
  }, step);
697
791
  })
698
792
  }),
699
- open && warning ? createPortal(/* @__PURE__ */ jsx("div", {
793
+ /* @__PURE__ */ jsxs("div", {
794
+ className: "bl-scale-card__editors",
795
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", { children: "Available to page editors" }), /* @__PURE__ */ jsxs("p", { children: [
796
+ included.size,
797
+ " of ",
798
+ SHADE_STEPS.length,
799
+ " shades available · Choose which shades editors can select when building pages."
800
+ ] })] }), /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("button", {
801
+ type: "button",
802
+ disabled: readOnly,
803
+ onClick: () => onChange(setColorScaleInclude(state, "all")),
804
+ children: "Include all"
805
+ }), /* @__PURE__ */ jsx("button", {
806
+ type: "button",
807
+ disabled: readOnly,
808
+ onClick: () => onChange(setColorScaleInclude(state, "source")),
809
+ children: "Source only"
810
+ })] })]
811
+ }),
812
+ attentionOpen && warning ? /* @__PURE__ */ jsx(ThemePortal, { children: /* @__PURE__ */ jsx("div", {
700
813
  className: "bl-scale-card__modal",
701
814
  role: "dialog",
702
815
  "aria-modal": "true",
703
816
  "aria-labelledby": titleId,
704
817
  onClick: (event) => {
705
- if (event.target === event.currentTarget) setOpen(false);
818
+ if (event.target === event.currentTarget) setAttentionOpen(false);
706
819
  },
707
820
  children: /* @__PURE__ */ jsxs("div", {
708
821
  className: "bl-scale-card__panel",
@@ -714,7 +827,7 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
714
827
  children: readabilityHeadline(warning)
715
828
  }), /* @__PURE__ */ jsx("button", {
716
829
  type: "button",
717
- onClick: () => setOpen(false),
830
+ onClick: () => setAttentionOpen(false),
718
831
  "aria-label": "Close",
719
832
  children: "×"
720
833
  })]
@@ -723,69 +836,90 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
723
836
  /* @__PURE__ */ jsx("button", {
724
837
  type: "button",
725
838
  className: "bl-scale-card__panel-done",
726
- onClick: () => setOpen(false),
839
+ onClick: () => setAttentionOpen(false),
727
840
  children: "Keep editing this color"
728
841
  })
729
842
  ]
730
843
  })
731
- }), document.body) : null
844
+ }) }) : null
732
845
  ]
733
846
  });
734
847
  }
735
848
  const COLOR_SCALE_CARD_CSS = `
736
- .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)}
849
+ .bl-scale-card{padding:calc(var(--base,16px) * .8);border:1px solid var(--bl-preview-border,var(--theme-elevation-150,#e2e8f0));border-radius:var(--style-radius-m,12px);background:var(--bl-preview-card,var(--theme-elevation-0,#fff));color:var(--bl-preview-text,inherit)}
737
850
  .bl-scale-card__head{display:flex;justify-content:space-between;gap:1rem;align-items:flex-start}
738
851
  .bl-scale-card__title-row{position:relative;display:flex;align-items:center;flex-wrap:wrap;gap:.5rem}
739
852
  .bl-scale-card__title{margin:0;font-size:1rem}
740
- .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)}
853
+ .bl-scale-card__badge{font:inherit;font-size:10px;text-transform:uppercase;letter-spacing:.04em;padding:.15rem .45rem;border-radius:999px;border:0;background:#e2e8f0;color:#0f172a}
741
854
  .bl-scale-card__badge--attention{background:#fff1f2;color:#be123c;cursor:pointer}
742
- .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}
743
- .bl-scale-card__status[data-ok]{background:#ecfdf5;color:#047857}
744
- .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}
745
- .bl-scale-card__hint{margin:.35rem 0 0;color:var(--theme-elevation-500,#64748b);font-size:12px}
746
- .bl-scale-card__head-actions{display:flex;align-items:center;gap:.5rem}
747
- .bl-scale-card__mark{display:block;width:2.5rem;height:2.5rem;border-radius:8px;border:1px solid rgba(15,23,42,.12);flex:none}
855
+ .bl-scale-card__hint{margin:.35rem 0 0;color:var(--bl-preview-muted,var(--theme-elevation-500,#64748b));font-size:12px}
748
856
  .bl-scale-card__delete{border:0;background:none;color:#be123c;font:inherit;font-size:13px;font-weight:600;cursor:pointer}
749
857
  .bl-scale-card__row{display:grid;grid-template-columns:1fr 8rem;gap:.75rem;margin-top:1rem}
750
- .bl-scale-card__field{display:flex;flex-direction:column;gap:.35rem;font-size:12px;font-weight:600;color:var(--theme-elevation-600,#475569)}
751
- .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)}
752
- .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}
753
- .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}
754
- .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}
755
- .bl-scale-card__label-sample{display:flex;gap:.75rem;align-items:center;margin-top:1rem}
756
- .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}
757
- .bl-scale-card__label-sample p{margin:0;font-size:12px;color:var(--theme-elevation-500,#64748b)}
758
- .bl-scale-card__warn,.bl-scale-card__stale{margin:.75rem 0 0;padding:.65rem .75rem;border-radius:8px;font-size:12px}
759
- .bl-scale-card__warn{border:1px solid #a7f3d0;background:#ecfdf5;color:#065f46}
760
- .bl-scale-card__stale{border:1px solid #fde68a;background:#fffbeb;color:#92400e}
761
- .bl-scale-card__regen{width:100%;margin-top:.85rem;min-height:2.25rem;border:0;border-radius:8px;background:#0f172a;color:#fff;font:inherit;font-weight:600;cursor:pointer}
762
- .bl-scale-card__regen:disabled{background:#1e293b;color:#f8fafc;opacity:1;cursor:not-allowed}
763
- .bl-scale-card__scale-head{display:flex;justify-content:space-between;margin:1rem 0 .4rem;font-size:12px;color:var(--theme-elevation-500,#64748b)}
764
- .bl-scale-card__scale-head button{border:0;background:none;padding:0;font:inherit;color:inherit;text-decoration:underline;cursor:pointer}
858
+ .bl-scale-card__field{display:flex;flex-direction:column;gap:.35rem;font-size:12px;font-weight:600;color:var(--bl-preview-muted,var(--theme-elevation-600,#475569))}
859
+ .bl-scale-card__hex-wrap{display:flex;border:1px solid var(--bl-preview-border,var(--theme-elevation-150,#e2e8f0));border-radius:8px;background:var(--bl-preview-card,var(--theme-input-bg,#fff))}
860
+ .bl-scale-card__hex-wrap input[type=color]{width:2.75rem;height:2.25rem;padding:0;border:0;border-right:1px solid var(--bl-preview-border,var(--theme-elevation-150,#e2e8f0));border-radius:8px 0 0 8px;background:transparent;cursor:pointer;color-scheme:light;appearance:none;-webkit-appearance:none}
861
+ .bl-scale-card__hex-wrap input[type=color]::-webkit-color-swatch-wrapper{padding:0}
862
+ .bl-scale-card__hex-wrap input[type=color]::-webkit-color-swatch{border:0;border-radius:0;width:100%;height:100%}
863
+ .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;border-radius:0 8px 8px 0}
864
+ .bl-scale-card__field select{min-height:2.25rem;border:1px solid var(--bl-preview-border,var(--theme-elevation-150,#e2e8f0));border-radius:8px;padding:.35rem .5rem;background:var(--bl-preview-card,var(--theme-input-bg,#fff));color:inherit}
865
+ .bl-scale-card__family{display:flex;gap:.75rem;align-items:flex-start;margin-top:1rem;padding:.75rem .85rem;border-radius:8px}
866
+ .bl-scale-card__family[data-role=success]{border:1px solid #a7f3d0;background:#ecfdf5;color:#065f46}
867
+ .bl-scale-card__family[data-role=destructive]{border:1px solid #fecdd3;background:#fff1f2;color:#9f1239}
868
+ .bl-scale-card__family[data-drift]{box-shadow:0 0 0 2px #fbbf24}
869
+ .bl-scale-card__family-mark{flex:none;width:1.25rem;height:1.25rem;border-radius:999px;display:grid;place-items:center;font-size:11px;font-weight:700}
870
+ .bl-scale-card__family[data-role=success] .bl-scale-card__family-mark{background:#d1fae5;color:#047857}
871
+ .bl-scale-card__family[data-role=destructive] .bl-scale-card__family-mark{background:#ffe4e6;color:#be123c}
872
+ .bl-scale-card__family-title{font-size:12px;font-weight:600}
873
+ .bl-scale-card__family p{margin:.25rem 0 0;font-size:11px;line-height:1.45;opacity:.85}
874
+ .bl-scale-card__family-drift{font-weight:600;opacity:1}
875
+ .bl-scale-card__scale-head{display:flex;justify-content:space-between;margin:1rem 0 .4rem;font-size:12px;color:var(--bl-preview-muted,var(--theme-elevation-500,#64748b))}
765
876
  .bl-scale-card__steps{display:grid;grid-template-columns:repeat(11,minmax(0,1fr));gap:.45rem 3px}
766
877
  .bl-scale-card__step{display:flex;flex-direction:column;align-items:center;gap:.3rem;border:0;padding:0;background:transparent;cursor:pointer}
767
- .bl-scale-card__chip{position:relative;display:block;width:100%;height:2.15rem;border-radius:6px;overflow:hidden}
768
- .bl-scale-card__intent{position:absolute;top:3px;right:3px;width:1.15rem;height:1.15rem;border-radius:999px;display:grid;place-items:center;opacity:0;color:#be123c;background:#fff;box-shadow:0 1px 3px rgba(15,23,42,.28);transition:opacity .15s ease,transform .15s ease;pointer-events:none}
769
- .bl-scale-card__intent[data-intent=include]{color:#047857}
770
- .bl-scale-card__step:hover:not(:disabled) .bl-scale-card__chip{transform:translateY(-1px)}
771
- .bl-scale-card__step:hover:not(:disabled) .bl-scale-card__intent{opacity:1}
878
+ .bl-scale-card__chip-wrap{position:relative;display:block;width:100%}
879
+ .bl-scale-card__chip{display:block;width:100%;height:2.15rem;border-radius:6px;overflow:hidden}
880
+ .bl-scale-card__status{position:absolute;top:3px;right:3px;width:16px;height:16px;border-radius:999px;display:grid;place-items:center;pointer-events:none;background:rgba(255,255,255,.94);color:#0f172a;box-shadow:0 1px 3px rgba(15,23,42,.18)}
881
+ .bl-scale-card__status svg{display:block;width:10px;height:10px}
772
882
  .bl-scale-card__step-label{font-size:10px;line-height:1;color:var(--theme-elevation-600,#475569)}
773
- .bl-scale-card__step[data-source] .bl-scale-card__chip{box-shadow:0 0 0 2px var(--theme-elevation-800,#0f172a)}
774
- .bl-scale-card__step[data-excluded]{opacity:.55}
883
+ .bl-scale-card__step[data-source] .bl-scale-card__chip{box-shadow:0 0 0 2px #0f172a}
884
+ .bl-scale-card__step[data-excluded] .bl-scale-card__chip{opacity:.28;background-image:linear-gradient(45deg,transparent 44%,rgba(15,23,42,.8) 45%,rgba(15,23,42,.8) 55%,transparent 56%)}
885
+ .bl-scale-card__step[data-excluded] .bl-scale-card__status{color:#991b1b;background:rgba(255,255,255,.96)}
886
+ .bl-scale-card__step[data-excluded] .bl-scale-card__step-label{opacity:.35}
887
+ @keyframes bl-scale-shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-3px)}75%{transform:translateX(3px)}}
888
+ .bl-scale-card__step[data-shake]{animation:bl-scale-shake .18s}
775
889
  .bl-scale-card__step:disabled{cursor:default}
776
- .bl-scale-card__modal{position:fixed;inset:0;z-index:100000;display:grid;place-items:center;padding:1rem;background:rgba(15,23,42,.45)}
777
- .bl-scale-card__panel{width:min(32rem,100%);padding:1.15rem 1.2rem;border-radius:16px;background:#fff;color:#0f172a}
890
+ .bl-scale-card__editors{display:flex;flex-wrap:wrap;justify-content:space-between;gap:.75rem;align-items:flex-end;margin-top:.75rem;padding:.65rem .75rem;border:1px solid var(--bl-preview-border,var(--theme-elevation-150,#e2e8f0));border-radius:8px;background:var(--bl-preview-bg,var(--theme-elevation-50,#f8fafc))}
891
+ .bl-scale-card__editors > div > div{font-size:12px;font-weight:600;color:var(--bl-preview-text,var(--theme-elevation-700,#334155))}
892
+ .bl-scale-card__editors p{margin:.2rem 0 0;font-size:11px;color:var(--bl-preview-muted,var(--theme-elevation-500,#64748b))}
893
+ .bl-scale-card__editors button{border:0;background:none;padding:0;margin-left:.75rem;font:inherit;font-size:12px;font-weight:600;color:var(--theme-success-500,#1587ba);text-decoration:underline;cursor:pointer}
894
+ .bl-scale-card__editors span{display:flex;gap:0}
895
+ .bl-scale-card__modal{position:fixed;inset:0;z-index:10000;display:grid;place-items:center;padding:1rem;background:rgba(15,23,42,.45)}
896
+ .bl-scale-card__panel{width:min(32rem,100%);padding:1.15rem 1.2rem;border-radius:16px;background:#fff;color:#0f172a;color-scheme:light}
778
897
  .bl-scale-card__panel-head{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem}
779
898
  .bl-scale-card__panel-head h4{margin:0;font-size:1rem}
780
899
  .bl-scale-card__panel-head button{border:0;background:none;font:inherit;font-size:1.25rem;line-height:1;color:#64748b;cursor:pointer}
781
900
  .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}
782
- ` + READABILITY_LIST_CSS;
901
+ .bl-scale-card[data-preview=dark] .bl-scale-card__step[data-source] .bl-scale-card__chip{box-shadow:0 0 0 2px var(--bl-preview-text,#fafafa)}
902
+ ` + READABILITY_LIST_CSS + THEME_COLORS_LAYOUT_CSS;
783
903
  //#endregion
784
904
  //#region src/admin/color-scale-field.tsx
785
905
  const SYSTEM_ROLES = new Set(Object.keys(SYSTEM_COLOR_COPY));
786
906
  /**
787
- * One system (or library row) color: hex, source step, regenerate, stale,
788
- * include/exclude, automatic label. Writes the group's stored fields.
907
+ * Payload's hasMany select often stores a count on the field path and the
908
+ * chosen values at `path.0`, `path.1`, same shape as an array field.
909
+ */
910
+ function includedStepsFromForm(path, value, getDataByPath) {
911
+ if (Array.isArray(value)) return value;
912
+ const stored = getDataByPath?.(path);
913
+ if (Array.isArray(stored)) return stored;
914
+ const count = typeof value === "number" ? value : typeof stored === "number" ? stored : 0;
915
+ if (count <= 0) return void 0;
916
+ const steps = Array.from({ length: count }, (_, index) => getDataByPath?.(`${path}.${index}`));
917
+ if (steps.every((step) => step == null)) return void 0;
918
+ return steps.filter((step) => step != null).map(String);
919
+ }
920
+ /**
921
+ * One system (or library row) color: hex, source step, include/exclude.
922
+ * Writes the group's stored fields. A committed hex or step rebuilds the scale.
789
923
  */
790
924
  const ColorScaleField = ({ field, path, readOnly }) => {
791
925
  const hex = useField({ path: `${path}.hex` });
@@ -794,15 +928,17 @@ const ColorScaleField = ({ field, path, readOnly }) => {
794
928
  const stale = useField({ path: `${path}.stale` });
795
929
  const include = useField({ path: `${path}.include` });
796
930
  const includedSteps = useField({ path: `${path}.includedSteps` });
931
+ const { getDataByPath } = useForm();
797
932
  const key = path.split(".").pop() ?? "";
798
933
  const copy = SYSTEM_ROLES.has(key) ? SYSTEM_COLOR_COPY[key] : void 0;
934
+ const stepList = includedStepsFromForm(`${path}.includedSteps`, includedSteps.value, getDataByPath);
799
935
  const state = stateFromColorDoc({
800
936
  hex: hex.value,
801
937
  sourceStep: sourceStep.value,
802
938
  scale: scale.value,
803
939
  stale: stale.value,
804
940
  include: include.value === "source" || include.value === "custom" || include.value === "all" ? include.value : "all",
805
- includedSteps: includedSteps.value
941
+ includedSteps: stepList
806
942
  });
807
943
  function write(next) {
808
944
  const doc = colorDocFromState(next);
@@ -811,7 +947,7 @@ const ColorScaleField = ({ field, path, readOnly }) => {
811
947
  scale.setValue(doc.scale);
812
948
  stale.setValue(doc.stale);
813
949
  include.setValue(doc.include);
814
- includedSteps.setValue(doc.includedSteps ?? (doc.include === "source" ? [doc.sourceStep] : [...SHADE_STEPS]));
950
+ includedSteps.setValue((doc.includedSteps ?? (doc.include === "source" ? [doc.sourceStep] : [...SHADE_STEPS])).map(String));
815
951
  }
816
952
  const label = copy?.label ?? (typeof field.label === "string" ? field.label : key);
817
953
  return /* @__PURE__ */ jsxs("div", {
@@ -836,17 +972,17 @@ const ColorScaleField = ({ field, path, readOnly }) => {
836
972
  });
837
973
  };
838
974
  //#endregion
839
- //#region src/theme/types.ts
840
- const SYSTEM_COLOR_KEYS = [
841
- "primary",
842
- "secondary",
843
- "accent",
844
- "highlight",
845
- "success",
846
- "destructive"
847
- ];
975
+ //#region src/theme/library.ts
976
+ /**
977
+ * Payload's array form state stores the row count (`0`, `2`, …), not the
978
+ * rows. A custom Field that treats that number as the list will throw
979
+ * `rows.filter is not a function` on an empty Theme library.
980
+ */
981
+ function asLibraryRows(value) {
982
+ return Array.isArray(value) ? value : [];
983
+ }
848
984
  function themeColorKeys(doc) {
849
- const custom = (Array.isArray(doc?.colors?.library) ? doc.colors.library : []).map((row) => row.key?.trim()).filter((key) => Boolean(key));
985
+ const custom = asLibraryRows(doc?.colors?.library).map((row) => row.key?.trim()).filter((key) => Boolean(key));
850
986
  return [...SYSTEM_COLOR_KEYS, ...custom];
851
987
  }
852
988
  /**
@@ -856,7 +992,7 @@ function themeColorKeys(doc) {
856
992
  * has `key`.
857
993
  */
858
994
  function deleteLibraryColor(doc, key, replacement) {
859
- const library = [...Array.isArray(doc.colors?.library) ? doc.colors.library : []];
995
+ const library = [...asLibraryRows(doc.colors?.library)];
860
996
  const index = library.findIndex((row) => row.key === key);
861
997
  if (index === -1) throw new Error(`No custom color named ${JSON.stringify(key)}`);
862
998
  library.splice(index, 1);
@@ -879,43 +1015,42 @@ function slugify(value) {
879
1015
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
880
1016
  }
881
1017
  /**
882
- * Custom colors: the same scale controls as system colors, plus add and
883
- * in-use delete (one replacement scale; unused rows delete immediately).
1018
+ * Custom colors: the same scale controls as system colors. A row that is
1019
+ * not referenced on a page deletes immediately. Rewriting in-use tokens
1020
+ * (`rewriteColorToken`) waits on a site usage lookup — Theme cannot see
1021
+ * page assignments by itself.
884
1022
  */
885
- const LibraryField = ({ path, readOnly }) => {
886
- const { value, setValue } = useField({ path });
887
- const rows = Array.isArray(value) ? value : [];
1023
+ const LibraryField = ({ field, path, readOnly }) => {
1024
+ const dataPath = typeof field.admin?.custom?.dataPath === "string" ? field.admin.custom.dataPath : path;
1025
+ const { value, setValue } = useField({
1026
+ path: dataPath,
1027
+ hasRows: true
1028
+ });
1029
+ const { getDataByPath } = useForm();
1030
+ const rows = asLibraryRows(value).length > 0 ? asLibraryRows(value) : asLibraryRows(getDataByPath?.(dataPath));
888
1031
  const [adding, setAdding] = useState(false);
889
1032
  const [name, setName] = useState("");
890
- const [hex, setHex] = useState("#E07A5F");
891
- const [pendingDelete, setPendingDelete] = useState(null);
892
- const [replacement, setReplacement] = useState("");
1033
+ const [hex, setHex] = useState("");
1034
+ const [sourceStep, setSourceStep] = useState(500);
893
1035
  function write(next) {
894
1036
  setValue(next);
895
1037
  }
896
1038
  function add() {
897
1039
  const key = slugify(name);
898
- if (!key || rows.some((row) => row.key === key)) return;
1040
+ if (!key || !isThemeHex(hex) || rows.some((row) => row.key === key)) return;
899
1041
  write([...rows, {
900
1042
  key,
901
1043
  label: name.trim(),
902
- ...colorDocFromState(createColorScale(hex, 500))
1044
+ ...colorDocFromState(createColorScale(hex, sourceStep))
903
1045
  }]);
904
1046
  setName("");
1047
+ setHex("");
1048
+ setSourceStep(500);
905
1049
  setAdding(false);
906
1050
  }
907
- function remove(row, replaceWith) {
908
- write(deleteLibraryColor({ colors: { library: rows } }, row.key ?? "", replaceWith).colors?.library ?? []);
909
- setPendingDelete(null);
910
- setReplacement("");
1051
+ function remove(row) {
1052
+ write(deleteLibraryColor({ colors: { library: rows } }, row.key ?? "").colors?.library ?? []);
911
1053
  }
912
- const replacements = [...SYSTEM_COLOR_KEYS.map((key) => ({
913
- value: key,
914
- label: key[0].toUpperCase() + key.slice(1)
915
- })), ...rows.filter((row) => row.key && row.key !== pendingDelete?.key).map((row) => ({
916
- value: row.key ?? "",
917
- label: row.label ?? row.key ?? ""
918
- }))];
919
1054
  return /* @__PURE__ */ jsxs("div", {
920
1055
  className: "field-type bl-library",
921
1056
  children: [
@@ -931,37 +1066,76 @@ const LibraryField = ({ path, readOnly }) => {
931
1066
  }),
932
1067
  /* @__PURE__ */ jsxs("div", {
933
1068
  className: "bl-library__head",
934
- 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", {
1069
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", { children: "Additional colors" }), /* @__PURE__ */ jsx("p", { children: "Add optional named colors for special sections, campaigns, or components." })] }), /* @__PURE__ */ jsx("button", {
935
1070
  type: "button",
936
1071
  disabled: readOnly,
937
- onClick: () => setAdding(true),
1072
+ onClick: (event) => {
1073
+ event.preventDefault();
1074
+ event.stopPropagation();
1075
+ setAdding(true);
1076
+ },
938
1077
  children: "Add color"
939
1078
  })]
940
1079
  }),
941
- adding ? /* @__PURE__ */ jsxs("div", {
942
- className: "bl-library__add",
943
- children: [
944
- /* @__PURE__ */ jsxs("label", { children: ["Name", /* @__PURE__ */ jsx("input", {
945
- value: name,
946
- onChange: (event) => setName(event.target.value)
947
- })] }),
948
- /* @__PURE__ */ jsxs("label", { children: ["Hex", /* @__PURE__ */ jsx("input", {
949
- value: hex,
950
- onChange: (event) => setHex(event.target.value)
951
- })] }),
952
- /* @__PURE__ */ jsx("button", {
953
- type: "button",
954
- onClick: () => setAdding(false),
955
- children: "Cancel"
956
- }),
957
- /* @__PURE__ */ jsx("button", {
958
- type: "button",
959
- disabled: !slugify(name),
960
- onClick: add,
961
- children: "Create color scale"
962
- })
963
- ]
964
- }) : null,
1080
+ adding ? /* @__PURE__ */ jsx(ThemePortal, { children: /* @__PURE__ */ jsx("div", {
1081
+ className: "bl-library__modal",
1082
+ role: "dialog",
1083
+ "aria-modal": "true",
1084
+ "aria-labelledby": "bl-library-add-title",
1085
+ onClick: (event) => {
1086
+ if (event.target === event.currentTarget) setAdding(false);
1087
+ },
1088
+ children: /* @__PURE__ */ jsxs("div", {
1089
+ className: "bl-library__panel",
1090
+ children: [
1091
+ /* @__PURE__ */ jsxs("div", {
1092
+ className: "bl-library__panel-head",
1093
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", {
1094
+ id: "bl-library-add-title",
1095
+ children: "New color"
1096
+ }), /* @__PURE__ */ jsx("p", { children: "Give it a useful name, enter your brand hex, then say which scale step that hex is." })] }), /* @__PURE__ */ jsx("button", {
1097
+ type: "button",
1098
+ onClick: () => setAdding(false),
1099
+ "aria-label": "Close",
1100
+ children: "×"
1101
+ })]
1102
+ }),
1103
+ /* @__PURE__ */ jsxs("label", { children: ["Name", /* @__PURE__ */ jsx("input", {
1104
+ value: name,
1105
+ placeholder: "e.g. Coral",
1106
+ onChange: (event) => setName(event.target.value)
1107
+ })] }),
1108
+ /* @__PURE__ */ jsxs("label", { children: ["Brand hex", /* @__PURE__ */ jsx("input", {
1109
+ value: hex,
1110
+ placeholder: "#DB0082",
1111
+ spellCheck: false,
1112
+ autoComplete: "off",
1113
+ onChange: (event) => setHex(event.target.value.trim())
1114
+ })] }),
1115
+ /* @__PURE__ */ jsxs("label", { children: ["This color is", /* @__PURE__ */ jsx("select", {
1116
+ value: String(sourceStep),
1117
+ onChange: (event) => setSourceStep(Number(event.target.value)),
1118
+ children: SHADE_STEPS.map((step) => /* @__PURE__ */ jsx("option", {
1119
+ value: step,
1120
+ children: step
1121
+ }, step))
1122
+ })] }),
1123
+ /* @__PURE__ */ jsxs("div", {
1124
+ className: "bl-library__actions",
1125
+ children: [/* @__PURE__ */ jsx("button", {
1126
+ type: "button",
1127
+ onClick: () => setAdding(false),
1128
+ children: "Cancel"
1129
+ }), /* @__PURE__ */ jsx("button", {
1130
+ type: "button",
1131
+ disabled: !slugify(name) || !isThemeHex(hex),
1132
+ onClick: add,
1133
+ children: "Create color scale"
1134
+ })]
1135
+ })
1136
+ ]
1137
+ })
1138
+ }) }) : null,
965
1139
  /* @__PURE__ */ jsx("div", {
966
1140
  className: "bl-library__list",
967
1141
  children: rows.map((row, index) => /* @__PURE__ */ jsx(ColorScaleCard, {
@@ -978,85 +1152,30 @@ const LibraryField = ({ path, readOnly }) => {
978
1152
  };
979
1153
  write(copy);
980
1154
  },
981
- onDelete: readOnly ? void 0 : () => setPendingDelete(row),
1155
+ onDelete: readOnly ? void 0 : () => remove(row),
982
1156
  readOnly
983
1157
  }, row.key ?? index))
984
- }),
985
- pendingDelete ? /* @__PURE__ */ jsx("div", {
986
- className: "bl-library__modal",
987
- role: "dialog",
988
- "aria-modal": "true",
989
- "aria-labelledby": "bl-library-delete-title",
990
- children: /* @__PURE__ */ jsxs("div", {
991
- className: "bl-library__panel",
992
- children: [
993
- /* @__PURE__ */ jsxs("h2", {
994
- id: "bl-library-delete-title",
995
- children: ["Delete ", pendingDelete.label ?? pendingDelete.key]
996
- }),
997
- /* @__PURE__ */ jsxs("p", { children: [
998
- "Unused custom colors delete immediately. If this scale is in use, pick one replacement — every",
999
- " ",
1000
- /* @__PURE__ */ jsxs("code", { children: [pendingDelete.key, "-400"] }),
1001
- " becomes ",
1002
- /* @__PURE__ */ jsxs("code", { children: [replacement || "…", "-400"] }),
1003
- "."
1004
- ] }),
1005
- /* @__PURE__ */ jsxs("label", { children: ["Replacement scale", /* @__PURE__ */ jsxs("select", {
1006
- value: replacement,
1007
- onChange: (event) => setReplacement(event.target.value),
1008
- children: [/* @__PURE__ */ jsx("option", {
1009
- value: "",
1010
- children: "Choose a scale…"
1011
- }), replacements.map((option) => /* @__PURE__ */ jsx("option", {
1012
- value: option.value,
1013
- children: option.label
1014
- }, option.value))]
1015
- })] }),
1016
- /* @__PURE__ */ jsxs("div", {
1017
- className: "bl-library__actions",
1018
- children: [
1019
- /* @__PURE__ */ jsx("button", {
1020
- type: "button",
1021
- onClick: () => setPendingDelete(null),
1022
- children: "Cancel"
1023
- }),
1024
- /* @__PURE__ */ jsx("button", {
1025
- type: "button",
1026
- onClick: () => remove(pendingDelete),
1027
- children: "Delete immediately"
1028
- }),
1029
- /* @__PURE__ */ jsx("button", {
1030
- type: "button",
1031
- disabled: !replacement,
1032
- onClick: () => remove(pendingDelete, replacement),
1033
- children: "Replace & delete"
1034
- })
1035
- ]
1036
- })
1037
- ]
1038
- })
1039
- }) : null
1158
+ })
1040
1159
  ]
1041
1160
  });
1042
1161
  };
1043
1162
  const CSS$3 = `
1044
1163
  .bl-library__head{display:flex;justify-content:space-between;gap:1rem;align-items:flex-start;margin-bottom:.85rem}
1045
1164
  .bl-library__head h2{margin:0;font-size:1.1rem}
1046
- .bl-library__head p{margin:.35rem 0 0;font-size:13px;color:var(--theme-elevation-500,#64748b)}
1047
- .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}
1048
- .bl-library__list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.85rem}
1049
- @media (max-width:860px){.bl-library__list{grid-template-columns:1fr}}
1050
- .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}
1051
- .bl-library__add label{display:flex;flex-direction:column;gap:.3rem;font-size:12px;font-weight:600}
1052
- .bl-library__add input{min-height:2.1rem;padding:.3rem .55rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:8px}
1053
- .bl-library__modal{position:fixed;inset:0;z-index:80;display:grid;place-items:center;padding:1rem;background:rgba(15,23,42,.45)}
1054
- .bl-library__panel{width:min(28rem,100%);padding:1.2rem;border-radius:16px;background:#fff;color:#0f172a}
1165
+ .bl-library__head p{margin:.35rem 0 0;font-size:13px;color:color-mix(in srgb,var(--theme-text,#f8fafc) 92%,transparent)}
1166
+ .bl-library__head 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);color:var(--theme-text,#0f172a);font:inherit;cursor:pointer}
1167
+ .bl-library__list{display:grid;gap:.85rem}
1168
+ .bl-library__modal{position:fixed;inset:0;z-index:10000;display:grid;place-items:center;padding:1rem;background:rgba(15,23,42,.45)}
1169
+ .bl-library__panel{width:min(28rem,100%);padding:1.2rem;border-radius:16px;background:#fff;color:#0f172a;color-scheme:light;box-shadow:0 24px 48px rgba(15,23,42,.22)}
1170
+ .bl-library__panel-head{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem}
1171
+ .bl-library__panel-head button{border:0;background:none;font:inherit;font-size:1.25rem;line-height:1;color:#94a3b8;cursor:pointer}
1055
1172
  .bl-library__panel h2{margin:0}
1056
1173
  .bl-library__panel p{margin:.5rem 0 0;color:#475569;font-size:14px}
1057
1174
  .bl-library__panel label{display:flex;flex-direction:column;gap:.4rem;margin-top:1rem;font-size:13px;font-weight:600}
1058
- .bl-library__panel select{min-height:2.25rem;border:1px solid #cbd5e1;border-radius:8px;padding:.35rem .5rem}
1175
+ .bl-library__panel input,.bl-library__panel select{min-height:2.25rem;border:1px solid #cbd5e1;border-radius:8px;padding:.35rem .5rem;background:#fff;color:#0f172a}
1059
1176
  .bl-library__actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:.5rem;margin-top:1rem}
1177
+ .bl-library__actions button:last-child{border:0;background:#0f172a;color:#fff;font-weight:600}
1178
+ .bl-library__actions button:last-child:disabled{opacity:.4;cursor:not-allowed}
1060
1179
  `;
1061
1180
  //#endregion
1062
1181
  //#region src/admin/typography-pairing.tsx
@@ -1126,23 +1245,23 @@ function TypographyPairing({ headingFamily, bodyFamily }) {
1126
1245
  });
1127
1246
  }
1128
1247
  const TYPOGRAPHY_PAIRING_CSS = `
1129
- .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)}
1248
+ .bl-font-role{padding:1.1rem 1.15rem;border:1px solid var(--bl-preview-border,var(--theme-elevation-150,#e2e8f0));border-radius:12px;background:var(--bl-preview-card,var(--theme-elevation-0,#fff));color:var(--bl-preview-text,inherit)}
1130
1249
  .bl-font-role__head{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem}
1131
1250
  .bl-font-role__head h3{margin:0;font-size:1rem}
1132
- .bl-font-role__head p{margin:.3rem 0 0;font-size:12px;color:var(--theme-elevation-500,#64748b)}
1133
- .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)}
1134
- .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)}
1135
- .bl-font-role__preview > span{display:block;margin-bottom:.4rem;font-size:11px;color:var(--theme-elevation-500,#64748b)}
1251
+ .bl-font-role__head p{margin:.3rem 0 0;font-size:12px;color:var(--bl-preview-muted,var(--theme-elevation-500,#64748b))}
1252
+ .bl-font-role__badge{font-size:10px;text-transform:uppercase;letter-spacing:.04em;padding:.15rem .45rem;border-radius:999px;background:#e2e8f0;color:#0f172a}
1253
+ .bl-font-role__preview{margin-top:1rem;padding:.85rem .9rem;border:1px solid var(--bl-preview-border,var(--theme-elevation-150,#e2e8f0));border-radius:12px;background:var(--bl-preview-bg,var(--theme-elevation-50,#f8fafc))}
1254
+ .bl-font-role__preview > span{display:block;margin-bottom:.4rem;font-size:11px;color:var(--bl-preview-muted,var(--theme-elevation-500,#64748b))}
1136
1255
  .bl-font-role__heading{margin:0;font-size:1.5rem;line-height:1.2;font-weight:600}
1137
- .bl-font-role__body{margin:0;font-size:1rem;line-height:1.6;color:var(--theme-elevation-600,#475569)}
1138
- .bl-pairing{padding:0;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:12px;background:var(--theme-elevation-0,#fff);overflow:hidden}
1139
- .bl-pairing > header{padding:1rem 1.15rem;border-bottom:1px solid var(--theme-elevation-150,#e2e8f0)}
1256
+ .bl-font-role__body{margin:0;font-size:1rem;line-height:1.6;color:var(--bl-preview-muted,var(--theme-elevation-600,#475569))}
1257
+ .bl-pairing{padding:0;border:1px solid var(--bl-preview-border,var(--theme-elevation-150,#e2e8f0));border-radius:12px;background:var(--bl-preview-card,var(--theme-elevation-0,#fff));color:var(--bl-preview-text,inherit);overflow:hidden}
1258
+ .bl-pairing > header{padding:1rem 1.15rem;border-bottom:1px solid var(--bl-preview-border,var(--theme-elevation-150,#e2e8f0))}
1140
1259
  .bl-pairing > header h2{margin:0;font-size:1rem}
1141
- .bl-pairing > header p{margin:.3rem 0 0;font-size:12px;color:var(--theme-elevation-500,#64748b)}
1142
- .bl-pairing__page{padding:1.35rem 1.4rem 1.5rem}
1260
+ .bl-pairing > header p{margin:.3rem 0 0;font-size:12px;color:var(--bl-preview-muted,var(--theme-elevation-500,#64748b))}
1261
+ .bl-pairing__page{padding:1.35rem 1.4rem 1.5rem;background:var(--bl-preview-bg,transparent)}
1143
1262
  .bl-pairing__eyebrow{margin:0;font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:#7c1fa2}
1144
1263
  .bl-pairing__title{margin:.5rem 0 0;font-size:2rem;line-height:1.1;font-weight:600}
1145
- .bl-pairing__copy{margin:.75rem 0 0;max-width:36rem;font-size:1rem;line-height:1.6;color:var(--theme-elevation-600,#475569)}
1264
+ .bl-pairing__copy{margin:.75rem 0 0;max-width:36rem;font-size:1rem;line-height:1.6;color:var(--bl-preview-muted,var(--theme-elevation-600,#475569))}
1146
1265
  .bl-pairing__actions{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:1.1rem}
1147
1266
  .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}
1148
1267
  .bl-pairing__btn--ghost{background:#fff;color:#0f172a;border:1px solid #cbd5e1}
@@ -1250,8 +1369,11 @@ const FontField = ({ field, path, readOnly }) => {
1250
1369
  }
1251
1370
  const activeId = active >= 0 && open ? `${id}-option-${active}` : void 0;
1252
1371
  const faceIds = faces ? ids : isFontId(value) ? [value] : [];
1372
+ const canvas = usePreviewCanvas();
1253
1373
  return /* @__PURE__ */ jsxs("div", {
1254
- className: "field-type bl-font-field",
1374
+ className: `field-type bl-font-field ${canvas.className}`,
1375
+ "data-preview": canvas.mode,
1376
+ style: canvas.style,
1255
1377
  children: [
1256
1378
  /* @__PURE__ */ jsx("style", {
1257
1379
  href: "bl-pairing",
@@ -1395,7 +1517,7 @@ const CSS$2 = `
1395
1517
  .bl-font-field__trigger:disabled{color:var(--theme-elevation-400,#94a3b8);cursor:not-allowed}
1396
1518
  .bl-font-field__selected-label{display:block;font-size:11px;color:var(--theme-elevation-500,#64748b)}
1397
1519
  .bl-font-field__selected-name{display:block;margin-top:.15rem;font-weight:600}
1398
- .bl-font-field__modal{position:fixed;inset:0;z-index:80;display:grid;place-items:center;padding:1rem;background:rgba(15,23,42,.45)}
1520
+ .bl-font-field__modal{position:fixed;inset:0;z-index:10000;display:grid;place-items:center;padding:1rem;background:rgba(15,23,42,.45)}
1399
1521
  .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}
1400
1522
  .bl-font-field__panel-head{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem}
1401
1523
  .bl-font-field__panel-head p{margin:0;font-size:12px;color:#64748b}
@@ -1431,8 +1553,11 @@ const PairingField = ({ field }) => {
1431
1553
  const bodyFont = findFont(body);
1432
1554
  const headingFont = findFont(headingId);
1433
1555
  const ids = [body, headingId].filter(Boolean);
1556
+ const canvas = usePreviewCanvas();
1434
1557
  return /* @__PURE__ */ jsxs("div", {
1435
- className: "field-type bl-pairing-field",
1558
+ className: `field-type bl-pairing-field ${canvas.className}`,
1559
+ "data-preview": canvas.mode,
1560
+ style: canvas.style,
1436
1561
  children: [
1437
1562
  /* @__PURE__ */ jsx("style", {
1438
1563
  href: "bl-pairing",
@@ -1534,18 +1659,18 @@ function AppearanceChoices({ values, onChange, readOnly }) {
1534
1659
  });
1535
1660
  }
1536
1661
  const APPEARANCE_CHOICES_CSS = `
1537
- .bl-appearance{display:flex;flex-direction:column;gap:1.25rem}
1662
+ .bl-appearance{display:flex;flex-direction:column;gap:1.25rem;color:var(--bl-preview-text,inherit)}
1538
1663
  .bl-appearance__intro h2{margin:0;font-size:1.1rem}
1539
- .bl-appearance__intro p{margin:.35rem 0 0;font-size:13px;color:var(--theme-elevation-500,#64748b);max-width:40rem}
1540
- .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)}
1664
+ .bl-appearance__intro p{margin:.35rem 0 0;font-size:13px;color:var(--bl-preview-muted,var(--theme-elevation-500,#64748b));max-width:40rem}
1665
+ .bl-appearance__family{margin:0;padding:1rem 1.1rem 1.15rem;border:1px solid var(--bl-preview-border,var(--theme-elevation-150,#e2e8f0));border-radius:12px;background:var(--bl-preview-card,var(--theme-elevation-0,#fff))}
1541
1666
  .bl-appearance__family legend{padding:0;margin-bottom:.85rem}
1542
1667
  .bl-appearance__family legend strong{display:block;font-size:14px}
1543
- .bl-appearance__family legend span{display:block;margin-top:.25rem;font-size:12px;color:var(--theme-elevation-500,#64748b);font-weight:400}
1668
+ .bl-appearance__family legend span{display:block;margin-top:.25rem;font-size:12px;color:var(--bl-preview-muted,var(--theme-elevation-500,#64748b));font-weight:400}
1544
1669
  .bl-appearance__choices{display:grid;gap:.6rem}
1545
1670
  .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))}
1546
1671
  .bl-appearance__choices[data-family=radius]{grid-template-columns:repeat(4,minmax(0,1fr))}
1547
- .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}
1548
- .bl-appearance__choice[data-selected]{box-shadow:inset 0 0 0 2px #0f172a;border-color:#0f172a}
1672
+ .bl-appearance__choice{display:flex;flex-direction:column;gap:.65rem;padding:.75rem;border:1px solid var(--bl-preview-border,var(--theme-elevation-150,#e2e8f0));border-radius:12px;background:var(--bl-preview-bg,var(--theme-elevation-0,#fff));cursor:pointer}
1673
+ .bl-appearance__choice[data-selected]{box-shadow:inset 0 0 0 2px var(--bl-preview-text,#0f172a);border-color:var(--bl-preview-text,#0f172a)}
1549
1674
  .bl-appearance__choice input{position:absolute;opacity:0;pointer-events:none}
1550
1675
  .bl-appearance__copy{display:flex;flex-direction:column;gap:.2rem}
1551
1676
  .bl-appearance__copy strong{display:flex;justify-content:space-between;align-items:center;gap:.4rem;font-size:14px}
@@ -1618,11 +1743,14 @@ const AppearanceField = ({ path, readOnly }) => {
1618
1743
  motion,
1619
1744
  density
1620
1745
  };
1746
+ const canvas = usePreviewCanvas();
1621
1747
  function onChange(name, value) {
1622
1748
  fields[name].setValue(value);
1623
1749
  }
1624
1750
  return /* @__PURE__ */ jsxs("div", {
1625
- className: "field-type bl-appearance-field",
1751
+ className: `field-type bl-appearance-field ${canvas.className}`,
1752
+ "data-preview": canvas.mode,
1753
+ style: canvas.style,
1626
1754
  children: [/* @__PURE__ */ jsx("style", {
1627
1755
  href: "bl-appearance",
1628
1756
  precedence: "default",
@@ -1651,53 +1779,63 @@ const SWATCHES = [
1651
1779
  "950"
1652
1780
  ];
1653
1781
  function GreyScaleChoices({ value, onChange, readOnly }) {
1782
+ const canvas = usePreviewCanvas();
1654
1783
  return /* @__PURE__ */ jsxs("section", {
1655
- className: "bl-grey",
1656
- children: [/* @__PURE__ */ jsxs("header", {
1657
- className: "bl-grey__head",
1658
- 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", {
1659
- className: "bl-grey__badge",
1660
- children: "Required"
1661
- })]
1662
- }), /* @__PURE__ */ jsx("div", {
1663
- className: "bl-grey__choices",
1664
- children: FAMILIES.map((id) => {
1665
- const copy = presetHints.greyScale[id];
1666
- const selected = value === id;
1667
- return /* @__PURE__ */ jsxs("label", {
1668
- className: "bl-grey__choice",
1669
- "data-selected": selected ? "" : void 0,
1670
- children: [
1671
- /* @__PURE__ */ jsx("input", {
1672
- type: "radio",
1673
- name: "greyScale",
1674
- value: id,
1675
- checked: selected,
1676
- disabled: readOnly,
1677
- onChange: () => onChange(id)
1678
- }),
1679
- /* @__PURE__ */ jsx("span", {
1680
- className: "bl-grey__ramp",
1681
- "aria-hidden": "true",
1682
- children: SWATCHES.map((step) => /* @__PURE__ */ jsx("i", { style: { background: `hsl(${GREY_SCALES[id][step]})` } }, step))
1683
- }),
1684
- /* @__PURE__ */ jsxs("strong", { children: [copy.label, selected ? /* @__PURE__ */ jsx("em", {
1685
- "aria-hidden": "true",
1686
- children: "✓"
1687
- }) : null] }),
1688
- /* @__PURE__ */ jsx("span", { children: copy.hint })
1689
- ]
1690
- }, id);
1784
+ className: `bl-grey ${canvas.className}`,
1785
+ "data-preview": canvas.mode,
1786
+ style: canvas.style,
1787
+ children: [
1788
+ /* @__PURE__ */ jsxs("div", {
1789
+ className: "bl-grey__section",
1790
+ children: [/* @__PURE__ */ jsx("h2", { children: "Neutrals" }), /* @__PURE__ */ jsx("p", { children: "Choose the gray family used for backgrounds, borders, cards, inputs, and muted text." })]
1791
+ }),
1792
+ /* @__PURE__ */ jsxs("header", {
1793
+ className: "bl-grey__head",
1794
+ 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. Readability for these system neutrals is handled automatically." })] }), /* @__PURE__ */ jsx("span", {
1795
+ className: "bl-grey__badge",
1796
+ children: "Required"
1797
+ })]
1798
+ }),
1799
+ /* @__PURE__ */ jsx("div", {
1800
+ className: "bl-grey__choices",
1801
+ children: FAMILIES.map((id) => {
1802
+ const copy = presetHints.greyScale[id];
1803
+ const selected = value === id;
1804
+ return /* @__PURE__ */ jsxs("label", {
1805
+ className: "bl-grey__choice",
1806
+ "data-selected": selected ? "" : void 0,
1807
+ children: [
1808
+ /* @__PURE__ */ jsx("input", {
1809
+ type: "radio",
1810
+ name: "greyScale",
1811
+ value: id,
1812
+ checked: selected,
1813
+ disabled: readOnly,
1814
+ onChange: () => onChange(id)
1815
+ }),
1816
+ /* @__PURE__ */ jsx("span", {
1817
+ className: "bl-grey__ramp",
1818
+ "aria-hidden": "true",
1819
+ children: SWATCHES.map((step) => /* @__PURE__ */ jsx("i", { style: { background: `hsl(${GREY_SCALES[id][step]})` } }, step))
1820
+ }),
1821
+ /* @__PURE__ */ jsxs("strong", { children: [copy.label, selected ? /* @__PURE__ */ jsx("em", {
1822
+ "aria-hidden": "true",
1823
+ children: "✓"
1824
+ }) : null] }),
1825
+ /* @__PURE__ */ jsx("span", { children: copy.hint })
1826
+ ]
1827
+ }, id);
1828
+ })
1691
1829
  })
1692
- })]
1830
+ ]
1693
1831
  });
1694
1832
  }
1695
1833
  /**
1696
1834
  * Visual picker over the five grey-scale presets. Replaces the stock select
1697
1835
  * on Colors; the stored value is still `colors.greyScale`.
1698
1836
  */
1699
- const GreyScaleField = ({ path, readOnly }) => {
1700
- const { value, setValue } = useField({ path });
1837
+ const GreyScaleField = ({ field, path, readOnly }) => {
1838
+ const { value, setValue } = useField({ path: typeof field.admin?.custom?.dataPath === "string" ? field.admin.custom.dataPath : path });
1701
1839
  return /* @__PURE__ */ jsxs("div", {
1702
1840
  className: "field-type bl-grey-field",
1703
1841
  children: [/* @__PURE__ */ jsx("style", {
@@ -1713,33 +1851,82 @@ const GreyScaleField = ({ path, readOnly }) => {
1713
1851
  };
1714
1852
  const GREY_SCALE_FIELD_CSS = `
1715
1853
  .bl-grey{margin:0 0 1.75rem}
1716
- .bl-grey__head{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;margin-bottom:.85rem}
1854
+ .bl-grey__section{margin:1.25rem 0 .85rem}
1855
+ .bl-grey__section h2{margin:0;font-size:1.1rem}
1856
+ .bl-grey__section p{margin:.35rem 0 0;max-width:40rem;font-size:13px;color:color-mix(in srgb,var(--theme-text,#f8fafc) 92%,transparent)}
1857
+ .bl-grey__head{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;margin:0;padding:1.1rem 1.15rem .85rem;border:1px solid var(--bl-preview-border,var(--theme-elevation-150,#e2e8f0));border-bottom:0;border-radius:12px 12px 0 0;background:var(--bl-preview-card,var(--theme-elevation-0,#fff));color:var(--bl-preview-text,inherit)}
1717
1858
  .bl-grey__head h2{margin:0;font-size:1.1rem}
1718
- .bl-grey__head p{margin:.35rem 0 0;max-width:40rem;font-size:13px;color:var(--theme-elevation-500,#64748b)}
1719
- .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)}
1720
- .bl-grey__choices{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:.6rem}
1721
- .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}
1722
- .bl-grey__choice[data-selected]{box-shadow:inset 0 0 0 2px #0f172a;border-color:#0f172a}
1859
+ .bl-grey__head p{margin:.35rem 0 0;max-width:40rem;font-size:13px;color:var(--bl-preview-muted,var(--theme-elevation-500,#64748b))}
1860
+ .bl-grey__badge{font:inherit;font-size:10px;text-transform:uppercase;letter-spacing:.04em;padding:.15rem .45rem;border-radius:999px;background:#e2e8f0;color:#0f172a}
1861
+ .bl-grey__choices{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:.6rem;padding:0 1.15rem 1.15rem;border:1px solid var(--bl-preview-border,var(--theme-elevation-150,#e2e8f0));border-top:0;border-radius:0 0 12px 12px;background:var(--bl-preview-card,var(--theme-elevation-0,#fff));color:var(--bl-preview-text,inherit)}
1862
+ .bl-grey__choice{display:flex;flex-direction:column;gap:.45rem;padding:.75rem;border:1px solid var(--bl-preview-border,var(--theme-elevation-150,#e2e8f0));border-radius:12px;background:var(--bl-preview-bg,var(--theme-elevation-0,#fff));color:inherit;cursor:pointer}
1863
+ .bl-grey__choice[data-selected]{box-shadow:inset 0 0 0 2px var(--bl-preview-text,#0f172a);border-color:var(--bl-preview-text,#0f172a)}
1723
1864
  .bl-grey__choice input{position:absolute;opacity:0;pointer-events:none}
1724
1865
  .bl-grey__ramp{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:3px}
1725
1866
  .bl-grey__ramp i{display:block;height:2rem;border-radius:6px}
1726
1867
  .bl-grey__choice strong{display:flex;justify-content:space-between;align-items:center;gap:.4rem;font-size:14px}
1727
1868
  .bl-grey__choice em{font-style:normal}
1728
- .bl-grey__choice > span{font-size:12px;color:var(--theme-elevation-500,#64748b)}
1729
- [data-bl-theme-preview=dark] .bl-grey__head p,[data-bl-theme-preview=dark] .bl-grey__choice > span{color:#94a3b8}
1730
- [data-bl-theme-preview=dark] .bl-grey__choice{background:#0f172a;border-color:#334155;color:#f8fafc}
1731
- [data-bl-theme-preview=dark] .bl-grey__choice[data-selected]{box-shadow:inset 0 0 0 2px #f8fafc;border-color:#f8fafc}
1732
- [data-bl-theme-preview=dark] .bl-grey__badge{background:#1e293b;color:#94a3b8}
1869
+ .bl-grey__choice > span{font-size:12px;color:var(--bl-preview-muted,var(--theme-elevation-500,#64748b))}
1733
1870
  @media (max-width:720px){.bl-grey__choices{grid-template-columns:1fr}}
1734
1871
  `;
1735
1872
  //#endregion
1736
1873
  //#region src/admin/contrast-report.tsx
1874
+ function hexAt(fields, path) {
1875
+ const value = fields[path]?.value;
1876
+ return typeof value === "string" ? value : "";
1877
+ }
1737
1878
  /**
1738
- * Contrast used to be a Colors-tab list. Readability now lives on each
1739
- * color card: Needs attention opens a popup. This field stays in the
1740
- * schema so `contrastTarget` is still a Theme option; it paints nothing.
1879
+ * Fill + automatic label only. Target 7:1, Light and Dark. No gray
1880
+ * ContrastStrip. Warnings do not block save.
1741
1881
  */
1742
- const ContrastReport = () => null;
1882
+ const ContrastReport = ({ field }) => {
1883
+ const target = Number((field.admin?.custom)?.target) || 7;
1884
+ const hexes = useFormFields(([fields]) => {
1885
+ const record = fields;
1886
+ const system = Object.fromEntries(SYSTEM_COLOR_KEYS.map((key) => [key, hexAt(record, `colors.brand.${key}.hex`)]));
1887
+ const library = record["colors.library"]?.value;
1888
+ return {
1889
+ system,
1890
+ custom: Array.isArray(library) ? library.flatMap((row, index) => {
1891
+ const hex = typeof row === "object" && row && "hex" in row ? String(row.hex ?? "") : "";
1892
+ const name = typeof row === "object" && row && "label" in row ? String(row.label ?? row.key ?? "Custom") : "Custom";
1893
+ return hex ? [{
1894
+ id: `library-${index}`,
1895
+ name,
1896
+ hex
1897
+ }] : [];
1898
+ }) : []
1899
+ };
1900
+ });
1901
+ const last = useRef(null);
1902
+ const complete = SYSTEM_COLOR_KEYS.every((key) => isThemeHex(hexes.system[key]));
1903
+ 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);
1904
+ const warnings = last.current ?? [];
1905
+ const stale = Boolean(last.current) && !complete;
1906
+ return /* @__PURE__ */ jsxs("div", {
1907
+ className: "field-type bl-contrast-report",
1908
+ children: [
1909
+ /* @__PURE__ */ jsx("style", {
1910
+ href: "bl-readability",
1911
+ precedence: "default",
1912
+ children: READABILITY_LIST_CSS
1913
+ }),
1914
+ /* @__PURE__ */ jsx("style", {
1915
+ href: "bl-theme-colors-layout",
1916
+ precedence: "default",
1917
+ children: THEME_COLORS_LAYOUT_CSS
1918
+ }),
1919
+ stale ? /* @__PURE__ */ jsx("p", {
1920
+ className: "bl-contrast-report__stale",
1921
+ children: "Showing the last complete audit while a colour is being typed."
1922
+ }) : null,
1923
+ warnings.length > 0 ? /* @__PURE__ */ jsx(ReadabilityList, {
1924
+ warnings,
1925
+ target
1926
+ }) : /* @__PURE__ */ jsx("p", { children: "Enter hex colours to measure the automatic label on each fill." })
1927
+ ]
1928
+ });
1929
+ };
1743
1930
  //#endregion
1744
1931
  //#region src/admin/section-heading.tsx
1745
1932
  /**
@@ -1756,6 +1943,11 @@ const SectionHeading = ({ field }) => {
1756
1943
  precedence: "default",
1757
1944
  children: CSS$1
1758
1945
  }),
1946
+ /* @__PURE__ */ jsx("style", {
1947
+ href: "bl-theme-colors-layout",
1948
+ precedence: "default",
1949
+ children: THEME_COLORS_LAYOUT_CSS
1950
+ }),
1759
1951
  /* @__PURE__ */ jsx("h2", { children: custom.title }),
1760
1952
  custom.hint ? /* @__PURE__ */ jsx("p", { children: custom.hint }) : null
1761
1953
  ]
@@ -1765,7 +1957,7 @@ const CSS$1 = `
1765
1957
  .bl-section-heading{margin:1.25rem 0 .65rem}
1766
1958
  .bl-section-heading:first-child{margin-top:0}
1767
1959
  .bl-section-heading h2{margin:0;font-size:1.1rem}
1768
- .bl-section-heading p{margin:.35rem 0 0;font-size:13px;color:var(--theme-elevation-500,#64748b);max-width:40rem}
1960
+ .bl-section-heading p{margin:.35rem 0 0;font-size:13px;color:color-mix(in srgb,var(--theme-text,#f8fafc) 92%,transparent);max-width:40rem}
1769
1961
  `;
1770
1962
  //#endregion
1771
1963
  //#region src/theme/changes.ts
@@ -1847,7 +2039,7 @@ const OTHER_CHILDREN = {
1847
2039
  };
1848
2040
  function PublishConfirm({ childLabel, changes, shorts, onKeepEditing, onPublish, busy }) {
1849
2041
  const remaining = shorts?.filter((item) => !item.meets) ?? [];
1850
- return /* @__PURE__ */ jsx("div", {
2042
+ return /* @__PURE__ */ jsx(ThemePortal, { children: /* @__PURE__ */ jsx("div", {
1851
2043
  className: "bl-publish",
1852
2044
  role: "dialog",
1853
2045
  "aria-modal": "true",
@@ -1895,10 +2087,12 @@ function PublishConfirm({ childLabel, changes, shorts, onKeepEditing, onPublish,
1895
2087
  className: "bl-publish__actions",
1896
2088
  children: [/* @__PURE__ */ jsx("button", {
1897
2089
  type: "button",
2090
+ className: "bl-publish__cancel",
1898
2091
  onClick: onKeepEditing,
1899
- children: "Keep editing"
2092
+ children: "Cancel"
1900
2093
  }), /* @__PURE__ */ jsx("button", {
1901
2094
  type: "button",
2095
+ className: "bl-publish__go",
1902
2096
  disabled: busy || changes.length === 0,
1903
2097
  onClick: onPublish,
1904
2098
  children: remaining.length > 0 ? "Publish anyway" : "Publish"
@@ -1906,45 +2100,35 @@ function PublishConfirm({ childLabel, changes, shorts, onKeepEditing, onPublish,
1906
2100
  })
1907
2101
  ]
1908
2102
  })
1909
- });
2103
+ }) });
1910
2104
  }
1911
2105
  const PUBLISH_CONFIRM_CSS = `
1912
- .bl-publish{position:fixed;inset:0;z-index:80;display:grid;place-items:center;padding:1rem;background:rgba(15,23,42,.45)}
1913
- .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)}
1914
- .bl-publish__panel h2{margin:0}
2106
+ .bl-publish{position:fixed;inset:0;z-index:10000;display:grid;place-items:center;padding:1rem;background:rgba(15,23,42,.45)}
2107
+ .bl-publish__panel{width:min(36rem,100%);padding:1.25rem 1.35rem;border-radius:16px;background:#fff;color:#0f172a;color-scheme:light;box-shadow:0 24px 60px rgba(15,23,42,.2)}
2108
+ .bl-publish__panel h2{margin:0;color:#0f172a}
1915
2109
  .bl-publish__panel p{margin:.5rem 0 0;color:#475569}
1916
- .bl-publish__empty{font-style:italic}
1917
- .bl-publish__panel > ul{margin:.75rem 0 0;padding-left:1.1rem}
1918
- .bl-publish__shorts{margin-top:1rem;padding:.85rem .9rem;border-radius:10px;background:#fffbeb}
2110
+ .bl-publish__empty{font-style:italic;color:#64748b}
2111
+ .bl-publish__panel > ul{margin:.75rem 0 0;padding-left:1.1rem;color:#0f172a}
2112
+ .bl-publish__shorts{margin-top:1rem;padding:.85rem .9rem;border-radius:10px;background:#fffbeb;color:#0f172a}
1919
2113
  .bl-publish__shorts strong{display:block}
1920
2114
  .bl-publish__shorts ul{margin:.65rem 0 0;padding:0;list-style:none}
1921
- .bl-publish__shorts li{display:flex;gap:.65rem;align-items:center;margin-top:.45rem;font-size:13px}
2115
+ .bl-publish__shorts li{display:flex;gap:.65rem;align-items:center;margin-top:.45rem;font-size:13px;color:#0f172a}
1922
2116
  .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}
1923
- .bl-publish__shorts p{margin:.65rem 0 0;font-size:13px}
2117
+ .bl-publish__shorts p{margin:.65rem 0 0;font-size:13px;color:#475569}
1924
2118
  .bl-publish__actions{display:flex;justify-content:flex-end;gap:.6rem;margin-top:1.1rem}
1925
- .bl-publish__actions button{min-height:2.25rem;padding:.4rem .9rem;border-radius:8px;border:1px solid #cbd5e1;background:#fff;font:inherit;cursor:pointer}
1926
- .bl-publish__actions button:last-child{border:0;background:#0f172a;color:#fff;font-weight:600}
2119
+ .bl-publish__actions button{min-height:2.25rem;padding:.4rem .9rem;border-radius:8px;font:inherit;cursor:pointer}
2120
+ .bl-publish__cancel{border:1px solid #cbd5e1;background:#fff;color:#0f172a}
2121
+ .bl-publish__go{border:0;background:#0f172a;color:#fff;font-weight:600}
1927
2122
  .bl-publish__actions button:disabled{opacity:.45;cursor:not-allowed}
1928
2123
  `;
1929
2124
  //#endregion
1930
- //#region src/admin/theme-tab.ts
1931
- const THEME_TAB_LABELS = {
2125
+ //#region src/admin/publish-child.tsx
2126
+ const LABELS = {
1932
2127
  colors: "Colors",
1933
2128
  typography: "Typography",
1934
2129
  appearance: "Appearance",
1935
2130
  identity: "Identity"
1936
2131
  };
1937
- /**
1938
- * The selected Payload field tab on Theme. Defaults to Colors when the
1939
- * tabs have not painted yet (SSR, first paint).
1940
- */
1941
- function readActiveThemeTab(root = typeof document === "undefined" ? null : document) {
1942
- if (!root) return "colors";
1943
- const text = (root.querySelector(".tabs-field__tab-button[aria-selected=\"true\"]") ?? root.querySelector(".tabs-field__tab-button--active") ?? root.querySelector("[role=\"tab\"][aria-selected=\"true\"]"))?.textContent?.replace(/\s+/g, " ").trim();
1944
- return THEME_CHILDREN.find((id) => THEME_TAB_LABELS[id] === text) ?? "colors";
1945
- }
1946
- //#endregion
1947
- //#region src/admin/publish-child.tsx
1948
2132
  function asColor(value) {
1949
2133
  if (value && typeof value === "object") return value;
1950
2134
  if (typeof value === "string") return { hex: value };
@@ -1956,7 +2140,7 @@ function formDoc(fields) {
1956
2140
  hex: fields[`colors.brand.${key}.hex`]?.value,
1957
2141
  sourceStep: fields[`colors.brand.${key}.sourceStep`]?.value
1958
2142
  }])),
1959
- library: libraryRows(fields["colors.library"]?.value),
2143
+ library: Array.isArray(fields["colors.library"]?.value) ? fields["colors.library"]?.value : [],
1960
2144
  greyScale: fields["colors.greyScale"]?.value
1961
2145
  },
1962
2146
  typography: {
@@ -1980,7 +2164,7 @@ function publishedDoc(fields) {
1980
2164
  }
1981
2165
  function shortsFrom(doc) {
1982
2166
  const brand = doc.colors?.brand ?? {};
1983
- const custom = libraryRows(doc.colors?.library);
2167
+ const custom = doc.colors?.library ?? [];
1984
2168
  return [...SYSTEM_COLOR_KEYS.flatMap((key) => {
1985
2169
  const hex = asColor(brand[key])?.hex;
1986
2170
  const warning = typeof hex === "string" ? fillLabelWarning(key, SYSTEM_COLOR_COPY[key].label, hex) : null;
@@ -1991,12 +2175,13 @@ function shortsFrom(doc) {
1991
2175
  })];
1992
2176
  }
1993
2177
  /**
1994
- * Publish for the open Theme tab. Lives in Payload's document Save
1995
- * slot (`ThemeSaveButton`) so the header matches a Pages document.
2178
+ * Per-child Publish. Lives in the document Save slot so Payload's
2179
+ * `--doc-controls-height` row is the action row, not an empty gap.
1996
2180
  * Sets `publishChild` then submits; `beforeChange` writes only that slice.
2181
+ * On Colors, remaining automatic-label shorts open in this modal only.
1997
2182
  */
1998
- function ThemePublishControls({ child }) {
1999
- const label = THEME_TAB_LABELS[child];
2183
+ function ThemePublishActions({ child }) {
2184
+ const label = LABELS[child];
2000
2185
  const { submit } = useForm();
2001
2186
  const publishChild = useField({ path: "publishChild" });
2002
2187
  const snapshot = useFormFields(([fields]) => {
@@ -2031,9 +2216,9 @@ function ThemePublishControls({ child }) {
2031
2216
  /* @__PURE__ */ jsx("style", {
2032
2217
  href: "bl-publish-child",
2033
2218
  precedence: "default",
2034
- children: `${THEME_PREVIEW_TOGGLE_CSS}.bl-publish-child{display:flex;align-items:center;gap:.75rem}.bl-publish-child__button{min-height:2.25rem;padding:.4rem 1rem;border:0;border-radius:8px;background:#a80063;color:#fff;font:inherit;font-weight:600;cursor:pointer}`
2219
+ children: `${THEME_PREVIEW_TOGGLE_CSS}.bl-publish-child{display:flex;align-items:center;justify-content:flex-end;gap:.5rem}.bl-publish-child__button{min-height:2.25rem;padding:.4rem 1rem;border:0;border-radius:8px;background:var(--theme-text,#0f172a);color:var(--theme-bg,#fff);font:inherit;font-weight:600;cursor:pointer}`
2035
2220
  }),
2036
- child === "colors" ? /* @__PURE__ */ jsx(ThemePreviewToggle, {}) : null,
2221
+ child !== "identity" ? /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(ThemePreviewToggle, {}) }) : null,
2037
2222
  /* @__PURE__ */ jsxs("button", {
2038
2223
  type: "button",
2039
2224
  className: "bl-publish-child__button",
@@ -2053,17 +2238,47 @@ function ThemePublishControls({ child }) {
2053
2238
  ]
2054
2239
  });
2055
2240
  }
2056
- /** @deprecated Theme publish is the document Save button */
2241
+ /** @deprecated Alias — Theme pages use ThemePublishActions; tabbed Save used this name. */
2242
+ const ThemePublishControls = ThemePublishActions;
2057
2243
  const PublishChild = ({ field }) => {
2058
2244
  const child = (field.admin?.custom)?.child;
2059
- return /* @__PURE__ */ jsx(ThemePublishControls, { child: isThemeChild(child) ? child : "colors" });
2245
+ if (!isThemeChild(child)) return null;
2246
+ return /* @__PURE__ */ jsx(ThemePublishActions, { child });
2060
2247
  };
2061
2248
  //#endregion
2249
+ //#region src/admin/theme-tab.ts
2250
+ const THEME_TAB_LABELS = {
2251
+ colors: "Colors",
2252
+ typography: "Typography",
2253
+ appearance: "Appearance",
2254
+ identity: "Identity"
2255
+ };
2256
+ /**
2257
+ * The selected Payload field tab on Theme. Defaults to Colors when the
2258
+ * tabs have not painted yet (SSR, first paint).
2259
+ */
2260
+ function readActiveThemeTab(root = typeof document === "undefined" ? null : document) {
2261
+ if (!root) return "colors";
2262
+ const text = (root.querySelector(".tabs-field__tab-button[aria-selected=\"true\"]") ?? root.querySelector(".tabs-field__tab-button--active") ?? root.querySelector("[role=\"tab\"][aria-selected=\"true\"]"))?.textContent?.replace(/\s+/g, " ").trim();
2263
+ return THEME_CHILDREN.find((id) => THEME_TAB_LABELS[id] === text) ?? "colors";
2264
+ }
2265
+ //#endregion
2062
2266
  //#region src/admin/save-button.tsx
2063
2267
  /**
2064
- * Payload's document Save slot the same control row as Pages'
2065
- * "Publish changes". Theme has no drafts; this publishes only the
2066
- * open field tab. The tab is read after mount so SSR matches.
2268
+ * Light/Dark preview for Colors, Typography, and Appearance. Lives in
2269
+ * `beforeDocumentControls` so Payload's Save stays in the Save slot.
2270
+ * Save writes that page's slice onto the stored Theme row.
2271
+ */
2272
+ function ThemeDocumentControls() {
2273
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("style", {
2274
+ href: "bl-doc-controls",
2275
+ precedence: "default",
2276
+ children: THEME_PREVIEW_TOGGLE_CSS
2277
+ }), /* @__PURE__ */ jsx(ThemePreviewToggle, {})] });
2278
+ }
2279
+ /**
2280
+ * Import-map Save slot used by the older single-Global Theme. Theme pages
2281
+ * keep Payload's Save and put Light/Dark in `ThemeDocumentControls`.
2067
2282
  */
2068
2283
  function ThemeSaveButton() {
2069
2284
  const [child, setChild] = useState("colors");
@@ -2086,8 +2301,10 @@ function ThemeSaveButton() {
2086
2301
  }, []);
2087
2302
  return /* @__PURE__ */ jsx(ThemePublishControls, { child });
2088
2303
  }
2089
- /** Import-map alias — Theme still points `SaveButton` at this name. */
2090
- const HiddenSaveButton = ThemeSaveButton;
2304
+ /** @deprecated Import map alias — ThemeDocumentControls is the Save-adjacent slot. */
2305
+ function HiddenSaveButton() {
2306
+ return /* @__PURE__ */ jsx(ThemeDocumentControls, {});
2307
+ }
2091
2308
  //#endregion
2092
2309
  //#region src/admin/identity-fallback.tsx
2093
2310
  /**
@@ -2127,6 +2344,68 @@ const CSS = `
2127
2344
  .bl-identity-fallback figcaption{margin-top:.5rem;font-size:.8rem;color:var(--theme-elevation-600,#475569)}
2128
2345
  `;
2129
2346
  //#endregion
2130
- export { AppearanceField, ColorField, ColorScaleField, ContrastReport, FontField, GreyScaleField, HiddenSaveButton, IdentityFallback, LibraryField, PairingField, PublishChild, SectionHeading, ThemeSaveButton };
2347
+ //#region src/admin/look-field.tsx
2348
+ /**
2349
+ * Loads the Theme global and offers page-editor looks or included steps.
2350
+ * A custom scale published on Colors appears here on the next load.
2351
+ */
2352
+ const LookField = ({ field, path, readOnly }) => {
2353
+ const { value, setValue, showError, errorMessage } = useField({ path });
2354
+ const { config } = useConfig();
2355
+ const mode = (field.admin?.custom)?.mode ?? "look";
2356
+ const [options, setOptions] = useState(mode === "token" ? pageEditorTokens(null) : pageEditorLooks(null));
2357
+ const id = useId();
2358
+ const required = Boolean(field.required);
2359
+ useEffect(() => {
2360
+ const serverURL = config.serverURL ?? "";
2361
+ const api = config.routes?.api ?? "/api";
2362
+ const controller = new AbortController();
2363
+ fetch(`${serverURL}${api}/globals/theme?depth=0`, {
2364
+ signal: controller.signal,
2365
+ credentials: "include"
2366
+ }).then((response) => response.ok ? response.json() : null).then((doc) => {
2367
+ setOptions(mode === "token" ? pageEditorTokens(doc) : pageEditorLooks(doc));
2368
+ }).catch(() => {});
2369
+ return () => controller.abort();
2370
+ }, [
2371
+ config.routes?.api,
2372
+ config.serverURL,
2373
+ mode
2374
+ ]);
2375
+ return /* @__PURE__ */ jsxs("div", {
2376
+ className: "field-type",
2377
+ children: [
2378
+ /* @__PURE__ */ jsx(FieldLabel, {
2379
+ htmlFor: id,
2380
+ label: field.label,
2381
+ required
2382
+ }),
2383
+ /* @__PURE__ */ jsxs("select", {
2384
+ id,
2385
+ disabled: readOnly,
2386
+ value: value ?? "",
2387
+ onChange: (event) => setValue(event.target.value || void 0),
2388
+ children: [/* @__PURE__ */ jsx("option", {
2389
+ value: "",
2390
+ children: required ? "Select…" : "None"
2391
+ }), options.map((option) => /* @__PURE__ */ jsx("option", {
2392
+ value: option.value,
2393
+ children: option.label
2394
+ }, option.value))]
2395
+ }),
2396
+ /* @__PURE__ */ jsx(FieldDescription, {
2397
+ description: field.admin?.description,
2398
+ path
2399
+ }),
2400
+ /* @__PURE__ */ jsx(FieldError, {
2401
+ path,
2402
+ showError,
2403
+ message: errorMessage
2404
+ })
2405
+ ]
2406
+ });
2407
+ };
2408
+ //#endregion
2409
+ export { AppearanceField, ColorField, ColorScaleField, ContrastReport, FontField, GreyScaleField, HiddenSaveButton, IdentityFallback, LibraryField, LookField, PairingField, PublishChild, SectionHeading, ThemeDocumentControls, ThemeSaveButton };
2131
2410
 
2132
2411
  //# sourceMappingURL=admin.mjs.map