@bison-lab/payload-core 3.9.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,9 +1,10 @@
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
+ import { createPortal } from "react-dom";
7
8
  import { findFont, fontFaceCss, isFontId } from "@bison-lab/fonts";
8
9
  //#region src/admin/color-field.tsx
9
10
  const HEX_ERROR = "Enter a six-digit hex colour like #1e3a5f";
@@ -40,7 +41,7 @@ const ColorField = ({ field, path, readOnly }) => {
40
41
  /* @__PURE__ */ jsx("style", {
41
42
  href: "bl-color-field",
42
43
  precedence: "default",
43
- children: CSS$3
44
+ children: CSS$4
44
45
  }),
45
46
  /* @__PURE__ */ jsx(FieldLabel, {
46
47
  htmlFor: hexId,
@@ -99,7 +100,7 @@ const ColorField = ({ field, path, readOnly }) => {
99
100
  ]
100
101
  });
101
102
  };
102
- const CSS$3 = `
103
+ const CSS$4 = `
103
104
  .bl-color-field__control{display:flex;align-items:center;gap:calc(var(--base) * .4)}
104
105
  .bl-color-field__swatch{flex:none;width:calc(var(--base) * 1.6);height:calc(var(--base) * 1.6);border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:var(--theme-elevation-50)}
105
106
  .bl-color-field__picker{flex:none;width:calc(var(--base) * 2);height:calc(var(--base) * 2);padding:0;border:1px solid var(--theme-elevation-150);border-radius:var(--style-radius-s);background:transparent;cursor:pointer}
@@ -246,12 +247,12 @@ const SYSTEM_COLOR_COPY = {
246
247
  },
247
248
  success: {
248
249
  label: "Success",
249
- hint: "Theme chrome for confirmations. Not offered as a page-editor fill.",
250
+ hint: "Success messages and confirmations",
250
251
  group: "status"
251
252
  },
252
253
  destructive: {
253
254
  label: "Destructive",
254
- hint: "Theme chrome for errors and dangerous actions. Not offered as a page-editor fill.",
255
+ hint: "Errors, dangerous actions, and destructive confirmations",
255
256
  group: "status"
256
257
  }
257
258
  };
@@ -312,10 +313,6 @@ function destructiveHueWarning(hex) {
312
313
  if (h <= 30 || h >= 330) return null;
313
314
  return "Keep destructive colors recognizable — stay in a red family.";
314
315
  }
315
- function automaticLabel(hex) {
316
- if (!isThemeHex(hex)) return null;
317
- return contrastForeground(hexToHSL(hex));
318
- }
319
316
  function hslCss(value) {
320
317
  return `hsl(${value})`;
321
318
  }
@@ -348,33 +345,138 @@ function readabilityRatioLine(sample, target = 7) {
348
345
  const READABILITY_FIX = "Darken or lighten the hex, then regenerate the scale. The site picks light or dark type for you.";
349
346
  const READABILITY_PUBLISH_NOTE = "You can still publish. To improve a color, darken or lighten it and regenerate.";
350
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
351
396
  //#region src/admin/preview-mode.tsx
352
397
  const ATTR = "data-bl-theme-preview";
353
398
  const EVENT = "bl-theme-preview";
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";
354
414
  function readMode() {
355
- if (typeof document === "undefined") return "light";
356
- return document.documentElement.getAttribute(ATTR) === "dark" ? "dark" : "light";
415
+ return currentMode;
357
416
  }
358
- function setThemePreviewMode(mode) {
417
+ /** Drop leftovers from the old html-wide preview so Payload chrome stays Payload. */
418
+ function clearDocumentPreview() {
359
419
  if (typeof document === "undefined") return;
360
- document.documentElement.setAttribute(ATTR, mode);
361
- 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));
362
427
  }
363
428
  function useThemePreviewMode() {
364
429
  const [mode, setMode] = useState(readMode);
365
430
  useEffect(() => {
431
+ clearDocumentPreview();
366
432
  const sync = () => setMode(readMode());
433
+ sync();
367
434
  window.addEventListener(EVENT, sync);
368
435
  return () => window.removeEventListener(EVENT, sync);
369
436
  }, []);
370
437
  return mode;
371
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
+ }
372
474
  function ThemePreviewToggle() {
373
475
  const mode = useThemePreviewMode();
374
476
  return /* @__PURE__ */ jsxs("div", {
375
477
  className: "bl-preview-toggle",
376
478
  role: "group",
377
- "aria-label": "Preview colors in",
479
+ "aria-label": "Preview theme in",
378
480
  children: [/* @__PURE__ */ jsx("button", {
379
481
  type: "button",
380
482
  "data-on": mode === "light" ? "" : void 0,
@@ -391,7 +493,7 @@ function ThemePreviewToggle() {
391
493
  const THEME_PREVIEW_TOGGLE_CSS = `
392
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)}
393
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}
394
- .bl-preview-toggle button[data-on]{background:var(--theme-elevation-800,#0f172a);color:#fff}
496
+ .bl-preview-toggle button[data-on]{background:var(--theme-text,#0f172a);color:var(--theme-bg,#fff)}
395
497
  `;
396
498
  //#endregion
397
499
  //#region src/admin/readability-list.tsx
@@ -431,7 +533,7 @@ function ReadabilityDetail({ item, target = 7 }) {
431
533
  }
432
534
  function ReadabilityList({ warnings, target = 7 }) {
433
535
  const failing = warnings.filter((item) => !item.meets).length;
434
- return /* @__PURE__ */ jsxs("aside", {
536
+ return /* @__PURE__ */ jsxs("section", {
435
537
  className: "bl-readability",
436
538
  children: [/* @__PURE__ */ jsxs("div", {
437
539
  className: "bl-readability__head",
@@ -472,31 +574,74 @@ const READABILITY_LIST_CSS = `
472
574
  `;
473
575
  //#endregion
474
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
+ }
475
612
  function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onDelete, readOnly, role }) {
476
- const label = automaticLabel(state.hex);
477
613
  const warning = fillLabelWarning(id ?? name.toLowerCase(), name, state.hex);
478
- const previewMode = useThemePreviewMode();
479
- const preview = warning ? previewMode === "dark" ? warning.dark : warning.light : null;
480
- const [open, setOpen] = useState(false);
614
+ const canvas = usePreviewCanvas();
615
+ const [attentionOpen, setAttentionOpen] = useState(false);
616
+ const [shakeStep, setShakeStep] = useState(null);
617
+ const [hexDraft, setHexDraft] = useState(state.hex);
481
618
  const titleId = useId();
482
- const hueWarning = role === "success" ? successHueWarning(state.hex) : role === "destructive" ? destructiveHueWarning(state.hex) : null;
483
619
  const included = new Set(includedShadeSteps(state));
484
- function setHex(hex) {
485
- 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));
486
626
  }
487
- function setStep(step) {
488
- onChange(updateColorScale(state, { sourceStep: step }));
627
+ function commitStep(step) {
628
+ onChange(createColorScale(state.hex, step, state.include));
489
629
  }
490
630
  function toggleStep(step) {
491
- 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
+ }
492
636
  const next = new Set(included);
493
637
  if (next.has(step)) next.delete(step);
494
638
  else next.add(step);
495
639
  onChange(setColorScaleInclude(state, SHADE_STEPS.filter((item) => next.has(item))));
496
640
  }
497
641
  return /* @__PURE__ */ jsxs("article", {
498
- className: "bl-scale-card",
499
- "data-preview": previewMode,
642
+ className: `bl-scale-card ${canvas.className}`,
643
+ "data-preview": canvas.mode,
644
+ style: canvas.style,
500
645
  children: [
501
646
  /* @__PURE__ */ jsxs("header", {
502
647
  className: "bl-scale-card__head",
@@ -515,26 +660,11 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
515
660
  className: "bl-scale-card__badge",
516
661
  children: "Optional"
517
662
  }) : null,
518
- warning ? warning.meets ? /* @__PURE__ */ jsxs("span", {
519
- className: "bl-scale-card__status",
520
- "data-ok": "",
521
- title: `Text on ${name} looks readable`,
522
- children: [/* @__PURE__ */ jsx("span", {
523
- "aria-hidden": "true",
524
- children: "✓"
525
- }), /* @__PURE__ */ jsxs("span", {
526
- className: "bl-scale-card__sr",
527
- children: [
528
- "Text on ",
529
- name,
530
- " looks readable"
531
- ]
532
- })]
533
- }) : /* @__PURE__ */ jsx("button", {
663
+ warning && !warning.meets ? /* @__PURE__ */ jsx("button", {
534
664
  type: "button",
535
665
  className: "bl-scale-card__badge bl-scale-card__badge--attention",
536
666
  "aria-haspopup": "dialog",
537
- onClick: () => setOpen(true),
667
+ onClick: () => setAttentionOpen(true),
538
668
  children: "Needs attention"
539
669
  }) : null
540
670
  ]
@@ -551,7 +681,7 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
551
681
  }),
552
682
  /* @__PURE__ */ jsxs("div", {
553
683
  className: "bl-scale-card__row",
554
- children: [/* @__PURE__ */ jsxs("label", {
684
+ children: [/* @__PURE__ */ jsxs("div", {
555
685
  className: "bl-scale-card__field",
556
686
  children: [/* @__PURE__ */ jsx("span", { children: "Brand hex" }), /* @__PURE__ */ jsxs("span", {
557
687
  className: "bl-scale-card__hex-wrap",
@@ -559,17 +689,32 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
559
689
  type: "color",
560
690
  "aria-label": `${name} picker`,
561
691
  disabled: readOnly,
562
- value: state.hex,
563
- 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)
564
704
  }), /* @__PURE__ */ jsx("input", {
565
705
  type: "text",
566
706
  spellCheck: false,
567
707
  autoComplete: "off",
568
708
  disabled: readOnly,
569
- value: state.hex,
709
+ "aria-label": `${name} hex`,
710
+ value: hexDraft,
570
711
  onChange: (event) => {
571
712
  const next = event.target.value.trim();
572
- if (next) setHex(next);
713
+ setHexDraft(next);
714
+ if (isThemeHex(next)) commitHex(next);
715
+ },
716
+ onBlur: () => {
717
+ if (!isThemeHex(hexDraft)) setHexDraft(state.hex);
573
718
  }
574
719
  })]
575
720
  })]
@@ -578,7 +723,7 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
578
723
  children: [/* @__PURE__ */ jsx("span", { children: "This color is" }), /* @__PURE__ */ jsx("select", {
579
724
  disabled: readOnly,
580
725
  value: String(state.sourceStep),
581
- onChange: (event) => setStep(Number(event.target.value)),
726
+ onChange: (event) => commitStep(Number(event.target.value)),
582
727
  children: SHADE_STEPS.map((step) => /* @__PURE__ */ jsx("option", {
583
728
  value: step,
584
729
  children: step
@@ -586,49 +731,29 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
586
731
  })]
587
732
  })]
588
733
  }),
589
- /* @__PURE__ */ jsxs("div", {
590
- className: "bl-scale-card__label-sample",
591
- children: [/* @__PURE__ */ jsx("span", {
592
- className: "bl-scale-card__swatch",
593
- style: {
594
- background: preview ? hslCss(preview.fill) : state.hex,
595
- color: preview ? hslCss(preview.label) : label ? hslCss(label) : void 0
596
- },
597
- children: "Aa"
598
- }), /* @__PURE__ */ jsx("p", { children: previewMode === "dark" ? "Previewing this fill in Dark. A dark-enough hex lightens; otherwise it stays the typed color." : "The site chooses light or dark type on this fill. Darken or lighten the hex and regenerate to flip it. There is no ink picker." })]
599
- }),
600
- hueWarning ? /* @__PURE__ */ jsx("p", {
601
- className: "bl-scale-card__warn",
602
- children: hueWarning
603
- }) : null,
604
- state.stale ? /* @__PURE__ */ jsx("p", {
605
- className: "bl-scale-card__stale",
606
- children: "This scale is out of date. Regenerate to rebuild 50–950 around the hex."
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
+ ] })]
607
753
  }) : null,
608
- /* @__PURE__ */ jsx("button", {
609
- type: "button",
610
- className: "bl-scale-card__regen",
611
- disabled: readOnly || !state.stale,
612
- onClick: () => onChange(regenerateColorScale(state)),
613
- children: "Regenerate scale"
614
- }),
615
754
  /* @__PURE__ */ jsxs("div", {
616
755
  className: "bl-scale-card__scale-head",
617
- children: [/* @__PURE__ */ jsx("span", { children: "Generated scale" }), /* @__PURE__ */ jsxs("span", { children: [
618
- /* @__PURE__ */ jsx("button", {
619
- type: "button",
620
- disabled: readOnly,
621
- onClick: () => onChange(setColorScaleInclude(state, "all")),
622
- children: "Include all"
623
- }),
624
- " · ",
625
- /* @__PURE__ */ jsx("button", {
626
- type: "button",
627
- disabled: readOnly,
628
- onClick: () => onChange(setColorScaleInclude(state, "source")),
629
- children: "Source only"
630
- })
631
- ] })]
756
+ children: [/* @__PURE__ */ jsx("span", { children: "Generated scale" }), /* @__PURE__ */ jsxs("span", { children: [SHADE_STEPS.length, " shades"] })]
632
757
  }),
633
758
  /* @__PURE__ */ jsx("div", {
634
759
  className: "bl-scale-card__steps",
@@ -642,12 +767,22 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
642
767
  className: "bl-scale-card__step",
643
768
  "data-source": source ? "" : void 0,
644
769
  "data-excluded": on ? void 0 : "",
645
- disabled: readOnly || source,
646
- title: source ? `Source step ${step} cannot be excluded` : on ? `Exclude ${step} from page editors` : `Include ${step} for page editors`,
647
- onClick: () => toggleStep(step),
648
- children: [/* @__PURE__ */ jsx("span", {
649
- className: "bl-scale-card__chip",
650
- style: { background: hslCss(state.scale[step]) }
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 })]
651
786
  }), /* @__PURE__ */ jsx("span", {
652
787
  className: "bl-scale-card__step-label",
653
788
  children: step
@@ -655,13 +790,32 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
655
790
  }, step);
656
791
  })
657
792
  }),
658
- open && warning ? /* @__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", {
659
813
  className: "bl-scale-card__modal",
660
814
  role: "dialog",
661
815
  "aria-modal": "true",
662
816
  "aria-labelledby": titleId,
663
817
  onClick: (event) => {
664
- if (event.target === event.currentTarget) setOpen(false);
818
+ if (event.target === event.currentTarget) setAttentionOpen(false);
665
819
  },
666
820
  children: /* @__PURE__ */ jsxs("div", {
667
821
  className: "bl-scale-card__panel",
@@ -673,7 +827,7 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
673
827
  children: readabilityHeadline(warning)
674
828
  }), /* @__PURE__ */ jsx("button", {
675
829
  type: "button",
676
- onClick: () => setOpen(false),
830
+ onClick: () => setAttentionOpen(false),
677
831
  "aria-label": "Close",
678
832
  children: "×"
679
833
  })]
@@ -682,67 +836,90 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
682
836
  /* @__PURE__ */ jsx("button", {
683
837
  type: "button",
684
838
  className: "bl-scale-card__panel-done",
685
- onClick: () => setOpen(false),
839
+ onClick: () => setAttentionOpen(false),
686
840
  children: "Keep editing this color"
687
841
  })
688
842
  ]
689
843
  })
690
- }) : null
844
+ }) }) : null
691
845
  ]
692
846
  });
693
847
  }
694
848
  const COLOR_SCALE_CARD_CSS = `
695
- .bl-scale-card{padding:calc(var(--base,16px) * .8);border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:var(--style-radius-m,12px);background:var(--theme-elevation-0,#fff)}
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)}
696
850
  .bl-scale-card__head{display:flex;justify-content:space-between;gap:1rem;align-items:flex-start}
697
851
  .bl-scale-card__title-row{position:relative;display:flex;align-items:center;flex-wrap:wrap;gap:.5rem}
698
852
  .bl-scale-card__title{margin:0;font-size:1rem}
699
- .bl-scale-card__badge{font:inherit;font-size:10px;text-transform:uppercase;letter-spacing:.04em;padding:.15rem .45rem;border-radius:999px;border:0;background:var(--theme-elevation-100,#f1f5f9);color:var(--theme-elevation-500,#64748b)}
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}
700
854
  .bl-scale-card__badge--attention{background:#fff1f2;color:#be123c;cursor:pointer}
701
- .bl-scale-card__status{display:inline-grid;place-items:center;width:1.4rem;height:1.4rem;border-radius:999px;border:0;font:inherit;font-weight:700;line-height:1}
702
- .bl-scale-card__status[data-ok]{background:#ecfdf5;color:#047857}
703
- .bl-scale-card__sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
704
- .bl-scale-card__hint{margin:.35rem 0 0;color:var(--theme-elevation-500,#64748b);font-size:12px}
855
+ .bl-scale-card__hint{margin:.35rem 0 0;color:var(--bl-preview-muted,var(--theme-elevation-500,#64748b));font-size:12px}
705
856
  .bl-scale-card__delete{border:0;background:none;color:#be123c;font:inherit;font-size:13px;font-weight:600;cursor:pointer}
706
857
  .bl-scale-card__row{display:grid;grid-template-columns:1fr 8rem;gap:.75rem;margin-top:1rem}
707
- .bl-scale-card__field{display:flex;flex-direction:column;gap:.35rem;font-size:12px;font-weight:600;color:var(--theme-elevation-600,#475569)}
708
- .bl-scale-card__hex-wrap{display:flex;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:8px;overflow:hidden;background:var(--theme-input-bg,#fff)}
709
- .bl-scale-card__hex-wrap input[type=color]{width:2.75rem;height:2.25rem;padding:0;border:0;border-right:1px solid var(--theme-elevation-150,#e2e8f0);background:transparent;cursor:pointer}
710
- .bl-scale-card__hex-wrap input[type=text]{flex:1;min-width:0;border:0;padding:.4rem .7rem;font-family:var(--font-mono,ui-monospace,monospace);font-size:13px;background:transparent;color:inherit}
711
- .bl-scale-card__field select{min-height:2.25rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:8px;padding:.35rem .5rem;background:var(--theme-input-bg,#fff);color:inherit}
712
- .bl-scale-card__label-sample{display:flex;gap:.75rem;align-items:center;margin-top:1rem}
713
- .bl-scale-card__swatch{flex:none;display:inline-flex;align-items:center;justify-content:center;width:3rem;height:2.25rem;border-radius:8px;border:1px solid var(--theme-elevation-150,#e2e8f0);font-weight:700}
714
- .bl-scale-card__label-sample p{margin:0;font-size:12px;color:var(--theme-elevation-500,#64748b)}
715
- .bl-scale-card__warn,.bl-scale-card__stale{margin:.75rem 0 0;padding:.65rem .75rem;border-radius:8px;font-size:12px}
716
- .bl-scale-card__warn{border:1px solid #a7f3d0;background:#ecfdf5;color:#065f46}
717
- .bl-scale-card__stale{border:1px solid #fde68a;background:#fffbeb;color:#92400e}
718
- .bl-scale-card__regen{width:100%;margin-top:.85rem;min-height:2.25rem;border:0;border-radius:8px;background:var(--theme-elevation-800,#0f172a);color:#fff;font:inherit;font-weight:600;cursor:pointer}
719
- .bl-scale-card__regen:disabled{opacity:.45;cursor:not-allowed}
720
- .bl-scale-card__scale-head{display:flex;justify-content:space-between;margin:1rem 0 .4rem;font-size:12px;color:var(--theme-elevation-500,#64748b)}
721
- .bl-scale-card__scale-head button{border:0;background:none;padding:0;font:inherit;color:inherit;text-decoration:underline;cursor:pointer}
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))}
722
876
  .bl-scale-card__steps{display:grid;grid-template-columns:repeat(11,minmax(0,1fr));gap:.45rem 3px}
723
877
  .bl-scale-card__step{display:flex;flex-direction:column;align-items:center;gap:.3rem;border:0;padding:0;background:transparent;cursor:pointer}
724
- .bl-scale-card__chip{display:block;width:100%;height:2.15rem;border-radius:6px}
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}
725
882
  .bl-scale-card__step-label{font-size:10px;line-height:1;color:var(--theme-elevation-600,#475569)}
726
883
  .bl-scale-card__step[data-source] .bl-scale-card__chip{box-shadow:0 0 0 2px #0f172a}
727
- .bl-scale-card__step[data-excluded]{opacity:.35}
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}
728
889
  .bl-scale-card__step:disabled{cursor:default}
729
- .bl-scale-card__modal{position:fixed;inset:0;z-index:80;display:grid;place-items:center;padding:1rem;background:rgba(15,23,42,.45)}
730
- .bl-scale-card__panel{width:min(32rem,100%);padding:1.15rem 1.2rem;border-radius:16px;background:#fff;color:#0f172a}
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}
731
897
  .bl-scale-card__panel-head{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem}
732
898
  .bl-scale-card__panel-head h4{margin:0;font-size:1rem}
733
899
  .bl-scale-card__panel-head button{border:0;background:none;font:inherit;font-size:1.25rem;line-height:1;color:#64748b;cursor:pointer}
734
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}
735
- [data-bl-theme-preview=dark] .bl-scale-card{background:#0f172a;border-color:#334155;color:#f8fafc}
736
- [data-bl-theme-preview=dark] .bl-scale-card__hint,[data-bl-theme-preview=dark] .bl-scale-card__label-sample p,[data-bl-theme-preview=dark] .bl-scale-card__scale-head,[data-bl-theme-preview=dark] .bl-scale-card__step-label,[data-bl-theme-preview=dark] .bl-scale-card__field{color:#94a3b8}
737
- [data-bl-theme-preview=dark] .bl-scale-card__hex-wrap,[data-bl-theme-preview=dark] .bl-scale-card__field select{background:#1e293b;border-color:#334155;color:#f8fafc}
738
- [data-bl-theme-preview=dark] .bl-scale-card__step[data-source] .bl-scale-card__chip{box-shadow:0 0 0 2px #f8fafc}
739
- ` + READABILITY_LIST_CSS;
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;
740
903
  //#endregion
741
904
  //#region src/admin/color-scale-field.tsx
742
905
  const SYSTEM_ROLES = new Set(Object.keys(SYSTEM_COLOR_COPY));
743
906
  /**
744
- * One system (or library row) color: hex, source step, regenerate, stale,
745
- * 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.
746
923
  */
747
924
  const ColorScaleField = ({ field, path, readOnly }) => {
748
925
  const hex = useField({ path: `${path}.hex` });
@@ -751,15 +928,17 @@ const ColorScaleField = ({ field, path, readOnly }) => {
751
928
  const stale = useField({ path: `${path}.stale` });
752
929
  const include = useField({ path: `${path}.include` });
753
930
  const includedSteps = useField({ path: `${path}.includedSteps` });
931
+ const { getDataByPath } = useForm();
754
932
  const key = path.split(".").pop() ?? "";
755
933
  const copy = SYSTEM_ROLES.has(key) ? SYSTEM_COLOR_COPY[key] : void 0;
934
+ const stepList = includedStepsFromForm(`${path}.includedSteps`, includedSteps.value, getDataByPath);
756
935
  const state = stateFromColorDoc({
757
936
  hex: hex.value,
758
937
  sourceStep: sourceStep.value,
759
938
  scale: scale.value,
760
939
  stale: stale.value,
761
940
  include: include.value === "source" || include.value === "custom" || include.value === "all" ? include.value : "all",
762
- includedSteps: includedSteps.value
941
+ includedSteps: stepList
763
942
  });
764
943
  function write(next) {
765
944
  const doc = colorDocFromState(next);
@@ -768,7 +947,7 @@ const ColorScaleField = ({ field, path, readOnly }) => {
768
947
  scale.setValue(doc.scale);
769
948
  stale.setValue(doc.stale);
770
949
  include.setValue(doc.include);
771
- 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));
772
951
  }
773
952
  const label = copy?.label ?? (typeof field.label === "string" ? field.label : key);
774
953
  return /* @__PURE__ */ jsxs("div", {
@@ -793,17 +972,17 @@ const ColorScaleField = ({ field, path, readOnly }) => {
793
972
  });
794
973
  };
795
974
  //#endregion
796
- //#region src/theme/types.ts
797
- const SYSTEM_COLOR_KEYS = [
798
- "primary",
799
- "secondary",
800
- "accent",
801
- "highlight",
802
- "success",
803
- "destructive"
804
- ];
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
+ }
805
984
  function themeColorKeys(doc) {
806
- const custom = (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));
807
986
  return [...SYSTEM_COLOR_KEYS, ...custom];
808
987
  }
809
988
  /**
@@ -813,7 +992,7 @@ function themeColorKeys(doc) {
813
992
  * has `key`.
814
993
  */
815
994
  function deleteLibraryColor(doc, key, replacement) {
816
- const library = [...doc.colors?.library ?? []];
995
+ const library = [...asLibraryRows(doc.colors?.library)];
817
996
  const index = library.findIndex((row) => row.key === key);
818
997
  if (index === -1) throw new Error(`No custom color named ${JSON.stringify(key)}`);
819
998
  library.splice(index, 1);
@@ -836,43 +1015,42 @@ function slugify(value) {
836
1015
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
837
1016
  }
838
1017
  /**
839
- * Custom colors: the same scale controls as system colors, plus add and
840
- * 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.
841
1022
  */
842
- const LibraryField = ({ path, readOnly }) => {
843
- const { value, setValue } = useField({ path });
844
- const rows = 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));
845
1031
  const [adding, setAdding] = useState(false);
846
1032
  const [name, setName] = useState("");
847
- const [hex, setHex] = useState("#E07A5F");
848
- const [pendingDelete, setPendingDelete] = useState(null);
849
- const [replacement, setReplacement] = useState("");
1033
+ const [hex, setHex] = useState("");
1034
+ const [sourceStep, setSourceStep] = useState(500);
850
1035
  function write(next) {
851
1036
  setValue(next);
852
1037
  }
853
1038
  function add() {
854
1039
  const key = slugify(name);
855
- if (!key || rows.some((row) => row.key === key)) return;
1040
+ if (!key || !isThemeHex(hex) || rows.some((row) => row.key === key)) return;
856
1041
  write([...rows, {
857
1042
  key,
858
1043
  label: name.trim(),
859
- ...colorDocFromState(createColorScale(hex, 500))
1044
+ ...colorDocFromState(createColorScale(hex, sourceStep))
860
1045
  }]);
861
1046
  setName("");
1047
+ setHex("");
1048
+ setSourceStep(500);
862
1049
  setAdding(false);
863
1050
  }
864
- function remove(row, replaceWith) {
865
- write(deleteLibraryColor({ colors: { library: rows } }, row.key ?? "", replaceWith).colors?.library ?? []);
866
- setPendingDelete(null);
867
- setReplacement("");
1051
+ function remove(row) {
1052
+ write(deleteLibraryColor({ colors: { library: rows } }, row.key ?? "").colors?.library ?? []);
868
1053
  }
869
- const replacements = [...SYSTEM_COLOR_KEYS.map((key) => ({
870
- value: key,
871
- label: key[0].toUpperCase() + key.slice(1)
872
- })), ...rows.filter((row) => row.key && row.key !== pendingDelete?.key).map((row) => ({
873
- value: row.key ?? "",
874
- label: row.label ?? row.key ?? ""
875
- }))];
876
1054
  return /* @__PURE__ */ jsxs("div", {
877
1055
  className: "field-type bl-library",
878
1056
  children: [
@@ -884,41 +1062,80 @@ const LibraryField = ({ path, readOnly }) => {
884
1062
  /* @__PURE__ */ jsx("style", {
885
1063
  href: "bl-library",
886
1064
  precedence: "default",
887
- children: CSS$2
1065
+ children: CSS$3
888
1066
  }),
889
1067
  /* @__PURE__ */ jsxs("div", {
890
1068
  className: "bl-library__head",
891
- children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", { children: "Custom colors" }), /* @__PURE__ */ jsx("p", { children: "Named scales that sit with the house colors. Page editors pick them as keys (`coral-400`)." })] }), /* @__PURE__ */ jsx("button", {
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", {
892
1070
  type: "button",
893
1071
  disabled: readOnly,
894
- onClick: () => setAdding(true),
1072
+ onClick: (event) => {
1073
+ event.preventDefault();
1074
+ event.stopPropagation();
1075
+ setAdding(true);
1076
+ },
895
1077
  children: "Add color"
896
1078
  })]
897
1079
  }),
898
- adding ? /* @__PURE__ */ jsxs("div", {
899
- className: "bl-library__add",
900
- children: [
901
- /* @__PURE__ */ jsxs("label", { children: ["Name", /* @__PURE__ */ jsx("input", {
902
- value: name,
903
- onChange: (event) => setName(event.target.value)
904
- })] }),
905
- /* @__PURE__ */ jsxs("label", { children: ["Hex", /* @__PURE__ */ jsx("input", {
906
- value: hex,
907
- onChange: (event) => setHex(event.target.value)
908
- })] }),
909
- /* @__PURE__ */ jsx("button", {
910
- type: "button",
911
- onClick: () => setAdding(false),
912
- children: "Cancel"
913
- }),
914
- /* @__PURE__ */ jsx("button", {
915
- type: "button",
916
- disabled: !slugify(name),
917
- onClick: add,
918
- children: "Create color scale"
919
- })
920
- ]
921
- }) : null,
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,
922
1139
  /* @__PURE__ */ jsx("div", {
923
1140
  className: "bl-library__list",
924
1141
  children: rows.map((row, index) => /* @__PURE__ */ jsx(ColorScaleCard, {
@@ -935,84 +1152,30 @@ const LibraryField = ({ path, readOnly }) => {
935
1152
  };
936
1153
  write(copy);
937
1154
  },
938
- onDelete: readOnly ? void 0 : () => setPendingDelete(row),
1155
+ onDelete: readOnly ? void 0 : () => remove(row),
939
1156
  readOnly
940
1157
  }, row.key ?? index))
941
- }),
942
- pendingDelete ? /* @__PURE__ */ jsx("div", {
943
- className: "bl-library__modal",
944
- role: "dialog",
945
- "aria-modal": "true",
946
- "aria-labelledby": "bl-library-delete-title",
947
- children: /* @__PURE__ */ jsxs("div", {
948
- className: "bl-library__panel",
949
- children: [
950
- /* @__PURE__ */ jsxs("h2", {
951
- id: "bl-library-delete-title",
952
- children: ["Delete ", pendingDelete.label ?? pendingDelete.key]
953
- }),
954
- /* @__PURE__ */ jsxs("p", { children: [
955
- "Unused custom colors delete immediately. If this scale is in use, pick one replacement — every",
956
- " ",
957
- /* @__PURE__ */ jsxs("code", { children: [pendingDelete.key, "-400"] }),
958
- " becomes ",
959
- /* @__PURE__ */ jsxs("code", { children: [replacement || "…", "-400"] }),
960
- "."
961
- ] }),
962
- /* @__PURE__ */ jsxs("label", { children: ["Replacement scale", /* @__PURE__ */ jsxs("select", {
963
- value: replacement,
964
- onChange: (event) => setReplacement(event.target.value),
965
- children: [/* @__PURE__ */ jsx("option", {
966
- value: "",
967
- children: "Choose a scale…"
968
- }), replacements.map((option) => /* @__PURE__ */ jsx("option", {
969
- value: option.value,
970
- children: option.label
971
- }, option.value))]
972
- })] }),
973
- /* @__PURE__ */ jsxs("div", {
974
- className: "bl-library__actions",
975
- children: [
976
- /* @__PURE__ */ jsx("button", {
977
- type: "button",
978
- onClick: () => setPendingDelete(null),
979
- children: "Cancel"
980
- }),
981
- /* @__PURE__ */ jsx("button", {
982
- type: "button",
983
- onClick: () => remove(pendingDelete),
984
- children: "Delete immediately"
985
- }),
986
- /* @__PURE__ */ jsx("button", {
987
- type: "button",
988
- disabled: !replacement,
989
- onClick: () => remove(pendingDelete, replacement),
990
- children: "Replace & delete"
991
- })
992
- ]
993
- })
994
- ]
995
- })
996
- }) : null
1158
+ })
997
1159
  ]
998
1160
  });
999
1161
  };
1000
- const CSS$2 = `
1162
+ const CSS$3 = `
1001
1163
  .bl-library__head{display:flex;justify-content:space-between;gap:1rem;align-items:flex-start;margin-bottom:.85rem}
1002
1164
  .bl-library__head h2{margin:0;font-size:1.1rem}
1003
- .bl-library__head p{margin:.35rem 0 0;font-size:13px;color:var(--theme-elevation-500,#64748b)}
1004
- .bl-library__head button,.bl-library__add button,.bl-library__actions button{min-height:2.1rem;padding:.35rem .75rem;border-radius:8px;border:1px solid var(--theme-elevation-150,#e2e8f0);background:var(--theme-elevation-0,#fff);font:inherit;cursor:pointer}
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}
1005
1167
  .bl-library__list{display:grid;gap:.85rem}
1006
- .bl-library__add{display:flex;flex-wrap:wrap;gap:.6rem;align-items:end;margin-bottom:.85rem;padding:.85rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:12px}
1007
- .bl-library__add label{display:flex;flex-direction:column;gap:.3rem;font-size:12px;font-weight:600}
1008
- .bl-library__add input{min-height:2.1rem;padding:.3rem .55rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:8px}
1009
- .bl-library__modal{position:fixed;inset:0;z-index:80;display:grid;place-items:center;padding:1rem;background:rgba(15,23,42,.45)}
1010
- .bl-library__panel{width:min(28rem,100%);padding:1.2rem;border-radius:16px;background:#fff;color:#0f172a}
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}
1011
1172
  .bl-library__panel h2{margin:0}
1012
1173
  .bl-library__panel p{margin:.5rem 0 0;color:#475569;font-size:14px}
1013
1174
  .bl-library__panel label{display:flex;flex-direction:column;gap:.4rem;margin-top:1rem;font-size:13px;font-weight:600}
1014
- .bl-library__panel select{min-height:2.25rem;border:1px solid #cbd5e1;border-radius:8px;padding:.35rem .5rem}
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}
1015
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}
1016
1179
  `;
1017
1180
  //#endregion
1018
1181
  //#region src/admin/typography-pairing.tsx
@@ -1082,23 +1245,23 @@ function TypographyPairing({ headingFamily, bodyFamily }) {
1082
1245
  });
1083
1246
  }
1084
1247
  const TYPOGRAPHY_PAIRING_CSS = `
1085
- .bl-font-role{padding:1.1rem 1.15rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:12px;background:var(--theme-elevation-0,#fff)}
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)}
1086
1249
  .bl-font-role__head{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem}
1087
1250
  .bl-font-role__head h3{margin:0;font-size:1rem}
1088
- .bl-font-role__head p{margin:.3rem 0 0;font-size:12px;color:var(--theme-elevation-500,#64748b)}
1089
- .bl-font-role__badge{font-size:10px;text-transform:uppercase;letter-spacing:.04em;padding:.15rem .45rem;border-radius:999px;background:var(--theme-elevation-100,#f1f5f9);color:var(--theme-elevation-500,#64748b)}
1090
- .bl-font-role__preview{margin-top:1rem;padding:.85rem .9rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:12px;background:var(--theme-elevation-50,#f8fafc)}
1091
- .bl-font-role__preview > span{display:block;margin-bottom:.4rem;font-size:11px;color:var(--theme-elevation-500,#64748b)}
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))}
1092
1255
  .bl-font-role__heading{margin:0;font-size:1.5rem;line-height:1.2;font-weight:600}
1093
- .bl-font-role__body{margin:0;font-size:1rem;line-height:1.6;color:var(--theme-elevation-600,#475569)}
1094
- .bl-pairing{padding:0;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:12px;background:var(--theme-elevation-0,#fff);overflow:hidden}
1095
- .bl-pairing > header{padding:1rem 1.15rem;border-bottom:1px solid var(--theme-elevation-150,#e2e8f0)}
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))}
1096
1259
  .bl-pairing > header h2{margin:0;font-size:1rem}
1097
- .bl-pairing > header p{margin:.3rem 0 0;font-size:12px;color:var(--theme-elevation-500,#64748b)}
1098
- .bl-pairing__page{padding:1.35rem 1.4rem 1.5rem}
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)}
1099
1262
  .bl-pairing__eyebrow{margin:0;font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:#7c1fa2}
1100
1263
  .bl-pairing__title{margin:.5rem 0 0;font-size:2rem;line-height:1.1;font-weight:600}
1101
- .bl-pairing__copy{margin:.75rem 0 0;max-width:36rem;font-size:1rem;line-height:1.6;color:var(--theme-elevation-600,#475569)}
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))}
1102
1265
  .bl-pairing__actions{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:1.1rem}
1103
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}
1104
1267
  .bl-pairing__btn--ghost{background:#fff;color:#0f172a;border:1px solid #cbd5e1}
@@ -1206,8 +1369,11 @@ const FontField = ({ field, path, readOnly }) => {
1206
1369
  }
1207
1370
  const activeId = active >= 0 && open ? `${id}-option-${active}` : void 0;
1208
1371
  const faceIds = faces ? ids : isFontId(value) ? [value] : [];
1372
+ const canvas = usePreviewCanvas();
1209
1373
  return /* @__PURE__ */ jsxs("div", {
1210
- className: "field-type bl-font-field",
1374
+ className: `field-type bl-font-field ${canvas.className}`,
1375
+ "data-preview": canvas.mode,
1376
+ style: canvas.style,
1211
1377
  children: [
1212
1378
  /* @__PURE__ */ jsx("style", {
1213
1379
  href: "bl-pairing",
@@ -1217,7 +1383,7 @@ const FontField = ({ field, path, readOnly }) => {
1217
1383
  /* @__PURE__ */ jsx("style", {
1218
1384
  href: "bl-font-field",
1219
1385
  precedence: "default",
1220
- children: CSS$1
1386
+ children: CSS$2
1221
1387
  }),
1222
1388
  faceIds.length > 0 ? /* @__PURE__ */ jsx("style", {
1223
1389
  "data-bl-font-faces": "",
@@ -1343,7 +1509,7 @@ const FontField = ({ field, path, readOnly }) => {
1343
1509
  ]
1344
1510
  });
1345
1511
  };
1346
- const CSS$1 = `
1512
+ const CSS$2 = `
1347
1513
  .bl-font-field{margin-bottom:1rem}
1348
1514
  .bl-font-field .field-label{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
1349
1515
  .bl-font-field__trigger{display:flex;align-items:center;justify-content:space-between;width:100%;margin-top:1rem;min-height:3.25rem;padding:.7rem 1rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:12px;background:var(--theme-input-bg,#fff);color:inherit;font:inherit;text-align:left;cursor:pointer}
@@ -1351,7 +1517,7 @@ const CSS$1 = `
1351
1517
  .bl-font-field__trigger:disabled{color:var(--theme-elevation-400,#94a3b8);cursor:not-allowed}
1352
1518
  .bl-font-field__selected-label{display:block;font-size:11px;color:var(--theme-elevation-500,#64748b)}
1353
1519
  .bl-font-field__selected-name{display:block;margin-top:.15rem;font-weight:600}
1354
- .bl-font-field__modal{position:fixed;inset:0;z-index:80;display:grid;place-items:center;padding:1rem;background:rgba(15,23,42,.45)}
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)}
1355
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}
1356
1522
  .bl-font-field__panel-head{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem}
1357
1523
  .bl-font-field__panel-head p{margin:0;font-size:12px;color:#64748b}
@@ -1387,8 +1553,11 @@ const PairingField = ({ field }) => {
1387
1553
  const bodyFont = findFont(body);
1388
1554
  const headingFont = findFont(headingId);
1389
1555
  const ids = [body, headingId].filter(Boolean);
1556
+ const canvas = usePreviewCanvas();
1390
1557
  return /* @__PURE__ */ jsxs("div", {
1391
- className: "field-type bl-pairing-field",
1558
+ className: `field-type bl-pairing-field ${canvas.className}`,
1559
+ "data-preview": canvas.mode,
1560
+ style: canvas.style,
1392
1561
  children: [
1393
1562
  /* @__PURE__ */ jsx("style", {
1394
1563
  href: "bl-pairing",
@@ -1490,18 +1659,18 @@ function AppearanceChoices({ values, onChange, readOnly }) {
1490
1659
  });
1491
1660
  }
1492
1661
  const APPEARANCE_CHOICES_CSS = `
1493
- .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)}
1494
1663
  .bl-appearance__intro h2{margin:0;font-size:1.1rem}
1495
- .bl-appearance__intro p{margin:.35rem 0 0;font-size:13px;color:var(--theme-elevation-500,#64748b);max-width:40rem}
1496
- .bl-appearance__family{margin:0;padding:1rem 1.1rem 1.15rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:12px;background:var(--theme-elevation-0,#fff)}
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))}
1497
1666
  .bl-appearance__family legend{padding:0;margin-bottom:.85rem}
1498
1667
  .bl-appearance__family legend strong{display:block;font-size:14px}
1499
- .bl-appearance__family legend span{display:block;margin-top:.25rem;font-size:12px;color:var(--theme-elevation-500,#64748b);font-weight:400}
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}
1500
1669
  .bl-appearance__choices{display:grid;gap:.6rem}
1501
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))}
1502
1671
  .bl-appearance__choices[data-family=radius]{grid-template-columns:repeat(4,minmax(0,1fr))}
1503
- .bl-appearance__choice{display:flex;flex-direction:column;gap:.65rem;padding:.75rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:12px;background:var(--theme-elevation-0,#fff);cursor:pointer}
1504
- .bl-appearance__choice[data-selected]{box-shadow:inset 0 0 0 2px #0f172a;border-color:#0f172a}
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)}
1505
1674
  .bl-appearance__choice input{position:absolute;opacity:0;pointer-events:none}
1506
1675
  .bl-appearance__copy{display:flex;flex-direction:column;gap:.2rem}
1507
1676
  .bl-appearance__copy strong{display:flex;justify-content:space-between;align-items:center;gap:.4rem;font-size:14px}
@@ -1574,11 +1743,14 @@ const AppearanceField = ({ path, readOnly }) => {
1574
1743
  motion,
1575
1744
  density
1576
1745
  };
1746
+ const canvas = usePreviewCanvas();
1577
1747
  function onChange(name, value) {
1578
1748
  fields[name].setValue(value);
1579
1749
  }
1580
1750
  return /* @__PURE__ */ jsxs("div", {
1581
- className: "field-type bl-appearance-field",
1751
+ className: `field-type bl-appearance-field ${canvas.className}`,
1752
+ "data-preview": canvas.mode,
1753
+ style: canvas.style,
1582
1754
  children: [/* @__PURE__ */ jsx("style", {
1583
1755
  href: "bl-appearance",
1584
1756
  precedence: "default",
@@ -1607,53 +1779,63 @@ const SWATCHES = [
1607
1779
  "950"
1608
1780
  ];
1609
1781
  function GreyScaleChoices({ value, onChange, readOnly }) {
1782
+ const canvas = usePreviewCanvas();
1610
1783
  return /* @__PURE__ */ jsxs("section", {
1611
- className: "bl-grey",
1612
- children: [/* @__PURE__ */ jsxs("header", {
1613
- className: "bl-grey__head",
1614
- children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", { children: "Gray family" }), /* @__PURE__ */ jsx("p", { children: "Choose the neutral family used for page backgrounds, cards, borders, inputs, and muted text." })] }), /* @__PURE__ */ jsx("span", {
1615
- className: "bl-grey__badge",
1616
- children: "Required"
1617
- })]
1618
- }), /* @__PURE__ */ jsx("div", {
1619
- className: "bl-grey__choices",
1620
- children: FAMILIES.map((id) => {
1621
- const copy = presetHints.greyScale[id];
1622
- const selected = value === id;
1623
- return /* @__PURE__ */ jsxs("label", {
1624
- className: "bl-grey__choice",
1625
- "data-selected": selected ? "" : void 0,
1626
- children: [
1627
- /* @__PURE__ */ jsx("input", {
1628
- type: "radio",
1629
- name: "greyScale",
1630
- value: id,
1631
- checked: selected,
1632
- disabled: readOnly,
1633
- onChange: () => onChange(id)
1634
- }),
1635
- /* @__PURE__ */ jsx("span", {
1636
- className: "bl-grey__ramp",
1637
- "aria-hidden": "true",
1638
- children: SWATCHES.map((step) => /* @__PURE__ */ jsx("i", { style: { background: `hsl(${GREY_SCALES[id][step]})` } }, step))
1639
- }),
1640
- /* @__PURE__ */ jsxs("strong", { children: [copy.label, selected ? /* @__PURE__ */ jsx("em", {
1641
- "aria-hidden": "true",
1642
- children: "✓"
1643
- }) : null] }),
1644
- /* @__PURE__ */ jsx("span", { children: copy.hint })
1645
- ]
1646
- }, id);
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
+ })
1647
1829
  })
1648
- })]
1830
+ ]
1649
1831
  });
1650
1832
  }
1651
1833
  /**
1652
1834
  * Visual picker over the five grey-scale presets. Replaces the stock select
1653
1835
  * on Colors; the stored value is still `colors.greyScale`.
1654
1836
  */
1655
- const GreyScaleField = ({ path, readOnly }) => {
1656
- 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 });
1657
1839
  return /* @__PURE__ */ jsxs("div", {
1658
1840
  className: "field-type bl-grey-field",
1659
1841
  children: [/* @__PURE__ */ jsx("style", {
@@ -1669,23 +1851,22 @@ const GreyScaleField = ({ path, readOnly }) => {
1669
1851
  };
1670
1852
  const GREY_SCALE_FIELD_CSS = `
1671
1853
  .bl-grey{margin:0 0 1.75rem}
1672
- .bl-grey__head{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;margin-bottom:.85rem}
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)}
1673
1858
  .bl-grey__head h2{margin:0;font-size:1.1rem}
1674
- .bl-grey__head p{margin:.35rem 0 0;max-width:40rem;font-size:13px;color:var(--theme-elevation-500,#64748b)}
1675
- .bl-grey__badge{font:inherit;font-size:10px;text-transform:uppercase;letter-spacing:.04em;padding:.15rem .45rem;border-radius:999px;background:var(--theme-elevation-100,#f1f5f9);color:var(--theme-elevation-500,#64748b)}
1676
- .bl-grey__choices{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:.6rem}
1677
- .bl-grey__choice{display:flex;flex-direction:column;gap:.45rem;padding:.75rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:12px;background:var(--theme-elevation-0,#fff);cursor:pointer}
1678
- .bl-grey__choice[data-selected]{box-shadow:inset 0 0 0 2px #0f172a;border-color:#0f172a}
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)}
1679
1864
  .bl-grey__choice input{position:absolute;opacity:0;pointer-events:none}
1680
1865
  .bl-grey__ramp{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:3px}
1681
1866
  .bl-grey__ramp i{display:block;height:2rem;border-radius:6px}
1682
1867
  .bl-grey__choice strong{display:flex;justify-content:space-between;align-items:center;gap:.4rem;font-size:14px}
1683
1868
  .bl-grey__choice em{font-style:normal}
1684
- .bl-grey__choice > span{font-size:12px;color:var(--theme-elevation-500,#64748b)}
1685
- [data-bl-theme-preview=dark] .bl-grey__head p,[data-bl-theme-preview=dark] .bl-grey__choice > span{color:#94a3b8}
1686
- [data-bl-theme-preview=dark] .bl-grey__choice{background:#0f172a;border-color:#334155;color:#f8fafc}
1687
- [data-bl-theme-preview=dark] .bl-grey__choice[data-selected]{box-shadow:inset 0 0 0 2px #f8fafc;border-color:#f8fafc}
1688
- [data-bl-theme-preview=dark] .bl-grey__badge{background:#1e293b;color:#94a3b8}
1869
+ .bl-grey__choice > span{font-size:12px;color:var(--bl-preview-muted,var(--theme-elevation-500,#64748b))}
1689
1870
  @media (max-width:720px){.bl-grey__choices{grid-template-columns:1fr}}
1690
1871
  `;
1691
1872
  //#endregion
@@ -1730,6 +1911,11 @@ const ContrastReport = ({ field }) => {
1730
1911
  precedence: "default",
1731
1912
  children: READABILITY_LIST_CSS
1732
1913
  }),
1914
+ /* @__PURE__ */ jsx("style", {
1915
+ href: "bl-theme-colors-layout",
1916
+ precedence: "default",
1917
+ children: THEME_COLORS_LAYOUT_CSS
1918
+ }),
1733
1919
  stale ? /* @__PURE__ */ jsx("p", {
1734
1920
  className: "bl-contrast-report__stale",
1735
1921
  children: "Showing the last complete audit while a colour is being typed."
@@ -1755,21 +1941,30 @@ const SectionHeading = ({ field }) => {
1755
1941
  /* @__PURE__ */ jsx("style", {
1756
1942
  href: "bl-section-heading",
1757
1943
  precedence: "default",
1758
- children: CSS
1944
+ children: CSS$1
1945
+ }),
1946
+ /* @__PURE__ */ jsx("style", {
1947
+ href: "bl-theme-colors-layout",
1948
+ precedence: "default",
1949
+ children: THEME_COLORS_LAYOUT_CSS
1759
1950
  }),
1760
1951
  /* @__PURE__ */ jsx("h2", { children: custom.title }),
1761
1952
  custom.hint ? /* @__PURE__ */ jsx("p", { children: custom.hint }) : null
1762
1953
  ]
1763
1954
  });
1764
1955
  };
1765
- const CSS = `
1956
+ const CSS$1 = `
1766
1957
  .bl-section-heading{margin:1.25rem 0 .65rem}
1767
1958
  .bl-section-heading:first-child{margin-top:0}
1768
1959
  .bl-section-heading h2{margin:0;font-size:1.1rem}
1769
- .bl-section-heading p{margin:.35rem 0 0;font-size:13px;color:var(--theme-elevation-500,#64748b);max-width:40rem}
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}
1770
1961
  `;
1771
1962
  //#endregion
1772
1963
  //#region src/theme/changes.ts
1964
+ /** Payload may hand an empty array field as `{}`. `.map` on that crashes the Theme form. */
1965
+ function libraryRows(value) {
1966
+ return Array.isArray(value) ? value : [];
1967
+ }
1773
1968
  function colorLine(name, before, after) {
1774
1969
  const a = before && typeof before === "object" ? before : { hex: before };
1775
1970
  const b = after && typeof after === "object" ? after : { hex: after };
@@ -1789,8 +1984,8 @@ function themeChildChanges(child, published, incoming) {
1789
1984
  const line = colorLine(key[0].toUpperCase() + key.slice(1), published.colors?.brand?.[key], incoming.colors?.brand?.[key]);
1790
1985
  if (line) lines.push(line);
1791
1986
  }
1792
- const beforeKeys = (published.colors?.library ?? []).map((row) => row.key).join(", ");
1793
- const afterKeys = (incoming.colors?.library ?? []).map((row) => row.key).join(", ");
1987
+ const beforeKeys = libraryRows(published.colors?.library).map((row) => row.key).join(", ");
1988
+ const afterKeys = libraryRows(incoming.colors?.library).map((row) => row.key).join(", ");
1794
1989
  if (beforeKeys !== afterKeys) lines.push(`Custom colors: ${beforeKeys || "none"} → ${afterKeys || "none"}`);
1795
1990
  if ((published.colors?.greyScale ?? "") !== (incoming.colors?.greyScale ?? "")) lines.push(`Gray family: ${published.colors?.greyScale ?? "—"} → ${incoming.colors?.greyScale ?? "—"}`);
1796
1991
  return lines;
@@ -1844,7 +2039,7 @@ const OTHER_CHILDREN = {
1844
2039
  };
1845
2040
  function PublishConfirm({ childLabel, changes, shorts, onKeepEditing, onPublish, busy }) {
1846
2041
  const remaining = shorts?.filter((item) => !item.meets) ?? [];
1847
- return /* @__PURE__ */ jsx("div", {
2042
+ return /* @__PURE__ */ jsx(ThemePortal, { children: /* @__PURE__ */ jsx("div", {
1848
2043
  className: "bl-publish",
1849
2044
  role: "dialog",
1850
2045
  "aria-modal": "true",
@@ -1892,10 +2087,12 @@ function PublishConfirm({ childLabel, changes, shorts, onKeepEditing, onPublish,
1892
2087
  className: "bl-publish__actions",
1893
2088
  children: [/* @__PURE__ */ jsx("button", {
1894
2089
  type: "button",
2090
+ className: "bl-publish__cancel",
1895
2091
  onClick: onKeepEditing,
1896
- children: "Keep editing"
2092
+ children: "Cancel"
1897
2093
  }), /* @__PURE__ */ jsx("button", {
1898
2094
  type: "button",
2095
+ className: "bl-publish__go",
1899
2096
  disabled: busy || changes.length === 0,
1900
2097
  onClick: onPublish,
1901
2098
  children: remaining.length > 0 ? "Publish anyway" : "Publish"
@@ -1903,24 +2100,25 @@ function PublishConfirm({ childLabel, changes, shorts, onKeepEditing, onPublish,
1903
2100
  })
1904
2101
  ]
1905
2102
  })
1906
- });
2103
+ }) });
1907
2104
  }
1908
2105
  const PUBLISH_CONFIRM_CSS = `
1909
- .bl-publish{position:fixed;inset:0;z-index:80;display:grid;place-items:center;padding:1rem;background:rgba(15,23,42,.45)}
1910
- .bl-publish__panel{width:min(36rem,100%);padding:1.25rem 1.35rem;border-radius:16px;background:#fff;color:#0f172a;box-shadow:0 24px 60px rgba(15,23,42,.2)}
1911
- .bl-publish__panel h2{margin:0}
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}
1912
2109
  .bl-publish__panel p{margin:.5rem 0 0;color:#475569}
1913
- .bl-publish__empty{font-style:italic}
1914
- .bl-publish__panel > ul{margin:.75rem 0 0;padding-left:1.1rem}
1915
- .bl-publish__shorts{margin-top:1rem;padding:.85rem .9rem;border-radius:10px;background:#fffbeb}
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}
1916
2113
  .bl-publish__shorts strong{display:block}
1917
2114
  .bl-publish__shorts ul{margin:.65rem 0 0;padding:0;list-style:none}
1918
- .bl-publish__shorts li{display:flex;gap:.65rem;align-items:center;margin-top:.45rem;font-size:13px}
2115
+ .bl-publish__shorts li{display:flex;gap:.65rem;align-items:center;margin-top:.45rem;font-size:13px;color:#0f172a}
1919
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}
1920
- .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}
1921
2118
  .bl-publish__actions{display:flex;justify-content:flex-end;gap:.6rem;margin-top:1.1rem}
1922
- .bl-publish__actions button{min-height:2.25rem;padding:.4rem .9rem;border-radius:8px;border:1px solid #cbd5e1;background:#fff;font:inherit;cursor:pointer}
1923
- .bl-publish__actions button:last-child{border:0;background:#0f172a;color:#fff;font-weight:600}
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}
1924
2122
  .bl-publish__actions button:disabled{opacity:.45;cursor:not-allowed}
1925
2123
  `;
1926
2124
  //#endregion
@@ -1942,7 +2140,7 @@ function formDoc(fields) {
1942
2140
  hex: fields[`colors.brand.${key}.hex`]?.value,
1943
2141
  sourceStep: fields[`colors.brand.${key}.sourceStep`]?.value
1944
2142
  }])),
1945
- library: fields["colors.library"]?.value ?? [],
2143
+ library: Array.isArray(fields["colors.library"]?.value) ? fields["colors.library"]?.value : [],
1946
2144
  greyScale: fields["colors.greyScale"]?.value
1947
2145
  },
1948
2146
  typography: {
@@ -1977,13 +2175,13 @@ function shortsFrom(doc) {
1977
2175
  })];
1978
2176
  }
1979
2177
  /**
1980
- * Per-child Publish. Sets `publishChild` then submits; `beforeChange`
1981
- * writes only that slice. Confirmation lists this child's changes and,
1982
- * on Colors, remaining automatic-label shorts.
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.
2180
+ * Sets `publishChild` then submits; `beforeChange` writes only that slice.
2181
+ * On Colors, remaining automatic-label shorts open in this modal only.
1983
2182
  */
1984
- const PublishChild = ({ field }) => {
1985
- const child = (field.admin?.custom)?.child;
1986
- const label = isThemeChild(child) ? LABELS[child] : "Theme";
2183
+ function ThemePublishActions({ child }) {
2184
+ const label = LABELS[child];
1987
2185
  const { submit } = useForm();
1988
2186
  const publishChild = useField({ path: "publishChild" });
1989
2187
  const snapshot = useFormFields(([fields]) => {
@@ -1995,10 +2193,9 @@ const PublishChild = ({ field }) => {
1995
2193
  });
1996
2194
  const [open, setOpen] = useState(false);
1997
2195
  const [busy, setBusy] = useState(false);
1998
- const changes = useMemo(() => isThemeChild(child) ? themeChildChanges(child, snapshot.published, snapshot.incoming) : [], [child, snapshot]);
2196
+ const changes = useMemo(() => themeChildChanges(child, snapshot.published, snapshot.incoming), [child, snapshot]);
1999
2197
  const shorts = child === "colors" ? shortsFrom(snapshot.incoming) : void 0;
2000
2198
  async function publish() {
2001
- if (!isThemeChild(child)) return;
2002
2199
  setBusy(true);
2003
2200
  publishChild.setValue(child);
2004
2201
  try {
@@ -2009,7 +2206,7 @@ const PublishChild = ({ field }) => {
2009
2206
  }
2010
2207
  }
2011
2208
  return /* @__PURE__ */ jsxs("div", {
2012
- className: "field-type bl-publish-child",
2209
+ className: "bl-publish-child",
2013
2210
  children: [
2014
2211
  /* @__PURE__ */ jsx("style", {
2015
2212
  href: "bl-publish",
@@ -2019,9 +2216,9 @@ const PublishChild = ({ field }) => {
2019
2216
  /* @__PURE__ */ jsx("style", {
2020
2217
  href: "bl-publish-child",
2021
2218
  precedence: "default",
2022
- children: `${THEME_PREVIEW_TOGGLE_CSS}.bl-publish-child{position:sticky;top:0;z-index:15;display:flex;justify-content:flex-end;align-items:center;gap:.75rem;padding:.65rem 0;background:var(--theme-bg,#fff)}.bl-publish-child__button{min-height:2.25rem;padding:.4rem 1rem;border:0;border-radius:8px;background:var(--theme-elevation-800,#0f172a);color:#fff;font:inherit;font-weight:600;cursor:pointer}`
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}`
2023
2220
  }),
2024
- child === "colors" ? /* @__PURE__ */ jsx(ThemePreviewToggle, {}) : null,
2221
+ child !== "identity" ? /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(ThemePreviewToggle, {}) }) : null,
2025
2222
  /* @__PURE__ */ jsxs("button", {
2026
2223
  type: "button",
2027
2224
  className: "bl-publish-child__button",
@@ -2040,17 +2237,175 @@ const PublishChild = ({ field }) => {
2040
2237
  }) : null
2041
2238
  ]
2042
2239
  });
2240
+ }
2241
+ /** @deprecated Alias — Theme pages use ThemePublishActions; tabbed Save used this name. */
2242
+ const ThemePublishControls = ThemePublishActions;
2243
+ const PublishChild = ({ field }) => {
2244
+ const child = (field.admin?.custom)?.child;
2245
+ if (!isThemeChild(child)) return null;
2246
+ return /* @__PURE__ */ jsx(ThemePublishActions, { child });
2247
+ };
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"
2043
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
+ }
2044
2265
  //#endregion
2045
2266
  //#region src/admin/save-button.tsx
2046
2267
  /**
2047
- * Theme children publish themselves. The stock Save would write every
2048
- * dirty tab at once, so it is hidden.
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`.
2049
2282
  */
2283
+ function ThemeSaveButton() {
2284
+ const [child, setChild] = useState("colors");
2285
+ useEffect(() => {
2286
+ const sync = () => setChild(readActiveThemeTab());
2287
+ sync();
2288
+ const target = document.querySelector(".tabs-field") ?? document.body;
2289
+ const observer = new MutationObserver(sync);
2290
+ observer.observe(target, {
2291
+ subtree: true,
2292
+ attributes: true,
2293
+ attributeFilter: ["aria-selected", "class"],
2294
+ childList: true
2295
+ });
2296
+ target.addEventListener("click", sync);
2297
+ return () => {
2298
+ observer.disconnect();
2299
+ target.removeEventListener("click", sync);
2300
+ };
2301
+ }, []);
2302
+ return /* @__PURE__ */ jsx(ThemePublishControls, { child });
2303
+ }
2304
+ /** @deprecated Import map alias — ThemeDocumentControls is the Save-adjacent slot. */
2050
2305
  function HiddenSaveButton() {
2051
- return null;
2306
+ return /* @__PURE__ */ jsx(ThemeDocumentControls, {});
2052
2307
  }
2053
2308
  //#endregion
2054
- export { AppearanceField, ColorField, ColorScaleField, ContrastReport, FontField, GreyScaleField, HiddenSaveButton, LibraryField, PairingField, PublishChild, SectionHeading };
2309
+ //#region src/admin/identity-fallback.tsx
2310
+ /**
2311
+ * Identity child when the site has not registered a cupboard yet.
2312
+ * Shows the fallback lockup and mark; uploads wait on `logo.collection`.
2313
+ */
2314
+ const IdentityFallback = ({ field }) => {
2315
+ const fallback = (field.admin?.custom)?.fallback;
2316
+ return /* @__PURE__ */ jsxs("section", {
2317
+ className: "bl-identity-fallback",
2318
+ children: [
2319
+ /* @__PURE__ */ jsx("style", {
2320
+ href: "bl-identity-fallback",
2321
+ precedence: "default",
2322
+ children: CSS
2323
+ }),
2324
+ /* @__PURE__ */ jsxs("header", { children: [/* @__PURE__ */ jsx("h2", { children: "Site identity" }), /* @__PURE__ */ jsx("p", { children: "Today's lockup and mark until a brand editor uploads replacements." })] }),
2325
+ /* @__PURE__ */ jsxs("div", {
2326
+ className: "bl-identity-fallback__grid",
2327
+ children: [fallback?.lockup ? /* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsx("img", {
2328
+ src: fallback.lockup.url,
2329
+ alt: fallback.lockup.alt
2330
+ }), /* @__PURE__ */ jsx("figcaption", { children: "Lockup" })] }) : null, fallback?.mark ? /* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsx("img", {
2331
+ src: fallback.mark.url,
2332
+ alt: fallback.mark.alt
2333
+ }), /* @__PURE__ */ jsx("figcaption", { children: "Mark" })] }) : null]
2334
+ })
2335
+ ]
2336
+ });
2337
+ };
2338
+ const CSS = `
2339
+ .bl-identity-fallback header h2{margin:0;font-size:1.1rem}
2340
+ .bl-identity-fallback header p{margin:.35rem 0 1rem;color:var(--theme-elevation-600,#475569)}
2341
+ .bl-identity-fallback__grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));gap:1rem}
2342
+ .bl-identity-fallback figure{margin:0;padding:1rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:10px;background:var(--theme-elevation-50,#f8fafc)}
2343
+ .bl-identity-fallback img{display:block;max-width:100%;height:4rem;object-fit:contain}
2344
+ .bl-identity-fallback figcaption{margin-top:.5rem;font-size:.8rem;color:var(--theme-elevation-600,#475569)}
2345
+ `;
2346
+ //#endregion
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 };
2055
2410
 
2056
2411
  //# sourceMappingURL=admin.mjs.map