@cancia/toolbar 0.5.2 → 0.12.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/cancia.js CHANGED
@@ -1,3 +1,149 @@
1
+ // src/richtext.ts
2
+ import { PT_STYLES, portableTextSubsetSchema } from "@cancia/astro/richtext";
3
+ function parseRichValue(raw) {
4
+ if (raw === void 0) return null;
5
+ const trimmed = raw.trim();
6
+ if (trimmed === "") return [];
7
+ if (!trimmed.startsWith("[")) {
8
+ return [
9
+ {
10
+ _type: "block",
11
+ _key: "legacy",
12
+ style: "normal",
13
+ markDefs: [],
14
+ children: [{ _type: "span", _key: "legacy0", text: raw, marks: [] }]
15
+ }
16
+ ];
17
+ }
18
+ try {
19
+ const parsed = portableTextSubsetSchema.safeParse(JSON.parse(trimmed));
20
+ return parsed.success ? parsed.data : [];
21
+ } catch {
22
+ return [];
23
+ }
24
+ }
25
+ function serializeRichValue(blocks) {
26
+ return blocks.length === 0 ? "" : JSON.stringify(blocks);
27
+ }
28
+ var STYLE_TAG = {
29
+ normal: "p",
30
+ h2: "h2",
31
+ h3: "h3",
32
+ blockquote: "blockquote"
33
+ };
34
+ function renderRichToDom(target, blocks) {
35
+ target.replaceChildren();
36
+ let listWrap = null;
37
+ let listKind = null;
38
+ for (const block of blocks) {
39
+ if (block.listItem) {
40
+ const wantTag = block.listItem === "number" ? "ol" : "ul";
41
+ if (!listWrap || listKind !== block.listItem) {
42
+ listWrap = document.createElement(wantTag);
43
+ listKind = block.listItem;
44
+ target.appendChild(listWrap);
45
+ }
46
+ const li = document.createElement("li");
47
+ appendSpans(li, block);
48
+ listWrap.appendChild(li);
49
+ continue;
50
+ }
51
+ listWrap = null;
52
+ listKind = null;
53
+ const el = document.createElement(STYLE_TAG[block.style] ?? "p");
54
+ appendSpans(el, block);
55
+ target.appendChild(el);
56
+ }
57
+ }
58
+ function appendSpans(parent, block) {
59
+ const markDefs = new Map(
60
+ (block.markDefs ?? []).map((d) => [d._key, d])
61
+ );
62
+ for (const span of block.children) {
63
+ let node = document.createTextNode(span.text);
64
+ for (const mark of span.marks ?? []) {
65
+ if (mark === "strong" || mark === "em") {
66
+ const wrap = document.createElement(mark === "strong" ? "strong" : "em");
67
+ wrap.appendChild(node);
68
+ node = wrap;
69
+ continue;
70
+ }
71
+ const def = markDefs.get(mark);
72
+ if (def) {
73
+ const a = document.createElement("a");
74
+ a.setAttribute("href", def.href);
75
+ a.appendChild(node);
76
+ node = a;
77
+ }
78
+ }
79
+ parent.appendChild(node);
80
+ }
81
+ }
82
+ var STYLE_OF_TAG = {
83
+ P: "normal",
84
+ H2: "h2",
85
+ H3: "h3",
86
+ BLOCKQUOTE: "blockquote"
87
+ };
88
+ function domToRows(region) {
89
+ const rows = [];
90
+ const pushBlock = (el, style, listItem) => {
91
+ const text = inlineToShorthand(el);
92
+ if (text.trim() === "") return;
93
+ rows.push(listItem ? { text, style, listItem } : { text, style });
94
+ };
95
+ for (const child of region.children) {
96
+ const tag = child.tagName;
97
+ if (tag === "UL" || tag === "OL") {
98
+ const kind = tag === "OL" ? "number" : "bullet";
99
+ for (const li of child.children) {
100
+ if (li.tagName === "LI") pushBlock(li, "normal", kind);
101
+ }
102
+ continue;
103
+ }
104
+ if (child.classList.contains("cancia-richtext")) {
105
+ rows.push(...domToRows(child));
106
+ continue;
107
+ }
108
+ pushBlock(child, STYLE_OF_TAG[tag] ?? "normal");
109
+ }
110
+ if (rows.length === 0) {
111
+ const text = inlineToShorthand(region);
112
+ if (text.trim() !== "") rows.push({ text, style: "normal" });
113
+ }
114
+ return rows;
115
+ }
116
+ function inlineToShorthand(el) {
117
+ let out = "";
118
+ for (const node of el.childNodes) {
119
+ if (node.nodeType === Node.TEXT_NODE) {
120
+ out += (node.textContent ?? "").replace(/\s+/g, " ");
121
+ continue;
122
+ }
123
+ if (node.nodeType !== Node.ELEMENT_NODE) continue;
124
+ const child = node;
125
+ const inner = inlineToShorthand(child);
126
+ switch (child.tagName) {
127
+ case "STRONG":
128
+ case "B":
129
+ out += `**${inner}**`;
130
+ break;
131
+ case "EM":
132
+ case "I":
133
+ out += `*${inner}*`;
134
+ break;
135
+ case "A": {
136
+ const href = child.getAttribute("href") ?? "";
137
+ out += href ? `[${inner}](${href})` : inner;
138
+ break;
139
+ }
140
+ default:
141
+ out += inner;
142
+ }
143
+ }
144
+ return out;
145
+ }
146
+
1
147
  // src/state.ts
2
148
  var state = {
3
149
  config: null,
@@ -53,6 +199,11 @@ function applyOverlay() {
53
199
  el.src = savedValue;
54
200
  return;
55
201
  }
202
+ if (el.dataset.cmsType === "richtext") {
203
+ const blocks = parseRichValue(savedValue);
204
+ if (blocks) renderRichToDom(el, blocks);
205
+ return;
206
+ }
56
207
  if (el.dataset.cmsType === "link") {
57
208
  const link = parseLinkOverlay(savedValue);
58
209
  if (link.href && el.tagName === "A") el.setAttribute("href", link.href);
@@ -71,14 +222,19 @@ function applyOverlay() {
71
222
  });
72
223
  }
73
224
  function revertPending() {
74
- for (const [fullKey, { key, lang }] of state.pending) {
225
+ for (const [fullKey, { key }] of state.pending) {
75
226
  const savedValue = state.cmsData[fullKey] ?? "";
76
227
  document.querySelectorAll(`[data-cms="${key}"]`).forEach((el) => {
77
228
  if (el.tagName === "IMG") {
78
229
  el.src = savedValue;
79
- } else {
80
- el.textContent = savedValue;
230
+ return;
231
+ }
232
+ if (el.dataset.cmsType === "richtext") {
233
+ const blocks = parseRichValue(savedValue);
234
+ if (blocks) renderRichToDom(el, blocks);
235
+ return;
81
236
  }
237
+ el.textContent = savedValue;
82
238
  });
83
239
  }
84
240
  state.pending.clear();
@@ -242,21 +398,431 @@ async function flushPending() {
242
398
  if (failed > 0) throw new Error(`Cancia: ${failed} save(s) failed`);
243
399
  }
244
400
 
401
+ // src/tokens.ts
402
+ var tokens = {
403
+ // ── Surfaces ──────────────────────────────────────────────────────────────
404
+ // All three chrome levels are plain white. On a light theme, depth comes from
405
+ // the BORDER and the shadow, not from a lightness ramp: three subtly
406
+ // different off-whites just look like a rendering bug. The scale is still
407
+ // three names so component code keeps its layering vocabulary.
408
+ "surface-1": "#ffffff",
409
+ // the floating bar itself
410
+ "surface-2": "#ffffff",
411
+ // popups, panels
412
+ "surface-3": "#ffffff",
413
+ // the drawer / form (the topmost layer)
414
+ "surface-raised": "#f4f4f5",
415
+ // an input or row ON a surface — recessed, not raised
416
+ "surface-hover": "#f4f4f5",
417
+ "surface-active": "#e4e4e7",
418
+ // ── Text ──────────────────────────────────────────────────────────────────
419
+ // Four steps only. More than four and nothing reads as deliberate.
420
+ // Opaque hex, not white-alpha: these sit on white, and an alpha black over a
421
+ // translucent surface picks up whatever the page beneath happens to be.
422
+ "fg-strong": "#18181b",
423
+ // headings, input text
424
+ "fg": "#3f3f46",
425
+ // body
426
+ "fg-muted": "#71717a",
427
+ // labels, help text
428
+ "fg-faint": "#a1a1aa",
429
+ // placeholders, disabled
430
+ // ── Borders ───────────────────────────────────────────────────────────────
431
+ // On a light surface a 1px border does more work than a large shadow — it is
432
+ // the primary way a panel separates itself from the page behind it.
433
+ "border": "#e4e4e7",
434
+ "border-strong": "#d4d4d8",
435
+ // ── Accent ────────────────────────────────────────────────────────────────
436
+ // Near-black, NOT a brand colour. See the note on tokenCss(): the site's
437
+ // configured accentColor is opt-in, because a client's brand colour is
438
+ // frequently pale or neon and produces an unreadable button on light chrome.
439
+ "accent": "#18181b",
440
+ "accent-fg": "#ffffff",
441
+ "accent-soft": "rgba(24, 24, 27, 0.06)",
442
+ // recomputed when an accent is supplied
443
+ "accent-ring": "rgba(24, 24, 27, 0.28)",
444
+ // recomputed when an accent is supplied
445
+ // ── Status ────────────────────────────────────────────────────────────────
446
+ // Retuned for light: the dark theme used pastel-bright status colours that
447
+ // glowed on near-black and wash out to illegible on white. These are the
448
+ // mid-weight variants that hold contrast against a white surface.
449
+ "danger": "#dc2626",
450
+ "danger-soft": "rgba(220, 38, 38, 0.08)",
451
+ "success": "#16a34a",
452
+ // Amber. Marks an incomplete-but-not-broken state — chiefly a list entry that
453
+ // exists in another locale but is NOT translated into the active one.
454
+ "warning": "#d97706",
455
+ "warning-soft": "rgba(217, 119, 6, 0.10)",
456
+ // ── Radii ─────────────────────────────────────────────────────────────────
457
+ "radius-sm": "6px",
458
+ "radius": "10px",
459
+ "radius-lg": "14px",
460
+ "radius-full": "999px",
461
+ // ── Spacing ───────────────────────────────────────────────────────────────
462
+ // A 4px scale. Every gap/padding in the UI is one of these.
463
+ "space-1": "4px",
464
+ "space-2": "8px",
465
+ "space-3": "12px",
466
+ "space-4": "16px",
467
+ "space-5": "20px",
468
+ "space-6": "24px",
469
+ // ── Typography ────────────────────────────────────────────────────────────
470
+ // The system stack, so the editor never waits on a webfont or inherits a
471
+ // display face from the page it is injected into.
472
+ "font": '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
473
+ "text-xs": "11px",
474
+ "text-sm": "12px",
475
+ "text-base": "13px",
476
+ "text-lg": "15px",
477
+ // ── Elevation ─────────────────────────────────────────────────────────────
478
+ // Retuned for light surfaces. The dark theme's shadows were 0.3–0.5 alpha
479
+ // black, which on white reads as a grey smear rather than elevation. Light UI
480
+ // wants soft, low-opacity shadows in two layers — a tight contact shadow plus
481
+ // a wide ambient one — with the 1px `border` doing most of the separating.
482
+ // The `inset` top highlight is gone: it simulated light catching the top edge
483
+ // of a dark glass panel and is invisible (or dirty) on white.
484
+ "shadow-sm": "0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0, 0, 0, 0.06)",
485
+ "shadow": "0 2px 4px rgba(0, 0, 0, 0.04), 0 8px 24px rgba(0, 0, 0, 0.08)",
486
+ "shadow-lg": "0 4px 8px rgba(0, 0, 0, 0.05), 0 16px 48px rgba(0, 0, 0, 0.12)",
487
+ // ── Motion ────────────────────────────────────────────────────────────────
488
+ // Two curves, and a rule for choosing between them.
489
+ //
490
+ // `ease` is critically damped — it settles without overshoot, which is what
491
+ // almost all UI wants. Overshoot on a menu that simply appeared reads as
492
+ // noise; overshoot is only earned when the user's own gesture carried
493
+ // momentum into it (a flick, a drag release).
494
+ //
495
+ // `ease-spring` is the momentum curve: a slight overshoot that makes a
496
+ // element feel thrown rather than placed. Reserve it for motion the user
497
+ // initiated with a gesture, and for the toolbar's own entrance (which should
498
+ // feel like it arrives, not like it blinks on).
499
+ // Three curves, chosen by what the element is DOING — not by taste.
500
+ //
501
+ // entering or exiting the screen -> ease-out
502
+ // already on screen, moving -> ease-in-out
503
+ // hover / colour change -> ease
504
+ //
505
+ // `ease-in` is deliberately absent. Its slow start delays visual feedback,
506
+ // which reads as a sluggish interface; this file previously carried a
507
+ // cubic-bezier(0.7, 0, 0.84, 0) exit that did exactly that.
508
+ //
509
+ // These are named after the standard easing set so the intent is legible at
510
+ // the call site: `ease-out-quart` is a strong ease-out, not a magic tuple.
511
+ "ease": "cubic-bezier(0.25, 0.1, 0.25, 1)",
512
+ // hover, colour — gentle, asymmetric
513
+ "ease-out": "cubic-bezier(0.165, 0.84, 0.44, 1)",
514
+ // quart: enter/exit, the default
515
+ "ease-out-soft": "cubic-bezier(0.25, 0.46, 0.45, 0.94)",
516
+ // quad: small/short moves
517
+ "ease-in-out": "cubic-bezier(0.645, 0.045, 0.355, 1)",
518
+ // cubic: on-screen movement
519
+ "ease-spring": "cubic-bezier(0.34, 1.35, 0.64, 1)",
520
+ // overshoot — momentum only
521
+ // Durations. UI animation stays under 300ms; past that an interface starts
522
+ // to feel like it is waiting on itself. Larger surfaces get the longer end,
523
+ // and an exit runs ~20% faster than the matching entrance because nobody
524
+ // wants to watch something leave.
525
+ "duration-fast": "0.12s",
526
+ // press feedback, hover — must feel instant
527
+ "duration": "0.2s",
528
+ // the default: popups, tooltips, panels
529
+ "duration-slow": "0.28s",
530
+ // the largest surfaces (drawer, list panel)
531
+ "duration-exit": "0.16s",
532
+ // exits: ~20% faster than the entrance
533
+ // ── Effects ───────────────────────────────────────────────────────────────
534
+ // Surfaces are opaque white now, so the blur is mostly inert — it is kept so
535
+ // a consumer who overrides a surface to a translucent value still gets the
536
+ // frosted treatment. The `saturate(180%)` boost is dropped: it existed to
537
+ // give dark glass richness, and over a light surface it pushes whatever page
538
+ // colour bleeds through toward the garish.
539
+ "blur": "blur(16px)",
540
+ // ── Layering ──────────────────────────────────────────────────────────────
541
+ // The toolbar must sit above the host page's own stacking contexts, so these
542
+ // live at the very top of the range.
543
+ "z-overlay": "2147483645",
544
+ "z-panel": "2147483646",
545
+ "z-bar": "2147483647"
546
+ };
547
+ function v(name) {
548
+ return `var(--cancia-${name})`;
549
+ }
550
+ function parseHex(hex) {
551
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
552
+ if (!m) return null;
553
+ let h = m[1];
554
+ if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
555
+ return {
556
+ r: parseInt(h.slice(0, 2), 16),
557
+ g: parseInt(h.slice(2, 4), 16),
558
+ b: parseInt(h.slice(4, 6), 16)
559
+ };
560
+ }
561
+ function tokenCss(accent4, useAccent = false) {
562
+ const resolved = { ...tokens };
563
+ const rgb = accent4 ? parseHex(accent4) : null;
564
+ if (useAccent && accent4 && rgb) {
565
+ resolved["accent"] = accent4;
566
+ resolved["accent-soft"] = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.10)`;
567
+ resolved["accent-ring"] = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.35)`;
568
+ }
569
+ const decls = Object.entries(resolved).map(([k, val]) => ` --cancia-${k}: ${val};`).join("\n");
570
+ return `:root {
571
+ ${decls}
572
+ }`;
573
+ }
574
+
575
+ // src/styles.ts
576
+ var injected = false;
577
+ function injectBaseStyles(accent4, useAccent = false) {
578
+ if (injected) return;
579
+ injected = true;
580
+ const style = document.createElement("style");
581
+ style.dataset.cancia = "tokens";
582
+ style.textContent = `
583
+ ${tokenCss(accent4, useAccent)}
584
+
585
+ @keyframes cancia-in {
586
+ from { opacity: 0; transform: translateY(4px) scale(0.98); }
587
+ to { opacity: 1; transform: translateY(0) scale(1); }
588
+ }
589
+ @keyframes cancia-fade {
590
+ from { opacity: 0; }
591
+ to { opacity: 1; }
592
+ }
593
+ @keyframes cancia-spin {
594
+ to { transform: rotate(360deg); }
595
+ }
596
+
597
+ /* Scoped reset. The toolbar is injected into somebody else's page, whose
598
+ global styles WILL otherwise reach our elements \u2014 a site-wide
599
+ \`button { text-transform: uppercase }\` would rewrite our labels. */
600
+ [data-cancia-ui], [data-cancia-ui] * {
601
+ box-sizing: border-box;
602
+ font-family: ${v("font")};
603
+ text-transform: none;
604
+ letter-spacing: normal;
605
+ line-height: 1.45;
606
+ margin: 0;
607
+ }
608
+ [data-cancia-ui] button {
609
+ font: inherit;
610
+ color: inherit;
611
+ background: none;
612
+ border: none;
613
+ cursor: pointer;
614
+ }
615
+ [data-cancia-ui] input,
616
+ [data-cancia-ui] textarea,
617
+ [data-cancia-ui] select {
618
+ font: inherit;
619
+ color: inherit;
620
+ }
621
+ [data-cancia-ui] ::placeholder { color: ${v("fg-faint")}; }
622
+
623
+ /* Focus is shown with our own ring so it matches the accent and is consistent
624
+ across browsers, rather than inheriting whatever the host page's UA default
625
+ or global \`:focus\` rule happens to be. */
626
+ [data-cancia-ui] :focus-visible {
627
+ outline: 2px solid ${v("accent-ring")};
628
+ outline-offset: 2px;
629
+ }
630
+
631
+ /* Reduced motion: neutralise every animation and transition on our subtree.
632
+ This is deliberately a blanket rule keyed on the [data-cancia-ui] marker \u2014
633
+ which is exactly why markUi() must be called on every top-level container we
634
+ create. Any new motion added anywhere in the toolbar is covered by this
635
+ automatically, as long as it lives inside a marked subtree and is expressed
636
+ as a CSS animation or transition (both of the mechanisms we use).
637
+
638
+ Note this zeroes DURATION, not the properties themselves: an element still
639
+ lands on its final state instantly, so nothing is left mid-transition. */
640
+ @media (prefers-reduced-motion: reduce) {
641
+ [data-cancia-ui], [data-cancia-ui] * {
642
+ animation-duration: 0.01ms !important;
643
+ transition-duration: 0.01ms !important;
644
+ }
645
+ }
646
+ `;
647
+ document.head.appendChild(style);
648
+ }
649
+ function markUi(el) {
650
+ el.dataset.canciaUi = "";
651
+ return el;
652
+ }
653
+ var surface = (level = 2) => `
654
+ background: ${v(`surface-${level}`)};
655
+ backdrop-filter: ${v("blur")};
656
+ -webkit-backdrop-filter: ${v("blur")};
657
+ border: 1px solid ${v("border")};
658
+ border-radius: ${v("radius-lg")};
659
+ box-shadow: ${v("shadow")};
660
+ color: ${v("fg")};
661
+ `;
662
+ var button = (variant = "ghost") => {
663
+ const base = `
664
+ display: inline-flex; align-items: center; justify-content: center;
665
+ gap: ${v("space-2")};
666
+ height: 30px;
667
+ padding: 0 ${v("space-3")};
668
+ border-radius: ${v("radius-sm")};
669
+ font-size: ${v("text-sm")};
670
+ font-weight: 500;
671
+ white-space: nowrap;
672
+ transition: background ${v("duration-fast")} ${v("ease")},
673
+ color ${v("duration-fast")} ${v("ease")},
674
+ opacity ${v("duration-fast")} ${v("ease")};
675
+ `;
676
+ if (variant === "primary") {
677
+ return `${base}
678
+ background: ${v("accent")};
679
+ color: ${v("accent-fg")};
680
+ `;
681
+ }
682
+ if (variant === "danger") {
683
+ return `${base}
684
+ background: transparent;
685
+ color: ${v("danger")};
686
+ `;
687
+ }
688
+ return `${base}
689
+ background: transparent;
690
+ color: ${v("fg")};
691
+ `;
692
+ };
693
+ var iconButton = (size = 30) => `
694
+ display: inline-flex; align-items: center; justify-content: center;
695
+ width: ${size}px; height: ${size}px;
696
+ border-radius: ${v("radius-sm")};
697
+ color: ${v("fg-muted")};
698
+ transition: background ${v("duration-fast")} ${v("ease")},
699
+ color ${v("duration-fast")} ${v("ease")};
700
+ `;
701
+ var actionButton = () => `
702
+ display: inline-flex; align-items: center; justify-content: center;
703
+ gap: ${v("space-2")};
704
+ height: 38px;
705
+ padding: 0 ${v("space-4")};
706
+ /* radius-pill so the button's curve echoes the pill-shaped bar containing
707
+ it. A small square radius inside a fully-round container reads as two
708
+ unrelated shapes. */
709
+ border-radius: ${v("radius-full")};
710
+ font-size: ${v("text-base")};
711
+ font-weight: 500;
712
+ color: ${v("fg")};
713
+ background: transparent;
714
+ white-space: nowrap;
715
+ transition: background ${v("duration-fast")} ${v("ease")},
716
+ color ${v("duration-fast")} ${v("ease")},
717
+ transform ${v("duration-fast")} ${v("ease")};
718
+ `;
719
+ var input = () => `
720
+ width: 100%;
721
+ padding: ${v("space-2")} ${v("space-3")};
722
+ background: ${v("surface-raised")};
723
+ color: ${v("fg-strong")};
724
+ border: 1px solid ${v("border")};
725
+ border-radius: ${v("radius-sm")};
726
+ font-size: ${v("text-base")};
727
+ outline: none;
728
+ caret-color: ${v("accent")};
729
+ transition: border-color ${v("duration-fast")} ${v("ease")},
730
+ background ${v("duration-fast")} ${v("ease")};
731
+ `;
732
+ var label = () => `
733
+ display: block;
734
+ font-size: ${v("text-sm")};
735
+ font-weight: 500;
736
+ letter-spacing: normal;
737
+ text-transform: none;
738
+ color: ${v("fg-strong")};
739
+ `;
740
+ var hint = () => `
741
+ font-size: ${v("text-xs")};
742
+ color: ${v("fg-muted")};
743
+ line-height: 1.5;
744
+ `;
745
+ var group = () => `
746
+ display: flex;
747
+ flex-direction: column;
748
+ gap: ${v("space-2")};
749
+ padding-left: ${v("space-3")};
750
+ border-left: 1px solid ${v("border")};
751
+ margin-left: 1px;
752
+ `;
753
+ var groupRow = () => `
754
+ display: flex;
755
+ align-items: flex-start;
756
+ gap: ${v("space-2")};
757
+ `;
758
+ function attachHover(el, opts = {}) {
759
+ const bg = opts.bg ?? v("surface-hover");
760
+ const color = opts.color;
761
+ const priorBg = el.style.background;
762
+ const priorColor = el.style.color;
763
+ el.addEventListener("mouseenter", () => {
764
+ el.style.background = bg;
765
+ if (color) el.style.color = color;
766
+ });
767
+ el.addEventListener("mouseleave", () => {
768
+ el.style.background = priorBg;
769
+ if (color) el.style.color = priorColor;
770
+ });
771
+ }
772
+ function attachPress(el, scale = 0.96) {
773
+ const down = (e) => {
774
+ if (el.disabled) return;
775
+ el.setPointerCapture?.(e.pointerId);
776
+ el.style.transform = `scale(${scale})`;
777
+ };
778
+ const up = () => {
779
+ el.style.transform = "";
780
+ };
781
+ el.addEventListener("pointerdown", down);
782
+ el.addEventListener("pointerup", up);
783
+ el.addEventListener("pointercancel", up);
784
+ }
785
+ function attachInputFocus(el) {
786
+ el.addEventListener("focus", () => {
787
+ el.style.borderColor = v("accent-ring");
788
+ el.style.background = v("surface-hover");
789
+ });
790
+ el.addEventListener("blur", () => {
791
+ el.style.borderColor = v("border");
792
+ el.style.background = v("surface-raised");
793
+ });
794
+ }
795
+
245
796
  // src/highlight.ts
246
797
  var CMS_SELECTOR = "[data-cms], [data-cms-list]";
247
798
  var onSelect = null;
248
799
  var currentHighlighted = null;
249
800
  var cleanupFns = [];
250
801
  var scrollRAF = null;
802
+ var scrollEndTimer = null;
251
803
  var overlayEl = null;
252
804
  var tooltipEl = null;
253
805
  var styleInjected = false;
254
806
  function accent() {
255
- return state.config?.accentColor ?? "#6366f1";
807
+ const useAccent = state.config?.toolbarAccent === true;
808
+ return (useAccent ? state.config?.accentColor : void 0) ?? "#18181b";
809
+ }
810
+ function withAlpha(color, alpha) {
811
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color.trim());
812
+ if (m) {
813
+ let h = m[1];
814
+ if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
815
+ const r = parseInt(h.slice(0, 2), 16);
816
+ const g = parseInt(h.slice(2, 4), 16);
817
+ const b = parseInt(h.slice(4, 6), 16);
818
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
819
+ }
820
+ return `rgba(24, 24, 27, ${alpha})`;
256
821
  }
257
822
  function fieldType(el) {
258
823
  if (el.dataset.cmsType === "image") return "image";
259
824
  if (el.dataset.cmsType === "link") return "link";
825
+ if (el.dataset.cmsType === "richtext") return "richtext";
260
826
  if (el.tagName === "IMG") return "image";
261
827
  return "text";
262
828
  }
@@ -268,9 +834,12 @@ function injectStyles() {
268
834
  styleInjected = true;
269
835
  const s = document.createElement("style");
270
836
  s.textContent = `
837
+ /* A plain fade. The marker used to scale from 0.98, which was right for a
838
+ box growing into place but wrong for an underline \u2014 a scaling underline
839
+ reads as sliding sideways from its centre. Opacity only. */
271
840
  @keyframes cancia-highlight-in {
272
- from { opacity: 0; transform: scale(0.98); }
273
- to { opacity: 1; transform: scale(1); }
841
+ from { opacity: 0; }
842
+ to { opacity: 1; }
274
843
  }
275
844
  @keyframes cancia-tooltip-in {
276
845
  from { opacity: 0; transform: scale(0.95) translateY(3px); }
@@ -283,15 +852,19 @@ function getOrCreateOverlay() {
283
852
  if (!overlayEl) {
284
853
  overlayEl = document.createElement("div");
285
854
  overlayEl.dataset.canciaOverlay = "1";
855
+ markUi(overlayEl);
286
856
  overlayEl.style.cssText = `
287
857
  position: fixed;
858
+ top: 0; left: 0;
288
859
  pointer-events: none !important;
289
860
  box-sizing: border-box;
290
- border-radius: 5px;
861
+ background: transparent;
862
+ border: 0;
291
863
  z-index: 2147483644;
292
- will-change: top, left, width, height, opacity;
293
- transition: top 0.08s cubic-bezier(0.16,1,0.3,1), left 0.08s cubic-bezier(0.16,1,0.3,1),
294
- width 0.08s cubic-bezier(0.16,1,0.3,1), height 0.08s cubic-bezier(0.16,1,0.3,1);
864
+ will-change: transform, opacity;
865
+ contain: layout style;
866
+ transition: transform ${v("duration-fast")} ${v("ease-out-soft")},
867
+ opacity ${v("duration-fast")} ${v("ease")};
295
868
  `;
296
869
  document.body.appendChild(overlayEl);
297
870
  }
@@ -301,37 +874,41 @@ function getOrCreateTooltip() {
301
874
  if (!tooltipEl) {
302
875
  tooltipEl = document.createElement("div");
303
876
  tooltipEl.dataset.canciaTooltip = "1";
877
+ markUi(tooltipEl);
304
878
  tooltipEl.style.cssText = `
305
879
  position: fixed;
306
880
  pointer-events: none !important;
307
- z-index: 2147483645;
308
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
309
- font-size: 11px;
881
+ z-index: ${v("z-overlay")};
882
+ font-family: ${v("font")};
883
+ font-size: ${v("text-xs")};
310
884
  font-weight: 500;
311
885
  letter-spacing: 0.02em;
312
- color: #fff;
313
- background: rgba(10,10,12,0.92);
314
- backdrop-filter: blur(8px);
315
- -webkit-backdrop-filter: blur(8px);
316
- border: 1px solid rgba(255,255,255,0.1);
317
- padding: 4px 8px;
318
- border-radius: 6px;
886
+ color: ${v("fg-strong")};
887
+ background: ${v("surface-1")};
888
+ backdrop-filter: ${v("blur")};
889
+ -webkit-backdrop-filter: ${v("blur")};
890
+ border: 1px solid ${v("border-strong")};
891
+ padding: ${v("space-1")} ${v("space-2")};
892
+ border-radius: ${v("radius-sm")};
319
893
  white-space: nowrap;
320
894
  max-width: 260px;
321
895
  overflow: hidden;
322
896
  text-overflow: ellipsis;
323
- box-shadow: 0 2px 8px rgba(0,0,0,0.3);
897
+ box-shadow: ${v("shadow-sm")};
324
898
  `;
325
899
  document.body.appendChild(tooltipEl);
326
900
  }
327
901
  return tooltipEl;
328
902
  }
329
903
  var lastOverlayEl = null;
904
+ var LIST_FALLBACK = "#059669";
330
905
  function listAccent(hex) {
331
- if (!/^#[0-9a-f]{6}$/i.test(hex)) return "#059669";
906
+ if (!/^#[0-9a-f]{6}$/i.test(hex)) return LIST_FALLBACK;
332
907
  const r = parseInt(hex.slice(1, 3), 16);
333
908
  const g = parseInt(hex.slice(3, 5), 16);
334
909
  const b = parseInt(hex.slice(5, 7), 16);
910
+ const spread = Math.max(r, g, b) - Math.min(r, g, b);
911
+ if (spread < 24) return LIST_FALLBACK;
335
912
  const shifted = [g, b, r].map((c) => c.toString(16).padStart(2, "0")).join("");
336
913
  return `#${shifted}`;
337
914
  }
@@ -352,52 +929,54 @@ var LIST_ICON = `<svg width="10" height="10" viewBox="0 0 14 14" fill="none" str
352
929
  <rect x="1" y="7.5" width="3" height="3" rx="0.5"/>
353
930
  <path d="M6 3.5h7M6 9h7"/>
354
931
  </svg>`;
932
+ var KIND_LABEL = {
933
+ text: "Text",
934
+ image: "Image",
935
+ link: "Link",
936
+ richtext: "Rich text"
937
+ };
355
938
  function positionOverlay(el, animate = false) {
356
939
  const rect = el.getBoundingClientRect();
357
940
  const mode = elementMode(el);
358
- const a = mode === "list" ? listAccent(accent()) : accent();
941
+ const isList = mode === "list";
942
+ const a = isList ? listAccent(accent()) : accent();
359
943
  const overlay = getOrCreateOverlay();
360
944
  const tooltip = getOrCreateTooltip();
361
945
  const padding = 3;
362
- overlay.style.top = `${rect.top - padding}px`;
363
- overlay.style.left = `${rect.left - padding}px`;
946
+ overlay.style.transform = `translate(${rect.left - padding}px, ${rect.top - padding}px)`;
364
947
  overlay.style.width = `${rect.width + padding * 2}px`;
365
948
  overlay.style.height = `${rect.height + padding * 2}px`;
366
949
  if (lastOverlayEl !== el) {
367
950
  lastOverlayEl = el;
368
- overlay.style.border = `2px ${mode === "list" ? "dashed" : "solid"} ${a}`;
369
- overlay.style.background = `${a}12`;
370
- let badgeIcon;
371
- let badgeLabel;
951
+ if (isList) {
952
+ overlay.style.border = `1px dashed ${a}`;
953
+ overlay.style.borderRadius = "6px";
954
+ } else {
955
+ overlay.style.border = "0";
956
+ overlay.style.borderRadius = "0";
957
+ }
958
+ let tagIcon;
959
+ let tagLabel;
372
960
  let tooltipText;
373
- if (mode === "list") {
374
- badgeIcon = LIST_ICON;
375
- badgeLabel = "list";
961
+ if (isList) {
962
+ tagIcon = LIST_ICON;
963
+ tagLabel = "List";
376
964
  tooltipText = `list: ${el.dataset.cmsList}`;
377
965
  } else {
378
966
  const type = fieldType(el);
379
- badgeIcon = type === "image" ? IMAGE_ICON : type === "link" ? LINK_ICON : FIELD_ICON;
380
- badgeLabel = type;
967
+ tagIcon = type === "image" ? IMAGE_ICON : type === "link" ? LINK_ICON : FIELD_ICON;
968
+ tagLabel = KIND_LABEL[type];
381
969
  tooltipText = el.dataset.cms ?? "";
382
970
  }
383
- overlay.innerHTML = `
384
- <div style="
385
- position: absolute; top: -1px; left: -1px;
386
- background: ${a}; color: #fff;
387
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
388
- font-size: 10px; font-weight: 600; letter-spacing: 0.04em;
389
- padding: 2px 6px; border-radius: 3px 0 4px 0;
390
- display: flex; align-items: center; gap: 4px;
391
- line-height: 1;
392
- ">
393
- ${badgeIcon}
394
- ${badgeLabel}
395
- </div>
396
- `;
397
- tooltip.textContent = tooltipText;
971
+ overlay.style.border = `2px solid ${withAlpha(a, isList ? 0.55 : 0.45)}`;
972
+ overlay.style.background = withAlpha(a, 0.05);
973
+ overlay.style.borderRadius = "4px";
974
+ overlay.style.borderStyle = isList ? "dashed" : "solid";
975
+ overlay.innerHTML = "";
976
+ tooltip.textContent = tagLabel ? `${tagLabel} ${tooltipText}` : tooltipText;
398
977
  }
399
978
  overlay.style.display = "block";
400
- if (animate) overlay.style.animation = "cancia-highlight-in 0.12s ease-out forwards";
979
+ if (animate) overlay.style.animation = `cancia-highlight-in ${v("duration-fast")} ${v("ease-out")} forwards`;
401
980
  tooltip.style.display = "block";
402
981
  if (animate) tooltip.style.animation = "cancia-tooltip-in 0.1s ease-out forwards";
403
982
  const tooltipMargin = 8;
@@ -422,13 +1001,19 @@ function hideOverlay() {
422
1001
  }
423
1002
  }
424
1003
  function handleScroll() {
425
- if (scrollRAF !== null) return;
426
- scrollRAF = requestAnimationFrame(() => {
427
- scrollRAF = null;
428
- if (currentHighlighted) {
1004
+ if (overlayEl && overlayEl.style.display !== "none") {
1005
+ overlayEl.style.opacity = "0";
1006
+ if (tooltipEl) tooltipEl.style.opacity = "0";
1007
+ }
1008
+ if (scrollEndTimer) clearTimeout(scrollEndTimer);
1009
+ scrollEndTimer = setTimeout(() => {
1010
+ scrollEndTimer = null;
1011
+ if (currentHighlighted && document.contains(currentHighlighted)) {
429
1012
  positionOverlay(currentHighlighted);
1013
+ if (overlayEl) overlayEl.style.opacity = "1";
1014
+ if (tooltipEl) tooltipEl.style.opacity = "1";
430
1015
  }
431
- });
1016
+ }, 140);
432
1017
  }
433
1018
  function handleMouseOver(e) {
434
1019
  const target = e.target.closest(CMS_SELECTOR);
@@ -524,13 +1109,18 @@ function onPendingChange(cb) {
524
1109
  }
525
1110
 
526
1111
  // src/popup.ts
1112
+ import {
1113
+ portableTextToRows,
1114
+ rowsToPortableText
1115
+ } from "@cancia/astro/richtext";
527
1116
  var popupEl = null;
528
1117
  var outsideListener = null;
529
1118
  var keyListener = null;
530
1119
  var dragover = false;
531
1120
  var inputHandlers = /* @__PURE__ */ new WeakMap();
532
1121
  function accent2() {
533
- return state.config?.accentColor ?? "#6366f1";
1122
+ const useAccent = state.config?.toolbarAccent === true;
1123
+ return (useAccent ? state.config?.accentColor : void 0) ?? "#18181b";
534
1124
  }
535
1125
  function getPopupPosition(anchor) {
536
1126
  const rect = anchor.getBoundingClientRect();
@@ -565,15 +1155,15 @@ function buildHeader(key, onClose) {
565
1155
  const keyParts = key.split(".");
566
1156
  titleEl.textContent = keyParts[keyParts.length - 1].replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
567
1157
  titleEl.style.cssText = `
568
- font-size: 13px; font-weight: 600;
569
- color: rgba(255,255,255,0.9);
1158
+ font-size: ${v("text-base")}; font-weight: 600;
1159
+ color: ${v("fg")};
570
1160
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
571
1161
  `;
572
1162
  const keyEl = document.createElement("span");
573
1163
  keyEl.textContent = key;
574
1164
  keyEl.style.cssText = `
575
1165
  font-size: 10px; font-family: "SF Mono", "Fira Code", ui-monospace, monospace;
576
- color: rgba(255,255,255,0.22); letter-spacing: 0.03em;
1166
+ color: ${v("fg-faint")}; letter-spacing: 0.03em;
577
1167
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
578
1168
  `;
579
1169
  titleWrap.appendChild(titleEl);
@@ -591,7 +1181,7 @@ function buildTextPopup(key, anchorEl, onClose) {
591
1181
  wrap.appendChild(buildHeader(key, onClose));
592
1182
  if (langs.length > 1) {
593
1183
  const tabs = document.createElement("div");
594
- tabs.style.cssText = `display: flex; gap: 0; margin-bottom: 12px; border-bottom: 1px solid rgba(255,255,255,0.06);`;
1184
+ tabs.style.cssText = `display: flex; gap: 0; margin-bottom: ${v("space-3")}; border-bottom: 1px solid ${v("border")};`;
595
1185
  const renderTabs2 = () => {
596
1186
  tabs.innerHTML = "";
597
1187
  langs.forEach((lang) => {
@@ -601,17 +1191,17 @@ function buildTextPopup(key, anchorEl, onClose) {
601
1191
  tab.style.cssText = `
602
1192
  padding: 5px 10px 6px; border: none; border-bottom: 2px solid;
603
1193
  margin-bottom: -1px;
604
- font-size: 11px; font-weight: 600; cursor: pointer; letter-spacing: 0.06em;
1194
+ font-size: ${v("text-xs")}; font-weight: 600; cursor: pointer; letter-spacing: 0.06em;
605
1195
  background: transparent;
606
- border-bottom-color: ${isActive ? accent2() : "transparent"};
607
- color: ${isActive ? "#fff" : "rgba(255,255,255,0.3)"};
608
- transition: color 0.15s, border-color 0.15s;
1196
+ border-bottom-color: ${isActive ? v("accent") : "transparent"};
1197
+ color: ${isActive ? v("fg-strong") : v("fg-faint")};
1198
+ transition: color ${v("duration-fast")} ${v("ease")}, border-color ${v("duration-fast")} ${v("ease")};
609
1199
  `;
610
1200
  tab.addEventListener("mouseenter", () => {
611
- if (!isActive) tab.style.color = "rgba(255,255,255,0.6)";
1201
+ if (!isActive) tab.style.color = v("fg");
612
1202
  });
613
1203
  tab.addEventListener("mouseleave", () => {
614
- if (!isActive) tab.style.color = "rgba(255,255,255,0.3)";
1204
+ if (!isActive) tab.style.color = v("fg-faint");
615
1205
  });
616
1206
  tab.addEventListener("click", () => {
617
1207
  const current = wrap.querySelector("textarea");
@@ -649,8 +1239,8 @@ function buildTextPopup(key, anchorEl, onClose) {
649
1239
  const renderTextarea = (isInit = false) => {
650
1240
  if (!isInit && textarea) {
651
1241
  textarea.value = getValue(key, activeLang) || anchorEl.textContent?.trim() || "";
652
- textarea.style.borderColor = `${accent2()}66`;
653
- textarea.style.background = "rgba(255,255,255,0.05)";
1242
+ textarea.style.borderColor = v("accent-ring");
1243
+ textarea.style.background = v("surface-hover");
654
1244
  attachInputHandler();
655
1245
  return;
656
1246
  }
@@ -660,23 +1250,13 @@ function buildTextPopup(key, anchorEl, onClose) {
660
1250
  textarea.rows = 4;
661
1251
  textarea.placeholder = "Enter text\u2026";
662
1252
  textarea.style.cssText = `
663
- width: 100%; box-sizing: border-box;
664
- background: rgba(255,255,255,0.03); color: rgba(255,255,255,0.9);
665
- border: 1px solid rgba(255,255,255,0.07); border-radius: 10px;
666
- padding: 10px 12px;
667
- font-size: 13px; font-family: inherit; resize: none; outline: none;
668
- transition: border-color 0.18s, background 0.18s;
1253
+ ${input()}
1254
+ border-radius: ${v("radius")};
1255
+ padding: 10px ${v("space-3")};
1256
+ font-family: inherit; resize: none;
669
1257
  line-height: 1.55;
670
- caret-color: ${accent2()};
671
1258
  `;
672
- textarea.addEventListener("focus", () => {
673
- textarea.style.borderColor = `${accent2()}66`;
674
- textarea.style.background = "rgba(255,255,255,0.05)";
675
- });
676
- textarea.addEventListener("blur", () => {
677
- textarea.style.borderColor = "rgba(255,255,255,0.07)";
678
- textarea.style.background = "rgba(255,255,255,0.03)";
679
- });
1259
+ attachInputFocus(textarea);
680
1260
  attachInputHandler();
681
1261
  if (footerEl) {
682
1262
  wrap.insertBefore(textarea, footerEl);
@@ -752,31 +1332,21 @@ function buildLinkPopup(key, anchorEl, onClose) {
752
1332
  };
753
1333
  };
754
1334
  const inputStyle = `
755
- width: 100%; box-sizing: border-box;
756
- background: rgba(255,255,255,0.03); color: rgba(255,255,255,0.9);
757
- border: 1px solid rgba(255,255,255,0.07); border-radius: 10px;
758
- padding: 9px 12px;
759
- font-size: 13px; font-family: inherit; outline: none;
760
- transition: border-color 0.18s, background 0.18s;
761
- caret-color: ${accent2()};
1335
+ ${input()}
1336
+ border-radius: ${v("radius")};
1337
+ padding: 9px ${v("space-3")};
1338
+ font-family: inherit;
762
1339
  `;
763
- const makeLabelled = (text, input) => {
1340
+ const makeLabelled = (text, input2) => {
764
1341
  const field = document.createElement("div");
765
1342
  field.style.cssText = `display: flex; flex-direction: column; gap: 5px; margin-bottom: 10px;`;
766
1343
  const lab = document.createElement("div");
767
1344
  lab.textContent = text;
768
- lab.style.cssText = `font-size:10px;letter-spacing:0.08em;text-transform:uppercase;color:rgba(255,255,255,0.35);`;
769
- input.style.cssText = inputStyle;
770
- input.addEventListener("focus", () => {
771
- input.style.borderColor = `${accent2()}66`;
772
- input.style.background = "rgba(255,255,255,0.05)";
773
- });
774
- input.addEventListener("blur", () => {
775
- input.style.borderColor = "rgba(255,255,255,0.07)";
776
- input.style.background = "rgba(255,255,255,0.03)";
777
- });
1345
+ lab.style.cssText = label();
1346
+ input2.style.cssText = inputStyle;
1347
+ attachInputFocus(input2);
778
1348
  field.appendChild(lab);
779
- field.appendChild(input);
1349
+ field.appendChild(input2);
780
1350
  return field;
781
1351
  };
782
1352
  const labelInput = document.createElement("input");
@@ -791,7 +1361,7 @@ function buildLinkPopup(key, anchorEl, onClose) {
791
1361
  hrefInput.value = current.href;
792
1362
  if (langs.length > 1) {
793
1363
  const tabs = document.createElement("div");
794
- tabs.style.cssText = `display: flex; gap: 0; margin-bottom: 12px; border-bottom: 1px solid rgba(255,255,255,0.06);`;
1364
+ tabs.style.cssText = `display: flex; gap: 0; margin-bottom: ${v("space-3")}; border-bottom: 1px solid ${v("border")};`;
795
1365
  const renderTabs2 = () => {
796
1366
  tabs.innerHTML = "";
797
1367
  langs.forEach((lang) => {
@@ -801,11 +1371,11 @@ function buildLinkPopup(key, anchorEl, onClose) {
801
1371
  tab.style.cssText = `
802
1372
  padding: 5px 10px 6px; border: none; border-bottom: 2px solid;
803
1373
  margin-bottom: -1px;
804
- font-size: 11px; font-weight: 600; cursor: pointer; letter-spacing: 0.06em;
1374
+ font-size: ${v("text-xs")}; font-weight: 600; cursor: pointer; letter-spacing: 0.06em;
805
1375
  background: transparent;
806
- border-bottom-color: ${isActive ? accent2() : "transparent"};
807
- color: ${isActive ? "#fff" : "rgba(255,255,255,0.3)"};
808
- transition: color 0.15s, border-color 0.15s;
1376
+ border-bottom-color: ${isActive ? v("accent") : "transparent"};
1377
+ color: ${isActive ? v("fg-strong") : v("fg-faint")};
1378
+ transition: color ${v("duration-fast")} ${v("ease")}, border-color ${v("duration-fast")} ${v("ease")};
809
1379
  `;
810
1380
  tab.addEventListener("click", () => {
811
1381
  stage(activeLang);
@@ -823,12 +1393,12 @@ function buildLinkPopup(key, anchorEl, onClose) {
823
1393
  }
824
1394
  wrap.appendChild(makeLabelled("Label", labelInput));
825
1395
  wrap.appendChild(makeLabelled("URL", hrefInput));
826
- const hint = document.createElement("div");
827
- hint.style.cssText = `font-size:11px;color:rgba(255,255,255,0.28);margin:-4px 0 10px;line-height:1.45;`;
828
- hint.textContent = langs.length > 1 ? "Relative (/start), #anchor, mailto: and tel: all work. The URL is shared across languages." : "Relative (/start), #anchor, mailto: and tel: all work.";
829
- wrap.appendChild(hint);
1396
+ const hint2 = document.createElement("div");
1397
+ hint2.style.cssText = `${hint()} margin:-${v("space-1")} 0 10px;`;
1398
+ hint2.textContent = langs.length > 1 ? "Relative (/start), #anchor, mailto: and tel: all work. The URL is shared across languages." : "Relative (/start), #anchor, mailto: and tel: all work.";
1399
+ wrap.appendChild(hint2);
830
1400
  const warn = document.createElement("div");
831
- warn.style.cssText = `font-size:11px;color:#f0a; margin:-4px 0 10px; display:none;`;
1401
+ warn.style.cssText = `font-size:${v("text-xs")};color:${v("danger")}; margin:-${v("space-1")} 0 10px; display:none;`;
832
1402
  wrap.appendChild(warn);
833
1403
  const paint = () => {
834
1404
  labelNodes.forEach((n) => n.textContent = labelInput.value);
@@ -885,8 +1455,8 @@ function buildImagePopup(key, anchorEl, onClose) {
885
1455
  if (currentSrc && !currentSrc.startsWith("data:")) {
886
1456
  const previewWrap = document.createElement("div");
887
1457
  previewWrap.style.cssText = `
888
- border-radius: 10px; overflow: hidden; margin-bottom: 10px;
889
- border: 1px solid rgba(255,255,255,0.06);
1458
+ border-radius: ${v("radius")}; overflow: hidden; margin-bottom: 10px;
1459
+ border: 1px solid ${v("border")};
890
1460
  position: relative; height: 100px;
891
1461
  `;
892
1462
  const previewImg = document.createElement("img");
@@ -896,8 +1466,8 @@ function buildImagePopup(key, anchorEl, onClose) {
896
1466
  previewLabel.textContent = "Current";
897
1467
  previewLabel.style.cssText = `
898
1468
  position: absolute; bottom: 0; left: 0; right: 0;
899
- font-size: 10px; color: rgba(255,255,255,0.45); letter-spacing: 0.04em;
900
- padding: 16px 8px 6px;
1469
+ font-size: 10px; color: rgba(255,255,255,0.85); letter-spacing: 0.04em;
1470
+ padding: ${v("space-4")} ${v("space-2")} 6px;
901
1471
  background: linear-gradient(transparent, rgba(0,0,0,0.55));
902
1472
  `;
903
1473
  previewWrap.appendChild(previewImg);
@@ -907,23 +1477,23 @@ function buildImagePopup(key, anchorEl, onClose) {
907
1477
  const dropZone = document.createElement("label");
908
1478
  dropZone.style.cssText = `
909
1479
  display: flex; flex-direction: column; align-items: center; justify-content: center;
910
- gap: 8px;
911
- border: 1.5px dashed rgba(255,255,255,0.1); border-radius: 10px;
912
- padding: 24px 20px;
1480
+ gap: ${v("space-2")};
1481
+ border: 1.5px dashed ${v("border-strong")}; border-radius: ${v("radius")};
1482
+ padding: ${v("space-6")} ${v("space-5")};
913
1483
  cursor: pointer;
914
- transition: border-color 0.18s, background 0.18s;
915
- background: rgba(255,255,255,0.015);
1484
+ transition: border-color ${v("duration-fast")} ${v("ease")}, background ${v("duration-fast")} ${v("ease")};
1485
+ background: ${v("surface-raised")};
916
1486
  `;
917
1487
  const uploadIcon = document.createElement("div");
918
- uploadIcon.style.cssText = `color: rgba(255,255,255,0.35); transition: color 0.18s;`;
1488
+ uploadIcon.style.cssText = `color: ${v("fg-muted")}; transition: color ${v("duration-fast")} ${v("ease")};`;
919
1489
  uploadIcon.innerHTML = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
920
1490
  <path d="M12 15V3m0 0L8 7m4-4l4 4M2 17l.621 2.485A2 2 0 004.561 21h14.878a2 2 0 001.94-1.515L22 17"/>
921
1491
  </svg>`;
922
1492
  const dropText = document.createElement("div");
923
1493
  dropText.style.cssText = `text-align: center;`;
924
1494
  dropText.innerHTML = `
925
- <div style="font-size:12px;font-weight:500;color:rgba(255,255,255,0.5);">Drop an image</div>
926
- <div style="font-size:11px;color:rgba(255,255,255,0.25);margin-top:2px;">or click to browse</div>
1495
+ <div style="font-size:${v("text-sm")};font-weight:500;color:${v("fg-muted")};">Drop an image</div>
1496
+ <div style="font-size:${v("text-xs")};color:${v("fg-faint")};margin-top:2px;">or click to browse</div>
927
1497
  `;
928
1498
  dropZone.appendChild(uploadIcon);
929
1499
  dropZone.appendChild(dropText);
@@ -933,18 +1503,18 @@ function buildImagePopup(key, anchorEl, onClose) {
933
1503
  fileInput.style.display = "none";
934
1504
  dropZone.appendChild(fileInput);
935
1505
  const statusWrap = document.createElement("div");
936
- statusWrap.style.cssText = `margin-top: 8px; min-height: 18px;`;
1506
+ statusWrap.style.cssText = `margin-top: ${v("space-2")}; min-height: 18px;`;
937
1507
  const statusMsg = document.createElement("p");
938
- statusMsg.style.cssText = `font-size: 11px; color: rgba(255,255,255,0.35); margin: 0; text-align: center; transition: color 0.2s;`;
1508
+ statusMsg.style.cssText = `font-size: ${v("text-xs")}; color: ${v("fg-muted")}; margin: 0; text-align: center; transition: color ${v("duration")} ${v("ease")};`;
939
1509
  const progressBar = document.createElement("div");
940
1510
  progressBar.style.cssText = `
941
- height: 2px; border-radius: 2px; background: rgba(255,255,255,0.05);
1511
+ height: 2px; border-radius: 2px; background: ${v("surface-raised")};
942
1512
  overflow: hidden; margin-top: 6px; display: none;
943
1513
  `;
944
1514
  const progressFill = document.createElement("div");
945
1515
  progressFill.style.cssText = `
946
- height: 100%; border-radius: 2px; background: ${accent2()};
947
- width: 0%; transition: width 0.4s cubic-bezier(0.16, 1, 0.3, 1);
1516
+ height: 100%; border-radius: 2px; background: ${v("accent")};
1517
+ width: 0%; transition: width ${v("duration-slow")} ${v("ease-out")};
948
1518
  `;
949
1519
  progressBar.appendChild(progressFill);
950
1520
  statusWrap.appendChild(statusMsg);
@@ -952,20 +1522,20 @@ function buildImagePopup(key, anchorEl, onClose) {
952
1522
  const handleFile = async (file) => {
953
1523
  if (!file.type.startsWith("image/")) {
954
1524
  statusMsg.textContent = "Only image files are supported";
955
- statusMsg.style.color = "#f87171";
1525
+ statusMsg.style.color = v("danger");
956
1526
  return;
957
1527
  }
958
1528
  const maxMb = 10;
959
1529
  if (file.size > maxMb * 1024 * 1024) {
960
1530
  statusMsg.textContent = `File too large (max ${maxMb}MB)`;
961
- statusMsg.style.color = "#f87171";
1531
+ statusMsg.style.color = v("danger");
962
1532
  return;
963
1533
  }
964
- dropZone.style.borderColor = `${accent2()}55`;
965
- dropZone.style.background = `${accent2()}0a`;
966
- uploadIcon.style.color = accent2();
1534
+ dropZone.style.borderColor = v("accent-ring");
1535
+ dropZone.style.background = v("accent-soft");
1536
+ uploadIcon.style.color = v("accent");
967
1537
  statusMsg.textContent = "Uploading\u2026";
968
- statusMsg.style.color = "rgba(255,255,255,0.45)";
1538
+ statusMsg.style.color = v("fg-muted");
969
1539
  progressBar.style.display = "block";
970
1540
  progressFill.style.width = "0%";
971
1541
  try {
@@ -989,16 +1559,16 @@ function buildImagePopup(key, anchorEl, onClose) {
989
1559
  }
990
1560
  setTimeout(() => {
991
1561
  statusMsg.textContent = "Done";
992
- statusMsg.style.color = "#4ade80";
1562
+ statusMsg.style.color = v("success");
993
1563
  setTimeout(onClose, 600);
994
1564
  }, 200);
995
1565
  } catch {
996
1566
  progressBar.style.display = "none";
997
1567
  statusMsg.textContent = "Upload failed \u2014 try again";
998
- statusMsg.style.color = "#f87171";
999
- dropZone.style.borderColor = "rgba(255,255,255,0.1)";
1000
- dropZone.style.background = "rgba(255,255,255,0.015)";
1001
- uploadIcon.style.color = "rgba(255,255,255,0.35)";
1568
+ statusMsg.style.color = v("danger");
1569
+ dropZone.style.borderColor = v("border-strong");
1570
+ dropZone.style.background = v("surface-raised");
1571
+ uploadIcon.style.color = v("fg-muted");
1002
1572
  }
1003
1573
  };
1004
1574
  fileInput.addEventListener("change", () => {
@@ -1008,35 +1578,35 @@ function buildImagePopup(key, anchorEl, onClose) {
1008
1578
  e.preventDefault();
1009
1579
  if (!dragover) {
1010
1580
  dragover = true;
1011
- dropZone.style.borderColor = `${accent2()}88`;
1012
- dropZone.style.background = `${accent2()}0d`;
1013
- uploadIcon.style.color = accent2();
1581
+ dropZone.style.borderColor = v("accent-ring");
1582
+ dropZone.style.background = v("accent-soft");
1583
+ uploadIcon.style.color = v("accent");
1014
1584
  }
1015
1585
  });
1016
1586
  dropZone.addEventListener("dragleave", () => {
1017
1587
  dragover = false;
1018
- dropZone.style.borderColor = "rgba(255,255,255,0.1)";
1019
- dropZone.style.background = "rgba(255,255,255,0.015)";
1020
- uploadIcon.style.color = "rgba(255,255,255,0.35)";
1588
+ dropZone.style.borderColor = v("border-strong");
1589
+ dropZone.style.background = v("surface-raised");
1590
+ uploadIcon.style.color = v("fg-muted");
1021
1591
  });
1022
1592
  dropZone.addEventListener("drop", (e) => {
1023
1593
  e.preventDefault();
1024
1594
  dragover = false;
1025
- dropZone.style.borderColor = "rgba(255,255,255,0.1)";
1026
- dropZone.style.background = "rgba(255,255,255,0.015)";
1595
+ dropZone.style.borderColor = v("border-strong");
1596
+ dropZone.style.background = v("surface-raised");
1027
1597
  const file = e.dataTransfer?.files[0];
1028
1598
  if (file) handleFile(file);
1029
1599
  });
1030
1600
  dropZone.addEventListener("mouseenter", () => {
1031
1601
  if (!dragover) {
1032
- dropZone.style.borderColor = "rgba(255,255,255,0.18)";
1033
- dropZone.style.background = "rgba(255,255,255,0.03)";
1602
+ dropZone.style.borderColor = v("border-strong");
1603
+ dropZone.style.background = v("surface-hover");
1034
1604
  }
1035
1605
  });
1036
1606
  dropZone.addEventListener("mouseleave", () => {
1037
1607
  if (!dragover) {
1038
- dropZone.style.borderColor = "rgba(255,255,255,0.1)";
1039
- dropZone.style.background = "rgba(255,255,255,0.015)";
1608
+ dropZone.style.borderColor = v("border-strong");
1609
+ dropZone.style.background = v("surface-raised");
1040
1610
  }
1041
1611
  });
1042
1612
  wrap.appendChild(dropZone);
@@ -1047,67 +1617,246 @@ function makeCloseButton(onClose) {
1047
1617
  const btn = document.createElement("button");
1048
1618
  btn.style.cssText = `
1049
1619
  display: flex; align-items: center; justify-content: center;
1050
- width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;
1051
- background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.06);
1052
- cursor: pointer; color: rgba(255,255,255,0.35); padding: 0;
1053
- transition: background 0.15s, color 0.15s;
1620
+ width: 24px; height: 24px; border-radius: ${v("radius-sm")}; flex-shrink: 0;
1621
+ background: ${v("surface-raised")}; border: 1px solid ${v("border")};
1622
+ cursor: pointer; color: ${v("fg-muted")}; padding: 0;
1623
+ transition: background ${v("duration-fast")} ${v("ease")}, color ${v("duration-fast")} ${v("ease")};
1054
1624
  `;
1055
1625
  btn.innerHTML = `<svg width="9" height="9" viewBox="0 0 10 10" fill="none">
1056
1626
  <path d="M1 1l8 8M9 1L1 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
1057
1627
  </svg>`;
1058
- btn.addEventListener("mouseenter", () => {
1059
- btn.style.background = "rgba(255,255,255,0.08)";
1060
- btn.style.color = "rgba(255,255,255,0.75)";
1061
- });
1062
- btn.addEventListener("mouseleave", () => {
1063
- btn.style.background = "rgba(255,255,255,0.04)";
1064
- btn.style.color = "rgba(255,255,255,0.35)";
1065
- });
1628
+ attachHover(btn, { color: v("fg") });
1066
1629
  btn.addEventListener("click", onClose);
1067
1630
  return btn;
1068
1631
  }
1069
- function makePrimaryButton(label, color) {
1632
+ function makePrimaryButton(label2, color) {
1070
1633
  const btn = document.createElement("button");
1071
- btn.textContent = label;
1634
+ markUi(btn);
1635
+ btn.textContent = label2;
1072
1636
  btn.style.cssText = `
1073
- padding: 7px 16px; border-radius: 8px; border: none; cursor: pointer;
1074
- background: #fff; color: #0c0c0e;
1075
- font-size: 12px; font-weight: 600; letter-spacing: 0.01em;
1076
- transition: opacity 0.15s, transform 0.1s cubic-bezier(0.2, 0, 0, 1);
1637
+ padding: 7px ${v("space-4")}; border-radius: ${v("radius-sm")}; border: none; cursor: pointer;
1638
+ background: ${v("accent")}; color: ${v("accent-fg")};
1639
+ font-size: ${v("text-sm")}; font-weight: 600; letter-spacing: 0.01em;
1640
+ transition: opacity ${v("duration-fast")} ${v("ease")}, transform ${v("duration-fast")} ${v("ease")};
1077
1641
  `;
1078
1642
  btn.addEventListener("mouseenter", () => btn.style.opacity = "0.88");
1079
1643
  btn.addEventListener("mouseleave", () => btn.style.opacity = "1");
1644
+ attachPress(btn);
1080
1645
  return btn;
1081
1646
  }
1082
1647
  function applyPopupStyles(el) {
1648
+ markUi(el);
1083
1649
  el.style.cssText = `
1650
+ ${surface(2)}
1084
1651
  position: absolute;
1085
- z-index: 2147483646;
1652
+ z-index: ${v("z-panel")};
1086
1653
  width: 320px;
1087
- background: rgba(14, 14, 16, 0.97);
1088
- backdrop-filter: blur(24px) saturate(180%);
1089
- -webkit-backdrop-filter: blur(24px) saturate(180%);
1090
- border: 1px solid rgba(255,255,255,0.07);
1091
- border-radius: 14px;
1654
+ box-shadow: ${v("shadow-lg")};
1092
1655
  padding: 14px;
1093
- box-shadow: 0 0 0 1px rgba(0,0,0,0.4), 0 8px 24px rgba(0,0,0,0.5), 0 24px 64px rgba(0,0,0,0.4);
1094
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1095
- animation: cancia-popup-in 0.25s cubic-bezier(0.16, 1, 0.3, 1) both;
1656
+ font-family: ${v("font")};
1657
+ opacity: 0;
1658
+ transform: scale(0.93);
1659
+ transition: opacity ${v("duration")} ${v("ease")}, transform ${v("duration")} ${v("ease")};
1096
1660
  `;
1097
1661
  }
1662
+ function buildRichPopup(key, anchorEl, onClose) {
1663
+ const langs = state.config?.languages ?? ["en"];
1664
+ let activeLang = state.activeLang || langs[0];
1665
+ const wrap = document.createElement("div");
1666
+ wrap.dataset.canciaPopup = "1";
1667
+ applyPopupStyles(wrap);
1668
+ wrap.style.width = "460px";
1669
+ wrap.appendChild(buildHeader(key, onClose));
1670
+ const rowsWrap = document.createElement("div");
1671
+ rowsWrap.style.cssText = `display: flex; flex-direction: column; gap: ${v("space-2")}; max-height: 46vh; overflow-y: auto;`;
1672
+ wrap.appendChild(rowsWrap);
1673
+ let rows = [];
1674
+ function initialRows() {
1675
+ const stored = getValue(key, activeLang);
1676
+ if (stored) {
1677
+ const blocks = parseRichValue(stored);
1678
+ if (blocks && blocks.length) return portableTextToRows(blocks);
1679
+ if (blocks && blocks.length === 0) return [{ text: "", style: "normal" }];
1680
+ }
1681
+ const authored = domToRows(anchorEl);
1682
+ return authored.length ? authored : [{ text: "", style: "normal" }];
1683
+ }
1684
+ function makeRow(initial) {
1685
+ const row = document.createElement("div");
1686
+ row.style.cssText = `display: flex; gap: 6px; align-items: flex-start;`;
1687
+ const main = document.createElement("div");
1688
+ main.style.cssText = `flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 5px;`;
1689
+ const ta = document.createElement("textarea");
1690
+ ta.value = initial.text;
1691
+ ta.rows = 2;
1692
+ ta.placeholder = "Text \u2014 **bold**, *italic*, [label](https://\u2026)";
1693
+ ta.style.cssText = `
1694
+ ${input()}
1695
+ border-radius: ${v("radius")};
1696
+ padding: 8px 10px;
1697
+ font-family: inherit; resize: vertical;
1698
+ min-height: 46px; line-height: 1.55;
1699
+ `;
1700
+ attachInputFocus(ta);
1701
+ ta.addEventListener("input", commit);
1702
+ const controls = document.createElement("div");
1703
+ controls.style.cssText = `display: flex; gap: 6px;`;
1704
+ const styleSel = document.createElement("select");
1705
+ styleSel.style.cssText = `${input()} width: auto; flex: 1; padding: 4px 8px; font-size: ${v("text-xs")}; cursor: pointer;`;
1706
+ for (const [value, labelText] of [
1707
+ ["normal", "Normal"],
1708
+ ["h2", "Heading 2"],
1709
+ ["h3", "Heading 3"],
1710
+ ["blockquote", "Quote"]
1711
+ ]) {
1712
+ const o = document.createElement("option");
1713
+ o.value = value;
1714
+ o.textContent = labelText;
1715
+ if (initial.style === value) o.selected = true;
1716
+ styleSel.appendChild(o);
1717
+ }
1718
+ styleSel.addEventListener("change", commit);
1719
+ const listSel = document.createElement("select");
1720
+ listSel.style.cssText = styleSel.style.cssText;
1721
+ for (const [value, labelText] of [
1722
+ ["", "No list"],
1723
+ ["bullet", "Bulleted"],
1724
+ ["number", "Numbered"]
1725
+ ]) {
1726
+ const o = document.createElement("option");
1727
+ o.value = value;
1728
+ o.textContent = labelText;
1729
+ if ((initial.listItem ?? "") === value) o.selected = true;
1730
+ listSel.appendChild(o);
1731
+ }
1732
+ listSel.addEventListener("change", commit);
1733
+ controls.appendChild(styleSel);
1734
+ controls.appendChild(listSel);
1735
+ main.appendChild(ta);
1736
+ main.appendChild(controls);
1737
+ const removeBtn = document.createElement("button");
1738
+ removeBtn.type = "button";
1739
+ removeBtn.textContent = "\xD7";
1740
+ removeBtn.title = "Remove this block";
1741
+ removeBtn.style.cssText = `
1742
+ ${button("ghost")}
1743
+ flex-shrink: 0; height: auto; padding: 6px 9px;
1744
+ font-size: 15px; line-height: 1; color: ${v("fg-faint")};
1745
+ `;
1746
+ attachHover(removeBtn);
1747
+ removeBtn.addEventListener("click", () => {
1748
+ if (rows.length === 1) {
1749
+ ta.value = "";
1750
+ commit();
1751
+ return;
1752
+ }
1753
+ rows = rows.filter((r) => r.el !== row);
1754
+ row.remove();
1755
+ commit();
1756
+ });
1757
+ row.appendChild(main);
1758
+ row.appendChild(removeBtn);
1759
+ return {
1760
+ el: row,
1761
+ read: () => {
1762
+ const listItem = listSel.value;
1763
+ return {
1764
+ text: ta.value,
1765
+ style: styleSel.value,
1766
+ ...listItem ? { listItem } : {}
1767
+ };
1768
+ }
1769
+ };
1770
+ }
1771
+ function commit() {
1772
+ const docRows = rows.map((r) => r.read());
1773
+ const meaningful = docRows.filter((r) => r.text.trim() !== "");
1774
+ const blocks = meaningful.length ? rowsToPortableText(meaningful) : [];
1775
+ setPending(key, activeLang, serializeRichValue(blocks));
1776
+ onPendingChange();
1777
+ renderRichToDom(anchorEl, blocks);
1778
+ }
1779
+ function renderRows() {
1780
+ rowsWrap.replaceChildren();
1781
+ rows = initialRows().map(makeRow);
1782
+ for (const r of rows) rowsWrap.appendChild(r.el);
1783
+ }
1784
+ if (langs.length > 1) {
1785
+ const tabs = document.createElement("div");
1786
+ tabs.style.cssText = `display: flex; gap: 0; margin-bottom: ${v("space-3")}; border-bottom: 1px solid ${v("border")};`;
1787
+ const renderTabs2 = () => {
1788
+ tabs.replaceChildren();
1789
+ for (const lang of langs) {
1790
+ const isActive = lang === activeLang;
1791
+ const tab = document.createElement("button");
1792
+ tab.textContent = lang.toUpperCase();
1793
+ tab.style.cssText = `
1794
+ padding: 5px 10px 6px; border: none; border-bottom: 2px solid;
1795
+ margin-bottom: -1px;
1796
+ font-size: ${v("text-xs")}; font-weight: 600; cursor: pointer; letter-spacing: 0.06em;
1797
+ background: transparent;
1798
+ border-bottom-color: ${isActive ? v("accent") : "transparent"};
1799
+ color: ${isActive ? v("fg-strong") : v("fg-faint")};
1800
+ `;
1801
+ tab.addEventListener("click", () => {
1802
+ commit();
1803
+ activeLang = lang;
1804
+ state.activeLang = lang;
1805
+ applyOverlay();
1806
+ renderTabs2();
1807
+ renderRows();
1808
+ });
1809
+ tabs.appendChild(tab);
1810
+ }
1811
+ };
1812
+ renderTabs2();
1813
+ wrap.insertBefore(tabs, rowsWrap);
1814
+ }
1815
+ renderRows();
1816
+ const footer = document.createElement("div");
1817
+ footer.dataset.canciaFooter = "1";
1818
+ footer.style.cssText = `display: flex; justify-content: space-between; align-items: center; gap: ${v("space-2")}; margin-top: 10px;`;
1819
+ const addBtn = document.createElement("button");
1820
+ addBtn.type = "button";
1821
+ addBtn.textContent = "+ Add block";
1822
+ addBtn.style.cssText = `${button("ghost")} font-size: ${v("text-xs")};`;
1823
+ attachHover(addBtn);
1824
+ attachPress(addBtn);
1825
+ addBtn.addEventListener("click", () => {
1826
+ const r = makeRow({ text: "", style: "normal" });
1827
+ rows.push(r);
1828
+ rowsWrap.appendChild(r.el);
1829
+ r.el.querySelector("textarea")?.focus();
1830
+ });
1831
+ const saveBtn = makePrimaryButton("Save", accent2());
1832
+ saveBtn.dataset.canciaSave = "1";
1833
+ saveBtn.title = "Save (\u2318S)";
1834
+ saveBtn.addEventListener("click", () => {
1835
+ commit();
1836
+ onClose();
1837
+ });
1838
+ footer.appendChild(addBtn);
1839
+ footer.appendChild(saveBtn);
1840
+ wrap.appendChild(footer);
1841
+ return wrap;
1842
+ }
1098
1843
  function openPopup(key, fieldType2, anchorEl, onClose) {
1099
1844
  closePopup();
1100
1845
  const done = () => {
1101
1846
  onClose();
1102
1847
  closePopup();
1103
1848
  };
1104
- const popup = fieldType2 === "image" ? buildImagePopup(key, anchorEl, done) : fieldType2 === "link" ? buildLinkPopup(key, anchorEl, done) : buildTextPopup(key, anchorEl, done);
1849
+ const popup = fieldType2 === "image" ? buildImagePopup(key, anchorEl, done) : fieldType2 === "link" ? buildLinkPopup(key, anchorEl, done) : fieldType2 === "richtext" ? buildRichPopup(key, anchorEl, done) : buildTextPopup(key, anchorEl, done);
1105
1850
  document.body.appendChild(popup);
1106
1851
  popupEl = popup;
1107
1852
  const { top, left, origin } = getPopupPosition(anchorEl);
1108
1853
  popup.style.top = `${top}px`;
1109
1854
  popup.style.left = `${left}px`;
1110
1855
  popup.style.transformOrigin = origin;
1856
+ requestAnimationFrame(() => {
1857
+ popup.style.opacity = "1";
1858
+ popup.style.transform = "scale(1)";
1859
+ });
1111
1860
  outsideListener = (e) => {
1112
1861
  if (!popup.contains(e.target)) {
1113
1862
  closePopup();
@@ -1142,17 +1891,17 @@ function closePopup() {
1142
1891
  if (popupEl) {
1143
1892
  const el = popupEl;
1144
1893
  popupEl = null;
1145
- el.style.animation = "none";
1146
- el.style.transition = "opacity 0.18s cubic-bezier(0.4, 0, 1, 1), transform 0.18s cubic-bezier(0.4, 0, 1, 1)";
1894
+ el.style.transition = `opacity ${v("duration-exit")} ${v("ease-out")}, transform ${v("duration-exit")} ${v("ease-out")}`;
1147
1895
  el.style.opacity = "0";
1148
- el.style.transform = "scale(0.96) translateY(3px)";
1896
+ el.style.transform = "scale(0.93)";
1149
1897
  setTimeout(() => el.remove(), 200);
1150
1898
  }
1151
1899
  }
1152
1900
 
1153
1901
  // src/list-panel.ts
1154
- var PANEL_Z = 2147483646;
1155
- var BACKDROP_Z = 2147483646;
1902
+ var PANEL_Z = v("z-panel");
1903
+ var TOOLBAR_RESERVE = "100px";
1904
+ var BACKDROP_Z = v("z-overlay");
1156
1905
  var panelEl = null;
1157
1906
  var backdropEl = null;
1158
1907
  var styleInjected2 = false;
@@ -1165,6 +1914,7 @@ var currentOnTranslateEntry = null;
1165
1914
  function injectStyles2() {
1166
1915
  if (styleInjected2) return;
1167
1916
  styleInjected2 = true;
1917
+ injectBaseStyles(state.config?.accentColor, state.config?.toolbarAccent === true);
1168
1918
  const s = document.createElement("style");
1169
1919
  s.textContent = `
1170
1920
  @keyframes cancia-panel-in {
@@ -1182,9 +1932,6 @@ function injectStyles2() {
1182
1932
  `;
1183
1933
  document.head.appendChild(s);
1184
1934
  }
1185
- function accent3() {
1186
- return state.config?.accentColor ?? "#6366f1";
1187
- }
1188
1935
  function escapeHtml(s) {
1189
1936
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1190
1937
  }
@@ -1230,9 +1977,9 @@ function derivePreview(schema, data) {
1230
1977
  let thumbnail = "";
1231
1978
  for (const f of schema.fields) {
1232
1979
  if (f.widget !== "image") continue;
1233
- const v = data[f.name];
1234
- if (typeof v === "string" && v.trim()) {
1235
- thumbnail = v.trim();
1980
+ const v2 = data[f.name];
1981
+ if (typeof v2 === "string" && v2.trim()) {
1982
+ thumbnail = v2.trim();
1236
1983
  break;
1237
1984
  }
1238
1985
  }
@@ -1244,52 +1991,73 @@ function locales() {
1244
1991
  function buildShell(schema) {
1245
1992
  const panel = document.createElement("div");
1246
1993
  panel.dataset.canciaListPanel = "1";
1994
+ markUi(panel);
1247
1995
  panel.style.cssText = `
1996
+ ${surface(2)}
1248
1997
  position: fixed;
1249
1998
  top: 0; right: 0; bottom: 0;
1999
+ padding-bottom: ${TOOLBAR_RESERVE};
1250
2000
  width: min(420px, 100vw);
1251
- background: #fff;
1252
- color: #1a1a1d;
2001
+ color: ${v("fg")};
1253
2002
  z-index: ${PANEL_Z};
1254
- box-shadow: -8px 0 32px rgba(0,0,0,0.18);
2003
+ border: 0;
2004
+ border-left: 1px solid ${v("border")};
2005
+ border-radius: 0;
2006
+ box-shadow: ${v("shadow-lg")};
1255
2007
  display: flex;
1256
2008
  flex-direction: column;
1257
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1258
- animation: cancia-panel-in 0.22s cubic-bezier(0.16, 1, 0.3, 1) forwards;
2009
+ font-family: ${v("font")};
2010
+ /* The fixed positioning above also makes this the containing block for the
2011
+ absolutely-positioned list view and the form view that slides in over
2012
+ it; overflow:hidden clips both while they are off-stage. */
2013
+ overflow: hidden;
2014
+ animation: cancia-panel-in ${v("duration")} ${v("ease")} forwards;
1259
2015
  `;
1260
- panel.innerHTML = `
2016
+ const listView = document.createElement("div");
2017
+ listView.dataset.canciaListView = "1";
2018
+ listView.style.cssText = `
2019
+ position: absolute; inset: 0;
2020
+ display: flex; flex-direction: column;
2021
+ transition: transform ${v("duration")} ${v("ease")}, opacity ${v("duration")} ${v("ease")};
2022
+ `;
2023
+ listView.innerHTML = `
1261
2024
  <header style="
1262
2025
  display: flex; align-items: center; justify-content: space-between;
1263
- padding: 16px 20px;
1264
- border-bottom: 1px solid #eaeaea;
2026
+ padding: ${v("space-4")} ${v("space-5")};
2027
+ border-bottom: 1px solid ${v("border")};
1265
2028
  ">
1266
2029
  <div>
1267
- <div style="font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: #777;">List</div>
1268
- <div style="font-size: 17px; font-weight: 600; margin-top: 2px;">${escapeHtml(schema.label)}</div>
2030
+ <div style="font-size: ${v("text-xs")}; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: ${v("fg-muted")};">List</div>
2031
+ <div style="font-size: 17px; font-weight: 600; margin-top: 2px; color: ${v("fg-strong")};">${escapeHtml(schema.label)}</div>
1269
2032
  </div>
1270
2033
  <button data-cancia-close style="
2034
+ ${iconButton()}
1271
2035
  appearance: none; border: 0; background: transparent;
1272
- cursor: pointer; padding: 6px; border-radius: 6px;
1273
- color: #555; transition: background 0.12s, color 0.12s;
2036
+ cursor: pointer;
1274
2037
  " aria-label="Close panel">
1275
2038
  <svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1276
2039
  <path d="M4 4l10 10M14 4L4 14"/>
1277
2040
  </svg>
1278
2041
  </button>
1279
2042
  </header>
2043
+ <!-- No overflow-x here. This row holds one short tab per configured
2044
+ locale \u2014 two or three at most \u2014 so a scroll container was solving a
2045
+ problem that does not occur, while permanently costing a scrollbar
2046
+ gutter and (on Windows, where scrollbars are not overlaid) a visible
2047
+ bar under the tabs. If a site ever ships enough locales to overflow,
2048
+ wrapping is the right answer, not scrolling. -->
1280
2049
  <div data-cancia-tabs style="
1281
- display: flex; gap: 4px;
1282
- padding: 8px 16px 0;
1283
- border-bottom: 1px solid #f3f3f3;
1284
- overflow-x: auto;
2050
+ display: flex; flex-wrap: wrap; gap: ${v("space-1")};
2051
+ padding: ${v("space-2")} ${v("space-4")} 0;
2052
+ border-bottom: 1px solid ${v("border")};
1285
2053
  "></div>
1286
- <div style="padding: 12px 20px; border-bottom: 1px solid #f3f3f3;">
2054
+ <div style="padding: ${v("space-3")} ${v("space-5")}; border-bottom: 1px solid ${v("border")};">
1287
2055
  <button data-cancia-add style="
1288
- appearance: none; border: 1px dashed ${accent3()}; background: ${accent3()}10;
1289
- color: ${accent3()}; font-weight: 600; font-size: 13px;
1290
- padding: 10px 14px; border-radius: 8px; width: 100%; cursor: pointer;
2056
+ ${button("ghost")}
2057
+ appearance: none; border: 1px dashed ${v("border-strong")}; background: ${v("accent-soft")};
2058
+ color: ${v("accent")}; font-weight: 600; font-size: ${v("text-base")};
2059
+ padding: 10px 14px; height: auto; width: 100%; cursor: pointer;
1291
2060
  display: flex; align-items: center; justify-content: center; gap: 6px;
1292
- transition: background 0.12s;
1293
2061
  ">
1294
2062
  <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1295
2063
  <path d="M7 2v10M2 7h10"/>
@@ -1299,13 +2067,18 @@ function buildShell(schema) {
1299
2067
  </div>
1300
2068
  <div data-cancia-entries style="
1301
2069
  flex: 1; overflow-y: auto;
1302
- padding: 8px 12px 16px;
2070
+ padding: ${v("space-2")} ${v("space-3")} ${v("space-4")};
1303
2071
  ">
1304
- <div data-cancia-loading style="text-align: center; padding: 32px 12px; color: #888; font-size: 13px;">Loading\u2026</div>
2072
+ <div data-cancia-loading style="text-align: center; padding: 32px ${v("space-3")}; color: ${v("fg-muted")}; font-size: ${v("text-base")};">Loading\u2026</div>
1305
2073
  </div>
1306
2074
  `;
1307
- const body = panel.querySelector("[data-cancia-entries]");
1308
- const tabsRow = panel.querySelector("[data-cancia-tabs]");
2075
+ panel.appendChild(listView);
2076
+ const body = listView.querySelector("[data-cancia-entries]");
2077
+ const tabsRow = listView.querySelector("[data-cancia-tabs]");
2078
+ const closeBtn = listView.querySelector("[data-cancia-close]");
2079
+ if (closeBtn) attachHover(closeBtn, { bg: v("surface-hover"), color: v("fg-strong") });
2080
+ const addBtn = listView.querySelector("[data-cancia-add]");
2081
+ if (addBtn) attachHover(addBtn, { bg: v("accent-soft") });
1309
2082
  return { panel, body, tabsRow };
1310
2083
  }
1311
2084
  function renderTabs(tabsRow, activeLocale, onSwitch) {
@@ -1321,22 +2094,22 @@ function renderTabs(tabsRow, activeLocale, onSwitch) {
1321
2094
  const isActive = loc === activeLocale;
1322
2095
  tab.style.cssText = `
1323
2096
  appearance: none; border: 0; background: transparent;
1324
- font-family: inherit; font-size: 12px; font-weight: 600;
2097
+ font-family: inherit; font-size: ${v("text-sm")}; font-weight: 600;
1325
2098
  letter-spacing: 0.04em; text-transform: uppercase;
1326
- padding: 8px 10px 9px;
2099
+ padding: ${v("space-2")} 10px 9px;
1327
2100
  cursor: ${isActive ? "default" : "pointer"};
1328
- color: ${isActive ? accent3() : "#777"};
1329
- border-bottom: 2px solid ${isActive ? accent3() : "transparent"};
2101
+ color: ${isActive ? v("accent") : v("fg-muted")};
2102
+ border-bottom: 2px solid ${isActive ? v("accent") : "transparent"};
1330
2103
  margin-bottom: -1px;
1331
2104
  transition: color 0.12s, border-color 0.12s;
1332
2105
  `;
1333
2106
  tab.textContent = loc;
1334
2107
  if (!isActive) {
1335
2108
  tab.addEventListener("mouseenter", () => {
1336
- tab.style.color = "#333";
2109
+ tab.style.color = v("fg-strong");
1337
2110
  });
1338
2111
  tab.addEventListener("mouseleave", () => {
1339
- tab.style.color = "#777";
2112
+ tab.style.color = v("fg-muted");
1340
2113
  });
1341
2114
  tab.addEventListener("click", () => onSwitch(loc));
1342
2115
  }
@@ -1365,7 +2138,7 @@ function renderEntries(body, schema, activeLocale, entriesInLocale, translations
1365
2138
  });
1366
2139
  if (rows.length === 0) {
1367
2140
  body.innerHTML = `
1368
- <div style="text-align: center; padding: 40px 12px; color: #888; font-size: 13px;">
2141
+ <div style="text-align: center; padding: 40px ${v("space-3")}; color: ${v("fg-muted")}; font-size: ${v("text-base")};">
1369
2142
  No entries yet. Click "Add ${escapeHtml(schema.labelSingular.toLowerCase())}" above to create one.
1370
2143
  </div>
1371
2144
  `;
@@ -1391,7 +2164,7 @@ function renderEntries(body, schema, activeLocale, entriesInLocale, translations
1391
2164
  }
1392
2165
  const err = document.createElement("div");
1393
2166
  err.textContent = "Couldn't save the new order. Reverted.";
1394
- err.style.cssText = `text-align:center; padding:8px; color:#c0392b; font-size:12px;`;
2167
+ err.style.cssText = `text-align:center; padding:${v("space-2")}; color:${v("danger")}; font-size:${v("text-sm")};`;
1395
2168
  body.insertBefore(err, body.firstChild);
1396
2169
  setTimeout(() => err.remove(), 3e3);
1397
2170
  }
@@ -1399,7 +2172,7 @@ function renderEntries(body, schema, activeLocale, entriesInLocale, translations
1399
2172
  rows.forEach((row) => {
1400
2173
  const isStub = row.entry === null;
1401
2174
  const wrap = document.createElement("div");
1402
- wrap.style.cssText = `display: flex; align-items: stretch; gap: 4px; margin-bottom: 6px;`;
2175
+ wrap.style.cssText = `display: flex; align-items: stretch; gap: ${v("space-1")}; margin-bottom: 6px;`;
1403
2176
  const handle = document.createElement("div");
1404
2177
  handle.textContent = "\u22EE\u22EE";
1405
2178
  handle.title = "Drag to reorder";
@@ -1407,14 +2180,22 @@ function renderEntries(body, schema, activeLocale, entriesInLocale, translations
1407
2180
  handle.style.cssText = `
1408
2181
  cursor: grab; user-select: none;
1409
2182
  display: flex; align-items: center; justify-content: center;
1410
- color: #bbb; font-size: 13px; letter-spacing: -2px;
2183
+ color: ${v("fg-faint")}; font-size: ${v("text-base")}; letter-spacing: -2px;
1411
2184
  padding: 0 2px; flex-shrink: 0;
2185
+ transition: color 0.12s;
1412
2186
  `;
2187
+ handle.addEventListener("mouseenter", () => {
2188
+ handle.style.color = v("accent");
2189
+ });
2190
+ handle.addEventListener("mouseleave", () => {
2191
+ handle.style.color = v("fg-faint");
2192
+ });
1413
2193
  const item = document.createElement("button");
1414
2194
  item.style.cssText = `
1415
- appearance: none; border: 1px solid transparent; background: ${isStub ? "#fff8ee" : "#fafafa"};
2195
+ appearance: none; border: 1px solid transparent;
2196
+ background: ${isStub ? v("warning-soft") : v("surface-raised")};
1416
2197
  text-align: left; flex: 1; min-width: 0;
1417
- padding: 12px 14px; border-radius: 8px; cursor: pointer;
2198
+ padding: ${v("space-3")} 14px; border-radius: ${v("radius-sm")}; cursor: pointer;
1418
2199
  transition: background 0.12s, border-color 0.12s, transform 0.12s;
1419
2200
  display: block;
1420
2201
  `;
@@ -1458,27 +2239,36 @@ function renderEntries(body, schema, activeLocale, entriesInLocale, translations
1458
2239
  const titleValue = row.entry.data[schema.titleField];
1459
2240
  const title = typeof titleValue === "string" && titleValue.trim().length > 0 ? titleValue : `(untitled ${schema.labelSingular.toLowerCase()})`;
1460
2241
  const { subtitle, thumbnail } = derivePreview(schema, row.entry.data);
2242
+ const isDraftRow = !!schema.draftField && row.entry.data[schema.draftField] === true;
2243
+ const draftBadge = isDraftRow ? `<span style="
2244
+ flex-shrink: 0; margin-left: ${v("space-2")}; padding: 1px 6px;
2245
+ font-size: 11px; font-weight: 600; line-height: 1.5;
2246
+ border-radius: ${v("radius-sm")}; color: ${v("fg-muted")};
2247
+ background: ${v("surface-raised")}; border: 1px solid ${v("border")};
2248
+ ">Draft</span>` : "";
1461
2249
  const textCol = `
1462
2250
  <div style="min-width: 0; flex: 1;">
1463
- <div style="font-weight: 600; font-size: 14px; color: #1a1a1d; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${escapeHtml(title)}</div>
1464
- ${subtitle ? `<div style="font-size: 12px; color: #666; margin-top: 4px; line-height: 1.4;">${escapeHtml(subtitle)}</div>` : ""}
2251
+ <div style="display: flex; align-items: center;">
2252
+ <span style="font-weight: 600; font-size: 14px; color: ${v("fg-strong")}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${escapeHtml(title)}</span>${draftBadge}
2253
+ </div>
2254
+ ${subtitle ? `<div style="font-size: ${v("text-sm")}; color: ${v("fg-muted")}; margin-top: ${v("space-1")}; line-height: 1.4;">${escapeHtml(subtitle)}</div>` : ""}
1465
2255
  </div>
1466
2256
  `;
1467
2257
  const thumb = thumbnail ? `<img src="${escapeHtml(thumbnail)}" alt="" loading="lazy" style="
1468
2258
  width: 44px; height: 44px; flex-shrink: 0; object-fit: cover;
1469
- border-radius: 6px; background: #eee; border: 1px solid #eaeaea;
2259
+ border-radius: ${v("radius-sm")}; background: ${v("surface-raised")}; border: 1px solid ${v("border")};
1470
2260
  " onerror="this.style.display='none'" />` : "";
1471
2261
  item.innerHTML = `
1472
- <div style="display: flex; align-items: center; gap: 12px;">
2262
+ <div style="display: flex; align-items: center; gap: ${v("space-3")};">
1473
2263
  ${thumb}${textCol}
1474
2264
  </div>
1475
2265
  `;
1476
2266
  item.addEventListener("mouseenter", () => {
1477
- item.style.background = "#f3f3f3";
1478
- item.style.borderColor = "#e3e3e3";
2267
+ item.style.background = v("surface-hover");
2268
+ item.style.borderColor = v("border-strong");
1479
2269
  });
1480
2270
  item.addEventListener("mouseleave", () => {
1481
- item.style.background = "#fafafa";
2271
+ item.style.background = v("surface-raised");
1482
2272
  item.style.borderColor = "transparent";
1483
2273
  });
1484
2274
  item.addEventListener("click", () => currentOnEditEntry?.(row.entry, activeLocale));
@@ -1488,23 +2278,26 @@ function renderEntries(body, schema, activeLocale, entriesInLocale, translations
1488
2278
  const sourceTitleValue = sourceEntry?.data[schema.titleField];
1489
2279
  const sourceTitle = typeof sourceTitleValue === "string" && sourceTitleValue.trim().length > 0 ? sourceTitleValue : row.id;
1490
2280
  item.innerHTML = `
1491
- <div style="display: flex; align-items: center; gap: 8px;">
2281
+ <div style="display: flex; align-items: center; gap: ${v("space-2")};">
1492
2282
  <span style="
1493
2283
  font-size: 9px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
1494
- background: #f5b400; color: #fff;
2284
+ /* The light theme's warning token is a mid amber (#d97706), dark
2285
+ enough to carry WHITE text \u2014 so the chip now uses accent-fg and
2286
+ the near-black literal that used to live here is gone. */
2287
+ background: ${v("warning")}; color: ${v("accent-fg")};
1495
2288
  padding: 2px 6px; border-radius: 4px;
1496
2289
  ">Not translated</span>
1497
- <span style="font-size: 11px; color: #888;">from ${escapeHtml(sourceLocale)}</span>
2290
+ <span style="font-size: ${v("text-xs")}; color: ${v("fg-muted")};">from ${escapeHtml(sourceLocale)}</span>
1498
2291
  </div>
1499
- <div style="font-weight: 600; font-size: 14px; color: #6b5616; margin-top: 6px;">${escapeHtml(sourceTitle)}</div>
1500
- <div style="font-size: 11px; color: #b08800; margin-top: 4px;">Click to translate into ${escapeHtml(activeLocale)}</div>
2292
+ <div style="font-weight: 600; font-size: 14px; color: ${v("fg-strong")}; margin-top: 6px;">${escapeHtml(sourceTitle)}</div>
2293
+ <div style="font-size: ${v("text-xs")}; color: ${v("warning")}; margin-top: ${v("space-1")};">Click to translate into ${escapeHtml(activeLocale)}</div>
1501
2294
  `;
1502
2295
  item.addEventListener("mouseenter", () => {
1503
- item.style.background = "#fff2d5";
1504
- item.style.borderColor = "#f5d27a";
2296
+ item.style.background = "rgba(217, 119, 6, 0.18)";
2297
+ item.style.borderColor = v("warning");
1505
2298
  });
1506
2299
  item.addEventListener("mouseleave", () => {
1507
- item.style.background = "#fff8ee";
2300
+ item.style.background = v("warning-soft");
1508
2301
  item.style.borderColor = "transparent";
1509
2302
  });
1510
2303
  item.addEventListener(
@@ -1523,11 +2316,12 @@ async function openListPanel(opts) {
1523
2316
  injectStyles2();
1524
2317
  const backdrop = document.createElement("div");
1525
2318
  backdrop.dataset.canciaPanelBackdrop = "1";
2319
+ markUi(backdrop);
1526
2320
  backdrop.style.cssText = `
1527
2321
  position: fixed; inset: 0;
1528
- background: rgba(10,10,12,0.32);
2322
+ background: rgba(10,10,12,0.20);
1529
2323
  z-index: ${BACKDROP_Z};
1530
- animation: cancia-backdrop-in 0.18s ease-out forwards;
2324
+ animation: cancia-backdrop-in ${v("duration")} ${v("ease-out")} forwards;
1531
2325
  `;
1532
2326
  backdrop.addEventListener("click", () => closeListPanel());
1533
2327
  document.body.appendChild(backdrop);
@@ -1577,7 +2371,7 @@ async function refreshListPanel() {
1577
2371
  } catch (err) {
1578
2372
  if (!panelEl) return;
1579
2373
  body.innerHTML = `
1580
- <div style="text-align: center; padding: 32px 12px; color: #c0392b; font-size: 13px;">
2374
+ <div style="text-align: center; padding: 32px ${v("space-3")}; color: ${v("danger")}; font-size: ${v("text-base")};">
1581
2375
  Failed to load entries: ${escapeHtml(err instanceof Error ? err.message : String(err))}
1582
2376
  </div>
1583
2377
  `;
@@ -1585,7 +2379,7 @@ async function refreshListPanel() {
1585
2379
  }
1586
2380
  function closeListPanel() {
1587
2381
  if (panelEl) {
1588
- panelEl.style.animation = "cancia-panel-out 0.18s cubic-bezier(0.7, 0, 0.84, 0) forwards";
2382
+ panelEl.style.animation = `cancia-panel-out ${v("duration-exit")} ${v("ease-out")} forwards`;
1589
2383
  const el = panelEl;
1590
2384
  setTimeout(() => el.remove(), 180);
1591
2385
  panelEl = null;
@@ -1593,7 +2387,7 @@ function closeListPanel() {
1593
2387
  if (backdropEl) {
1594
2388
  const el = backdropEl;
1595
2389
  el.style.opacity = "0";
1596
- el.style.transition = "opacity 0.18s ease-out";
2390
+ el.style.transition = `opacity ${v("duration-exit")} ${v("ease-out")}`;
1597
2391
  setTimeout(() => el.remove(), 180);
1598
2392
  backdropEl = null;
1599
2393
  }
@@ -1607,65 +2401,146 @@ function closeListPanel() {
1607
2401
  function isListPanelOpen() {
1608
2402
  return panelEl !== null;
1609
2403
  }
2404
+ function listViewEl() {
2405
+ return panelEl?.querySelector("[data-cancia-list-view]") ?? null;
2406
+ }
2407
+ function pushPanelView(view) {
2408
+ const panel = panelEl;
2409
+ const list = listViewEl();
2410
+ if (!panel || !list) return false;
2411
+ view.style.position = "absolute";
2412
+ view.style.inset = "0";
2413
+ view.style.display = "flex";
2414
+ view.style.flexDirection = "column";
2415
+ view.style.background = v("surface-3");
2416
+ view.style.transform = "translateX(100%)";
2417
+ view.style.transition = `transform ${v("duration")} ${v("ease")}`;
2418
+ panel.appendChild(view);
2419
+ void view.offsetWidth;
2420
+ view.style.transform = "translateX(0)";
2421
+ list.style.transition = `transform ${v("duration")} ${v("ease")}, opacity ${v("duration")} ${v("ease")}`;
2422
+ void list.offsetWidth;
2423
+ list.style.transform = "translateX(-25%)";
2424
+ list.style.opacity = "0.4";
2425
+ list.setAttribute("aria-hidden", "true");
2426
+ list.style.pointerEvents = "none";
2427
+ return true;
2428
+ }
2429
+ function popPanelView(view) {
2430
+ const list = listViewEl();
2431
+ view.style.transition = `transform ${v("duration-exit")} ${v("ease-out")}`;
2432
+ view.style.transform = "translateX(100%)";
2433
+ if (list) {
2434
+ list.style.transition = `transform ${v("duration-exit")} ${v("ease-out")}, opacity ${v("duration-exit")} ${v("ease-out")}`;
2435
+ list.style.transform = "translateX(0)";
2436
+ list.style.opacity = "1";
2437
+ list.removeAttribute("aria-hidden");
2438
+ list.style.pointerEvents = "";
2439
+ }
2440
+ let removed = false;
2441
+ const done = () => {
2442
+ if (removed) return;
2443
+ removed = true;
2444
+ view.remove();
2445
+ };
2446
+ view.addEventListener("transitionend", done, { once: true });
2447
+ setTimeout(done, 400);
2448
+ }
1610
2449
 
1611
2450
  // src/entry-modal.ts
1612
2451
  import {
1613
- rowsToPortableText,
1614
- portableTextToRows,
1615
- portableTextSubsetSchema,
1616
- PT_STYLES
2452
+ rowsToPortableText as rowsToPortableText2,
2453
+ portableTextToRows as portableTextToRows2,
2454
+ portableTextSubsetSchema as portableTextSubsetSchema2,
2455
+ PT_STYLES as PT_STYLES2
1617
2456
  } from "@cancia/astro/richtext";
1618
2457
  import { slugify } from "@cancia/astro/schema";
1619
- var MODAL_Z = 2147483647;
1620
- var BACKDROP_Z2 = 2147483646;
2458
+ var MODAL_Z = v("z-bar");
2459
+ var BACKDROP_Z2 = v("z-panel");
1621
2460
  var modalEl = null;
1622
2461
  var backdropEl2 = null;
1623
2462
  var escListener = null;
1624
2463
  var styleInjected3 = false;
2464
+ var mountedInPanel = false;
1625
2465
  function injectStyles3() {
1626
2466
  if (styleInjected3) return;
1627
2467
  styleInjected3 = true;
2468
+ injectBaseStyles(state.config?.accentColor, state.config?.toolbarAccent === true);
1628
2469
  const s = document.createElement("style");
1629
2470
  s.textContent = `
1630
- @keyframes cancia-modal-in {
1631
- from { opacity: 0; transform: translate(-50%, calc(-50% + 8px)) scale(0.985); }
1632
- to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
1633
- }
1634
- @keyframes cancia-modal-out {
1635
- from { opacity: 1; transform: translate(-50%, -50%) scale(1); }
1636
- to { opacity: 0; transform: translate(-50%, calc(-50% + 8px)) scale(0.985); }
1637
- }
1638
2471
  .cancia-form-input:focus,
1639
2472
  .cancia-form-textarea:focus,
1640
2473
  .cancia-form-select:focus {
1641
- border-color: var(--cancia-accent-border, rgba(99,102,241,0.6));
1642
- background: rgba(255,255,255,0.05);
2474
+ border-color: var(--cancia-accent-border, ${v("accent-ring")});
2475
+ background: ${v("surface-hover")};
1643
2476
  outline: none;
1644
2477
  }
1645
2478
  .cancia-form-input::placeholder,
1646
2479
  .cancia-form-textarea::placeholder {
1647
- color: rgba(255,255,255,0.25);
2480
+ color: ${v("fg-faint")};
1648
2481
  }
1649
2482
  .cancia-form-select option {
1650
- background: #15151a;
1651
- color: rgba(255,255,255,0.9);
2483
+ /* An <option> is painted by the OS, so it cannot be translucent \u2014 this
2484
+ stays an opaque hex matching surface-3 (now white). */
2485
+ background: #ffffff;
2486
+ color: ${v("fg")};
1652
2487
  }
1653
2488
  .cancia-field-error {
1654
- color: #ff8786;
1655
- font-size: 11px;
2489
+ color: ${v("danger")};
2490
+ font-size: ${v("text-xs")};
1656
2491
  margin-top: 5px;
1657
2492
  line-height: 1.35;
1658
2493
  }
1659
2494
  `;
1660
2495
  document.head.appendChild(s);
1661
2496
  }
1662
- function accent4() {
1663
- return state.config?.accentColor ?? "#6366f1";
2497
+ function accent3() {
2498
+ const useAccent = state.config?.toolbarAccent === true;
2499
+ return (useAccent ? state.config?.accentColor : void 0) ?? "#18181b";
1664
2500
  }
1665
2501
  function accentBorder() {
1666
- const a = accent4();
2502
+ const a = accent3();
1667
2503
  if (/^#[0-9a-f]{6}$/i.test(a)) return `${a}99`;
1668
- return "rgba(99,102,241,0.6)";
2504
+ return "rgba(24,24,27,0.28)";
2505
+ }
2506
+ function attachRowReveal(row, targets) {
2507
+ const apply = () => {
2508
+ const on = row.dataset.canciaHover === "1" || row.contains(document.activeElement);
2509
+ for (const t of targets) {
2510
+ t.style.opacity = on ? "1" : "0";
2511
+ if (t.dataset.canciaCollapsible === "1") {
2512
+ t.style.maxHeight = on ? "40px" : "0";
2513
+ }
2514
+ }
2515
+ };
2516
+ row.addEventListener("pointerenter", () => {
2517
+ row.dataset.canciaHover = "1";
2518
+ apply();
2519
+ });
2520
+ row.addEventListener("pointerleave", () => {
2521
+ row.dataset.canciaHover = "0";
2522
+ apply();
2523
+ });
2524
+ row.addEventListener("focusin", apply);
2525
+ row.addEventListener("focusout", () => requestAnimationFrame(apply));
2526
+ apply();
2527
+ }
2528
+ function humanise(label2) {
2529
+ if (!label2) return label2;
2530
+ if (/\s/.test(label2)) return label2;
2531
+ const PROPER = {
2532
+ linkedin: "LinkedIn",
2533
+ github: "GitHub",
2534
+ youtube: "YouTube",
2535
+ tiktok: "TikTok",
2536
+ whatsapp: "WhatsApp",
2537
+ facebook: "Facebook",
2538
+ instagram: "Instagram"
2539
+ };
2540
+ const exact = PROPER[label2.toLowerCase()];
2541
+ if (exact) return exact;
2542
+ const spaced = label2.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim();
2543
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
1669
2544
  }
1670
2545
  function isoToLocalInput(iso) {
1671
2546
  if (!iso) return "";
@@ -1681,44 +2556,39 @@ function localInputToIso(local) {
1681
2556
  return d.toISOString();
1682
2557
  }
1683
2558
  var INPUT_BASE = `
1684
- width: 100%; box-sizing: border-box;
1685
- background: rgba(255,255,255,0.03);
1686
- color: rgba(255,255,255,0.9);
1687
- border: 1px solid rgba(255,255,255,0.07);
1688
- border-radius: 8px;
1689
- padding: 8px 11px;
1690
- font-size: 13px;
2559
+ ${input()}
2560
+ box-sizing: border-box;
1691
2561
  font-family: inherit;
1692
2562
  line-height: 1.5;
1693
- outline: none;
1694
- transition: border-color 0.18s, background 0.18s;
1695
2563
  `;
2564
+ var SELECT_CHEVRON = `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M2.5 3.75l2.5 2.5 2.5-2.5' stroke='%2371717a' stroke-width='1.3' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>")`;
1696
2565
  var MAX_FIELD_DEPTH = 6;
1697
2566
  function renderField(field, initial, depth = 0) {
1698
2567
  const wrapper = document.createElement("div");
1699
- wrapper.style.cssText = `margin-bottom: 14px;`;
2568
+ wrapper.style.cssText = `margin-bottom: ${v("space-4")};`;
1700
2569
  const labelRow = document.createElement("label");
1701
2570
  labelRow.style.cssText = `
2571
+ ${label()}
1702
2572
  display: flex; align-items: baseline; justify-content: space-between;
1703
- gap: 8px;
1704
- font-size: 11px; font-weight: 600;
1705
- color: rgba(255,255,255,0.75);
1706
- letter-spacing: 0.04em;
2573
+ gap: ${v("space-2")};
1707
2574
  margin-bottom: 5px;
1708
2575
  `;
1709
2576
  const labelText = document.createElement("span");
1710
- labelText.textContent = field.label;
1711
- if (field.required) {
1712
- const star = document.createElement("span");
1713
- star.textContent = " *";
1714
- star.style.color = "rgba(255,135,134,0.8)";
1715
- labelText.appendChild(star);
1716
- }
2577
+ labelText.textContent = humanise(field.label);
1717
2578
  labelRow.appendChild(labelText);
2579
+ if (!field.required) {
2580
+ const opt = document.createElement("span");
2581
+ opt.textContent = "Optional";
2582
+ opt.style.cssText = `
2583
+ font-size: ${v("text-xs")}; font-weight: 400;
2584
+ color: ${v("fg-faint")}; text-transform: none; letter-spacing: normal;
2585
+ `;
2586
+ labelRow.appendChild(opt);
2587
+ }
1718
2588
  if (field.label) wrapper.appendChild(labelRow);
1719
2589
  if (field.description) {
1720
2590
  const help = document.createElement("div");
1721
- help.style.cssText = `font-size: 11px; color: rgba(255,255,255,0.35); margin-bottom: 6px; line-height: 1.4;`;
2591
+ help.style.cssText = `${hint()} margin-bottom: 6px;`;
1722
2592
  help.textContent = field.description;
1723
2593
  wrapper.appendChild(help);
1724
2594
  }
@@ -1769,7 +2639,7 @@ function renderField(field, initial, depth = 0) {
1769
2639
  case "textarea": {
1770
2640
  const ta = document.createElement("textarea");
1771
2641
  ta.className = "cancia-form-textarea";
1772
- ta.style.cssText = `${INPUT_BASE} resize: vertical; min-height: 110px; max-height: 320px; caret-color: ${accent4()};`;
2642
+ ta.style.cssText = `${INPUT_BASE} resize: vertical; min-height: 110px; max-height: 320px; caret-color: ${v("accent")};`;
1773
2643
  ta.rows = 5;
1774
2644
  if (field.placeholder) ta.placeholder = field.placeholder;
1775
2645
  if (typeof initial === "string") ta.value = initial;
@@ -1782,10 +2652,10 @@ function renderField(field, initial, depth = 0) {
1782
2652
  row.style.cssText = `display: flex; align-items: center; gap: 9px; cursor: pointer; user-select: none; padding: 6px 0;`;
1783
2653
  const cb = document.createElement("input");
1784
2654
  cb.type = "checkbox";
1785
- cb.style.cssText = `width: 16px; height: 16px; accent-color: ${accent4()};`;
2655
+ cb.style.cssText = `width: 16px; height: 16px; accent-color: ${v("accent")};`;
1786
2656
  if (initial === true) cb.checked = true;
1787
2657
  const txt = document.createElement("span");
1788
- txt.style.cssText = `font-size: 13px; color: rgba(255,255,255,0.7);`;
2658
+ txt.style.cssText = `font-size: ${v("text-base")}; color: ${v("fg")};`;
1789
2659
  txt.textContent = field.placeholder ?? `Enable ${field.label.toLowerCase()}`;
1790
2660
  row.appendChild(cb);
1791
2661
  row.appendChild(txt);
@@ -1796,7 +2666,7 @@ function renderField(field, initial, depth = 0) {
1796
2666
  case "select": {
1797
2667
  const sel = document.createElement("select");
1798
2668
  sel.className = "cancia-form-select";
1799
- sel.style.cssText = `${INPUT_BASE} appearance: none; padding-right: 30px; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M2.5 3.75l2.5 2.5 2.5-2.5' stroke='rgba(255,255,255,0.4)' stroke-width='1.3' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>"); background-repeat: no-repeat; background-position: right 10px center; cursor: pointer;`;
2669
+ sel.style.cssText = `${INPUT_BASE} appearance: none; padding-right: 30px; background-image: ${SELECT_CHEVRON}; background-repeat: no-repeat; background-position: right 10px center; cursor: pointer;`;
1800
2670
  if (!field.required) {
1801
2671
  const empty = document.createElement("option");
1802
2672
  empty.value = "";
@@ -1820,19 +2690,19 @@ function renderField(field, initial, depth = 0) {
1820
2690
  const container = document.createElement("div");
1821
2691
  container.style.cssText = `
1822
2692
  display: flex; gap: 10px; align-items: stretch;
1823
- background: rgba(255,255,255,0.02);
1824
- border: 1px dashed rgba(255,255,255,0.1);
1825
- border-radius: 10px;
2693
+ background: ${v("surface-raised")};
2694
+ border: 1px dashed ${v("border-strong")};
2695
+ border-radius: ${v("radius")};
1826
2696
  padding: 10px;
1827
2697
  `;
1828
2698
  const preview = document.createElement("div");
1829
2699
  preview.style.cssText = `
1830
2700
  width: 72px; height: 72px; flex-shrink: 0;
1831
- background: rgba(255,255,255,0.04) no-repeat center / cover;
1832
- border: 1px solid rgba(255,255,255,0.06);
1833
- border-radius: 6px;
2701
+ background: ${v("surface-raised")} no-repeat center / cover;
2702
+ border: 1px solid ${v("border")};
2703
+ border-radius: ${v("radius-sm")};
1834
2704
  display: flex; align-items: center; justify-content: center;
1835
- color: rgba(255,255,255,0.25);
2705
+ color: ${v("fg-faint")};
1836
2706
  `;
1837
2707
  const updatePreview = (url) => {
1838
2708
  if (url) {
@@ -1855,29 +2725,28 @@ function renderField(field, initial, depth = 0) {
1855
2725
  const uploadBtn = document.createElement("button");
1856
2726
  uploadBtn.type = "button";
1857
2727
  uploadBtn.style.cssText = `
1858
- appearance: none; cursor: pointer;
1859
- background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.85);
1860
- border: 1px solid rgba(255,255,255,0.07);
1861
- font-size: 11px; font-weight: 500; letter-spacing: 0.02em;
1862
- padding: 5px 10px; border-radius: 6px;
1863
- transition: background 0.15s;
2728
+ ${button("ghost")}
2729
+ background: ${v("surface-hover")};
2730
+ color: ${v("fg")};
2731
+ border: 1px solid ${v("border")};
2732
+ height: auto;
2733
+ font-size: ${v("text-xs")}; letter-spacing: 0.02em;
2734
+ padding: 5px 10px;
2735
+ cursor: pointer;
1864
2736
  `;
1865
2737
  uploadBtn.textContent = "Upload\u2026";
1866
- uploadBtn.addEventListener("mouseenter", () => {
1867
- uploadBtn.style.background = "rgba(255,255,255,0.1)";
1868
- });
1869
- uploadBtn.addEventListener("mouseleave", () => {
1870
- uploadBtn.style.background = "rgba(255,255,255,0.06)";
1871
- });
2738
+ attachHover(uploadBtn, { bg: v("surface-active") });
1872
2739
  uploadBtn.addEventListener("click", () => fileInput.click());
1873
2740
  const clearBtn = document.createElement("button");
1874
2741
  clearBtn.type = "button";
1875
2742
  clearBtn.style.cssText = `
1876
- appearance: none; cursor: pointer;
1877
- background: transparent; color: rgba(255,255,255,0.4);
2743
+ ${button("ghost")}
2744
+ color: ${v("fg-muted")};
1878
2745
  border: 1px solid transparent;
1879
- font-size: 11px;
1880
- padding: 5px 8px; border-radius: 6px;
2746
+ height: auto;
2747
+ font-size: ${v("text-xs")};
2748
+ padding: 5px 8px;
2749
+ cursor: pointer;
1881
2750
  `;
1882
2751
  clearBtn.textContent = "Clear";
1883
2752
  clearBtn.addEventListener("click", () => {
@@ -1890,7 +2759,7 @@ function renderField(field, initial, depth = 0) {
1890
2759
  const urlField = document.createElement("input");
1891
2760
  urlField.type = "url";
1892
2761
  urlField.className = "cancia-form-input";
1893
- urlField.style.cssText = `${INPUT_BASE} font-size: 11px; padding: 6px 9px;`;
2762
+ urlField.style.cssText = `${INPUT_BASE} font-size: ${v("text-xs")}; padding: 6px 9px;`;
1894
2763
  urlField.placeholder = "https://\u2026 or upload";
1895
2764
  urlField.value = initialUrl;
1896
2765
  urlField.addEventListener("input", () => {
@@ -1898,7 +2767,7 @@ function renderField(field, initial, depth = 0) {
1898
2767
  updatePreview(currentUrl);
1899
2768
  });
1900
2769
  const progressEl = document.createElement("div");
1901
- progressEl.style.cssText = `font-size: 10px; color: rgba(255,255,255,0.5); height: 12px;`;
2770
+ progressEl.style.cssText = `font-size: 10px; color: ${v("fg-muted")}; height: 12px;`;
1902
2771
  right.appendChild(btnRow);
1903
2772
  right.appendChild(urlField);
1904
2773
  right.appendChild(progressEl);
@@ -1910,7 +2779,7 @@ function renderField(field, initial, depth = 0) {
1910
2779
  const file = fileInput.files?.[0];
1911
2780
  if (!file) return;
1912
2781
  uploadBtn.disabled = true;
1913
- progressEl.style.color = "rgba(255,255,255,0.5)";
2782
+ progressEl.style.color = v("fg-muted");
1914
2783
  try {
1915
2784
  const url = await uploadImage(file, (pct) => {
1916
2785
  progressEl.textContent = `Uploading\u2026 ${pct}%`;
@@ -1924,7 +2793,7 @@ function renderField(field, initial, depth = 0) {
1924
2793
  }, 1500);
1925
2794
  } catch (err) {
1926
2795
  progressEl.textContent = `Upload failed: ${err instanceof Error ? err.message : String(err)}`;
1927
- progressEl.style.color = "#ff8786";
2796
+ progressEl.style.color = v("danger");
1928
2797
  } finally {
1929
2798
  uploadBtn.disabled = false;
1930
2799
  fileInput.value = "";
@@ -1934,51 +2803,51 @@ function renderField(field, initial, depth = 0) {
1934
2803
  break;
1935
2804
  }
1936
2805
  case "datetime": {
1937
- const input = document.createElement("input");
1938
- input.type = "datetime-local";
1939
- input.className = "cancia-form-input";
1940
- input.style.cssText = `${INPUT_BASE} color-scheme: dark; caret-color: ${accent4()};`;
1941
- if (typeof initial === "string") input.value = isoToLocalInput(initial);
1942
- wrapper.appendChild(input);
2806
+ const input2 = document.createElement("input");
2807
+ input2.type = "datetime-local";
2808
+ input2.className = "cancia-form-input";
2809
+ input2.style.cssText = `${INPUT_BASE} color-scheme: light; caret-color: ${v("accent")};`;
2810
+ if (typeof initial === "string") input2.value = isoToLocalInput(initial);
2811
+ wrapper.appendChild(input2);
1943
2812
  getValue2 = () => {
1944
- const v = input.value.trim();
1945
- if (!v) return void 0;
1946
- return localInputToIso(v);
2813
+ const v2 = input2.value.trim();
2814
+ if (!v2) return void 0;
2815
+ return localInputToIso(v2);
1947
2816
  };
1948
2817
  break;
1949
2818
  }
1950
2819
  case "number": {
1951
- const input = document.createElement("input");
1952
- input.type = "number";
1953
- input.className = "cancia-form-input";
1954
- input.style.cssText = `${INPUT_BASE} caret-color: ${accent4()};`;
1955
- if (field.min !== void 0) input.min = String(field.min);
1956
- if (field.max !== void 0) input.max = String(field.max);
1957
- if (typeof initial === "number") input.value = String(initial);
1958
- else if (typeof initial === "string" && initial !== "") input.value = initial;
1959
- wrapper.appendChild(input);
2820
+ const input2 = document.createElement("input");
2821
+ input2.type = "number";
2822
+ input2.className = "cancia-form-input";
2823
+ input2.style.cssText = `${INPUT_BASE} caret-color: ${v("accent")};`;
2824
+ if (field.min !== void 0) input2.min = String(field.min);
2825
+ if (field.max !== void 0) input2.max = String(field.max);
2826
+ if (typeof initial === "number") input2.value = String(initial);
2827
+ else if (typeof initial === "string" && initial !== "") input2.value = initial;
2828
+ wrapper.appendChild(input2);
1960
2829
  getValue2 = () => {
1961
- const v = input.value.trim();
1962
- if (v === "") return void 0;
1963
- const n = Number(v);
2830
+ const v2 = input2.value.trim();
2831
+ if (v2 === "") return void 0;
2832
+ const n = Number(v2);
1964
2833
  return Number.isNaN(n) ? void 0 : n;
1965
2834
  };
1966
2835
  break;
1967
2836
  }
1968
2837
  default: {
1969
- const input = document.createElement("input");
1970
- input.type = field.widget === "url" ? "url" : field.widget === "email" ? "email" : "text";
1971
- input.className = "cancia-form-input";
1972
- input.style.cssText = `${INPUT_BASE} caret-color: ${accent4()};`;
1973
- if (field.placeholder) input.placeholder = field.placeholder;
1974
- if (field.minLength !== void 0) input.minLength = field.minLength;
1975
- if (field.maxLength !== void 0) input.maxLength = field.maxLength;
1976
- if (typeof initial === "string") input.value = initial;
1977
- wrapper.appendChild(input);
1978
- getValue2 = () => input.value;
1979
- onInput = (cb) => input.addEventListener("input", cb);
1980
- setValue = (v) => {
1981
- input.value = v;
2838
+ const input2 = document.createElement("input");
2839
+ input2.type = field.widget === "url" ? "url" : field.widget === "email" ? "email" : "text";
2840
+ input2.className = "cancia-form-input";
2841
+ input2.style.cssText = `${INPUT_BASE} caret-color: ${v("accent")};`;
2842
+ if (field.placeholder) input2.placeholder = field.placeholder;
2843
+ if (field.minLength !== void 0) input2.minLength = field.minLength;
2844
+ if (field.maxLength !== void 0) input2.maxLength = field.maxLength;
2845
+ if (typeof initial === "string") input2.value = initial;
2846
+ wrapper.appendChild(input2);
2847
+ getValue2 = () => input2.value;
2848
+ onInput = (cb) => input2.addEventListener("input", cb);
2849
+ setValue = (v2) => {
2850
+ input2.value = v2;
1982
2851
  };
1983
2852
  break;
1984
2853
  }
@@ -2007,18 +2876,14 @@ function renderArrayField(field, initial, setOwnError, depth) {
2007
2876
  const itemSchema = field.of;
2008
2877
  const container = document.createElement("div");
2009
2878
  container.style.cssText = `
2010
- display: flex; flex-direction: column; gap: 8px;
2011
- background: rgba(255,255,255,0.02);
2012
- border: 1px solid rgba(255,255,255,0.07);
2013
- border-radius: 10px;
2014
- padding: 10px;
2879
+ ${group()}
2015
2880
  `;
2016
2881
  const rowsWrap = document.createElement("div");
2017
- rowsWrap.style.cssText = `display: flex; flex-direction: column; gap: 8px;`;
2882
+ rowsWrap.style.cssText = `display: flex; flex-direction: column; gap: ${v("space-2")};`;
2018
2883
  container.appendChild(rowsWrap);
2019
2884
  if (!itemSchema || depth >= MAX_FIELD_DEPTH) {
2020
2885
  const note = document.createElement("div");
2021
- note.style.cssText = `font-size: 11px; color: rgba(255,255,255,0.4);`;
2886
+ note.style.cssText = `font-size: ${v("text-xs")}; color: ${v("fg-muted")};`;
2022
2887
  note.textContent = itemSchema ? "Nesting too deep to edit here." : "This array has no item schema.";
2023
2888
  container.appendChild(note);
2024
2889
  return { control: container, getValue: () => [], validate: () => ({ value: [], ok: true }) };
@@ -2028,11 +2893,7 @@ function renderArrayField(field, initial, setOwnError, depth) {
2028
2893
  function makeRow(itemValue) {
2029
2894
  const row = document.createElement("div");
2030
2895
  row.style.cssText = `
2031
- display: flex; align-items: flex-start; gap: 8px;
2032
- background: rgba(255,255,255,0.02);
2033
- border: 1px solid rgba(255,255,255,0.06);
2034
- border-radius: 8px;
2035
- padding: 8px;
2896
+ ${groupRow()}
2036
2897
  `;
2037
2898
  const handle = document.createElement("div");
2038
2899
  handle.textContent = "\u22EE\u22EE";
@@ -2040,10 +2901,12 @@ function renderArrayField(field, initial, setOwnError, depth) {
2040
2901
  handle.draggable = true;
2041
2902
  handle.style.cssText = `
2042
2903
  cursor: grab; user-select: none;
2043
- color: rgba(255,255,255,0.35);
2044
- font-size: 13px; line-height: 1.2;
2045
- padding: 4px 2px; flex-shrink: 0;
2904
+ color: ${v("fg-faint")};
2905
+ font-size: ${v("text-base")}; line-height: 1.2;
2906
+ padding: ${v("space-1")} 2px; flex-shrink: 0;
2046
2907
  letter-spacing: -2px;
2908
+ opacity: 0;
2909
+ transition: opacity ${v("duration-fast")} ${v("ease")};
2047
2910
  `;
2048
2911
  const { wrapper, fieldState } = renderField(itemSchema, itemValue, depth + 1);
2049
2912
  wrapper.style.marginBottom = "0";
@@ -2054,19 +2917,16 @@ function renderArrayField(field, initial, setOwnError, depth) {
2054
2917
  removeBtn.textContent = "\xD7";
2055
2918
  removeBtn.title = "Remove";
2056
2919
  removeBtn.style.cssText = `
2057
- appearance: none; cursor: pointer; flex-shrink: 0;
2058
- background: transparent; border: 1px solid transparent;
2059
- color: rgba(255,135,134,0.7);
2920
+ ${button("danger")}
2921
+ flex-shrink: 0;
2922
+ border: 1px solid transparent;
2923
+ height: auto;
2060
2924
  font-size: 16px; line-height: 1;
2061
- padding: 2px 7px; border-radius: 6px;
2062
- transition: background 0.15s;
2925
+ padding: 2px 7px;
2926
+ cursor: pointer;
2063
2927
  `;
2064
- removeBtn.addEventListener("mouseenter", () => {
2065
- removeBtn.style.background = "rgba(255,135,134,0.1)";
2066
- });
2067
- removeBtn.addEventListener("mouseleave", () => {
2068
- removeBtn.style.background = "transparent";
2069
- });
2928
+ attachHover(removeBtn, { bg: v("danger-soft") });
2929
+ attachRowReveal(row, [handle, removeBtn]);
2070
2930
  row.appendChild(handle);
2071
2931
  row.appendChild(wrapper);
2072
2932
  row.appendChild(removeBtn);
@@ -2118,19 +2978,18 @@ function renderArrayField(field, initial, setOwnError, depth) {
2118
2978
  const itemLabel = itemSchema.label || "item";
2119
2979
  addBtn.textContent = `+ Add ${itemLabel.toLowerCase()}`;
2120
2980
  addBtn.style.cssText = `
2121
- appearance: none; cursor: pointer; align-self: flex-start;
2122
- background: rgba(255,255,255,0.05); color: rgba(255,255,255,0.8);
2123
- border: 1px solid rgba(255,255,255,0.08);
2124
- font-size: 11px; font-weight: 500;
2125
- padding: 6px 11px; border-radius: 7px;
2126
- transition: background 0.15s;
2981
+ ${button("ghost")}
2982
+ align-self: flex-start;
2983
+ background: transparent;
2984
+ color: ${v("fg-muted")};
2985
+ border: 0;
2986
+ height: auto;
2987
+ font-size: ${v("text-sm")};
2988
+ font-weight: 500;
2989
+ padding: 4px 0;
2990
+ cursor: pointer;
2127
2991
  `;
2128
- addBtn.addEventListener("mouseenter", () => {
2129
- addBtn.style.background = "rgba(255,255,255,0.1)";
2130
- });
2131
- addBtn.addEventListener("mouseleave", () => {
2132
- addBtn.style.background = "rgba(255,255,255,0.05)";
2133
- });
2992
+ attachHover(addBtn, { bg: v("surface-active") });
2134
2993
  addBtn.addEventListener("click", () => addRow(defaultForField(itemSchema)));
2135
2994
  container.appendChild(addBtn);
2136
2995
  const collect = () => rows.map((r) => r.state.getValue());
@@ -2160,15 +3019,12 @@ function renderObjectField(field, initial, setOwnError, depth) {
2160
3019
  const subInitial = initial && typeof initial === "object" && !Array.isArray(initial) ? initial : {};
2161
3020
  const fieldset = document.createElement("div");
2162
3021
  fieldset.style.cssText = `
2163
- display: flex; flex-direction: column; gap: 2px;
2164
- background: rgba(255,255,255,0.02);
2165
- border: 1px solid rgba(255,255,255,0.07);
2166
- border-radius: 10px;
2167
- padding: 10px 10px 0;
3022
+ ${group()}
3023
+ gap: 2px;
2168
3024
  `;
2169
3025
  if (depth >= MAX_FIELD_DEPTH) {
2170
3026
  const note = document.createElement("div");
2171
- note.style.cssText = `font-size: 11px; color: rgba(255,255,255,0.4); padding-bottom: 10px;`;
3027
+ note.style.cssText = `font-size: ${v("text-xs")}; color: ${v("fg-muted")}; padding-bottom: 10px;`;
2172
3028
  note.textContent = "Nesting too deep to edit here.";
2173
3029
  fieldset.appendChild(note);
2174
3030
  return { control: fieldset, getValue: () => ({}), validate: () => ({ value: {}, ok: true }) };
@@ -2182,8 +3038,8 @@ function renderObjectField(field, initial, setOwnError, depth) {
2182
3038
  const collect = () => {
2183
3039
  const out = {};
2184
3040
  for (const c of childStates) {
2185
- const v = c.getValue();
2186
- if (v !== void 0 && v !== "") out[c.field.name] = v;
3041
+ const v2 = c.getValue();
3042
+ if (v2 !== void 0 && v2 !== "") out[c.field.name] = v2;
2187
3043
  }
2188
3044
  return out;
2189
3045
  };
@@ -2206,7 +3062,7 @@ function renderReferenceField(field, initial) {
2206
3062
  const targetList = field.referenceList;
2207
3063
  const sel = document.createElement("select");
2208
3064
  sel.className = "cancia-form-select";
2209
- sel.style.cssText = `${INPUT_BASE} appearance: none; padding-right: 30px; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M2.5 3.75l2.5 2.5 2.5-2.5' stroke='rgba(255,255,255,0.4)' stroke-width='1.3' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>"); background-repeat: no-repeat; background-position: right 10px center; cursor: pointer;`;
3065
+ sel.style.cssText = `${INPUT_BASE} appearance: none; padding-right: 30px; background-image: ${SELECT_CHEVRON}; background-repeat: no-repeat; background-position: right 10px center; cursor: pointer;`;
2210
3066
  const opt = (value, text, selected = false) => {
2211
3067
  const o = document.createElement("option");
2212
3068
  o.value = value;
@@ -2229,8 +3085,8 @@ function renderReferenceField(field, initial) {
2229
3085
  const titleField = state.schemas[targetList]?.titleField;
2230
3086
  const titleOf = (entry) => {
2231
3087
  if (titleField) {
2232
- const v = entry.data[titleField];
2233
- if (typeof v === "string" && v.trim()) return v;
3088
+ const v2 = entry.data[titleField];
3089
+ if (typeof v2 === "string" && v2.trim()) return v2;
2234
3090
  }
2235
3091
  return `(untitled \xB7 ${entry.id})`;
2236
3092
  };
@@ -2277,25 +3133,17 @@ function renderRichTextField(field, initial, setOwnError) {
2277
3133
  ];
2278
3134
  const container = document.createElement("div");
2279
3135
  container.style.cssText = `
2280
- display: flex; flex-direction: column; gap: 8px;
2281
- background: rgba(255,255,255,0.02);
2282
- border: 1px solid rgba(255,255,255,0.07);
2283
- border-radius: 10px;
2284
- padding: 10px;
3136
+ ${group()}
2285
3137
  `;
2286
3138
  const rowsWrap = document.createElement("div");
2287
- rowsWrap.style.cssText = `display: flex; flex-direction: column; gap: 8px;`;
3139
+ rowsWrap.style.cssText = `display: flex; flex-direction: column; gap: ${v("space-2")};`;
2288
3140
  container.appendChild(rowsWrap);
2289
3141
  const rows = [];
2290
3142
  let dragging = null;
2291
3143
  function makeRow(initialRow) {
2292
3144
  const row = document.createElement("div");
2293
3145
  row.style.cssText = `
2294
- display: flex; align-items: flex-start; gap: 8px;
2295
- background: rgba(255,255,255,0.02);
2296
- border: 1px solid rgba(255,255,255,0.06);
2297
- border-radius: 8px;
2298
- padding: 8px;
3146
+ ${groupRow()}
2299
3147
  `;
2300
3148
  const handle = document.createElement("div");
2301
3149
  handle.textContent = "\u22EE\u22EE";
@@ -2303,25 +3151,33 @@ function renderRichTextField(field, initial, setOwnError) {
2303
3151
  handle.draggable = true;
2304
3152
  handle.style.cssText = `
2305
3153
  cursor: grab; user-select: none;
2306
- color: rgba(255,255,255,0.35);
2307
- font-size: 13px; line-height: 1.2;
2308
- padding: 4px 2px; flex-shrink: 0;
3154
+ color: ${v("fg-faint")};
3155
+ font-size: ${v("text-base")}; line-height: 1.2;
3156
+ padding: ${v("space-1")} 2px; flex-shrink: 0;
2309
3157
  letter-spacing: -2px;
3158
+ opacity: 0;
3159
+ transition: opacity ${v("duration-fast")} ${v("ease")};
2310
3160
  `;
2311
3161
  const main = document.createElement("div");
2312
3162
  main.style.cssText = `flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px;`;
2313
3163
  const ta = document.createElement("textarea");
2314
3164
  ta.className = "cancia-form-textarea";
2315
- ta.style.cssText = `${INPUT_BASE} resize: vertical; min-height: 54px; max-height: 240px; caret-color: ${accent4()};`;
3165
+ ta.style.cssText = `${INPUT_BASE} resize: vertical; min-height: 54px; max-height: 240px; caret-color: ${v("accent")};`;
2316
3166
  ta.rows = 2;
2317
3167
  ta.placeholder = "Text \u2014 use **bold**, *italic*, [label](https://\u2026)";
2318
3168
  ta.value = initialRow.text;
2319
3169
  const controls = document.createElement("div");
2320
- controls.style.cssText = `display: flex; gap: 6px;`;
3170
+ controls.style.cssText = `
3171
+ display: flex; gap: 6px;
3172
+ max-height: 0; opacity: 0; overflow: hidden;
3173
+ transition: max-height ${v("duration-fast")} ${v("ease-out")},
3174
+ opacity ${v("duration-fast")} ${v("ease")};
3175
+ `;
3176
+ controls.dataset.canciaCollapsible = "1";
2321
3177
  const styleSel = document.createElement("select");
2322
3178
  styleSel.className = "cancia-form-select";
2323
- styleSel.style.cssText = `${INPUT_BASE} width: auto; flex: 1; appearance: none; padding: 5px 26px 5px 9px; font-size: 11px; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M2.5 3.75l2.5 2.5 2.5-2.5' stroke='rgba(255,255,255,0.4)' stroke-width='1.3' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>"); background-repeat: no-repeat; background-position: right 9px center; cursor: pointer;`;
2324
- for (const s of PT_STYLES) {
3179
+ styleSel.style.cssText = `${INPUT_BASE} width: auto; flex: 1; appearance: none; padding: 5px 26px 5px 9px; font-size: ${v("text-xs")}; background-image: ${SELECT_CHEVRON}; background-repeat: no-repeat; background-position: right 9px center; cursor: pointer;`;
3180
+ for (const s of PT_STYLES2) {
2325
3181
  const o = document.createElement("option");
2326
3182
  o.value = s;
2327
3183
  o.textContent = STYLE_LABELS[s];
@@ -2331,10 +3187,10 @@ function renderRichTextField(field, initial, setOwnError) {
2331
3187
  const listSel = document.createElement("select");
2332
3188
  listSel.className = "cancia-form-select";
2333
3189
  listSel.style.cssText = styleSel.style.cssText;
2334
- for (const { value, label } of LIST_LABELS) {
3190
+ for (const { value, label: label2 } of LIST_LABELS) {
2335
3191
  const o = document.createElement("option");
2336
3192
  o.value = value;
2337
- o.textContent = label;
3193
+ o.textContent = label2;
2338
3194
  if ((initialRow.listItem ?? "") === value) o.selected = true;
2339
3195
  listSel.appendChild(o);
2340
3196
  }
@@ -2347,19 +3203,16 @@ function renderRichTextField(field, initial, setOwnError) {
2347
3203
  removeBtn.textContent = "\xD7";
2348
3204
  removeBtn.title = "Remove block";
2349
3205
  removeBtn.style.cssText = `
2350
- appearance: none; cursor: pointer; flex-shrink: 0;
2351
- background: transparent; border: 1px solid transparent;
2352
- color: rgba(255,135,134,0.7);
3206
+ ${button("danger")}
3207
+ flex-shrink: 0;
3208
+ border: 1px solid transparent;
3209
+ height: auto;
2353
3210
  font-size: 16px; line-height: 1;
2354
- padding: 2px 7px; border-radius: 6px;
2355
- transition: background 0.15s;
3211
+ padding: 2px 7px;
3212
+ cursor: pointer;
2356
3213
  `;
2357
- removeBtn.addEventListener("mouseenter", () => {
2358
- removeBtn.style.background = "rgba(255,135,134,0.1)";
2359
- });
2360
- removeBtn.addEventListener("mouseleave", () => {
2361
- removeBtn.style.background = "transparent";
2362
- });
3214
+ attachHover(removeBtn, { bg: v("danger-soft") });
3215
+ attachRowReveal(row, [handle, removeBtn, controls]);
2363
3216
  row.appendChild(handle);
2364
3217
  row.appendChild(main);
2365
3218
  row.appendChild(removeBtn);
@@ -2414,8 +3267,8 @@ function renderRichTextField(field, initial, setOwnError) {
2414
3267
  rowsWrap.appendChild(rec.el);
2415
3268
  }
2416
3269
  const initialRows = (() => {
2417
- const parsed = portableTextSubsetSchema.safeParse(initial);
2418
- if (parsed.success && parsed.data.length > 0) return portableTextToRows(parsed.data);
3270
+ const parsed = portableTextSubsetSchema2.safeParse(initial);
3271
+ if (parsed.success && parsed.data.length > 0) return portableTextToRows2(parsed.data);
2419
3272
  return [{ text: "", style: "normal" }];
2420
3273
  })();
2421
3274
  for (const r of initialRows) addRow(r);
@@ -2423,24 +3276,23 @@ function renderRichTextField(field, initial, setOwnError) {
2423
3276
  addBtn.type = "button";
2424
3277
  addBtn.textContent = "+ Add block";
2425
3278
  addBtn.style.cssText = `
2426
- appearance: none; cursor: pointer; align-self: flex-start;
2427
- background: rgba(255,255,255,0.05); color: rgba(255,255,255,0.8);
2428
- border: 1px solid rgba(255,255,255,0.08);
2429
- font-size: 11px; font-weight: 500;
2430
- padding: 6px 11px; border-radius: 7px;
2431
- transition: background 0.15s;
3279
+ ${button("ghost")}
3280
+ align-self: flex-start;
3281
+ background: transparent;
3282
+ color: ${v("fg-muted")};
3283
+ border: 0;
3284
+ height: auto;
3285
+ font-size: ${v("text-sm")};
3286
+ font-weight: 500;
3287
+ padding: 4px 0;
3288
+ cursor: pointer;
2432
3289
  `;
2433
- addBtn.addEventListener("mouseenter", () => {
2434
- addBtn.style.background = "rgba(255,255,255,0.1)";
2435
- });
2436
- addBtn.addEventListener("mouseleave", () => {
2437
- addBtn.style.background = "rgba(255,255,255,0.05)";
2438
- });
3290
+ attachHover(addBtn, { bg: v("surface-active") });
2439
3291
  addBtn.addEventListener("click", () => addRow({ text: "", style: "normal" }));
2440
3292
  container.appendChild(addBtn);
2441
3293
  const serialise = () => {
2442
3294
  const editorRows = rows.map((r) => r.read()).filter((r) => r.text.trim() !== "");
2443
- return rowsToPortableText(editorRows);
3295
+ return rowsToPortableText2(editorRows);
2444
3296
  };
2445
3297
  return {
2446
3298
  control: container,
@@ -2448,7 +3300,7 @@ function renderRichTextField(field, initial, setOwnError) {
2448
3300
  validate: () => {
2449
3301
  setOwnError(null);
2450
3302
  const value = serialise();
2451
- const parsed = portableTextSubsetSchema.safeParse(value);
3303
+ const parsed = portableTextSubsetSchema2.safeParse(value);
2452
3304
  if (!parsed.success) {
2453
3305
  setOwnError("This rich-text content is not valid. Check links and formatting.");
2454
3306
  return { value, ok: false };
@@ -2568,77 +3420,106 @@ function openEntryModal(opts) {
2568
3420
  const isEdit = opts.entry !== null;
2569
3421
  const isTranslate = !isEdit && (opts.translateFromEntry ?? null) !== null;
2570
3422
  document.documentElement.style.setProperty("--cancia-accent-border", accentBorder());
2571
- const backdrop = document.createElement("div");
2572
- backdrop.dataset.canciaModalBackdrop = "1";
2573
- backdrop.style.cssText = `
2574
- position: fixed; inset: 0;
2575
- background: rgba(8,8,10,0.55);
2576
- backdrop-filter: blur(3px);
2577
- -webkit-backdrop-filter: blur(3px);
2578
- z-index: ${BACKDROP_Z2};
2579
- opacity: 0;
2580
- transition: opacity 0.2s ease-out;
2581
- `;
2582
- document.body.appendChild(backdrop);
2583
- requestAnimationFrame(() => {
2584
- backdrop.style.opacity = "1";
2585
- });
2586
- backdropEl2 = backdrop;
2587
3423
  const modal = document.createElement("div");
2588
3424
  modal.dataset.canciaModal = "1";
2589
- modal.style.cssText = `
2590
- position: fixed;
2591
- top: 50%; left: 50%;
2592
- transform: translate(-50%, -50%);
2593
- width: min(480px, calc(100vw - 32px));
2594
- max-height: min(680px, calc(100vh - 48px));
2595
- display: flex; flex-direction: column;
2596
- background: rgba(14, 14, 16, 0.97);
2597
- backdrop-filter: blur(24px) saturate(180%);
2598
- -webkit-backdrop-filter: blur(24px) saturate(180%);
2599
- border: 1px solid rgba(255,255,255,0.07);
2600
- border-radius: 14px;
2601
- box-shadow: 0 0 0 1px rgba(0,0,0,0.4), 0 8px 24px rgba(0,0,0,0.5), 0 24px 64px rgba(0,0,0,0.4);
2602
- z-index: ${MODAL_Z};
2603
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2604
- color: rgba(255,255,255,0.9);
2605
- animation: cancia-modal-in 0.22s cubic-bezier(0.16, 1, 0.3, 1) forwards;
2606
- overflow: hidden;
2607
- `;
2608
- document.body.appendChild(modal);
3425
+ markUi(modal);
3426
+ document.documentElement.style.setProperty("--cancia-accent-border", accentBorder());
3427
+ mountedInPanel = pushPanelView(modal);
3428
+ if (!mountedInPanel) {
3429
+ const backdrop = document.createElement("div");
3430
+ backdrop.dataset.canciaModalBackdrop = "1";
3431
+ markUi(backdrop);
3432
+ backdrop.style.cssText = `
3433
+ position: fixed; inset: 0;
3434
+ background: rgba(8,8,10,0.45);
3435
+ backdrop-filter: blur(3px);
3436
+ -webkit-backdrop-filter: blur(3px);
3437
+ z-index: ${BACKDROP_Z2};
3438
+ opacity: 0;
3439
+ transition: opacity ${v("duration")} ${v("ease-out")};
3440
+ `;
3441
+ document.body.appendChild(backdrop);
3442
+ requestAnimationFrame(() => {
3443
+ backdrop.style.opacity = "1";
3444
+ });
3445
+ backdropEl2 = backdrop;
3446
+ backdrop.addEventListener("click", () => closeEntryModal());
3447
+ modal.style.cssText = `
3448
+ ${surface(3)}
3449
+ position: fixed;
3450
+ top: 50%; left: 50%;
3451
+ width: min(480px, calc(100vw - 32px));
3452
+ max-height: min(680px, calc(100vh - 48px));
3453
+ display: flex; flex-direction: column;
3454
+ box-shadow: ${v("shadow-lg")};
3455
+ z-index: ${MODAL_Z};
3456
+ overflow: hidden;
3457
+ opacity: 0;
3458
+ transform: translate(-50%, calc(-50% + 8px)) scale(0.985);
3459
+ transition: opacity ${v("duration")} ${v("ease")}, transform ${v("duration")} ${v("ease")};
3460
+ `;
3461
+ document.body.appendChild(modal);
3462
+ requestAnimationFrame(() => {
3463
+ modal.style.opacity = "1";
3464
+ modal.style.transform = "translate(-50%, -50%) scale(1)";
3465
+ });
3466
+ }
2609
3467
  modalEl = modal;
2610
3468
  const header = document.createElement("div");
2611
3469
  header.style.cssText = `
2612
- display: flex; align-items: center; justify-content: space-between;
2613
- padding: 14px 16px 12px;
2614
- border-bottom: 1px solid rgba(255,255,255,0.06);
3470
+ display: flex; align-items: center; gap: ${v("space-2")};
3471
+ padding: 14px ${v("space-4")} ${v("space-3")};
3472
+ border-bottom: 1px solid ${v("border")};
2615
3473
  flex-shrink: 0;
2616
3474
  `;
3475
+ if (mountedInPanel) {
3476
+ const backBtn = document.createElement("button");
3477
+ backBtn.type = "button";
3478
+ backBtn.setAttribute("aria-label", "Back to list");
3479
+ backBtn.title = "Back to list";
3480
+ backBtn.style.cssText = `
3481
+ ${iconButton(28)}
3482
+ flex-shrink: 0; padding: 0; cursor: pointer;
3483
+ background: transparent; border: 0;
3484
+ `;
3485
+ backBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
3486
+ <path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/>
3487
+ </svg>`;
3488
+ attachHover(backBtn, { bg: v("surface-hover"), color: v("fg-strong") });
3489
+ attachPress(backBtn);
3490
+ backBtn.addEventListener("click", () => closeEntryModal());
3491
+ header.appendChild(backBtn);
3492
+ }
2617
3493
  const titleWrap = document.createElement("div");
2618
- titleWrap.style.cssText = `display: flex; flex-direction: column; gap: 2px; min-width: 0;`;
3494
+ titleWrap.style.cssText = `display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1;`;
2619
3495
  const eyebrow = document.createElement("span");
2620
- eyebrow.style.cssText = `
2621
- font-size: 10px; font-weight: 600;
2622
- color: rgba(255,255,255,0.32);
2623
- letter-spacing: 0.08em; text-transform: uppercase;
2624
- `;
3496
+ eyebrow.style.cssText = `${label()} display: inline;`;
2625
3497
  const action = isEdit ? "Edit" : isTranslate ? "Translate" : "New";
2626
3498
  eyebrow.textContent = `${action} ${opts.schema.labelSingular.toLowerCase()} \xB7 ${opts.locale}`;
2627
3499
  const titleEl = document.createElement("span");
2628
3500
  titleEl.style.cssText = `
2629
3501
  font-size: 14px; font-weight: 600;
2630
- color: rgba(255,255,255,0.92);
3502
+ color: ${v("fg-strong")};
2631
3503
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
2632
3504
  `;
2633
3505
  titleEl.textContent = opts.schema.label;
2634
3506
  titleWrap.appendChild(eyebrow);
2635
3507
  titleWrap.appendChild(titleEl);
2636
3508
  header.appendChild(titleWrap);
2637
- header.appendChild(makeCloseButton(() => closeEntryModal()));
3509
+ header.appendChild(
3510
+ makeCloseButton(() => {
3511
+ if (mountedInPanel) {
3512
+ closeEntryModal();
3513
+ closeListPanel();
3514
+ } else {
3515
+ closeEntryModal();
3516
+ }
3517
+ })
3518
+ );
2638
3519
  modal.appendChild(header);
2639
3520
  const body = document.createElement("div");
2640
3521
  body.style.cssText = `
2641
- padding: 14px 16px 4px;
3522
+ padding: 14px ${v("space-4")} ${v("space-1")};
2642
3523
  overflow-y: auto;
2643
3524
  flex: 1 1 auto;
2644
3525
  min-height: 0;
@@ -2647,14 +3528,14 @@ function openEntryModal(opts) {
2647
3528
  const formError = document.createElement("div");
2648
3529
  formError.style.cssText = `
2649
3530
  display: none;
2650
- background: rgba(255,135,134,0.08);
2651
- color: #ff8786;
2652
- border: 1px solid rgba(255,135,134,0.18);
2653
- border-radius: 8px;
3531
+ background: ${v("danger-soft")};
3532
+ color: ${v("danger")};
3533
+ border: 1px solid ${v("danger-soft")};
3534
+ border-radius: ${v("radius-sm")};
2654
3535
  padding: 9px 11px;
2655
- font-size: 12px;
3536
+ font-size: ${v("text-sm")};
2656
3537
  line-height: 1.4;
2657
- margin-bottom: 12px;
3538
+ margin-bottom: ${v("space-3")};
2658
3539
  `;
2659
3540
  body.appendChild(formError);
2660
3541
  function showFormError(msg) {
@@ -2668,14 +3549,14 @@ function openEntryModal(opts) {
2668
3549
  if (isTranslate) {
2669
3550
  const banner = document.createElement("div");
2670
3551
  banner.style.cssText = `
2671
- background: rgba(245,180,0,0.1);
2672
- color: #f5b400;
2673
- border: 1px solid rgba(245,180,0,0.25);
2674
- border-radius: 8px;
3552
+ background: ${v("warning-soft")};
3553
+ color: ${v("warning")};
3554
+ border: 1px solid ${v("warning-soft")};
3555
+ border-radius: ${v("radius-sm")};
2675
3556
  padding: 9px 11px;
2676
- font-size: 12px;
3557
+ font-size: ${v("text-sm")};
2677
3558
  line-height: 1.4;
2678
- margin-bottom: 12px;
3559
+ margin-bottom: ${v("space-3")};
2679
3560
  `;
2680
3561
  const src = opts.translateFromEntry;
2681
3562
  banner.textContent = `Translating from ${src.locale} into ${opts.locale}. Fields are pre-filled from the source.`;
@@ -2694,32 +3575,26 @@ function openEntryModal(opts) {
2694
3575
  footer.style.cssText = `
2695
3576
  display: flex; align-items: center; justify-content: space-between;
2696
3577
  gap: 10px;
2697
- padding: 12px 16px;
2698
- border-top: 1px solid rgba(255,255,255,0.06);
2699
- background: rgba(0,0,0,0.18);
3578
+ padding: ${v("space-3")} ${v("space-4")};
3579
+ border-top: 1px solid ${v("border")};
3580
+ background: ${v("surface-raised")};
2700
3581
  flex-shrink: 0;
2701
3582
  `;
2702
3583
  const leftActions = document.createElement("div");
2703
3584
  const rightActions = document.createElement("div");
2704
- rightActions.style.cssText = `display: flex; gap: 8px;`;
3585
+ rightActions.style.cssText = `display: flex; gap: ${v("space-2")};`;
2705
3586
  if (isEdit) {
2706
3587
  const deleteBtn = document.createElement("button");
2707
3588
  deleteBtn.type = "button";
2708
3589
  deleteBtn.style.cssText = `
2709
- appearance: none; cursor: pointer;
2710
- background: transparent; border: 1px solid transparent;
2711
- color: #ff8786;
2712
- font-size: 12px; font-weight: 500;
2713
- padding: 6px 10px; border-radius: 7px;
2714
- transition: background 0.15s;
3590
+ ${button("danger")}
3591
+ border: 1px solid transparent;
3592
+ height: auto;
3593
+ padding: 6px 10px;
3594
+ cursor: pointer;
2715
3595
  `;
2716
3596
  deleteBtn.textContent = "Delete";
2717
- deleteBtn.addEventListener("mouseenter", () => {
2718
- deleteBtn.style.background = "rgba(255,135,134,0.08)";
2719
- });
2720
- deleteBtn.addEventListener("mouseleave", () => {
2721
- deleteBtn.style.background = "transparent";
2722
- });
3597
+ attachHover(deleteBtn, { bg: v("danger-soft") });
2723
3598
  deleteBtn.addEventListener("click", async () => {
2724
3599
  if (!confirm(`Delete the ${opts.locale} version of this ${opts.schema.labelSingular.toLowerCase()}? This can't be undone.`)) return;
2725
3600
  deleteBtn.disabled = true;
@@ -2737,25 +3612,17 @@ function openEntryModal(opts) {
2737
3612
  const cancelBtn = document.createElement("button");
2738
3613
  cancelBtn.type = "button";
2739
3614
  cancelBtn.style.cssText = `
2740
- appearance: none; cursor: pointer;
2741
- background: rgba(255,255,255,0.04);
2742
- border: 1px solid rgba(255,255,255,0.07);
2743
- color: rgba(255,255,255,0.75);
2744
- font-size: 12px; font-weight: 500;
2745
- padding: 7px 14px; border-radius: 8px;
2746
- transition: background 0.15s, color 0.15s;
3615
+ ${button("ghost")}
3616
+ background: ${v("surface-raised")};
3617
+ border: 1px solid ${v("border")};
3618
+ height: auto;
3619
+ padding: 7px 14px;
3620
+ cursor: pointer;
2747
3621
  `;
2748
3622
  cancelBtn.textContent = "Cancel";
2749
- cancelBtn.addEventListener("mouseenter", () => {
2750
- cancelBtn.style.background = "rgba(255,255,255,0.08)";
2751
- cancelBtn.style.color = "rgba(255,255,255,0.9)";
2752
- });
2753
- cancelBtn.addEventListener("mouseleave", () => {
2754
- cancelBtn.style.background = "rgba(255,255,255,0.04)";
2755
- cancelBtn.style.color = "rgba(255,255,255,0.75)";
2756
- });
3623
+ attachHover(cancelBtn, { bg: v("surface-hover"), color: v("fg-strong") });
2757
3624
  cancelBtn.addEventListener("click", () => closeEntryModal());
2758
- const saveBtn = makePrimaryButton(isEdit ? "Save" : "Create", accent4());
3625
+ const saveBtn = makePrimaryButton(isEdit ? "Save" : "Create", accent3());
2759
3626
  saveBtn.addEventListener("click", async () => {
2760
3627
  clearFormError();
2761
3628
  const { data, ok } = preValidate(fieldStates);
@@ -2798,7 +3665,6 @@ function openEntryModal(opts) {
2798
3665
  footer.appendChild(leftActions);
2799
3666
  footer.appendChild(rightActions);
2800
3667
  modal.appendChild(footer);
2801
- backdrop.addEventListener("click", () => closeEntryModal());
2802
3668
  escListener = (e) => {
2803
3669
  if (e.key === "Escape") {
2804
3670
  e.stopPropagation();
@@ -2813,11 +3679,18 @@ function openEntryModal(opts) {
2813
3679
  }
2814
3680
  function closeEntryModal() {
2815
3681
  if (modalEl) {
2816
- modalEl.style.animation = "cancia-modal-out 0.16s cubic-bezier(0.7, 0, 0.84, 0) forwards";
2817
3682
  const el = modalEl;
2818
- setTimeout(() => el.remove(), 160);
2819
3683
  modalEl = null;
3684
+ if (mountedInPanel) {
3685
+ popPanelView(el);
3686
+ } else {
3687
+ el.style.transition = `opacity 0.16s ${v("ease-out")}, transform 0.16s ${v("ease-out")}`;
3688
+ el.style.opacity = "0";
3689
+ el.style.transform = "translate(-50%, calc(-50% + 8px)) scale(0.985)";
3690
+ setTimeout(() => el.remove(), 180);
3691
+ }
2820
3692
  }
3693
+ mountedInPanel = false;
2821
3694
  if (backdropEl2) {
2822
3695
  const el = backdropEl2;
2823
3696
  el.style.opacity = "0";
@@ -2838,13 +3711,12 @@ var toolbarEl = null;
2838
3711
  var pendingPanelEl = null;
2839
3712
  var isExpanded = false;
2840
3713
  var expandedEscListener = null;
2841
- function accent5() {
2842
- return state.config?.accentColor ?? "#6366f1";
2843
- }
3714
+ var revealTimer = null;
2844
3715
  var styleInjected4 = false;
2845
3716
  function injectStyles4() {
2846
3717
  if (styleInjected4) return;
2847
3718
  styleInjected4 = true;
3719
+ injectBaseStyles(state.config?.accentColor, state.config?.toolbarAccent === true);
2848
3720
  const s = document.createElement("style");
2849
3721
  s.textContent = `
2850
3722
  @keyframes cancia-enter {
@@ -2885,20 +3757,30 @@ function injectStyles4() {
2885
3757
  to { opacity: 1; transform: translateX(-50%) translateY(0); }
2886
3758
  }
2887
3759
  [data-cancia-toolbar] * { box-sizing: border-box; }
2888
- [data-cancia-toolbar] button:active:not(:disabled) { transform: scale(0.92) !important; }
2889
3760
  [data-cancia-popup] * { box-sizing: border-box; }
2890
- [data-cancia-popup] button:active:not(:disabled) { transform: scale(0.94) !important; }
3761
+ /* Press feedback for popup buttons. The TOOLBAR's buttons deliberately do
3762
+ NOT use :active \u2014 they use attachPress() on pointerdown instead, because
3763
+ :active only lands after the browser's own hit-test and reads as lag.
3764
+ An !important here would also override that inline transform. */
3765
+ [data-cancia-popup] button:active:not(:disabled) { transform: scale(0.96); }
2891
3766
  /* Protect stroke-based icons from host page "svg { fill: currentColor }" rules */
2892
3767
  [data-cancia-toolbar] svg[fill="none"] { fill: none !important; }
2893
3768
  [data-cancia-toolbar] svg[fill="none"] :not([fill]) { fill: none !important; }
2894
3769
  [data-cancia-popup] svg[fill="none"] { fill: none !important; }
2895
3770
  [data-cancia-popup] svg[fill="none"] :not([fill]) { fill: none !important; }
2896
- /* Reset cosmetic host CSS leaking into toolbar buttons */
3771
+ /* Reset cosmetic host CSS leaking into toolbar buttons.
3772
+ NOTE: font-* and color are deliberately NOT unset here \u2014 the buttons now
3773
+ carry visible text labels, and unsetting those would strip the label's
3774
+ typography back to the UA default. The scoped reset in styles.ts already
3775
+ neutralises host typography for [data-cancia-ui] subtrees. */
2897
3776
  [data-cancia-toolbar] :where(button) {
2898
3777
  background: unset; border: unset; border-radius: unset; padding: unset;
2899
- margin: unset; color: unset; font-family: unset; font-weight: unset;
2900
- font-size: unset; line-height: unset; letter-spacing: unset;
2901
- box-shadow: unset; outline: unset; text-transform: unset;
3778
+ margin: unset; box-shadow: unset; outline: unset;
3779
+ }
3780
+ /* Labels must never be transformed by a host \`button { text-transform }\`. */
3781
+ [data-cancia-toolbar] [data-cancia-label] {
3782
+ text-transform: none;
3783
+ letter-spacing: normal;
2902
3784
  }
2903
3785
  `;
2904
3786
  document.head.appendChild(s);
@@ -2910,27 +3792,28 @@ var tooltipVisible = false;
2910
3792
  function getOrCreateBtnTooltip() {
2911
3793
  if (!btnTooltipEl) {
2912
3794
  btnTooltipEl = document.createElement("div");
3795
+ markUi(btnTooltipEl);
2913
3796
  btnTooltipEl.style.cssText = `
2914
3797
  position: fixed;
2915
3798
  pointer-events: none;
2916
- z-index: 2147483646;
2917
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2918
- font-size: 11px; font-weight: 500; letter-spacing: 0.02em;
2919
- color: #fff;
2920
- background: rgba(10,10,12,0.92);
2921
- backdrop-filter: blur(8px);
2922
- -webkit-backdrop-filter: blur(8px);
2923
- border: 1px solid rgba(255,255,255,0.1);
2924
- padding: 4px 8px; border-radius: 6px;
3799
+ z-index: ${v("z-panel")};
3800
+ font-size: ${v("text-xs")}; font-weight: 500; letter-spacing: 0.02em;
3801
+ color: ${v("fg-strong")};
3802
+ background: ${v("surface-1")};
3803
+ backdrop-filter: ${v("blur")};
3804
+ -webkit-backdrop-filter: ${v("blur")};
3805
+ border: 1px solid ${v("border-strong")};
3806
+ padding: ${v("space-1")} ${v("space-2")};
3807
+ border-radius: ${v("radius-sm")};
2925
3808
  white-space: nowrap;
2926
- box-shadow: 0 2px 8px rgba(0,0,0,0.3);
3809
+ box-shadow: ${v("shadow-sm")};
2927
3810
  display: none;
2928
3811
  `;
2929
3812
  document.body.appendChild(btnTooltipEl);
2930
3813
  }
2931
3814
  return btnTooltipEl;
2932
3815
  }
2933
- function showBtnTooltip(btn, label) {
3816
+ function showBtnTooltip(btn, label2) {
2934
3817
  if (tooltipHideTimer) {
2935
3818
  clearTimeout(tooltipHideTimer);
2936
3819
  tooltipHideTimer = null;
@@ -2942,9 +3825,9 @@ function showBtnTooltip(btn, label) {
2942
3825
  const doShow = () => {
2943
3826
  tooltipVisible = true;
2944
3827
  const tooltip = getOrCreateBtnTooltip();
2945
- tooltip.textContent = label;
3828
+ tooltip.textContent = label2;
2946
3829
  tooltip.style.display = "block";
2947
- tooltip.style.animation = "cancia-tooltip-in 0.12s cubic-bezier(0.16,1,0.3,1) both";
3830
+ tooltip.style.animation = `cancia-tooltip-in ${v("duration-fast")} ${v("ease-out")} both`;
2948
3831
  const rect = btn.getBoundingClientRect();
2949
3832
  const tooltipH = 26;
2950
3833
  const gap = 8;
@@ -2972,37 +3855,39 @@ function buildToolbar() {
2972
3855
  injectStyles4();
2973
3856
  const bar = document.createElement("div");
2974
3857
  bar.dataset.canciaToolbar = "1";
3858
+ markUi(bar);
2975
3859
  bar.style.cssText = `
2976
3860
  position: fixed;
2977
- bottom: 24px;
2978
- right: 24px;
2979
- z-index: 2147483647;
2980
- width: 44px;
2981
- height: 44px;
2982
- border-radius: 22px;
2983
- background: rgba(12, 12, 14, 0.92);
2984
- backdrop-filter: blur(16px) saturate(180%);
2985
- -webkit-backdrop-filter: blur(16px) saturate(180%);
2986
- border: 1px solid rgba(255,255,255,0.08);
2987
- box-shadow: 0 2px 8px rgba(0,0,0,0.3), 0 8px 32px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.05);
2988
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2989
- font-size: 13px;
2990
- color: #f0f0f0;
3861
+ bottom: ${v("space-6")};
3862
+ right: ${v("space-6")};
3863
+ z-index: ${v("z-bar")};
3864
+ width: 52px;
3865
+ height: 52px;
3866
+ border-radius: ${v("radius-full")};
3867
+ background: ${v("surface-1")};
3868
+ backdrop-filter: ${v("blur")};
3869
+ -webkit-backdrop-filter: ${v("blur")};
3870
+ border: 1px solid ${v("border")};
3871
+ box-shadow: ${v("shadow")};
3872
+ font-size: ${v("text-base")};
3873
+ color: ${v("fg")};
2991
3874
  user-select: none;
2992
3875
  cursor: pointer;
2993
3876
  overflow: hidden;
2994
3877
  display: flex;
2995
3878
  align-items: center;
2996
3879
  justify-content: center;
2997
- transition: width 0.45s cubic-bezier(0.19, 1, 0.22, 1), border-radius 0.45s cubic-bezier(0.19, 1, 0.22, 1);
2998
- animation: cancia-enter 0.5s cubic-bezier(0.34, 1.2, 0.64, 1) both;
3880
+ transition: width ${v("duration-slow")} ${v("ease")},
3881
+ border-radius ${v("duration-slow")} ${v("ease")};
3882
+ animation: cancia-enter ${v("duration-slow")} ${v("ease-spring")} both;
2999
3883
  `;
3000
3884
  const collapseIcon = document.createElement("div");
3001
3885
  collapseIcon.style.cssText = `
3002
3886
  position: absolute;
3003
3887
  display: flex; align-items: center; justify-content: center;
3004
- color: rgba(255,255,255,0.7);
3005
- transition: opacity 0.15s, transform 0.15s cubic-bezier(0.2,0,0,1);
3888
+ color: ${v("fg")};
3889
+ transition: opacity ${v("duration-fast")} ${v("ease")},
3890
+ transform ${v("duration-fast")} ${v("ease")};
3006
3891
  pointer-events: none;
3007
3892
  `;
3008
3893
  collapseIcon.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
@@ -3013,16 +3898,17 @@ function buildToolbar() {
3013
3898
  controls.style.cssText = `
3014
3899
  display: flex;
3015
3900
  align-items: center;
3016
- gap: 0.375rem;
3901
+ gap: ${v("space-1")};
3017
3902
  padding: 5px;
3018
3903
  white-space: nowrap;
3019
3904
  opacity: 0;
3905
+ transform: scale(0.6);
3020
3906
  pointer-events: none;
3021
3907
  transform-origin: right center;
3022
3908
  `;
3023
3909
  let editActive = false;
3024
- const editBtn = makeIconButton(
3025
- `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
3910
+ const editBtn = makeActionButton(
3911
+ `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
3026
3912
  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
3027
3913
  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
3028
3914
  </svg>`,
@@ -3031,38 +3917,43 @@ function buildToolbar() {
3031
3917
  );
3032
3918
  controls.appendChild(editBtn);
3033
3919
  const canPublish = state.config?.canPublish ?? true;
3034
- const publishBtn = makeIconButton(
3035
- `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
3920
+ const publishBtn = makeActionButton(
3921
+ `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
3036
3922
  <path d="M22 2L11 13"/>
3037
3923
  <path d="M22 2L15 22l-4-9-9-4 20-7z"/>
3038
3924
  </svg>`,
3039
- canPublish ? "Publish" : "Publish (no publish method configured)",
3925
+ "Publish",
3040
3926
  () => handlePublish(publishBtn)
3041
3927
  );
3928
+ if (!canPublish) {
3929
+ publishBtn.title = "No publish method is configured for this site.";
3930
+ }
3042
3931
  if (!canPublish) {
3043
3932
  publishBtn.disabled = true;
3044
3933
  publishBtn.style.opacity = "0.3";
3045
3934
  publishBtn.style.cursor = "not-allowed";
3046
3935
  }
3047
3936
  controls.appendChild(publishBtn);
3048
- const logoutBtn = makeIconButton(
3049
- `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
3937
+ const logoutBtn = makeActionButton(
3938
+ `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
3050
3939
  <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
3051
3940
  <path d="M16 17l5-5-5-5"/>
3052
3941
  <path d="M21 12H9"/>
3053
3942
  </svg>`,
3054
- "Log out",
3943
+ "Sign out",
3055
3944
  () => state.onLogout?.()
3056
3945
  );
3057
3946
  controls.appendChild(logoutBtn);
3058
3947
  controls.appendChild(makeDivider());
3059
3948
  const collapseBtn = makeIconButton(
3060
- `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
3949
+ `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
3061
3950
  <path d="M6 6l12 12M18 6L6 18"/>
3062
3951
  </svg>`,
3063
3952
  "Close",
3064
3953
  () => collapse(bar, collapseIcon, controls)
3065
3954
  );
3955
+ collapseBtn.style.width = "38px";
3956
+ collapseBtn.style.height = "38px";
3066
3957
  controls.appendChild(collapseBtn);
3067
3958
  bar.appendChild(collapseIcon);
3068
3959
  bar.appendChild(controls);
@@ -3070,10 +3961,10 @@ function buildToolbar() {
3070
3961
  if (!isExpanded) expand(bar, collapseIcon, controls);
3071
3962
  });
3072
3963
  bar.addEventListener("mouseenter", () => {
3073
- if (!isExpanded) bar.style.background = "rgba(24, 24, 28, 0.96)";
3964
+ if (!isExpanded) bar.style.background = v("surface-2");
3074
3965
  });
3075
3966
  bar.addEventListener("mouseleave", () => {
3076
- bar.style.background = "rgba(12, 12, 14, 0.92)";
3967
+ bar.style.background = v("surface-1");
3077
3968
  });
3078
3969
  onPendingChange(() => {
3079
3970
  const count = state.pending.size;
@@ -3089,9 +3980,10 @@ function buildToolbar() {
3089
3980
  state.editMode = editActive;
3090
3981
  const svgEl = editBtn.querySelector("svg");
3091
3982
  if (editActive) {
3092
- editBtn.style.background = `${accent5()}22`;
3093
- editBtn.style.color = accent5();
3094
- if (svgEl) svgEl.style.stroke = accent5();
3983
+ editBtn.dataset.canciaActive = "1";
3984
+ editBtn.style.background = v("accent-soft");
3985
+ editBtn.style.color = v("accent");
3986
+ if (svgEl) svgEl.style.stroke = v("accent");
3095
3987
  editBtn.dataset.canciaTooltip = "Stop editing (Esc)";
3096
3988
  attachHighlight((selection) => {
3097
3989
  if (selection.kind === "field") {
@@ -3155,8 +4047,9 @@ function buildToolbar() {
3155
4047
  };
3156
4048
  document.addEventListener("keydown", editModeEscListener, true);
3157
4049
  } else {
4050
+ delete editBtn.dataset.canciaActive;
3158
4051
  editBtn.style.background = "transparent";
3159
- editBtn.style.color = "rgba(255,255,255,0.85)";
4052
+ editBtn.style.color = v("fg-strong");
3160
4053
  if (svgEl) svgEl.style.stroke = "";
3161
4054
  editBtn.dataset.canciaTooltip = "Edit";
3162
4055
  detachHighlight();
@@ -3183,40 +4076,49 @@ function expand(bar, icon, controls) {
3183
4076
  controls.style.visibility = "hidden";
3184
4077
  controls.style.opacity = "0";
3185
4078
  controls.style.pointerEvents = "none";
3186
- bar.style.width = "max-content";
3187
4079
  bar.style.borderRadius = "100px";
3188
- requestAnimationFrame(() => {
3189
- const naturalW = bar.scrollWidth;
3190
- bar.style.width = "44px";
4080
+ bar.style.width = "max-content";
4081
+ const naturalW = bar.scrollWidth;
4082
+ bar.style.width = "52px";
4083
+ controls.style.visibility = "";
4084
+ void bar.offsetWidth;
4085
+ bar.style.width = `${naturalW}px`;
4086
+ bar.style.cursor = "default";
4087
+ icon.style.opacity = "0";
4088
+ icon.style.transform = "scale(0.5) rotate(-90deg)";
4089
+ revealTimer = setTimeout(() => {
4090
+ revealTimer = null;
4091
+ if (!isExpanded) return;
4092
+ controls.style.pointerEvents = "auto";
3191
4093
  controls.style.visibility = "";
3192
- requestAnimationFrame(() => {
3193
- bar.style.width = `${naturalW}px`;
3194
- bar.style.borderRadius = "100px";
3195
- bar.style.cursor = "default";
3196
- icon.style.opacity = "0";
3197
- icon.style.transform = "scale(0.5) rotate(-90deg)";
3198
- setTimeout(() => {
3199
- controls.style.pointerEvents = "auto";
3200
- controls.style.animation = "cancia-controls-in 0.4s cubic-bezier(0.19, 1, 0.22, 1) both";
3201
- controls.style.opacity = "1";
3202
- }, 80);
3203
- });
3204
- });
4094
+ controls.style.animation = "none";
4095
+ controls.style.transition = `opacity ${v("duration")} ${v("ease")}, transform ${v("duration")} ${v("ease")}`;
4096
+ void controls.offsetWidth;
4097
+ controls.style.opacity = "1";
4098
+ controls.style.transform = "scale(1)";
4099
+ }, 80);
3205
4100
  }
3206
4101
  function collapse(bar, icon, controls) {
3207
4102
  if (!isExpanded) return;
3208
4103
  isExpanded = false;
3209
4104
  hideBtnTooltip();
4105
+ if (revealTimer !== null) {
4106
+ clearTimeout(revealTimer);
4107
+ revealTimer = null;
4108
+ }
3210
4109
  if (expandedEscListener) {
3211
4110
  document.removeEventListener("keydown", expandedEscListener, true);
3212
4111
  expandedEscListener = null;
3213
4112
  }
3214
4113
  controls.style.pointerEvents = "none";
3215
- controls.style.animation = "cancia-controls-out 0.15s cubic-bezier(0.4, 0, 1, 1) both";
4114
+ controls.style.animation = "none";
4115
+ controls.style.transition = `opacity ${v("duration-fast")} ${v("ease-out")}, transform ${v("duration-fast")} ${v("ease-out")}`;
4116
+ controls.style.opacity = "0";
4117
+ controls.style.transform = "scale(0.6)";
3216
4118
  setTimeout(() => {
3217
- controls.style.opacity = "0";
3218
- bar.style.width = "44px";
3219
- bar.style.borderRadius = "22px";
4119
+ if (isExpanded) return;
4120
+ bar.style.width = "52px";
4121
+ bar.style.borderRadius = "26px";
3220
4122
  bar.style.cursor = "pointer";
3221
4123
  icon.style.opacity = "1";
3222
4124
  icon.style.transform = "scale(1) rotate(0deg)";
@@ -3260,42 +4162,87 @@ async function handlePublish(btn) {
3260
4162
  }
3261
4163
  function showToast(message, type) {
3262
4164
  const toast = document.createElement("div");
3263
- const color = type === "success" ? "#4ade80" : "#f87171";
4165
+ markUi(toast);
4166
+ const color = type === "success" ? v("success") : v("danger");
3264
4167
  toast.style.cssText = `
3265
- position: fixed; bottom: 80px; right: 24px; z-index: 2147483647;
3266
- display: flex; align-items: center; gap: 8px;
3267
- background: rgba(12, 12, 14, 0.95);
3268
- backdrop-filter: blur(16px);
3269
- border: 1px solid rgba(255,255,255,0.08);
3270
- border-radius: 10px; padding: 10px 14px;
3271
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3272
- font-size: 13px; font-weight: 500; color: #f0f0f0;
3273
- box-shadow: 0 4px 24px rgba(0,0,0,0.4);
4168
+ position: fixed; bottom: 80px; right: ${v("space-6")}; z-index: ${v("z-bar")};
4169
+ display: flex; align-items: center; gap: ${v("space-2")};
4170
+ background: ${v("surface-1")};
4171
+ backdrop-filter: ${v("blur")};
4172
+ -webkit-backdrop-filter: ${v("blur")};
4173
+ border: 1px solid ${v("border")};
4174
+ border-radius: ${v("radius")}; padding: 10px 14px;
4175
+ font-size: ${v("text-base")}; font-weight: 500; color: ${v("fg-strong")};
4176
+ box-shadow: ${v("shadow")};
3274
4177
  pointer-events: none;
3275
- animation: cancia-fade-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
4178
+ opacity: 0; transform: translateY(4px);
4179
+ transition: opacity ${v("duration")} ${v("ease")}, transform ${v("duration")} ${v("ease")};
3276
4180
  `;
3277
4181
  const dot = document.createElement("span");
3278
4182
  dot.style.cssText = `width: 7px; height: 7px; border-radius: 50%; background: ${color}; flex-shrink: 0;`;
3279
- const label = document.createElement("span");
3280
- label.textContent = message;
4183
+ const label2 = document.createElement("span");
4184
+ label2.textContent = message;
3281
4185
  toast.appendChild(dot);
3282
- toast.appendChild(label);
4186
+ toast.appendChild(label2);
3283
4187
  document.body.appendChild(toast);
4188
+ requestAnimationFrame(() => {
4189
+ toast.style.opacity = "1";
4190
+ toast.style.transform = "translateY(0)";
4191
+ });
3284
4192
  setTimeout(() => {
3285
- toast.style.transition = "opacity 0.25s cubic-bezier(0.4,0,1,1), transform 0.25s cubic-bezier(0.4,0,1,1)";
4193
+ toast.style.transition = `opacity ${v("duration-fast")} ${v("ease-out")}, transform ${v("duration-fast")} ${v("ease-out")}`;
3286
4194
  toast.style.opacity = "0";
3287
4195
  toast.style.transform = "translateY(4px)";
3288
4196
  setTimeout(() => toast.remove(), 300);
3289
4197
  }, 2e3);
3290
4198
  }
4199
+ function makeActionButton(svg, labelText, onClick) {
4200
+ const btn = document.createElement("button");
4201
+ btn.type = "button";
4202
+ btn.style.cssText = actionButton();
4203
+ btn.setAttribute("aria-label", labelText);
4204
+ const icon = document.createElement("span");
4205
+ icon.style.cssText = `display: flex; flex-shrink: 0;`;
4206
+ icon.innerHTML = svg;
4207
+ const svgEl = icon.querySelector("svg");
4208
+ if (svgEl) {
4209
+ svgEl.style.cssText = "display:block;flex-shrink:0;overflow:visible;";
4210
+ svgEl.setAttribute("stroke-width", "1.6");
4211
+ }
4212
+ const label2 = document.createElement("span");
4213
+ label2.textContent = labelText;
4214
+ label2.dataset.canciaLabel = "1";
4215
+ btn.appendChild(icon);
4216
+ btn.appendChild(label2);
4217
+ btn.addEventListener("mouseenter", () => {
4218
+ if (!btn.disabled && btn.dataset.canciaActive !== "1") {
4219
+ btn.style.background = v("surface-hover");
4220
+ btn.style.color = v("fg-strong");
4221
+ }
4222
+ });
4223
+ btn.addEventListener("mouseleave", () => {
4224
+ if (btn.dataset.canciaActive !== "1") {
4225
+ btn.style.background = "transparent";
4226
+ btn.style.color = v("fg");
4227
+ }
4228
+ });
4229
+ attachPress(btn);
4230
+ btn.addEventListener("click", (e) => {
4231
+ e.stopPropagation();
4232
+ onClick();
4233
+ });
4234
+ return btn;
4235
+ }
3291
4236
  function makeIconButton(svg, title, onClick) {
3292
4237
  const btn = document.createElement("button");
3293
4238
  btn.style.cssText = `
3294
- display: flex; align-items: center; justify-content: center;
3295
- width: 34px; height: 34px; border-radius: 50%;
3296
- border: none; background: transparent;
3297
- cursor: pointer; color: rgba(255,255,255,0.85); flex-shrink: 0; padding: 0;
3298
- transition: color 0.15s, background 0.15s, transform 0.1s cubic-bezier(0.2,0,0,1);
4239
+ ${iconButton(34)}
4240
+ border-radius: ${v("radius-full")};
4241
+ color: ${v("fg-strong")};
4242
+ flex-shrink: 0; padding: 0;
4243
+ transition: color ${v("duration-fast")} ${v("ease")},
4244
+ background ${v("duration-fast")} ${v("ease")},
4245
+ transform 0.1s ${v("ease")};
3299
4246
  `;
3300
4247
  btn.innerHTML = svg;
3301
4248
  const svgEl = btn.querySelector("svg");
@@ -3306,15 +4253,12 @@ function makeIconButton(svg, title, onClick) {
3306
4253
  btn.dataset.canciaTooltip = title;
3307
4254
  btn.addEventListener("mouseenter", () => {
3308
4255
  if (!btn.disabled) {
3309
- btn.style.background = "rgba(255,255,255,0.1)";
4256
+ btn.style.background = v("surface-active");
3310
4257
  showBtnTooltip(btn, btn.dataset.canciaTooltip ?? title);
3311
4258
  }
3312
4259
  });
3313
4260
  btn.addEventListener("mouseleave", () => {
3314
- const activeColor = state.config?.accentColor ?? "#6366f1";
3315
- if (!btn.style.background.includes(activeColor.slice(1, 7))) {
3316
- btn.style.background = "transparent";
3317
- }
4261
+ if (btn.dataset.canciaActive !== "1") btn.style.background = "transparent";
3318
4262
  hideBtnTooltip();
3319
4263
  });
3320
4264
  btn.addEventListener("click", (e) => {
@@ -3326,45 +4270,45 @@ function makeIconButton(svg, title, onClick) {
3326
4270
  }
3327
4271
  function makeDivider() {
3328
4272
  const d = document.createElement("span");
3329
- d.style.cssText = `width: 1px; height: 14px; background: rgba(255,255,255,0.08); flex-shrink: 0; margin: 0 1px;`;
4273
+ d.style.cssText = `width: 1px; height: 14px; background: ${v("border")}; flex-shrink: 0; margin: 0 1px;`;
3330
4274
  return d;
3331
4275
  }
3332
4276
  function showPendingPanel(count) {
3333
4277
  if (!pendingPanelEl) {
3334
4278
  pendingPanelEl = document.createElement("div");
4279
+ markUi(pendingPanelEl);
3335
4280
  pendingPanelEl.style.cssText = `
3336
4281
  position: fixed;
3337
4282
  bottom: 80px;
3338
- right: 24px;
3339
- z-index: 2147483646;
4283
+ right: ${v("space-6")};
4284
+ z-index: ${v("z-panel")};
3340
4285
  display: flex;
3341
4286
  align-items: center;
3342
4287
  justify-content: space-between;
3343
- gap: 12px;
3344
- background: rgba(12, 12, 14, 0.92);
3345
- backdrop-filter: blur(16px) saturate(180%);
3346
- -webkit-backdrop-filter: blur(16px) saturate(180%);
3347
- border: 1px solid rgba(255,255,255,0.08);
3348
- border-radius: 12px;
4288
+ gap: ${v("space-3")};
4289
+ background: ${v("surface-1")};
4290
+ backdrop-filter: ${v("blur")};
4291
+ -webkit-backdrop-filter: ${v("blur")};
4292
+ border: 1px solid ${v("border")};
4293
+ border-radius: ${v("radius")};
3349
4294
  padding: 0;
3350
- box-shadow: 0 2px 8px rgba(0,0,0,0.3), 0 8px 32px rgba(0,0,0,0.4);
3351
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3352
- animation: cancia-fade-in 0.25s cubic-bezier(0.16,1,0.3,1) both;
4295
+ box-shadow: ${v("shadow")};
3353
4296
  width: max-content;
3354
4297
  overflow: hidden;
4298
+ opacity: 0; transform: translateY(4px);
4299
+ transition: opacity ${v("duration")} ${v("ease")}, transform ${v("duration")} ${v("ease")};
3355
4300
  `;
3356
- const label2 = document.createElement("span");
3357
- label2.dataset.canciaPendingLabel = "1";
3358
- label2.style.cssText = `font-size: 12px; font-weight: 500; color: rgba(255,255,255,0.5); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;`;
4301
+ const label3 = document.createElement("span");
4302
+ label3.dataset.canciaPendingLabel = "1";
4303
+ label3.style.cssText = `font-size: ${v("text-sm")}; font-weight: 500; color: ${v("fg-muted")}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;`;
3359
4304
  const saveBtn = document.createElement("button");
3360
4305
  saveBtn.dataset.canciaSaveBtn = "1";
3361
4306
  saveBtn.title = "Save changes";
3362
4307
  saveBtn.style.cssText = `
3363
- padding: 5px 10px; border-radius: 7px; border: none; cursor: pointer;
3364
- background: #fff; color: #0c0c0e;
3365
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3366
- font-size: 12px; font-weight: 600; letter-spacing: 0.01em;
3367
- transition: opacity 0.15s, transform 0.1s cubic-bezier(0.2,0,0,1);
4308
+ padding: 5px 10px; border-radius: ${v("radius-sm")}; border: none; cursor: pointer;
4309
+ background: ${v("accent")}; color: ${v("accent-fg")};
4310
+ font-size: ${v("text-sm")}; font-weight: 600; letter-spacing: 0.01em;
4311
+ transition: opacity ${v("duration-fast")} ${v("ease")}, transform ${v("duration-fast")} ${v("ease")};
3368
4312
  flex-shrink: 0;
3369
4313
  `;
3370
4314
  saveBtn.textContent = "Save";
@@ -3374,6 +4318,7 @@ function showPendingPanel(count) {
3374
4318
  saveBtn.addEventListener("mouseleave", () => {
3375
4319
  saveBtn.style.opacity = "1";
3376
4320
  });
4321
+ attachPress(saveBtn);
3377
4322
  saveBtn.addEventListener("click", (e) => {
3378
4323
  e.stopPropagation();
3379
4324
  handleSave(saveBtn);
@@ -3381,22 +4326,22 @@ function showPendingPanel(count) {
3381
4326
  const undoBtn = document.createElement("button");
3382
4327
  undoBtn.title = "Discard changes";
3383
4328
  undoBtn.style.cssText = `
3384
- padding: 5px 10px; border-radius: 7px; border: 1px solid rgba(255,255,255,0.1); cursor: pointer;
3385
- background: transparent; color: rgba(255,255,255,0.5);
3386
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3387
- font-size: 12px; font-weight: 500; letter-spacing: 0.01em;
3388
- transition: color 0.15s, border-color 0.15s;
4329
+ padding: 5px 10px; border-radius: ${v("radius-sm")}; border: 1px solid ${v("border")}; cursor: pointer;
4330
+ background: transparent; color: ${v("fg-muted")};
4331
+ font-size: ${v("text-sm")}; font-weight: 500; letter-spacing: 0.01em;
4332
+ transition: color ${v("duration-fast")} ${v("ease")}, border-color ${v("duration-fast")} ${v("ease")};
3389
4333
  flex-shrink: 0;
3390
4334
  `;
3391
4335
  undoBtn.textContent = "Discard";
3392
4336
  undoBtn.addEventListener("mouseenter", () => {
3393
- undoBtn.style.color = "rgba(255,255,255,0.8)";
3394
- undoBtn.style.borderColor = "rgba(255,255,255,0.2)";
4337
+ undoBtn.style.color = v("fg-strong");
4338
+ undoBtn.style.borderColor = v("border-strong");
3395
4339
  });
3396
4340
  undoBtn.addEventListener("mouseleave", () => {
3397
- undoBtn.style.color = "rgba(255,255,255,0.5)";
3398
- undoBtn.style.borderColor = "rgba(255,255,255,0.1)";
4341
+ undoBtn.style.color = v("fg-muted");
4342
+ undoBtn.style.borderColor = v("border");
3399
4343
  });
4344
+ attachPress(undoBtn);
3400
4345
  undoBtn.addEventListener("click", (e) => {
3401
4346
  e.stopPropagation();
3402
4347
  revertPending();
@@ -3411,32 +4356,36 @@ function showPendingPanel(count) {
3411
4356
  row.style.cssText = `
3412
4357
  grid-area: 1/1; display: flex; align-items: center; gap: 6px;
3413
4358
  padding: 7px 7px 7px 12px;
3414
- transition: transform 200ms cubic-bezier(0.785,0.135,0.15,0.86);
4359
+ transition: transform ${v("duration")} ${v("ease-out")};
3415
4360
  `;
3416
- row.appendChild(label2);
4361
+ row.appendChild(label3);
3417
4362
  row.appendChild(undoBtn);
3418
4363
  row.appendChild(saveBtn);
3419
4364
  const flash = document.createElement("div");
3420
4365
  flash.dataset.canciaPanelFlash = "1";
3421
4366
  flash.style.cssText = `
3422
4367
  grid-area: 1/1; display: flex; align-items: center; gap: 7px;
3423
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3424
- font-size: 12px; font-weight: 500; color: rgba(255,255,255,0.8); white-space: nowrap;
3425
- padding: 7px 12px;
4368
+ font-size: ${v("text-sm")}; font-weight: 500; color: ${v("fg-strong")}; white-space: nowrap;
4369
+ padding: 7px ${v("space-3")};
3426
4370
  transform: translateY(-150%);
3427
- transition: transform 200ms cubic-bezier(0.785,0.135,0.15,0.86);
4371
+ transition: transform ${v("duration")} ${v("ease-out")};
3428
4372
  `;
3429
4373
  slot.appendChild(row);
3430
4374
  slot.appendChild(flash);
3431
4375
  pendingPanelEl.appendChild(slot);
3432
4376
  document.body.appendChild(pendingPanelEl);
4377
+ const panel = pendingPanelEl;
4378
+ requestAnimationFrame(() => {
4379
+ panel.style.opacity = "1";
4380
+ panel.style.transform = "translateY(0)";
4381
+ });
3433
4382
  }
3434
- const label = pendingPanelEl.querySelector("[data-cancia-pending-label]");
3435
- if (label) label.textContent = `${count} unsaved change${count === 1 ? "" : "s"}`;
4383
+ const label2 = pendingPanelEl.querySelector("[data-cancia-pending-label]");
4384
+ if (label2) label2.textContent = `${count} unsaved change${count === 1 ? "" : "s"}`;
3436
4385
  }
3437
4386
  function flashPanelMessage(message, type) {
3438
4387
  if (!pendingPanelEl) return;
3439
- const color = type === "success" ? "#4ade80" : "#f87171";
4388
+ const color = type === "success" ? v("success") : v("danger");
3440
4389
  const row = pendingPanelEl.querySelector("[data-cancia-panel-row]");
3441
4390
  const flash = pendingPanelEl.querySelector("[data-cancia-panel-flash]");
3442
4391
  if (!row || !flash) return;
@@ -3449,7 +4398,7 @@ function hidePendingPanel() {
3449
4398
  if (!pendingPanelEl) return;
3450
4399
  const panel = pendingPanelEl;
3451
4400
  pendingPanelEl = null;
3452
- panel.style.transition = "opacity 0.2s cubic-bezier(0.4,0,1,1), transform 0.2s cubic-bezier(0.4,0,1,1)";
4401
+ panel.style.transition = `opacity 0.2s ${v("ease-out")}, transform 0.2s ${v("ease-out")}`;
3453
4402
  panel.style.opacity = "0";
3454
4403
  panel.style.transform = "translateY(4px)";
3455
4404
  setTimeout(() => panel.remove(), 220);
@@ -3469,7 +4418,7 @@ function unmountToolbar() {
3469
4418
  pendingPanelEl?.remove();
3470
4419
  pendingPanelEl = null;
3471
4420
  if (toolbarEl) {
3472
- toolbarEl.style.animation = "cancia-exit 0.25s cubic-bezier(0.4, 0, 1, 1) both";
4421
+ toolbarEl.style.animation = `cancia-exit ${v("duration-exit")} ${v("ease-out")} both`;
3473
4422
  setTimeout(() => {
3474
4423
  toolbarEl?.remove();
3475
4424
  toolbarEl = null;