@bison-lab/payload-core 3.9.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/admin.mjs CHANGED
@@ -4,6 +4,7 @@ import { FieldDescription, FieldError, FieldLabel, useField, useForm, useFormFie
4
4
  import { GREY_SCALES, SHADE_STEPS, contrastForeground, contrastRatio, createColorScale, hexToHSL, hslToString, includedShadeSteps, isThemeHex, presetHints, regenerateColorScale, setColorScaleInclude, updateColorScale } 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}
@@ -351,9 +352,16 @@ const READABILITY_PUBLISH_NOTE = "You can still publish. To improve a color, dar
351
352
  //#region src/admin/preview-mode.tsx
352
353
  const ATTR = "data-bl-theme-preview";
353
354
  const EVENT = "bl-theme-preview";
355
+ function adminIsDark() {
356
+ if (typeof document === "undefined") return false;
357
+ const root = document.documentElement;
358
+ return root.getAttribute("data-theme") === "dark" || root.getAttribute("data-theme-pref") === "dark" || root.classList.contains("dark");
359
+ }
354
360
  function readMode() {
355
361
  if (typeof document === "undefined") return "light";
356
- return document.documentElement.getAttribute(ATTR) === "dark" ? "dark" : "light";
362
+ const set = document.documentElement.getAttribute(ATTR);
363
+ if (set === "dark" || set === "light") return set;
364
+ return adminIsDark() ? "dark" : "light";
357
365
  }
358
366
  function setThemePreviewMode(mode) {
359
367
  if (typeof document === "undefined") return;
@@ -361,9 +369,10 @@ function setThemePreviewMode(mode) {
361
369
  window.dispatchEvent(new Event(EVENT));
362
370
  }
363
371
  function useThemePreviewMode() {
364
- const [mode, setMode] = useState(readMode);
372
+ const [mode, setMode] = useState("light");
365
373
  useEffect(() => {
366
374
  const sync = () => setMode(readMode());
375
+ sync();
367
376
  window.addEventListener(EVENT, sync);
368
377
  return () => window.removeEventListener(EVENT, sync);
369
378
  }, []);
@@ -374,24 +383,36 @@ function ThemePreviewToggle() {
374
383
  return /* @__PURE__ */ jsxs("div", {
375
384
  className: "bl-preview-toggle",
376
385
  role: "group",
377
- "aria-label": "Preview colors in",
378
- children: [/* @__PURE__ */ jsx("button", {
379
- type: "button",
380
- "data-on": mode === "light" ? "" : void 0,
381
- onClick: () => setThemePreviewMode("light"),
382
- children: "Light"
383
- }), /* @__PURE__ */ jsx("button", {
384
- type: "button",
385
- "data-on": mode === "dark" ? "" : void 0,
386
- onClick: () => setThemePreviewMode("dark"),
387
- children: "Dark"
386
+ "aria-label": "Preview fills in",
387
+ children: [/* @__PURE__ */ jsx("span", {
388
+ className: "bl-preview-toggle__label",
389
+ children: "Preview fills"
390
+ }), /* @__PURE__ */ jsxs("div", {
391
+ className: "bl-preview-toggle__pills",
392
+ children: [/* @__PURE__ */ jsx("button", {
393
+ type: "button",
394
+ "aria-pressed": mode === "light",
395
+ onClick: () => setThemePreviewMode("light"),
396
+ children: "Light"
397
+ }), /* @__PURE__ */ jsx("button", {
398
+ type: "button",
399
+ "aria-pressed": mode === "dark",
400
+ onClick: () => setThemePreviewMode("dark"),
401
+ children: "Dark"
402
+ })]
388
403
  })]
389
404
  });
390
405
  }
391
406
  const THEME_PREVIEW_TOGGLE_CSS = `
392
- .bl-preview-toggle{display:inline-flex;padding:3px;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:10px;background:var(--theme-elevation-0,#fff)}
393
- .bl-preview-toggle button{min-height:2rem;padding:.25rem .75rem;border:0;border-radius:7px;background:transparent;color:var(--theme-elevation-600,#475569);font:inherit;font-size:13px;font-weight:600;cursor:pointer}
394
- .bl-preview-toggle button[data-on]{background:var(--theme-elevation-800,#0f172a);color:#fff}
407
+ .bl-preview-toggle{display:inline-flex;align-items:center;gap:.6rem}
408
+ .bl-preview-toggle__label{font-size:12px;font-weight:600;color:#a1a1aa}
409
+ .bl-preview-toggle__pills{display:inline-flex;padding:3px;border:1px solid #3f3f46;border-radius:10px;background:#18181b}
410
+ .bl-preview-toggle__pills button{min-height:2rem;padding:.25rem .8rem;border:0;border-radius:7px;background:transparent;color:#d4d4d8;font:inherit;font-size:13px;font-weight:600;cursor:pointer}
411
+ .bl-preview-toggle__pills button[aria-pressed=true]{background:#f4f4f5;color:#18181b;box-shadow:0 1px 2px rgba(0,0,0,.25)}
412
+ html[data-theme=light] .bl-preview-toggle__label{color:#52525b}
413
+ html[data-theme=light] .bl-preview-toggle__pills{border-color:#d4d4d8;background:#e4e4e7}
414
+ html[data-theme=light] .bl-preview-toggle__pills button{color:#3f3f46}
415
+ html[data-theme=light] .bl-preview-toggle__pills button[aria-pressed=true]{background:#fff;color:#18181b;box-shadow:0 1px 2px rgba(15,23,42,.12)}
395
416
  `;
396
417
  //#endregion
397
418
  //#region src/admin/readability-list.tsx
@@ -429,25 +450,6 @@ function ReadabilityDetail({ item, target = 7 }) {
429
450
  ]
430
451
  });
431
452
  }
432
- function ReadabilityList({ warnings, target = 7 }) {
433
- const failing = warnings.filter((item) => !item.meets).length;
434
- return /* @__PURE__ */ jsxs("aside", {
435
- className: "bl-readability",
436
- children: [/* @__PURE__ */ jsxs("div", {
437
- className: "bl-readability__head",
438
- children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", { children: "Readability checks" }), /* @__PURE__ */ jsx("p", { children: "Text the site puts on each fill. Warnings do not block save." })] }), /* @__PURE__ */ jsx("span", {
439
- "data-attention": failing ? "" : void 0,
440
- children: failing === 0 ? "All look readable" : `${failing} may be hard to read`
441
- })]
442
- }), /* @__PURE__ */ jsx("ul", { children: warnings.map((item) => /* @__PURE__ */ jsxs("li", {
443
- "data-failing": item.meets ? void 0 : "",
444
- children: [/* @__PURE__ */ jsx("strong", { children: readabilityHeadline(item) }), /* @__PURE__ */ jsx(ReadabilityDetail, {
445
- item,
446
- target
447
- })]
448
- }, item.id)) })]
449
- });
450
- }
451
453
  const READABILITY_LIST_CSS = `
452
454
  .bl-readability{padding:calc(var(--base,16px) * .8);border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:var(--style-radius-m,12px);background:var(--theme-elevation-0,#fff)}
453
455
  .bl-readability__head{display:flex;justify-content:space-between;gap:1rem;align-items:flex-start}
@@ -541,13 +543,20 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
541
543
  }), hint ? /* @__PURE__ */ jsx("p", {
542
544
  className: "bl-scale-card__hint",
543
545
  children: hint
544
- }) : null] }), onDelete ? /* @__PURE__ */ jsx("button", {
545
- type: "button",
546
- className: "bl-scale-card__delete",
547
- disabled: readOnly,
548
- onClick: onDelete,
549
- children: "Delete"
550
- }) : null]
546
+ }) : null] }), /* @__PURE__ */ jsxs("div", {
547
+ className: "bl-scale-card__head-actions",
548
+ children: [/* @__PURE__ */ jsx("span", {
549
+ className: "bl-scale-card__mark",
550
+ style: { background: state.hex },
551
+ "aria-hidden": "true"
552
+ }), onDelete ? /* @__PURE__ */ jsx("button", {
553
+ type: "button",
554
+ className: "bl-scale-card__delete",
555
+ disabled: readOnly,
556
+ onClick: onDelete,
557
+ children: "Delete"
558
+ }) : null]
559
+ })]
551
560
  }),
552
561
  /* @__PURE__ */ jsxs("div", {
553
562
  className: "bl-scale-card__row",
@@ -647,7 +656,39 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
647
656
  onClick: () => toggleStep(step),
648
657
  children: [/* @__PURE__ */ jsx("span", {
649
658
  className: "bl-scale-card__chip",
650
- style: { background: hslCss(state.scale[step]) }
659
+ style: { background: hslCss(state.scale[step]) },
660
+ children: source ? null : /* @__PURE__ */ jsx("span", {
661
+ className: "bl-scale-card__intent",
662
+ "data-intent": on ? "exclude" : "include",
663
+ "aria-hidden": "true",
664
+ children: on ? /* @__PURE__ */ jsxs("svg", {
665
+ viewBox: "0 0 24 24",
666
+ width: "16",
667
+ height: "16",
668
+ children: [/* @__PURE__ */ jsx("circle", {
669
+ cx: "12",
670
+ cy: "12",
671
+ r: "8",
672
+ fill: "none",
673
+ stroke: "currentColor",
674
+ strokeWidth: "2.4"
675
+ }), /* @__PURE__ */ jsx("path", {
676
+ d: "M7.2 16.8 16.8 7.2",
677
+ stroke: "currentColor",
678
+ strokeWidth: "2.4"
679
+ })]
680
+ }) : /* @__PURE__ */ jsx("svg", {
681
+ viewBox: "0 0 24 24",
682
+ width: "16",
683
+ height: "16",
684
+ children: /* @__PURE__ */ jsx("path", {
685
+ d: "M12 6v12M6 12h12",
686
+ fill: "none",
687
+ stroke: "currentColor",
688
+ strokeWidth: "2.6"
689
+ })
690
+ })
691
+ })
651
692
  }), /* @__PURE__ */ jsx("span", {
652
693
  className: "bl-scale-card__step-label",
653
694
  children: step
@@ -655,7 +696,7 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
655
696
  }, step);
656
697
  })
657
698
  }),
658
- open && warning ? /* @__PURE__ */ jsx("div", {
699
+ open && warning ? createPortal(/* @__PURE__ */ jsx("div", {
659
700
  className: "bl-scale-card__modal",
660
701
  role: "dialog",
661
702
  "aria-modal": "true",
@@ -687,7 +728,7 @@ function ColorScaleCard({ id, name, hint, required, custom, state, onChange, onD
687
728
  })
688
729
  ]
689
730
  })
690
- }) : null
731
+ }), document.body) : null
691
732
  ]
692
733
  });
693
734
  }
@@ -702,6 +743,8 @@ const COLOR_SCALE_CARD_CSS = `
702
743
  .bl-scale-card__status[data-ok]{background:#ecfdf5;color:#047857}
703
744
  .bl-scale-card__sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
704
745
  .bl-scale-card__hint{margin:.35rem 0 0;color:var(--theme-elevation-500,#64748b);font-size:12px}
746
+ .bl-scale-card__head-actions{display:flex;align-items:center;gap:.5rem}
747
+ .bl-scale-card__mark{display:block;width:2.5rem;height:2.5rem;border-radius:8px;border:1px solid rgba(15,23,42,.12);flex:none}
705
748
  .bl-scale-card__delete{border:0;background:none;color:#be123c;font:inherit;font-size:13px;font-weight:600;cursor:pointer}
706
749
  .bl-scale-card__row{display:grid;grid-template-columns:1fr 8rem;gap:.75rem;margin-top:1rem}
707
750
  .bl-scale-card__field{display:flex;flex-direction:column;gap:.35rem;font-size:12px;font-weight:600;color:var(--theme-elevation-600,#475569)}
@@ -715,27 +758,27 @@ const COLOR_SCALE_CARD_CSS = `
715
758
  .bl-scale-card__warn,.bl-scale-card__stale{margin:.75rem 0 0;padding:.65rem .75rem;border-radius:8px;font-size:12px}
716
759
  .bl-scale-card__warn{border:1px solid #a7f3d0;background:#ecfdf5;color:#065f46}
717
760
  .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}
761
+ .bl-scale-card__regen{width:100%;margin-top:.85rem;min-height:2.25rem;border:0;border-radius:8px;background:#0f172a;color:#fff;font:inherit;font-weight:600;cursor:pointer}
762
+ .bl-scale-card__regen:disabled{background:#1e293b;color:#f8fafc;opacity:1;cursor:not-allowed}
720
763
  .bl-scale-card__scale-head{display:flex;justify-content:space-between;margin:1rem 0 .4rem;font-size:12px;color:var(--theme-elevation-500,#64748b)}
721
764
  .bl-scale-card__scale-head button{border:0;background:none;padding:0;font:inherit;color:inherit;text-decoration:underline;cursor:pointer}
722
765
  .bl-scale-card__steps{display:grid;grid-template-columns:repeat(11,minmax(0,1fr));gap:.45rem 3px}
723
766
  .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}
767
+ .bl-scale-card__chip{position:relative;display:block;width:100%;height:2.15rem;border-radius:6px;overflow:hidden}
768
+ .bl-scale-card__intent{position:absolute;top:3px;right:3px;width:1.15rem;height:1.15rem;border-radius:999px;display:grid;place-items:center;opacity:0;color:#be123c;background:#fff;box-shadow:0 1px 3px rgba(15,23,42,.28);transition:opacity .15s ease,transform .15s ease;pointer-events:none}
769
+ .bl-scale-card__intent[data-intent=include]{color:#047857}
770
+ .bl-scale-card__step:hover:not(:disabled) .bl-scale-card__chip{transform:translateY(-1px)}
771
+ .bl-scale-card__step:hover:not(:disabled) .bl-scale-card__intent{opacity:1}
725
772
  .bl-scale-card__step-label{font-size:10px;line-height:1;color:var(--theme-elevation-600,#475569)}
726
- .bl-scale-card__step[data-source] .bl-scale-card__chip{box-shadow:0 0 0 2px #0f172a}
727
- .bl-scale-card__step[data-excluded]{opacity:.35}
773
+ .bl-scale-card__step[data-source] .bl-scale-card__chip{box-shadow:0 0 0 2px var(--theme-elevation-800,#0f172a)}
774
+ .bl-scale-card__step[data-excluded]{opacity:.55}
728
775
  .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)}
776
+ .bl-scale-card__modal{position:fixed;inset:0;z-index:100000;display:grid;place-items:center;padding:1rem;background:rgba(15,23,42,.45)}
730
777
  .bl-scale-card__panel{width:min(32rem,100%);padding:1.15rem 1.2rem;border-radius:16px;background:#fff;color:#0f172a}
731
778
  .bl-scale-card__panel-head{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem}
732
779
  .bl-scale-card__panel-head h4{margin:0;font-size:1rem}
733
780
  .bl-scale-card__panel-head button{border:0;background:none;font:inherit;font-size:1.25rem;line-height:1;color:#64748b;cursor:pointer}
734
781
  .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
782
  ` + READABILITY_LIST_CSS;
740
783
  //#endregion
741
784
  //#region src/admin/color-scale-field.tsx
@@ -803,7 +846,7 @@ const SYSTEM_COLOR_KEYS = [
803
846
  "destructive"
804
847
  ];
805
848
  function themeColorKeys(doc) {
806
- const custom = (doc?.colors?.library ?? []).map((row) => row.key?.trim()).filter((key) => Boolean(key));
849
+ const custom = (Array.isArray(doc?.colors?.library) ? doc.colors.library : []).map((row) => row.key?.trim()).filter((key) => Boolean(key));
807
850
  return [...SYSTEM_COLOR_KEYS, ...custom];
808
851
  }
809
852
  /**
@@ -813,7 +856,7 @@ function themeColorKeys(doc) {
813
856
  * has `key`.
814
857
  */
815
858
  function deleteLibraryColor(doc, key, replacement) {
816
- const library = [...doc.colors?.library ?? []];
859
+ const library = [...Array.isArray(doc.colors?.library) ? doc.colors.library : []];
817
860
  const index = library.findIndex((row) => row.key === key);
818
861
  if (index === -1) throw new Error(`No custom color named ${JSON.stringify(key)}`);
819
862
  library.splice(index, 1);
@@ -841,7 +884,7 @@ function slugify(value) {
841
884
  */
842
885
  const LibraryField = ({ path, readOnly }) => {
843
886
  const { value, setValue } = useField({ path });
844
- const rows = value ?? [];
887
+ const rows = Array.isArray(value) ? value : [];
845
888
  const [adding, setAdding] = useState(false);
846
889
  const [name, setName] = useState("");
847
890
  const [hex, setHex] = useState("#E07A5F");
@@ -884,7 +927,7 @@ const LibraryField = ({ path, readOnly }) => {
884
927
  /* @__PURE__ */ jsx("style", {
885
928
  href: "bl-library",
886
929
  precedence: "default",
887
- children: CSS$2
930
+ children: CSS$3
888
931
  }),
889
932
  /* @__PURE__ */ jsxs("div", {
890
933
  className: "bl-library__head",
@@ -997,12 +1040,13 @@ const LibraryField = ({ path, readOnly }) => {
997
1040
  ]
998
1041
  });
999
1042
  };
1000
- const CSS$2 = `
1043
+ const CSS$3 = `
1001
1044
  .bl-library__head{display:flex;justify-content:space-between;gap:1rem;align-items:flex-start;margin-bottom:.85rem}
1002
1045
  .bl-library__head h2{margin:0;font-size:1.1rem}
1003
1046
  .bl-library__head p{margin:.35rem 0 0;font-size:13px;color:var(--theme-elevation-500,#64748b)}
1004
1047
  .bl-library__head button,.bl-library__add button,.bl-library__actions button{min-height:2.1rem;padding:.35rem .75rem;border-radius:8px;border:1px solid var(--theme-elevation-150,#e2e8f0);background:var(--theme-elevation-0,#fff);font:inherit;cursor:pointer}
1005
- .bl-library__list{display:grid;gap:.85rem}
1048
+ .bl-library__list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.85rem}
1049
+ @media (max-width:860px){.bl-library__list{grid-template-columns:1fr}}
1006
1050
  .bl-library__add{display:flex;flex-wrap:wrap;gap:.6rem;align-items:end;margin-bottom:.85rem;padding:.85rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:12px}
1007
1051
  .bl-library__add label{display:flex;flex-direction:column;gap:.3rem;font-size:12px;font-weight:600}
1008
1052
  .bl-library__add input{min-height:2.1rem;padding:.3rem .55rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:8px}
@@ -1217,7 +1261,7 @@ const FontField = ({ field, path, readOnly }) => {
1217
1261
  /* @__PURE__ */ jsx("style", {
1218
1262
  href: "bl-font-field",
1219
1263
  precedence: "default",
1220
- children: CSS$1
1264
+ children: CSS$2
1221
1265
  }),
1222
1266
  faceIds.length > 0 ? /* @__PURE__ */ jsx("style", {
1223
1267
  "data-bl-font-faces": "",
@@ -1343,7 +1387,7 @@ const FontField = ({ field, path, readOnly }) => {
1343
1387
  ]
1344
1388
  });
1345
1389
  };
1346
- const CSS$1 = `
1390
+ const CSS$2 = `
1347
1391
  .bl-font-field{margin-bottom:1rem}
1348
1392
  .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
1393
  .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}
@@ -1690,57 +1734,12 @@ const GREY_SCALE_FIELD_CSS = `
1690
1734
  `;
1691
1735
  //#endregion
1692
1736
  //#region src/admin/contrast-report.tsx
1693
- function hexAt(fields, path) {
1694
- const value = fields[path]?.value;
1695
- return typeof value === "string" ? value : "";
1696
- }
1697
1737
  /**
1698
- * Fill + automatic label only. Target 7:1, Light and Dark. No gray
1699
- * ContrastStrip. Warnings do not block save.
1738
+ * Contrast used to be a Colors-tab list. Readability now lives on each
1739
+ * color card: Needs attention opens a popup. This field stays in the
1740
+ * schema so `contrastTarget` is still a Theme option; it paints nothing.
1700
1741
  */
1701
- const ContrastReport = ({ field }) => {
1702
- const target = Number((field.admin?.custom)?.target) || 7;
1703
- const hexes = useFormFields(([fields]) => {
1704
- const record = fields;
1705
- const system = Object.fromEntries(SYSTEM_COLOR_KEYS.map((key) => [key, hexAt(record, `colors.brand.${key}.hex`)]));
1706
- const library = record["colors.library"]?.value;
1707
- return {
1708
- system,
1709
- custom: Array.isArray(library) ? library.flatMap((row, index) => {
1710
- const hex = typeof row === "object" && row && "hex" in row ? String(row.hex ?? "") : "";
1711
- const name = typeof row === "object" && row && "label" in row ? String(row.label ?? row.key ?? "Custom") : "Custom";
1712
- return hex ? [{
1713
- id: `library-${index}`,
1714
- name,
1715
- hex
1716
- }] : [];
1717
- }) : []
1718
- };
1719
- });
1720
- const last = useRef(null);
1721
- const complete = SYSTEM_COLOR_KEYS.every((key) => isThemeHex(hexes.system[key]));
1722
- if (complete) last.current = [...SYSTEM_COLOR_KEYS.map((key) => fillLabelWarning(key, SYSTEM_COLOR_COPY[key].label, hexes.system[key], target)), ...hexes.custom.map((row) => fillLabelWarning(row.id, row.name, row.hex, target))].filter((item) => item !== null);
1723
- const warnings = last.current ?? [];
1724
- const stale = Boolean(last.current) && !complete;
1725
- return /* @__PURE__ */ jsxs("div", {
1726
- className: "field-type bl-contrast-report",
1727
- children: [
1728
- /* @__PURE__ */ jsx("style", {
1729
- href: "bl-readability",
1730
- precedence: "default",
1731
- children: READABILITY_LIST_CSS
1732
- }),
1733
- stale ? /* @__PURE__ */ jsx("p", {
1734
- className: "bl-contrast-report__stale",
1735
- children: "Showing the last complete audit while a colour is being typed."
1736
- }) : null,
1737
- warnings.length > 0 ? /* @__PURE__ */ jsx(ReadabilityList, {
1738
- warnings,
1739
- target
1740
- }) : /* @__PURE__ */ jsx("p", { children: "Enter hex colours to measure the automatic label on each fill." })
1741
- ]
1742
- });
1743
- };
1742
+ const ContrastReport = () => null;
1744
1743
  //#endregion
1745
1744
  //#region src/admin/section-heading.tsx
1746
1745
  /**
@@ -1755,14 +1754,14 @@ const SectionHeading = ({ field }) => {
1755
1754
  /* @__PURE__ */ jsx("style", {
1756
1755
  href: "bl-section-heading",
1757
1756
  precedence: "default",
1758
- children: CSS
1757
+ children: CSS$1
1759
1758
  }),
1760
1759
  /* @__PURE__ */ jsx("h2", { children: custom.title }),
1761
1760
  custom.hint ? /* @__PURE__ */ jsx("p", { children: custom.hint }) : null
1762
1761
  ]
1763
1762
  });
1764
1763
  };
1765
- const CSS = `
1764
+ const CSS$1 = `
1766
1765
  .bl-section-heading{margin:1.25rem 0 .65rem}
1767
1766
  .bl-section-heading:first-child{margin-top:0}
1768
1767
  .bl-section-heading h2{margin:0;font-size:1.1rem}
@@ -1770,6 +1769,10 @@ const CSS = `
1770
1769
  `;
1771
1770
  //#endregion
1772
1771
  //#region src/theme/changes.ts
1772
+ /** Payload may hand an empty array field as `{}`. `.map` on that crashes the Theme form. */
1773
+ function libraryRows(value) {
1774
+ return Array.isArray(value) ? value : [];
1775
+ }
1773
1776
  function colorLine(name, before, after) {
1774
1777
  const a = before && typeof before === "object" ? before : { hex: before };
1775
1778
  const b = after && typeof after === "object" ? after : { hex: after };
@@ -1789,8 +1792,8 @@ function themeChildChanges(child, published, incoming) {
1789
1792
  const line = colorLine(key[0].toUpperCase() + key.slice(1), published.colors?.brand?.[key], incoming.colors?.brand?.[key]);
1790
1793
  if (line) lines.push(line);
1791
1794
  }
1792
- const beforeKeys = (published.colors?.library ?? []).map((row) => row.key).join(", ");
1793
- const afterKeys = (incoming.colors?.library ?? []).map((row) => row.key).join(", ");
1795
+ const beforeKeys = libraryRows(published.colors?.library).map((row) => row.key).join(", ");
1796
+ const afterKeys = libraryRows(incoming.colors?.library).map((row) => row.key).join(", ");
1794
1797
  if (beforeKeys !== afterKeys) lines.push(`Custom colors: ${beforeKeys || "none"} → ${afterKeys || "none"}`);
1795
1798
  if ((published.colors?.greyScale ?? "") !== (incoming.colors?.greyScale ?? "")) lines.push(`Gray family: ${published.colors?.greyScale ?? "—"} → ${incoming.colors?.greyScale ?? "—"}`);
1796
1799
  return lines;
@@ -1924,13 +1927,24 @@ const PUBLISH_CONFIRM_CSS = `
1924
1927
  .bl-publish__actions button:disabled{opacity:.45;cursor:not-allowed}
1925
1928
  `;
1926
1929
  //#endregion
1927
- //#region src/admin/publish-child.tsx
1928
- const LABELS = {
1930
+ //#region src/admin/theme-tab.ts
1931
+ const THEME_TAB_LABELS = {
1929
1932
  colors: "Colors",
1930
1933
  typography: "Typography",
1931
1934
  appearance: "Appearance",
1932
1935
  identity: "Identity"
1933
1936
  };
1937
+ /**
1938
+ * The selected Payload field tab on Theme. Defaults to Colors when the
1939
+ * tabs have not painted yet (SSR, first paint).
1940
+ */
1941
+ function readActiveThemeTab(root = typeof document === "undefined" ? null : document) {
1942
+ if (!root) return "colors";
1943
+ const text = (root.querySelector(".tabs-field__tab-button[aria-selected=\"true\"]") ?? root.querySelector(".tabs-field__tab-button--active") ?? root.querySelector("[role=\"tab\"][aria-selected=\"true\"]"))?.textContent?.replace(/\s+/g, " ").trim();
1944
+ return THEME_CHILDREN.find((id) => THEME_TAB_LABELS[id] === text) ?? "colors";
1945
+ }
1946
+ //#endregion
1947
+ //#region src/admin/publish-child.tsx
1934
1948
  function asColor(value) {
1935
1949
  if (value && typeof value === "object") return value;
1936
1950
  if (typeof value === "string") return { hex: value };
@@ -1942,7 +1956,7 @@ function formDoc(fields) {
1942
1956
  hex: fields[`colors.brand.${key}.hex`]?.value,
1943
1957
  sourceStep: fields[`colors.brand.${key}.sourceStep`]?.value
1944
1958
  }])),
1945
- library: fields["colors.library"]?.value ?? [],
1959
+ library: libraryRows(fields["colors.library"]?.value),
1946
1960
  greyScale: fields["colors.greyScale"]?.value
1947
1961
  },
1948
1962
  typography: {
@@ -1966,7 +1980,7 @@ function publishedDoc(fields) {
1966
1980
  }
1967
1981
  function shortsFrom(doc) {
1968
1982
  const brand = doc.colors?.brand ?? {};
1969
- const custom = doc.colors?.library ?? [];
1983
+ const custom = libraryRows(doc.colors?.library);
1970
1984
  return [...SYSTEM_COLOR_KEYS.flatMap((key) => {
1971
1985
  const hex = asColor(brand[key])?.hex;
1972
1986
  const warning = typeof hex === "string" ? fillLabelWarning(key, SYSTEM_COLOR_COPY[key].label, hex) : null;
@@ -1977,13 +1991,12 @@ function shortsFrom(doc) {
1977
1991
  })];
1978
1992
  }
1979
1993
  /**
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.
1994
+ * Publish for the open Theme tab. Lives in Payload's document Save
1995
+ * slot (`ThemeSaveButton`) so the header matches a Pages document.
1996
+ * Sets `publishChild` then submits; `beforeChange` writes only that slice.
1983
1997
  */
1984
- const PublishChild = ({ field }) => {
1985
- const child = (field.admin?.custom)?.child;
1986
- const label = isThemeChild(child) ? LABELS[child] : "Theme";
1998
+ function ThemePublishControls({ child }) {
1999
+ const label = THEME_TAB_LABELS[child];
1987
2000
  const { submit } = useForm();
1988
2001
  const publishChild = useField({ path: "publishChild" });
1989
2002
  const snapshot = useFormFields(([fields]) => {
@@ -1995,10 +2008,9 @@ const PublishChild = ({ field }) => {
1995
2008
  });
1996
2009
  const [open, setOpen] = useState(false);
1997
2010
  const [busy, setBusy] = useState(false);
1998
- const changes = useMemo(() => isThemeChild(child) ? themeChildChanges(child, snapshot.published, snapshot.incoming) : [], [child, snapshot]);
2011
+ const changes = useMemo(() => themeChildChanges(child, snapshot.published, snapshot.incoming), [child, snapshot]);
1999
2012
  const shorts = child === "colors" ? shortsFrom(snapshot.incoming) : void 0;
2000
2013
  async function publish() {
2001
- if (!isThemeChild(child)) return;
2002
2014
  setBusy(true);
2003
2015
  publishChild.setValue(child);
2004
2016
  try {
@@ -2009,7 +2021,7 @@ const PublishChild = ({ field }) => {
2009
2021
  }
2010
2022
  }
2011
2023
  return /* @__PURE__ */ jsxs("div", {
2012
- className: "field-type bl-publish-child",
2024
+ className: "bl-publish-child",
2013
2025
  children: [
2014
2026
  /* @__PURE__ */ jsx("style", {
2015
2027
  href: "bl-publish",
@@ -2019,7 +2031,7 @@ const PublishChild = ({ field }) => {
2019
2031
  /* @__PURE__ */ jsx("style", {
2020
2032
  href: "bl-publish-child",
2021
2033
  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}`
2034
+ children: `${THEME_PREVIEW_TOGGLE_CSS}.bl-publish-child{display:flex;align-items:center;gap:.75rem}.bl-publish-child__button{min-height:2.25rem;padding:.4rem 1rem;border:0;border-radius:8px;background:#a80063;color:#fff;font:inherit;font-weight:600;cursor:pointer}`
2023
2035
  }),
2024
2036
  child === "colors" ? /* @__PURE__ */ jsx(ThemePreviewToggle, {}) : null,
2025
2037
  /* @__PURE__ */ jsxs("button", {
@@ -2040,17 +2052,81 @@ const PublishChild = ({ field }) => {
2040
2052
  }) : null
2041
2053
  ]
2042
2054
  });
2055
+ }
2056
+ /** @deprecated Theme publish is the document Save button */
2057
+ const PublishChild = ({ field }) => {
2058
+ const child = (field.admin?.custom)?.child;
2059
+ return /* @__PURE__ */ jsx(ThemePublishControls, { child: isThemeChild(child) ? child : "colors" });
2043
2060
  };
2044
2061
  //#endregion
2045
2062
  //#region src/admin/save-button.tsx
2046
2063
  /**
2047
- * Theme children publish themselves. The stock Save would write every
2048
- * dirty tab at once, so it is hidden.
2064
+ * Payload's document Save slot the same control row as Pages'
2065
+ * "Publish changes". Theme has no drafts; this publishes only the
2066
+ * open field tab. The tab is read after mount so SSR matches.
2049
2067
  */
2050
- function HiddenSaveButton() {
2051
- return null;
2068
+ function ThemeSaveButton() {
2069
+ const [child, setChild] = useState("colors");
2070
+ useEffect(() => {
2071
+ const sync = () => setChild(readActiveThemeTab());
2072
+ sync();
2073
+ const target = document.querySelector(".tabs-field") ?? document.body;
2074
+ const observer = new MutationObserver(sync);
2075
+ observer.observe(target, {
2076
+ subtree: true,
2077
+ attributes: true,
2078
+ attributeFilter: ["aria-selected", "class"],
2079
+ childList: true
2080
+ });
2081
+ target.addEventListener("click", sync);
2082
+ return () => {
2083
+ observer.disconnect();
2084
+ target.removeEventListener("click", sync);
2085
+ };
2086
+ }, []);
2087
+ return /* @__PURE__ */ jsx(ThemePublishControls, { child });
2052
2088
  }
2089
+ /** Import-map alias — Theme still points `SaveButton` at this name. */
2090
+ const HiddenSaveButton = ThemeSaveButton;
2091
+ //#endregion
2092
+ //#region src/admin/identity-fallback.tsx
2093
+ /**
2094
+ * Identity child when the site has not registered a cupboard yet.
2095
+ * Shows the fallback lockup and mark; uploads wait on `logo.collection`.
2096
+ */
2097
+ const IdentityFallback = ({ field }) => {
2098
+ const fallback = (field.admin?.custom)?.fallback;
2099
+ return /* @__PURE__ */ jsxs("section", {
2100
+ className: "bl-identity-fallback",
2101
+ children: [
2102
+ /* @__PURE__ */ jsx("style", {
2103
+ href: "bl-identity-fallback",
2104
+ precedence: "default",
2105
+ children: CSS
2106
+ }),
2107
+ /* @__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." })] }),
2108
+ /* @__PURE__ */ jsxs("div", {
2109
+ className: "bl-identity-fallback__grid",
2110
+ children: [fallback?.lockup ? /* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsx("img", {
2111
+ src: fallback.lockup.url,
2112
+ alt: fallback.lockup.alt
2113
+ }), /* @__PURE__ */ jsx("figcaption", { children: "Lockup" })] }) : null, fallback?.mark ? /* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsx("img", {
2114
+ src: fallback.mark.url,
2115
+ alt: fallback.mark.alt
2116
+ }), /* @__PURE__ */ jsx("figcaption", { children: "Mark" })] }) : null]
2117
+ })
2118
+ ]
2119
+ });
2120
+ };
2121
+ const CSS = `
2122
+ .bl-identity-fallback header h2{margin:0;font-size:1.1rem}
2123
+ .bl-identity-fallback header p{margin:.35rem 0 1rem;color:var(--theme-elevation-600,#475569)}
2124
+ .bl-identity-fallback__grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));gap:1rem}
2125
+ .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)}
2126
+ .bl-identity-fallback img{display:block;max-width:100%;height:4rem;object-fit:contain}
2127
+ .bl-identity-fallback figcaption{margin-top:.5rem;font-size:.8rem;color:var(--theme-elevation-600,#475569)}
2128
+ `;
2053
2129
  //#endregion
2054
- export { AppearanceField, ColorField, ColorScaleField, ContrastReport, FontField, GreyScaleField, HiddenSaveButton, LibraryField, PairingField, PublishChild, SectionHeading };
2130
+ export { AppearanceField, ColorField, ColorScaleField, ContrastReport, FontField, GreyScaleField, HiddenSaveButton, IdentityFallback, LibraryField, PairingField, PublishChild, SectionHeading, ThemeSaveButton };
2055
2131
 
2056
2132
  //# sourceMappingURL=admin.mjs.map