@cancia/toolbar 0.5.1 → 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,7 +199,12 @@ function applyOverlay() {
53
199
  el.src = savedValue;
54
200
  return;
55
201
  }
56
- if (el.dataset.cmsType === "link" || el.tagName === "A") {
202
+ if (el.dataset.cmsType === "richtext") {
203
+ const blocks = parseRichValue(savedValue);
204
+ if (blocks) renderRichToDom(el, blocks);
205
+ return;
206
+ }
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);
59
210
  const labelEls = el.querySelectorAll("[data-cms-label]");
@@ -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,23 +398,432 @@ 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
- if (el.tagName === "A") return "link";
262
827
  return "text";
263
828
  }
264
829
  function elementMode(el) {
@@ -269,9 +834,12 @@ function injectStyles() {
269
834
  styleInjected = true;
270
835
  const s = document.createElement("style");
271
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. */
272
840
  @keyframes cancia-highlight-in {
273
- from { opacity: 0; transform: scale(0.98); }
274
- to { opacity: 1; transform: scale(1); }
841
+ from { opacity: 0; }
842
+ to { opacity: 1; }
275
843
  }
276
844
  @keyframes cancia-tooltip-in {
277
845
  from { opacity: 0; transform: scale(0.95) translateY(3px); }
@@ -284,15 +852,19 @@ function getOrCreateOverlay() {
284
852
  if (!overlayEl) {
285
853
  overlayEl = document.createElement("div");
286
854
  overlayEl.dataset.canciaOverlay = "1";
855
+ markUi(overlayEl);
287
856
  overlayEl.style.cssText = `
288
857
  position: fixed;
858
+ top: 0; left: 0;
289
859
  pointer-events: none !important;
290
860
  box-sizing: border-box;
291
- border-radius: 5px;
861
+ background: transparent;
862
+ border: 0;
292
863
  z-index: 2147483644;
293
- will-change: top, left, width, height, opacity;
294
- transition: top 0.08s cubic-bezier(0.16,1,0.3,1), left 0.08s cubic-bezier(0.16,1,0.3,1),
295
- 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")};
296
868
  `;
297
869
  document.body.appendChild(overlayEl);
298
870
  }
@@ -302,37 +874,41 @@ function getOrCreateTooltip() {
302
874
  if (!tooltipEl) {
303
875
  tooltipEl = document.createElement("div");
304
876
  tooltipEl.dataset.canciaTooltip = "1";
877
+ markUi(tooltipEl);
305
878
  tooltipEl.style.cssText = `
306
879
  position: fixed;
307
880
  pointer-events: none !important;
308
- z-index: 2147483645;
309
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
310
- font-size: 11px;
881
+ z-index: ${v("z-overlay")};
882
+ font-family: ${v("font")};
883
+ font-size: ${v("text-xs")};
311
884
  font-weight: 500;
312
885
  letter-spacing: 0.02em;
313
- color: #fff;
314
- background: rgba(10,10,12,0.92);
315
- backdrop-filter: blur(8px);
316
- -webkit-backdrop-filter: blur(8px);
317
- border: 1px solid rgba(255,255,255,0.1);
318
- padding: 4px 8px;
319
- 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")};
320
893
  white-space: nowrap;
321
894
  max-width: 260px;
322
895
  overflow: hidden;
323
896
  text-overflow: ellipsis;
324
- box-shadow: 0 2px 8px rgba(0,0,0,0.3);
897
+ box-shadow: ${v("shadow-sm")};
325
898
  `;
326
899
  document.body.appendChild(tooltipEl);
327
900
  }
328
901
  return tooltipEl;
329
902
  }
330
903
  var lastOverlayEl = null;
904
+ var LIST_FALLBACK = "#059669";
331
905
  function listAccent(hex) {
332
- if (!/^#[0-9a-f]{6}$/i.test(hex)) return "#059669";
906
+ if (!/^#[0-9a-f]{6}$/i.test(hex)) return LIST_FALLBACK;
333
907
  const r = parseInt(hex.slice(1, 3), 16);
334
908
  const g = parseInt(hex.slice(3, 5), 16);
335
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;
336
912
  const shifted = [g, b, r].map((c) => c.toString(16).padStart(2, "0")).join("");
337
913
  return `#${shifted}`;
338
914
  }
@@ -353,52 +929,54 @@ var LIST_ICON = `<svg width="10" height="10" viewBox="0 0 14 14" fill="none" str
353
929
  <rect x="1" y="7.5" width="3" height="3" rx="0.5"/>
354
930
  <path d="M6 3.5h7M6 9h7"/>
355
931
  </svg>`;
932
+ var KIND_LABEL = {
933
+ text: "Text",
934
+ image: "Image",
935
+ link: "Link",
936
+ richtext: "Rich text"
937
+ };
356
938
  function positionOverlay(el, animate = false) {
357
939
  const rect = el.getBoundingClientRect();
358
940
  const mode = elementMode(el);
359
- const a = mode === "list" ? listAccent(accent()) : accent();
941
+ const isList = mode === "list";
942
+ const a = isList ? listAccent(accent()) : accent();
360
943
  const overlay = getOrCreateOverlay();
361
944
  const tooltip = getOrCreateTooltip();
362
945
  const padding = 3;
363
- overlay.style.top = `${rect.top - padding}px`;
364
- overlay.style.left = `${rect.left - padding}px`;
946
+ overlay.style.transform = `translate(${rect.left - padding}px, ${rect.top - padding}px)`;
365
947
  overlay.style.width = `${rect.width + padding * 2}px`;
366
948
  overlay.style.height = `${rect.height + padding * 2}px`;
367
949
  if (lastOverlayEl !== el) {
368
950
  lastOverlayEl = el;
369
- overlay.style.border = `2px ${mode === "list" ? "dashed" : "solid"} ${a}`;
370
- overlay.style.background = `${a}12`;
371
- let badgeIcon;
372
- 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;
373
960
  let tooltipText;
374
- if (mode === "list") {
375
- badgeIcon = LIST_ICON;
376
- badgeLabel = "list";
961
+ if (isList) {
962
+ tagIcon = LIST_ICON;
963
+ tagLabel = "List";
377
964
  tooltipText = `list: ${el.dataset.cmsList}`;
378
965
  } else {
379
966
  const type = fieldType(el);
380
- badgeIcon = type === "image" ? IMAGE_ICON : type === "link" ? LINK_ICON : FIELD_ICON;
381
- badgeLabel = type;
967
+ tagIcon = type === "image" ? IMAGE_ICON : type === "link" ? LINK_ICON : FIELD_ICON;
968
+ tagLabel = KIND_LABEL[type];
382
969
  tooltipText = el.dataset.cms ?? "";
383
970
  }
384
- overlay.innerHTML = `
385
- <div style="
386
- position: absolute; top: -1px; left: -1px;
387
- background: ${a}; color: #fff;
388
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
389
- font-size: 10px; font-weight: 600; letter-spacing: 0.04em;
390
- padding: 2px 6px; border-radius: 3px 0 4px 0;
391
- display: flex; align-items: center; gap: 4px;
392
- line-height: 1;
393
- ">
394
- ${badgeIcon}
395
- ${badgeLabel}
396
- </div>
397
- `;
398
- 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;
399
977
  }
400
978
  overlay.style.display = "block";
401
- 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`;
402
980
  tooltip.style.display = "block";
403
981
  if (animate) tooltip.style.animation = "cancia-tooltip-in 0.1s ease-out forwards";
404
982
  const tooltipMargin = 8;
@@ -423,13 +1001,19 @@ function hideOverlay() {
423
1001
  }
424
1002
  }
425
1003
  function handleScroll() {
426
- if (scrollRAF !== null) return;
427
- scrollRAF = requestAnimationFrame(() => {
428
- scrollRAF = null;
429
- 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)) {
430
1012
  positionOverlay(currentHighlighted);
1013
+ if (overlayEl) overlayEl.style.opacity = "1";
1014
+ if (tooltipEl) tooltipEl.style.opacity = "1";
431
1015
  }
432
- });
1016
+ }, 140);
433
1017
  }
434
1018
  function handleMouseOver(e) {
435
1019
  const target = e.target.closest(CMS_SELECTOR);
@@ -525,13 +1109,18 @@ function onPendingChange(cb) {
525
1109
  }
526
1110
 
527
1111
  // src/popup.ts
1112
+ import {
1113
+ portableTextToRows,
1114
+ rowsToPortableText
1115
+ } from "@cancia/astro/richtext";
528
1116
  var popupEl = null;
529
1117
  var outsideListener = null;
530
1118
  var keyListener = null;
531
1119
  var dragover = false;
532
1120
  var inputHandlers = /* @__PURE__ */ new WeakMap();
533
1121
  function accent2() {
534
- return state.config?.accentColor ?? "#6366f1";
1122
+ const useAccent = state.config?.toolbarAccent === true;
1123
+ return (useAccent ? state.config?.accentColor : void 0) ?? "#18181b";
535
1124
  }
536
1125
  function getPopupPosition(anchor) {
537
1126
  const rect = anchor.getBoundingClientRect();
@@ -566,15 +1155,15 @@ function buildHeader(key, onClose) {
566
1155
  const keyParts = key.split(".");
567
1156
  titleEl.textContent = keyParts[keyParts.length - 1].replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
568
1157
  titleEl.style.cssText = `
569
- font-size: 13px; font-weight: 600;
570
- color: rgba(255,255,255,0.9);
1158
+ font-size: ${v("text-base")}; font-weight: 600;
1159
+ color: ${v("fg")};
571
1160
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
572
1161
  `;
573
1162
  const keyEl = document.createElement("span");
574
1163
  keyEl.textContent = key;
575
1164
  keyEl.style.cssText = `
576
1165
  font-size: 10px; font-family: "SF Mono", "Fira Code", ui-monospace, monospace;
577
- color: rgba(255,255,255,0.22); letter-spacing: 0.03em;
1166
+ color: ${v("fg-faint")}; letter-spacing: 0.03em;
578
1167
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
579
1168
  `;
580
1169
  titleWrap.appendChild(titleEl);
@@ -592,7 +1181,7 @@ function buildTextPopup(key, anchorEl, onClose) {
592
1181
  wrap.appendChild(buildHeader(key, onClose));
593
1182
  if (langs.length > 1) {
594
1183
  const tabs = document.createElement("div");
595
- 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")};`;
596
1185
  const renderTabs2 = () => {
597
1186
  tabs.innerHTML = "";
598
1187
  langs.forEach((lang) => {
@@ -602,17 +1191,17 @@ function buildTextPopup(key, anchorEl, onClose) {
602
1191
  tab.style.cssText = `
603
1192
  padding: 5px 10px 6px; border: none; border-bottom: 2px solid;
604
1193
  margin-bottom: -1px;
605
- 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;
606
1195
  background: transparent;
607
- border-bottom-color: ${isActive ? accent2() : "transparent"};
608
- color: ${isActive ? "#fff" : "rgba(255,255,255,0.3)"};
609
- 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")};
610
1199
  `;
611
1200
  tab.addEventListener("mouseenter", () => {
612
- if (!isActive) tab.style.color = "rgba(255,255,255,0.6)";
1201
+ if (!isActive) tab.style.color = v("fg");
613
1202
  });
614
1203
  tab.addEventListener("mouseleave", () => {
615
- if (!isActive) tab.style.color = "rgba(255,255,255,0.3)";
1204
+ if (!isActive) tab.style.color = v("fg-faint");
616
1205
  });
617
1206
  tab.addEventListener("click", () => {
618
1207
  const current = wrap.querySelector("textarea");
@@ -650,8 +1239,8 @@ function buildTextPopup(key, anchorEl, onClose) {
650
1239
  const renderTextarea = (isInit = false) => {
651
1240
  if (!isInit && textarea) {
652
1241
  textarea.value = getValue(key, activeLang) || anchorEl.textContent?.trim() || "";
653
- textarea.style.borderColor = `${accent2()}66`;
654
- textarea.style.background = "rgba(255,255,255,0.05)";
1242
+ textarea.style.borderColor = v("accent-ring");
1243
+ textarea.style.background = v("surface-hover");
655
1244
  attachInputHandler();
656
1245
  return;
657
1246
  }
@@ -661,23 +1250,13 @@ function buildTextPopup(key, anchorEl, onClose) {
661
1250
  textarea.rows = 4;
662
1251
  textarea.placeholder = "Enter text\u2026";
663
1252
  textarea.style.cssText = `
664
- width: 100%; box-sizing: border-box;
665
- background: rgba(255,255,255,0.03); color: rgba(255,255,255,0.9);
666
- border: 1px solid rgba(255,255,255,0.07); border-radius: 10px;
667
- padding: 10px 12px;
668
- font-size: 13px; font-family: inherit; resize: none; outline: none;
669
- 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;
670
1257
  line-height: 1.55;
671
- caret-color: ${accent2()};
672
1258
  `;
673
- textarea.addEventListener("focus", () => {
674
- textarea.style.borderColor = `${accent2()}66`;
675
- textarea.style.background = "rgba(255,255,255,0.05)";
676
- });
677
- textarea.addEventListener("blur", () => {
678
- textarea.style.borderColor = "rgba(255,255,255,0.07)";
679
- textarea.style.background = "rgba(255,255,255,0.03)";
680
- });
1259
+ attachInputFocus(textarea);
681
1260
  attachInputHandler();
682
1261
  if (footerEl) {
683
1262
  wrap.insertBefore(textarea, footerEl);
@@ -753,31 +1332,21 @@ function buildLinkPopup(key, anchorEl, onClose) {
753
1332
  };
754
1333
  };
755
1334
  const inputStyle = `
756
- width: 100%; box-sizing: border-box;
757
- background: rgba(255,255,255,0.03); color: rgba(255,255,255,0.9);
758
- border: 1px solid rgba(255,255,255,0.07); border-radius: 10px;
759
- padding: 9px 12px;
760
- font-size: 13px; font-family: inherit; outline: none;
761
- transition: border-color 0.18s, background 0.18s;
762
- caret-color: ${accent2()};
1335
+ ${input()}
1336
+ border-radius: ${v("radius")};
1337
+ padding: 9px ${v("space-3")};
1338
+ font-family: inherit;
763
1339
  `;
764
- const makeLabelled = (text, input) => {
1340
+ const makeLabelled = (text, input2) => {
765
1341
  const field = document.createElement("div");
766
1342
  field.style.cssText = `display: flex; flex-direction: column; gap: 5px; margin-bottom: 10px;`;
767
1343
  const lab = document.createElement("div");
768
1344
  lab.textContent = text;
769
- lab.style.cssText = `font-size:10px;letter-spacing:0.08em;text-transform:uppercase;color:rgba(255,255,255,0.35);`;
770
- input.style.cssText = inputStyle;
771
- input.addEventListener("focus", () => {
772
- input.style.borderColor = `${accent2()}66`;
773
- input.style.background = "rgba(255,255,255,0.05)";
774
- });
775
- input.addEventListener("blur", () => {
776
- input.style.borderColor = "rgba(255,255,255,0.07)";
777
- input.style.background = "rgba(255,255,255,0.03)";
778
- });
1345
+ lab.style.cssText = label();
1346
+ input2.style.cssText = inputStyle;
1347
+ attachInputFocus(input2);
779
1348
  field.appendChild(lab);
780
- field.appendChild(input);
1349
+ field.appendChild(input2);
781
1350
  return field;
782
1351
  };
783
1352
  const labelInput = document.createElement("input");
@@ -792,7 +1361,7 @@ function buildLinkPopup(key, anchorEl, onClose) {
792
1361
  hrefInput.value = current.href;
793
1362
  if (langs.length > 1) {
794
1363
  const tabs = document.createElement("div");
795
- 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")};`;
796
1365
  const renderTabs2 = () => {
797
1366
  tabs.innerHTML = "";
798
1367
  langs.forEach((lang) => {
@@ -802,11 +1371,11 @@ function buildLinkPopup(key, anchorEl, onClose) {
802
1371
  tab.style.cssText = `
803
1372
  padding: 5px 10px 6px; border: none; border-bottom: 2px solid;
804
1373
  margin-bottom: -1px;
805
- 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;
806
1375
  background: transparent;
807
- border-bottom-color: ${isActive ? accent2() : "transparent"};
808
- color: ${isActive ? "#fff" : "rgba(255,255,255,0.3)"};
809
- 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")};
810
1379
  `;
811
1380
  tab.addEventListener("click", () => {
812
1381
  stage(activeLang);
@@ -824,12 +1393,12 @@ function buildLinkPopup(key, anchorEl, onClose) {
824
1393
  }
825
1394
  wrap.appendChild(makeLabelled("Label", labelInput));
826
1395
  wrap.appendChild(makeLabelled("URL", hrefInput));
827
- const hint = document.createElement("div");
828
- hint.style.cssText = `font-size:11px;color:rgba(255,255,255,0.28);margin:-4px 0 10px;line-height:1.45;`;
829
- 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.";
830
- 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);
831
1400
  const warn = document.createElement("div");
832
- 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;`;
833
1402
  wrap.appendChild(warn);
834
1403
  const paint = () => {
835
1404
  labelNodes.forEach((n) => n.textContent = labelInput.value);
@@ -886,8 +1455,8 @@ function buildImagePopup(key, anchorEl, onClose) {
886
1455
  if (currentSrc && !currentSrc.startsWith("data:")) {
887
1456
  const previewWrap = document.createElement("div");
888
1457
  previewWrap.style.cssText = `
889
- border-radius: 10px; overflow: hidden; margin-bottom: 10px;
890
- 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")};
891
1460
  position: relative; height: 100px;
892
1461
  `;
893
1462
  const previewImg = document.createElement("img");
@@ -897,8 +1466,8 @@ function buildImagePopup(key, anchorEl, onClose) {
897
1466
  previewLabel.textContent = "Current";
898
1467
  previewLabel.style.cssText = `
899
1468
  position: absolute; bottom: 0; left: 0; right: 0;
900
- font-size: 10px; color: rgba(255,255,255,0.45); letter-spacing: 0.04em;
901
- 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;
902
1471
  background: linear-gradient(transparent, rgba(0,0,0,0.55));
903
1472
  `;
904
1473
  previewWrap.appendChild(previewImg);
@@ -908,23 +1477,23 @@ function buildImagePopup(key, anchorEl, onClose) {
908
1477
  const dropZone = document.createElement("label");
909
1478
  dropZone.style.cssText = `
910
1479
  display: flex; flex-direction: column; align-items: center; justify-content: center;
911
- gap: 8px;
912
- border: 1.5px dashed rgba(255,255,255,0.1); border-radius: 10px;
913
- 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")};
914
1483
  cursor: pointer;
915
- transition: border-color 0.18s, background 0.18s;
916
- 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")};
917
1486
  `;
918
1487
  const uploadIcon = document.createElement("div");
919
- 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")};`;
920
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">
921
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"/>
922
1491
  </svg>`;
923
1492
  const dropText = document.createElement("div");
924
1493
  dropText.style.cssText = `text-align: center;`;
925
1494
  dropText.innerHTML = `
926
- <div style="font-size:12px;font-weight:500;color:rgba(255,255,255,0.5);">Drop an image</div>
927
- <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>
928
1497
  `;
929
1498
  dropZone.appendChild(uploadIcon);
930
1499
  dropZone.appendChild(dropText);
@@ -934,18 +1503,18 @@ function buildImagePopup(key, anchorEl, onClose) {
934
1503
  fileInput.style.display = "none";
935
1504
  dropZone.appendChild(fileInput);
936
1505
  const statusWrap = document.createElement("div");
937
- statusWrap.style.cssText = `margin-top: 8px; min-height: 18px;`;
1506
+ statusWrap.style.cssText = `margin-top: ${v("space-2")}; min-height: 18px;`;
938
1507
  const statusMsg = document.createElement("p");
939
- 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")};`;
940
1509
  const progressBar = document.createElement("div");
941
1510
  progressBar.style.cssText = `
942
- height: 2px; border-radius: 2px; background: rgba(255,255,255,0.05);
1511
+ height: 2px; border-radius: 2px; background: ${v("surface-raised")};
943
1512
  overflow: hidden; margin-top: 6px; display: none;
944
1513
  `;
945
1514
  const progressFill = document.createElement("div");
946
1515
  progressFill.style.cssText = `
947
- height: 100%; border-radius: 2px; background: ${accent2()};
948
- 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")};
949
1518
  `;
950
1519
  progressBar.appendChild(progressFill);
951
1520
  statusWrap.appendChild(statusMsg);
@@ -953,20 +1522,20 @@ function buildImagePopup(key, anchorEl, onClose) {
953
1522
  const handleFile = async (file) => {
954
1523
  if (!file.type.startsWith("image/")) {
955
1524
  statusMsg.textContent = "Only image files are supported";
956
- statusMsg.style.color = "#f87171";
1525
+ statusMsg.style.color = v("danger");
957
1526
  return;
958
1527
  }
959
1528
  const maxMb = 10;
960
1529
  if (file.size > maxMb * 1024 * 1024) {
961
1530
  statusMsg.textContent = `File too large (max ${maxMb}MB)`;
962
- statusMsg.style.color = "#f87171";
1531
+ statusMsg.style.color = v("danger");
963
1532
  return;
964
1533
  }
965
- dropZone.style.borderColor = `${accent2()}55`;
966
- dropZone.style.background = `${accent2()}0a`;
967
- uploadIcon.style.color = accent2();
1534
+ dropZone.style.borderColor = v("accent-ring");
1535
+ dropZone.style.background = v("accent-soft");
1536
+ uploadIcon.style.color = v("accent");
968
1537
  statusMsg.textContent = "Uploading\u2026";
969
- statusMsg.style.color = "rgba(255,255,255,0.45)";
1538
+ statusMsg.style.color = v("fg-muted");
970
1539
  progressBar.style.display = "block";
971
1540
  progressFill.style.width = "0%";
972
1541
  try {
@@ -990,16 +1559,16 @@ function buildImagePopup(key, anchorEl, onClose) {
990
1559
  }
991
1560
  setTimeout(() => {
992
1561
  statusMsg.textContent = "Done";
993
- statusMsg.style.color = "#4ade80";
1562
+ statusMsg.style.color = v("success");
994
1563
  setTimeout(onClose, 600);
995
1564
  }, 200);
996
1565
  } catch {
997
1566
  progressBar.style.display = "none";
998
1567
  statusMsg.textContent = "Upload failed \u2014 try again";
999
- statusMsg.style.color = "#f87171";
1000
- dropZone.style.borderColor = "rgba(255,255,255,0.1)";
1001
- dropZone.style.background = "rgba(255,255,255,0.015)";
1002
- 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");
1003
1572
  }
1004
1573
  };
1005
1574
  fileInput.addEventListener("change", () => {
@@ -1009,35 +1578,35 @@ function buildImagePopup(key, anchorEl, onClose) {
1009
1578
  e.preventDefault();
1010
1579
  if (!dragover) {
1011
1580
  dragover = true;
1012
- dropZone.style.borderColor = `${accent2()}88`;
1013
- dropZone.style.background = `${accent2()}0d`;
1014
- uploadIcon.style.color = accent2();
1581
+ dropZone.style.borderColor = v("accent-ring");
1582
+ dropZone.style.background = v("accent-soft");
1583
+ uploadIcon.style.color = v("accent");
1015
1584
  }
1016
1585
  });
1017
1586
  dropZone.addEventListener("dragleave", () => {
1018
1587
  dragover = false;
1019
- dropZone.style.borderColor = "rgba(255,255,255,0.1)";
1020
- dropZone.style.background = "rgba(255,255,255,0.015)";
1021
- 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");
1022
1591
  });
1023
1592
  dropZone.addEventListener("drop", (e) => {
1024
1593
  e.preventDefault();
1025
1594
  dragover = false;
1026
- dropZone.style.borderColor = "rgba(255,255,255,0.1)";
1027
- dropZone.style.background = "rgba(255,255,255,0.015)";
1595
+ dropZone.style.borderColor = v("border-strong");
1596
+ dropZone.style.background = v("surface-raised");
1028
1597
  const file = e.dataTransfer?.files[0];
1029
1598
  if (file) handleFile(file);
1030
1599
  });
1031
1600
  dropZone.addEventListener("mouseenter", () => {
1032
1601
  if (!dragover) {
1033
- dropZone.style.borderColor = "rgba(255,255,255,0.18)";
1034
- dropZone.style.background = "rgba(255,255,255,0.03)";
1602
+ dropZone.style.borderColor = v("border-strong");
1603
+ dropZone.style.background = v("surface-hover");
1035
1604
  }
1036
1605
  });
1037
1606
  dropZone.addEventListener("mouseleave", () => {
1038
1607
  if (!dragover) {
1039
- dropZone.style.borderColor = "rgba(255,255,255,0.1)";
1040
- dropZone.style.background = "rgba(255,255,255,0.015)";
1608
+ dropZone.style.borderColor = v("border-strong");
1609
+ dropZone.style.background = v("surface-raised");
1041
1610
  }
1042
1611
  });
1043
1612
  wrap.appendChild(dropZone);
@@ -1048,67 +1617,246 @@ function makeCloseButton(onClose) {
1048
1617
  const btn = document.createElement("button");
1049
1618
  btn.style.cssText = `
1050
1619
  display: flex; align-items: center; justify-content: center;
1051
- width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;
1052
- background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.06);
1053
- cursor: pointer; color: rgba(255,255,255,0.35); padding: 0;
1054
- 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")};
1055
1624
  `;
1056
1625
  btn.innerHTML = `<svg width="9" height="9" viewBox="0 0 10 10" fill="none">
1057
1626
  <path d="M1 1l8 8M9 1L1 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
1058
1627
  </svg>`;
1059
- btn.addEventListener("mouseenter", () => {
1060
- btn.style.background = "rgba(255,255,255,0.08)";
1061
- btn.style.color = "rgba(255,255,255,0.75)";
1062
- });
1063
- btn.addEventListener("mouseleave", () => {
1064
- btn.style.background = "rgba(255,255,255,0.04)";
1065
- btn.style.color = "rgba(255,255,255,0.35)";
1066
- });
1628
+ attachHover(btn, { color: v("fg") });
1067
1629
  btn.addEventListener("click", onClose);
1068
1630
  return btn;
1069
1631
  }
1070
- function makePrimaryButton(label, color) {
1632
+ function makePrimaryButton(label2, color) {
1071
1633
  const btn = document.createElement("button");
1072
- btn.textContent = label;
1634
+ markUi(btn);
1635
+ btn.textContent = label2;
1073
1636
  btn.style.cssText = `
1074
- padding: 7px 16px; border-radius: 8px; border: none; cursor: pointer;
1075
- background: #fff; color: #0c0c0e;
1076
- font-size: 12px; font-weight: 600; letter-spacing: 0.01em;
1077
- 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")};
1078
1641
  `;
1079
1642
  btn.addEventListener("mouseenter", () => btn.style.opacity = "0.88");
1080
1643
  btn.addEventListener("mouseleave", () => btn.style.opacity = "1");
1644
+ attachPress(btn);
1081
1645
  return btn;
1082
1646
  }
1083
1647
  function applyPopupStyles(el) {
1648
+ markUi(el);
1084
1649
  el.style.cssText = `
1650
+ ${surface(2)}
1085
1651
  position: absolute;
1086
- z-index: 2147483646;
1652
+ z-index: ${v("z-panel")};
1087
1653
  width: 320px;
1088
- background: rgba(14, 14, 16, 0.97);
1089
- backdrop-filter: blur(24px) saturate(180%);
1090
- -webkit-backdrop-filter: blur(24px) saturate(180%);
1091
- border: 1px solid rgba(255,255,255,0.07);
1092
- border-radius: 14px;
1654
+ box-shadow: ${v("shadow-lg")};
1093
1655
  padding: 14px;
1094
- 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);
1095
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1096
- 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")};
1097
1660
  `;
1098
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
+ }
1099
1843
  function openPopup(key, fieldType2, anchorEl, onClose) {
1100
1844
  closePopup();
1101
1845
  const done = () => {
1102
1846
  onClose();
1103
1847
  closePopup();
1104
1848
  };
1105
- 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);
1106
1850
  document.body.appendChild(popup);
1107
1851
  popupEl = popup;
1108
1852
  const { top, left, origin } = getPopupPosition(anchorEl);
1109
1853
  popup.style.top = `${top}px`;
1110
1854
  popup.style.left = `${left}px`;
1111
1855
  popup.style.transformOrigin = origin;
1856
+ requestAnimationFrame(() => {
1857
+ popup.style.opacity = "1";
1858
+ popup.style.transform = "scale(1)";
1859
+ });
1112
1860
  outsideListener = (e) => {
1113
1861
  if (!popup.contains(e.target)) {
1114
1862
  closePopup();
@@ -1143,17 +1891,17 @@ function closePopup() {
1143
1891
  if (popupEl) {
1144
1892
  const el = popupEl;
1145
1893
  popupEl = null;
1146
- el.style.animation = "none";
1147
- 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")}`;
1148
1895
  el.style.opacity = "0";
1149
- el.style.transform = "scale(0.96) translateY(3px)";
1896
+ el.style.transform = "scale(0.93)";
1150
1897
  setTimeout(() => el.remove(), 200);
1151
1898
  }
1152
1899
  }
1153
1900
 
1154
1901
  // src/list-panel.ts
1155
- var PANEL_Z = 2147483646;
1156
- var BACKDROP_Z = 2147483646;
1902
+ var PANEL_Z = v("z-panel");
1903
+ var TOOLBAR_RESERVE = "100px";
1904
+ var BACKDROP_Z = v("z-overlay");
1157
1905
  var panelEl = null;
1158
1906
  var backdropEl = null;
1159
1907
  var styleInjected2 = false;
@@ -1166,6 +1914,7 @@ var currentOnTranslateEntry = null;
1166
1914
  function injectStyles2() {
1167
1915
  if (styleInjected2) return;
1168
1916
  styleInjected2 = true;
1917
+ injectBaseStyles(state.config?.accentColor, state.config?.toolbarAccent === true);
1169
1918
  const s = document.createElement("style");
1170
1919
  s.textContent = `
1171
1920
  @keyframes cancia-panel-in {
@@ -1183,9 +1932,6 @@ function injectStyles2() {
1183
1932
  `;
1184
1933
  document.head.appendChild(s);
1185
1934
  }
1186
- function accent3() {
1187
- return state.config?.accentColor ?? "#6366f1";
1188
- }
1189
1935
  function escapeHtml(s) {
1190
1936
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1191
1937
  }
@@ -1231,9 +1977,9 @@ function derivePreview(schema, data) {
1231
1977
  let thumbnail = "";
1232
1978
  for (const f of schema.fields) {
1233
1979
  if (f.widget !== "image") continue;
1234
- const v = data[f.name];
1235
- if (typeof v === "string" && v.trim()) {
1236
- thumbnail = v.trim();
1980
+ const v2 = data[f.name];
1981
+ if (typeof v2 === "string" && v2.trim()) {
1982
+ thumbnail = v2.trim();
1237
1983
  break;
1238
1984
  }
1239
1985
  }
@@ -1245,52 +1991,73 @@ function locales() {
1245
1991
  function buildShell(schema) {
1246
1992
  const panel = document.createElement("div");
1247
1993
  panel.dataset.canciaListPanel = "1";
1994
+ markUi(panel);
1248
1995
  panel.style.cssText = `
1996
+ ${surface(2)}
1249
1997
  position: fixed;
1250
1998
  top: 0; right: 0; bottom: 0;
1999
+ padding-bottom: ${TOOLBAR_RESERVE};
1251
2000
  width: min(420px, 100vw);
1252
- background: #fff;
1253
- color: #1a1a1d;
2001
+ color: ${v("fg")};
1254
2002
  z-index: ${PANEL_Z};
1255
- 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")};
1256
2007
  display: flex;
1257
2008
  flex-direction: column;
1258
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1259
- 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;
1260
2015
  `;
1261
- 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 = `
1262
2024
  <header style="
1263
2025
  display: flex; align-items: center; justify-content: space-between;
1264
- padding: 16px 20px;
1265
- border-bottom: 1px solid #eaeaea;
2026
+ padding: ${v("space-4")} ${v("space-5")};
2027
+ border-bottom: 1px solid ${v("border")};
1266
2028
  ">
1267
2029
  <div>
1268
- <div style="font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: #777;">List</div>
1269
- <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>
1270
2032
  </div>
1271
2033
  <button data-cancia-close style="
2034
+ ${iconButton()}
1272
2035
  appearance: none; border: 0; background: transparent;
1273
- cursor: pointer; padding: 6px; border-radius: 6px;
1274
- color: #555; transition: background 0.12s, color 0.12s;
2036
+ cursor: pointer;
1275
2037
  " aria-label="Close panel">
1276
2038
  <svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1277
2039
  <path d="M4 4l10 10M14 4L4 14"/>
1278
2040
  </svg>
1279
2041
  </button>
1280
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. -->
1281
2049
  <div data-cancia-tabs style="
1282
- display: flex; gap: 4px;
1283
- padding: 8px 16px 0;
1284
- border-bottom: 1px solid #f3f3f3;
1285
- 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")};
1286
2053
  "></div>
1287
- <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")};">
1288
2055
  <button data-cancia-add style="
1289
- appearance: none; border: 1px dashed ${accent3()}; background: ${accent3()}10;
1290
- color: ${accent3()}; font-weight: 600; font-size: 13px;
1291
- 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;
1292
2060
  display: flex; align-items: center; justify-content: center; gap: 6px;
1293
- transition: background 0.12s;
1294
2061
  ">
1295
2062
  <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1296
2063
  <path d="M7 2v10M2 7h10"/>
@@ -1300,13 +2067,18 @@ function buildShell(schema) {
1300
2067
  </div>
1301
2068
  <div data-cancia-entries style="
1302
2069
  flex: 1; overflow-y: auto;
1303
- padding: 8px 12px 16px;
2070
+ padding: ${v("space-2")} ${v("space-3")} ${v("space-4")};
1304
2071
  ">
1305
- <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>
1306
2073
  </div>
1307
2074
  `;
1308
- const body = panel.querySelector("[data-cancia-entries]");
1309
- 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") });
1310
2082
  return { panel, body, tabsRow };
1311
2083
  }
1312
2084
  function renderTabs(tabsRow, activeLocale, onSwitch) {
@@ -1322,22 +2094,22 @@ function renderTabs(tabsRow, activeLocale, onSwitch) {
1322
2094
  const isActive = loc === activeLocale;
1323
2095
  tab.style.cssText = `
1324
2096
  appearance: none; border: 0; background: transparent;
1325
- font-family: inherit; font-size: 12px; font-weight: 600;
2097
+ font-family: inherit; font-size: ${v("text-sm")}; font-weight: 600;
1326
2098
  letter-spacing: 0.04em; text-transform: uppercase;
1327
- padding: 8px 10px 9px;
2099
+ padding: ${v("space-2")} 10px 9px;
1328
2100
  cursor: ${isActive ? "default" : "pointer"};
1329
- color: ${isActive ? accent3() : "#777"};
1330
- border-bottom: 2px solid ${isActive ? accent3() : "transparent"};
2101
+ color: ${isActive ? v("accent") : v("fg-muted")};
2102
+ border-bottom: 2px solid ${isActive ? v("accent") : "transparent"};
1331
2103
  margin-bottom: -1px;
1332
2104
  transition: color 0.12s, border-color 0.12s;
1333
2105
  `;
1334
2106
  tab.textContent = loc;
1335
2107
  if (!isActive) {
1336
2108
  tab.addEventListener("mouseenter", () => {
1337
- tab.style.color = "#333";
2109
+ tab.style.color = v("fg-strong");
1338
2110
  });
1339
2111
  tab.addEventListener("mouseleave", () => {
1340
- tab.style.color = "#777";
2112
+ tab.style.color = v("fg-muted");
1341
2113
  });
1342
2114
  tab.addEventListener("click", () => onSwitch(loc));
1343
2115
  }
@@ -1366,7 +2138,7 @@ function renderEntries(body, schema, activeLocale, entriesInLocale, translations
1366
2138
  });
1367
2139
  if (rows.length === 0) {
1368
2140
  body.innerHTML = `
1369
- <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")};">
1370
2142
  No entries yet. Click "Add ${escapeHtml(schema.labelSingular.toLowerCase())}" above to create one.
1371
2143
  </div>
1372
2144
  `;
@@ -1392,7 +2164,7 @@ function renderEntries(body, schema, activeLocale, entriesInLocale, translations
1392
2164
  }
1393
2165
  const err = document.createElement("div");
1394
2166
  err.textContent = "Couldn't save the new order. Reverted.";
1395
- 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")};`;
1396
2168
  body.insertBefore(err, body.firstChild);
1397
2169
  setTimeout(() => err.remove(), 3e3);
1398
2170
  }
@@ -1400,7 +2172,7 @@ function renderEntries(body, schema, activeLocale, entriesInLocale, translations
1400
2172
  rows.forEach((row) => {
1401
2173
  const isStub = row.entry === null;
1402
2174
  const wrap = document.createElement("div");
1403
- 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;`;
1404
2176
  const handle = document.createElement("div");
1405
2177
  handle.textContent = "\u22EE\u22EE";
1406
2178
  handle.title = "Drag to reorder";
@@ -1408,14 +2180,22 @@ function renderEntries(body, schema, activeLocale, entriesInLocale, translations
1408
2180
  handle.style.cssText = `
1409
2181
  cursor: grab; user-select: none;
1410
2182
  display: flex; align-items: center; justify-content: center;
1411
- color: #bbb; font-size: 13px; letter-spacing: -2px;
2183
+ color: ${v("fg-faint")}; font-size: ${v("text-base")}; letter-spacing: -2px;
1412
2184
  padding: 0 2px; flex-shrink: 0;
2185
+ transition: color 0.12s;
1413
2186
  `;
2187
+ handle.addEventListener("mouseenter", () => {
2188
+ handle.style.color = v("accent");
2189
+ });
2190
+ handle.addEventListener("mouseleave", () => {
2191
+ handle.style.color = v("fg-faint");
2192
+ });
1414
2193
  const item = document.createElement("button");
1415
2194
  item.style.cssText = `
1416
- 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")};
1417
2197
  text-align: left; flex: 1; min-width: 0;
1418
- padding: 12px 14px; border-radius: 8px; cursor: pointer;
2198
+ padding: ${v("space-3")} 14px; border-radius: ${v("radius-sm")}; cursor: pointer;
1419
2199
  transition: background 0.12s, border-color 0.12s, transform 0.12s;
1420
2200
  display: block;
1421
2201
  `;
@@ -1459,27 +2239,36 @@ function renderEntries(body, schema, activeLocale, entriesInLocale, translations
1459
2239
  const titleValue = row.entry.data[schema.titleField];
1460
2240
  const title = typeof titleValue === "string" && titleValue.trim().length > 0 ? titleValue : `(untitled ${schema.labelSingular.toLowerCase()})`;
1461
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>` : "";
1462
2249
  const textCol = `
1463
2250
  <div style="min-width: 0; flex: 1;">
1464
- <div style="font-weight: 600; font-size: 14px; color: #1a1a1d; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${escapeHtml(title)}</div>
1465
- ${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>` : ""}
1466
2255
  </div>
1467
2256
  `;
1468
2257
  const thumb = thumbnail ? `<img src="${escapeHtml(thumbnail)}" alt="" loading="lazy" style="
1469
2258
  width: 44px; height: 44px; flex-shrink: 0; object-fit: cover;
1470
- border-radius: 6px; background: #eee; border: 1px solid #eaeaea;
2259
+ border-radius: ${v("radius-sm")}; background: ${v("surface-raised")}; border: 1px solid ${v("border")};
1471
2260
  " onerror="this.style.display='none'" />` : "";
1472
2261
  item.innerHTML = `
1473
- <div style="display: flex; align-items: center; gap: 12px;">
2262
+ <div style="display: flex; align-items: center; gap: ${v("space-3")};">
1474
2263
  ${thumb}${textCol}
1475
2264
  </div>
1476
2265
  `;
1477
2266
  item.addEventListener("mouseenter", () => {
1478
- item.style.background = "#f3f3f3";
1479
- item.style.borderColor = "#e3e3e3";
2267
+ item.style.background = v("surface-hover");
2268
+ item.style.borderColor = v("border-strong");
1480
2269
  });
1481
2270
  item.addEventListener("mouseleave", () => {
1482
- item.style.background = "#fafafa";
2271
+ item.style.background = v("surface-raised");
1483
2272
  item.style.borderColor = "transparent";
1484
2273
  });
1485
2274
  item.addEventListener("click", () => currentOnEditEntry?.(row.entry, activeLocale));
@@ -1489,23 +2278,26 @@ function renderEntries(body, schema, activeLocale, entriesInLocale, translations
1489
2278
  const sourceTitleValue = sourceEntry?.data[schema.titleField];
1490
2279
  const sourceTitle = typeof sourceTitleValue === "string" && sourceTitleValue.trim().length > 0 ? sourceTitleValue : row.id;
1491
2280
  item.innerHTML = `
1492
- <div style="display: flex; align-items: center; gap: 8px;">
2281
+ <div style="display: flex; align-items: center; gap: ${v("space-2")};">
1493
2282
  <span style="
1494
2283
  font-size: 9px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
1495
- 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")};
1496
2288
  padding: 2px 6px; border-radius: 4px;
1497
2289
  ">Not translated</span>
1498
- <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>
1499
2291
  </div>
1500
- <div style="font-weight: 600; font-size: 14px; color: #6b5616; margin-top: 6px;">${escapeHtml(sourceTitle)}</div>
1501
- <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>
1502
2294
  `;
1503
2295
  item.addEventListener("mouseenter", () => {
1504
- item.style.background = "#fff2d5";
1505
- item.style.borderColor = "#f5d27a";
2296
+ item.style.background = "rgba(217, 119, 6, 0.18)";
2297
+ item.style.borderColor = v("warning");
1506
2298
  });
1507
2299
  item.addEventListener("mouseleave", () => {
1508
- item.style.background = "#fff8ee";
2300
+ item.style.background = v("warning-soft");
1509
2301
  item.style.borderColor = "transparent";
1510
2302
  });
1511
2303
  item.addEventListener(
@@ -1524,11 +2316,12 @@ async function openListPanel(opts) {
1524
2316
  injectStyles2();
1525
2317
  const backdrop = document.createElement("div");
1526
2318
  backdrop.dataset.canciaPanelBackdrop = "1";
2319
+ markUi(backdrop);
1527
2320
  backdrop.style.cssText = `
1528
2321
  position: fixed; inset: 0;
1529
- background: rgba(10,10,12,0.32);
2322
+ background: rgba(10,10,12,0.20);
1530
2323
  z-index: ${BACKDROP_Z};
1531
- animation: cancia-backdrop-in 0.18s ease-out forwards;
2324
+ animation: cancia-backdrop-in ${v("duration")} ${v("ease-out")} forwards;
1532
2325
  `;
1533
2326
  backdrop.addEventListener("click", () => closeListPanel());
1534
2327
  document.body.appendChild(backdrop);
@@ -1578,7 +2371,7 @@ async function refreshListPanel() {
1578
2371
  } catch (err) {
1579
2372
  if (!panelEl) return;
1580
2373
  body.innerHTML = `
1581
- <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")};">
1582
2375
  Failed to load entries: ${escapeHtml(err instanceof Error ? err.message : String(err))}
1583
2376
  </div>
1584
2377
  `;
@@ -1586,7 +2379,7 @@ async function refreshListPanel() {
1586
2379
  }
1587
2380
  function closeListPanel() {
1588
2381
  if (panelEl) {
1589
- 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`;
1590
2383
  const el = panelEl;
1591
2384
  setTimeout(() => el.remove(), 180);
1592
2385
  panelEl = null;
@@ -1594,7 +2387,7 @@ function closeListPanel() {
1594
2387
  if (backdropEl) {
1595
2388
  const el = backdropEl;
1596
2389
  el.style.opacity = "0";
1597
- el.style.transition = "opacity 0.18s ease-out";
2390
+ el.style.transition = `opacity ${v("duration-exit")} ${v("ease-out")}`;
1598
2391
  setTimeout(() => el.remove(), 180);
1599
2392
  backdropEl = null;
1600
2393
  }
@@ -1608,65 +2401,146 @@ function closeListPanel() {
1608
2401
  function isListPanelOpen() {
1609
2402
  return panelEl !== null;
1610
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
+ }
1611
2449
 
1612
2450
  // src/entry-modal.ts
1613
2451
  import {
1614
- rowsToPortableText,
1615
- portableTextToRows,
1616
- portableTextSubsetSchema,
1617
- PT_STYLES
2452
+ rowsToPortableText as rowsToPortableText2,
2453
+ portableTextToRows as portableTextToRows2,
2454
+ portableTextSubsetSchema as portableTextSubsetSchema2,
2455
+ PT_STYLES as PT_STYLES2
1618
2456
  } from "@cancia/astro/richtext";
1619
2457
  import { slugify } from "@cancia/astro/schema";
1620
- var MODAL_Z = 2147483647;
1621
- var BACKDROP_Z2 = 2147483646;
2458
+ var MODAL_Z = v("z-bar");
2459
+ var BACKDROP_Z2 = v("z-panel");
1622
2460
  var modalEl = null;
1623
2461
  var backdropEl2 = null;
1624
2462
  var escListener = null;
1625
2463
  var styleInjected3 = false;
2464
+ var mountedInPanel = false;
1626
2465
  function injectStyles3() {
1627
2466
  if (styleInjected3) return;
1628
2467
  styleInjected3 = true;
2468
+ injectBaseStyles(state.config?.accentColor, state.config?.toolbarAccent === true);
1629
2469
  const s = document.createElement("style");
1630
2470
  s.textContent = `
1631
- @keyframes cancia-modal-in {
1632
- from { opacity: 0; transform: translate(-50%, calc(-50% + 8px)) scale(0.985); }
1633
- to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
1634
- }
1635
- @keyframes cancia-modal-out {
1636
- from { opacity: 1; transform: translate(-50%, -50%) scale(1); }
1637
- to { opacity: 0; transform: translate(-50%, calc(-50% + 8px)) scale(0.985); }
1638
- }
1639
2471
  .cancia-form-input:focus,
1640
2472
  .cancia-form-textarea:focus,
1641
2473
  .cancia-form-select:focus {
1642
- border-color: var(--cancia-accent-border, rgba(99,102,241,0.6));
1643
- background: rgba(255,255,255,0.05);
2474
+ border-color: var(--cancia-accent-border, ${v("accent-ring")});
2475
+ background: ${v("surface-hover")};
1644
2476
  outline: none;
1645
2477
  }
1646
2478
  .cancia-form-input::placeholder,
1647
2479
  .cancia-form-textarea::placeholder {
1648
- color: rgba(255,255,255,0.25);
2480
+ color: ${v("fg-faint")};
1649
2481
  }
1650
2482
  .cancia-form-select option {
1651
- background: #15151a;
1652
- 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")};
1653
2487
  }
1654
2488
  .cancia-field-error {
1655
- color: #ff8786;
1656
- font-size: 11px;
2489
+ color: ${v("danger")};
2490
+ font-size: ${v("text-xs")};
1657
2491
  margin-top: 5px;
1658
2492
  line-height: 1.35;
1659
2493
  }
1660
2494
  `;
1661
2495
  document.head.appendChild(s);
1662
2496
  }
1663
- function accent4() {
1664
- return state.config?.accentColor ?? "#6366f1";
2497
+ function accent3() {
2498
+ const useAccent = state.config?.toolbarAccent === true;
2499
+ return (useAccent ? state.config?.accentColor : void 0) ?? "#18181b";
1665
2500
  }
1666
2501
  function accentBorder() {
1667
- const a = accent4();
2502
+ const a = accent3();
1668
2503
  if (/^#[0-9a-f]{6}$/i.test(a)) return `${a}99`;
1669
- 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);
1670
2544
  }
1671
2545
  function isoToLocalInput(iso) {
1672
2546
  if (!iso) return "";
@@ -1682,44 +2556,39 @@ function localInputToIso(local) {
1682
2556
  return d.toISOString();
1683
2557
  }
1684
2558
  var INPUT_BASE = `
1685
- width: 100%; box-sizing: border-box;
1686
- background: rgba(255,255,255,0.03);
1687
- color: rgba(255,255,255,0.9);
1688
- border: 1px solid rgba(255,255,255,0.07);
1689
- border-radius: 8px;
1690
- padding: 8px 11px;
1691
- font-size: 13px;
2559
+ ${input()}
2560
+ box-sizing: border-box;
1692
2561
  font-family: inherit;
1693
2562
  line-height: 1.5;
1694
- outline: none;
1695
- transition: border-color 0.18s, background 0.18s;
1696
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>")`;
1697
2565
  var MAX_FIELD_DEPTH = 6;
1698
2566
  function renderField(field, initial, depth = 0) {
1699
2567
  const wrapper = document.createElement("div");
1700
- wrapper.style.cssText = `margin-bottom: 14px;`;
2568
+ wrapper.style.cssText = `margin-bottom: ${v("space-4")};`;
1701
2569
  const labelRow = document.createElement("label");
1702
2570
  labelRow.style.cssText = `
2571
+ ${label()}
1703
2572
  display: flex; align-items: baseline; justify-content: space-between;
1704
- gap: 8px;
1705
- font-size: 11px; font-weight: 600;
1706
- color: rgba(255,255,255,0.75);
1707
- letter-spacing: 0.04em;
2573
+ gap: ${v("space-2")};
1708
2574
  margin-bottom: 5px;
1709
2575
  `;
1710
2576
  const labelText = document.createElement("span");
1711
- labelText.textContent = field.label;
1712
- if (field.required) {
1713
- const star = document.createElement("span");
1714
- star.textContent = " *";
1715
- star.style.color = "rgba(255,135,134,0.8)";
1716
- labelText.appendChild(star);
1717
- }
2577
+ labelText.textContent = humanise(field.label);
1718
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
+ }
1719
2588
  if (field.label) wrapper.appendChild(labelRow);
1720
2589
  if (field.description) {
1721
2590
  const help = document.createElement("div");
1722
- 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;`;
1723
2592
  help.textContent = field.description;
1724
2593
  wrapper.appendChild(help);
1725
2594
  }
@@ -1770,7 +2639,7 @@ function renderField(field, initial, depth = 0) {
1770
2639
  case "textarea": {
1771
2640
  const ta = document.createElement("textarea");
1772
2641
  ta.className = "cancia-form-textarea";
1773
- 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")};`;
1774
2643
  ta.rows = 5;
1775
2644
  if (field.placeholder) ta.placeholder = field.placeholder;
1776
2645
  if (typeof initial === "string") ta.value = initial;
@@ -1783,10 +2652,10 @@ function renderField(field, initial, depth = 0) {
1783
2652
  row.style.cssText = `display: flex; align-items: center; gap: 9px; cursor: pointer; user-select: none; padding: 6px 0;`;
1784
2653
  const cb = document.createElement("input");
1785
2654
  cb.type = "checkbox";
1786
- cb.style.cssText = `width: 16px; height: 16px; accent-color: ${accent4()};`;
2655
+ cb.style.cssText = `width: 16px; height: 16px; accent-color: ${v("accent")};`;
1787
2656
  if (initial === true) cb.checked = true;
1788
2657
  const txt = document.createElement("span");
1789
- 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")};`;
1790
2659
  txt.textContent = field.placeholder ?? `Enable ${field.label.toLowerCase()}`;
1791
2660
  row.appendChild(cb);
1792
2661
  row.appendChild(txt);
@@ -1797,7 +2666,7 @@ function renderField(field, initial, depth = 0) {
1797
2666
  case "select": {
1798
2667
  const sel = document.createElement("select");
1799
2668
  sel.className = "cancia-form-select";
1800
- 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;`;
1801
2670
  if (!field.required) {
1802
2671
  const empty = document.createElement("option");
1803
2672
  empty.value = "";
@@ -1821,19 +2690,19 @@ function renderField(field, initial, depth = 0) {
1821
2690
  const container = document.createElement("div");
1822
2691
  container.style.cssText = `
1823
2692
  display: flex; gap: 10px; align-items: stretch;
1824
- background: rgba(255,255,255,0.02);
1825
- border: 1px dashed rgba(255,255,255,0.1);
1826
- border-radius: 10px;
2693
+ background: ${v("surface-raised")};
2694
+ border: 1px dashed ${v("border-strong")};
2695
+ border-radius: ${v("radius")};
1827
2696
  padding: 10px;
1828
2697
  `;
1829
2698
  const preview = document.createElement("div");
1830
2699
  preview.style.cssText = `
1831
2700
  width: 72px; height: 72px; flex-shrink: 0;
1832
- background: rgba(255,255,255,0.04) no-repeat center / cover;
1833
- border: 1px solid rgba(255,255,255,0.06);
1834
- border-radius: 6px;
2701
+ background: ${v("surface-raised")} no-repeat center / cover;
2702
+ border: 1px solid ${v("border")};
2703
+ border-radius: ${v("radius-sm")};
1835
2704
  display: flex; align-items: center; justify-content: center;
1836
- color: rgba(255,255,255,0.25);
2705
+ color: ${v("fg-faint")};
1837
2706
  `;
1838
2707
  const updatePreview = (url) => {
1839
2708
  if (url) {
@@ -1856,29 +2725,28 @@ function renderField(field, initial, depth = 0) {
1856
2725
  const uploadBtn = document.createElement("button");
1857
2726
  uploadBtn.type = "button";
1858
2727
  uploadBtn.style.cssText = `
1859
- appearance: none; cursor: pointer;
1860
- background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.85);
1861
- border: 1px solid rgba(255,255,255,0.07);
1862
- font-size: 11px; font-weight: 500; letter-spacing: 0.02em;
1863
- padding: 5px 10px; border-radius: 6px;
1864
- 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;
1865
2736
  `;
1866
2737
  uploadBtn.textContent = "Upload\u2026";
1867
- uploadBtn.addEventListener("mouseenter", () => {
1868
- uploadBtn.style.background = "rgba(255,255,255,0.1)";
1869
- });
1870
- uploadBtn.addEventListener("mouseleave", () => {
1871
- uploadBtn.style.background = "rgba(255,255,255,0.06)";
1872
- });
2738
+ attachHover(uploadBtn, { bg: v("surface-active") });
1873
2739
  uploadBtn.addEventListener("click", () => fileInput.click());
1874
2740
  const clearBtn = document.createElement("button");
1875
2741
  clearBtn.type = "button";
1876
2742
  clearBtn.style.cssText = `
1877
- appearance: none; cursor: pointer;
1878
- background: transparent; color: rgba(255,255,255,0.4);
2743
+ ${button("ghost")}
2744
+ color: ${v("fg-muted")};
1879
2745
  border: 1px solid transparent;
1880
- font-size: 11px;
1881
- padding: 5px 8px; border-radius: 6px;
2746
+ height: auto;
2747
+ font-size: ${v("text-xs")};
2748
+ padding: 5px 8px;
2749
+ cursor: pointer;
1882
2750
  `;
1883
2751
  clearBtn.textContent = "Clear";
1884
2752
  clearBtn.addEventListener("click", () => {
@@ -1891,7 +2759,7 @@ function renderField(field, initial, depth = 0) {
1891
2759
  const urlField = document.createElement("input");
1892
2760
  urlField.type = "url";
1893
2761
  urlField.className = "cancia-form-input";
1894
- 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;`;
1895
2763
  urlField.placeholder = "https://\u2026 or upload";
1896
2764
  urlField.value = initialUrl;
1897
2765
  urlField.addEventListener("input", () => {
@@ -1899,7 +2767,7 @@ function renderField(field, initial, depth = 0) {
1899
2767
  updatePreview(currentUrl);
1900
2768
  });
1901
2769
  const progressEl = document.createElement("div");
1902
- 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;`;
1903
2771
  right.appendChild(btnRow);
1904
2772
  right.appendChild(urlField);
1905
2773
  right.appendChild(progressEl);
@@ -1911,7 +2779,7 @@ function renderField(field, initial, depth = 0) {
1911
2779
  const file = fileInput.files?.[0];
1912
2780
  if (!file) return;
1913
2781
  uploadBtn.disabled = true;
1914
- progressEl.style.color = "rgba(255,255,255,0.5)";
2782
+ progressEl.style.color = v("fg-muted");
1915
2783
  try {
1916
2784
  const url = await uploadImage(file, (pct) => {
1917
2785
  progressEl.textContent = `Uploading\u2026 ${pct}%`;
@@ -1925,7 +2793,7 @@ function renderField(field, initial, depth = 0) {
1925
2793
  }, 1500);
1926
2794
  } catch (err) {
1927
2795
  progressEl.textContent = `Upload failed: ${err instanceof Error ? err.message : String(err)}`;
1928
- progressEl.style.color = "#ff8786";
2796
+ progressEl.style.color = v("danger");
1929
2797
  } finally {
1930
2798
  uploadBtn.disabled = false;
1931
2799
  fileInput.value = "";
@@ -1935,51 +2803,51 @@ function renderField(field, initial, depth = 0) {
1935
2803
  break;
1936
2804
  }
1937
2805
  case "datetime": {
1938
- const input = document.createElement("input");
1939
- input.type = "datetime-local";
1940
- input.className = "cancia-form-input";
1941
- input.style.cssText = `${INPUT_BASE} color-scheme: dark; caret-color: ${accent4()};`;
1942
- if (typeof initial === "string") input.value = isoToLocalInput(initial);
1943
- 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);
1944
2812
  getValue2 = () => {
1945
- const v = input.value.trim();
1946
- if (!v) return void 0;
1947
- return localInputToIso(v);
2813
+ const v2 = input2.value.trim();
2814
+ if (!v2) return void 0;
2815
+ return localInputToIso(v2);
1948
2816
  };
1949
2817
  break;
1950
2818
  }
1951
2819
  case "number": {
1952
- const input = document.createElement("input");
1953
- input.type = "number";
1954
- input.className = "cancia-form-input";
1955
- input.style.cssText = `${INPUT_BASE} caret-color: ${accent4()};`;
1956
- if (field.min !== void 0) input.min = String(field.min);
1957
- if (field.max !== void 0) input.max = String(field.max);
1958
- if (typeof initial === "number") input.value = String(initial);
1959
- else if (typeof initial === "string" && initial !== "") input.value = initial;
1960
- 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);
1961
2829
  getValue2 = () => {
1962
- const v = input.value.trim();
1963
- if (v === "") return void 0;
1964
- const n = Number(v);
2830
+ const v2 = input2.value.trim();
2831
+ if (v2 === "") return void 0;
2832
+ const n = Number(v2);
1965
2833
  return Number.isNaN(n) ? void 0 : n;
1966
2834
  };
1967
2835
  break;
1968
2836
  }
1969
2837
  default: {
1970
- const input = document.createElement("input");
1971
- input.type = field.widget === "url" ? "url" : field.widget === "email" ? "email" : "text";
1972
- input.className = "cancia-form-input";
1973
- input.style.cssText = `${INPUT_BASE} caret-color: ${accent4()};`;
1974
- if (field.placeholder) input.placeholder = field.placeholder;
1975
- if (field.minLength !== void 0) input.minLength = field.minLength;
1976
- if (field.maxLength !== void 0) input.maxLength = field.maxLength;
1977
- if (typeof initial === "string") input.value = initial;
1978
- wrapper.appendChild(input);
1979
- getValue2 = () => input.value;
1980
- onInput = (cb) => input.addEventListener("input", cb);
1981
- setValue = (v) => {
1982
- 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;
1983
2851
  };
1984
2852
  break;
1985
2853
  }
@@ -2008,18 +2876,14 @@ function renderArrayField(field, initial, setOwnError, depth) {
2008
2876
  const itemSchema = field.of;
2009
2877
  const container = document.createElement("div");
2010
2878
  container.style.cssText = `
2011
- display: flex; flex-direction: column; gap: 8px;
2012
- background: rgba(255,255,255,0.02);
2013
- border: 1px solid rgba(255,255,255,0.07);
2014
- border-radius: 10px;
2015
- padding: 10px;
2879
+ ${group()}
2016
2880
  `;
2017
2881
  const rowsWrap = document.createElement("div");
2018
- rowsWrap.style.cssText = `display: flex; flex-direction: column; gap: 8px;`;
2882
+ rowsWrap.style.cssText = `display: flex; flex-direction: column; gap: ${v("space-2")};`;
2019
2883
  container.appendChild(rowsWrap);
2020
2884
  if (!itemSchema || depth >= MAX_FIELD_DEPTH) {
2021
2885
  const note = document.createElement("div");
2022
- 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")};`;
2023
2887
  note.textContent = itemSchema ? "Nesting too deep to edit here." : "This array has no item schema.";
2024
2888
  container.appendChild(note);
2025
2889
  return { control: container, getValue: () => [], validate: () => ({ value: [], ok: true }) };
@@ -2029,11 +2893,7 @@ function renderArrayField(field, initial, setOwnError, depth) {
2029
2893
  function makeRow(itemValue) {
2030
2894
  const row = document.createElement("div");
2031
2895
  row.style.cssText = `
2032
- display: flex; align-items: flex-start; gap: 8px;
2033
- background: rgba(255,255,255,0.02);
2034
- border: 1px solid rgba(255,255,255,0.06);
2035
- border-radius: 8px;
2036
- padding: 8px;
2896
+ ${groupRow()}
2037
2897
  `;
2038
2898
  const handle = document.createElement("div");
2039
2899
  handle.textContent = "\u22EE\u22EE";
@@ -2041,10 +2901,12 @@ function renderArrayField(field, initial, setOwnError, depth) {
2041
2901
  handle.draggable = true;
2042
2902
  handle.style.cssText = `
2043
2903
  cursor: grab; user-select: none;
2044
- color: rgba(255,255,255,0.35);
2045
- font-size: 13px; line-height: 1.2;
2046
- 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;
2047
2907
  letter-spacing: -2px;
2908
+ opacity: 0;
2909
+ transition: opacity ${v("duration-fast")} ${v("ease")};
2048
2910
  `;
2049
2911
  const { wrapper, fieldState } = renderField(itemSchema, itemValue, depth + 1);
2050
2912
  wrapper.style.marginBottom = "0";
@@ -2055,19 +2917,16 @@ function renderArrayField(field, initial, setOwnError, depth) {
2055
2917
  removeBtn.textContent = "\xD7";
2056
2918
  removeBtn.title = "Remove";
2057
2919
  removeBtn.style.cssText = `
2058
- appearance: none; cursor: pointer; flex-shrink: 0;
2059
- background: transparent; border: 1px solid transparent;
2060
- color: rgba(255,135,134,0.7);
2920
+ ${button("danger")}
2921
+ flex-shrink: 0;
2922
+ border: 1px solid transparent;
2923
+ height: auto;
2061
2924
  font-size: 16px; line-height: 1;
2062
- padding: 2px 7px; border-radius: 6px;
2063
- transition: background 0.15s;
2925
+ padding: 2px 7px;
2926
+ cursor: pointer;
2064
2927
  `;
2065
- removeBtn.addEventListener("mouseenter", () => {
2066
- removeBtn.style.background = "rgba(255,135,134,0.1)";
2067
- });
2068
- removeBtn.addEventListener("mouseleave", () => {
2069
- removeBtn.style.background = "transparent";
2070
- });
2928
+ attachHover(removeBtn, { bg: v("danger-soft") });
2929
+ attachRowReveal(row, [handle, removeBtn]);
2071
2930
  row.appendChild(handle);
2072
2931
  row.appendChild(wrapper);
2073
2932
  row.appendChild(removeBtn);
@@ -2119,19 +2978,18 @@ function renderArrayField(field, initial, setOwnError, depth) {
2119
2978
  const itemLabel = itemSchema.label || "item";
2120
2979
  addBtn.textContent = `+ Add ${itemLabel.toLowerCase()}`;
2121
2980
  addBtn.style.cssText = `
2122
- appearance: none; cursor: pointer; align-self: flex-start;
2123
- background: rgba(255,255,255,0.05); color: rgba(255,255,255,0.8);
2124
- border: 1px solid rgba(255,255,255,0.08);
2125
- font-size: 11px; font-weight: 500;
2126
- padding: 6px 11px; border-radius: 7px;
2127
- 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;
2128
2991
  `;
2129
- addBtn.addEventListener("mouseenter", () => {
2130
- addBtn.style.background = "rgba(255,255,255,0.1)";
2131
- });
2132
- addBtn.addEventListener("mouseleave", () => {
2133
- addBtn.style.background = "rgba(255,255,255,0.05)";
2134
- });
2992
+ attachHover(addBtn, { bg: v("surface-active") });
2135
2993
  addBtn.addEventListener("click", () => addRow(defaultForField(itemSchema)));
2136
2994
  container.appendChild(addBtn);
2137
2995
  const collect = () => rows.map((r) => r.state.getValue());
@@ -2161,15 +3019,12 @@ function renderObjectField(field, initial, setOwnError, depth) {
2161
3019
  const subInitial = initial && typeof initial === "object" && !Array.isArray(initial) ? initial : {};
2162
3020
  const fieldset = document.createElement("div");
2163
3021
  fieldset.style.cssText = `
2164
- display: flex; flex-direction: column; gap: 2px;
2165
- background: rgba(255,255,255,0.02);
2166
- border: 1px solid rgba(255,255,255,0.07);
2167
- border-radius: 10px;
2168
- padding: 10px 10px 0;
3022
+ ${group()}
3023
+ gap: 2px;
2169
3024
  `;
2170
3025
  if (depth >= MAX_FIELD_DEPTH) {
2171
3026
  const note = document.createElement("div");
2172
- 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;`;
2173
3028
  note.textContent = "Nesting too deep to edit here.";
2174
3029
  fieldset.appendChild(note);
2175
3030
  return { control: fieldset, getValue: () => ({}), validate: () => ({ value: {}, ok: true }) };
@@ -2183,8 +3038,8 @@ function renderObjectField(field, initial, setOwnError, depth) {
2183
3038
  const collect = () => {
2184
3039
  const out = {};
2185
3040
  for (const c of childStates) {
2186
- const v = c.getValue();
2187
- 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;
2188
3043
  }
2189
3044
  return out;
2190
3045
  };
@@ -2207,7 +3062,7 @@ function renderReferenceField(field, initial) {
2207
3062
  const targetList = field.referenceList;
2208
3063
  const sel = document.createElement("select");
2209
3064
  sel.className = "cancia-form-select";
2210
- 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;`;
2211
3066
  const opt = (value, text, selected = false) => {
2212
3067
  const o = document.createElement("option");
2213
3068
  o.value = value;
@@ -2230,8 +3085,8 @@ function renderReferenceField(field, initial) {
2230
3085
  const titleField = state.schemas[targetList]?.titleField;
2231
3086
  const titleOf = (entry) => {
2232
3087
  if (titleField) {
2233
- const v = entry.data[titleField];
2234
- if (typeof v === "string" && v.trim()) return v;
3088
+ const v2 = entry.data[titleField];
3089
+ if (typeof v2 === "string" && v2.trim()) return v2;
2235
3090
  }
2236
3091
  return `(untitled \xB7 ${entry.id})`;
2237
3092
  };
@@ -2278,25 +3133,17 @@ function renderRichTextField(field, initial, setOwnError) {
2278
3133
  ];
2279
3134
  const container = document.createElement("div");
2280
3135
  container.style.cssText = `
2281
- display: flex; flex-direction: column; gap: 8px;
2282
- background: rgba(255,255,255,0.02);
2283
- border: 1px solid rgba(255,255,255,0.07);
2284
- border-radius: 10px;
2285
- padding: 10px;
3136
+ ${group()}
2286
3137
  `;
2287
3138
  const rowsWrap = document.createElement("div");
2288
- rowsWrap.style.cssText = `display: flex; flex-direction: column; gap: 8px;`;
3139
+ rowsWrap.style.cssText = `display: flex; flex-direction: column; gap: ${v("space-2")};`;
2289
3140
  container.appendChild(rowsWrap);
2290
3141
  const rows = [];
2291
3142
  let dragging = null;
2292
3143
  function makeRow(initialRow) {
2293
3144
  const row = document.createElement("div");
2294
3145
  row.style.cssText = `
2295
- display: flex; align-items: flex-start; gap: 8px;
2296
- background: rgba(255,255,255,0.02);
2297
- border: 1px solid rgba(255,255,255,0.06);
2298
- border-radius: 8px;
2299
- padding: 8px;
3146
+ ${groupRow()}
2300
3147
  `;
2301
3148
  const handle = document.createElement("div");
2302
3149
  handle.textContent = "\u22EE\u22EE";
@@ -2304,25 +3151,33 @@ function renderRichTextField(field, initial, setOwnError) {
2304
3151
  handle.draggable = true;
2305
3152
  handle.style.cssText = `
2306
3153
  cursor: grab; user-select: none;
2307
- color: rgba(255,255,255,0.35);
2308
- font-size: 13px; line-height: 1.2;
2309
- 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;
2310
3157
  letter-spacing: -2px;
3158
+ opacity: 0;
3159
+ transition: opacity ${v("duration-fast")} ${v("ease")};
2311
3160
  `;
2312
3161
  const main = document.createElement("div");
2313
3162
  main.style.cssText = `flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px;`;
2314
3163
  const ta = document.createElement("textarea");
2315
3164
  ta.className = "cancia-form-textarea";
2316
- 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")};`;
2317
3166
  ta.rows = 2;
2318
3167
  ta.placeholder = "Text \u2014 use **bold**, *italic*, [label](https://\u2026)";
2319
3168
  ta.value = initialRow.text;
2320
3169
  const controls = document.createElement("div");
2321
- 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";
2322
3177
  const styleSel = document.createElement("select");
2323
3178
  styleSel.className = "cancia-form-select";
2324
- 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;`;
2325
- 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) {
2326
3181
  const o = document.createElement("option");
2327
3182
  o.value = s;
2328
3183
  o.textContent = STYLE_LABELS[s];
@@ -2332,10 +3187,10 @@ function renderRichTextField(field, initial, setOwnError) {
2332
3187
  const listSel = document.createElement("select");
2333
3188
  listSel.className = "cancia-form-select";
2334
3189
  listSel.style.cssText = styleSel.style.cssText;
2335
- for (const { value, label } of LIST_LABELS) {
3190
+ for (const { value, label: label2 } of LIST_LABELS) {
2336
3191
  const o = document.createElement("option");
2337
3192
  o.value = value;
2338
- o.textContent = label;
3193
+ o.textContent = label2;
2339
3194
  if ((initialRow.listItem ?? "") === value) o.selected = true;
2340
3195
  listSel.appendChild(o);
2341
3196
  }
@@ -2348,19 +3203,16 @@ function renderRichTextField(field, initial, setOwnError) {
2348
3203
  removeBtn.textContent = "\xD7";
2349
3204
  removeBtn.title = "Remove block";
2350
3205
  removeBtn.style.cssText = `
2351
- appearance: none; cursor: pointer; flex-shrink: 0;
2352
- background: transparent; border: 1px solid transparent;
2353
- color: rgba(255,135,134,0.7);
3206
+ ${button("danger")}
3207
+ flex-shrink: 0;
3208
+ border: 1px solid transparent;
3209
+ height: auto;
2354
3210
  font-size: 16px; line-height: 1;
2355
- padding: 2px 7px; border-radius: 6px;
2356
- transition: background 0.15s;
3211
+ padding: 2px 7px;
3212
+ cursor: pointer;
2357
3213
  `;
2358
- removeBtn.addEventListener("mouseenter", () => {
2359
- removeBtn.style.background = "rgba(255,135,134,0.1)";
2360
- });
2361
- removeBtn.addEventListener("mouseleave", () => {
2362
- removeBtn.style.background = "transparent";
2363
- });
3214
+ attachHover(removeBtn, { bg: v("danger-soft") });
3215
+ attachRowReveal(row, [handle, removeBtn, controls]);
2364
3216
  row.appendChild(handle);
2365
3217
  row.appendChild(main);
2366
3218
  row.appendChild(removeBtn);
@@ -2415,8 +3267,8 @@ function renderRichTextField(field, initial, setOwnError) {
2415
3267
  rowsWrap.appendChild(rec.el);
2416
3268
  }
2417
3269
  const initialRows = (() => {
2418
- const parsed = portableTextSubsetSchema.safeParse(initial);
2419
- 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);
2420
3272
  return [{ text: "", style: "normal" }];
2421
3273
  })();
2422
3274
  for (const r of initialRows) addRow(r);
@@ -2424,24 +3276,23 @@ function renderRichTextField(field, initial, setOwnError) {
2424
3276
  addBtn.type = "button";
2425
3277
  addBtn.textContent = "+ Add block";
2426
3278
  addBtn.style.cssText = `
2427
- appearance: none; cursor: pointer; align-self: flex-start;
2428
- background: rgba(255,255,255,0.05); color: rgba(255,255,255,0.8);
2429
- border: 1px solid rgba(255,255,255,0.08);
2430
- font-size: 11px; font-weight: 500;
2431
- padding: 6px 11px; border-radius: 7px;
2432
- 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;
2433
3289
  `;
2434
- addBtn.addEventListener("mouseenter", () => {
2435
- addBtn.style.background = "rgba(255,255,255,0.1)";
2436
- });
2437
- addBtn.addEventListener("mouseleave", () => {
2438
- addBtn.style.background = "rgba(255,255,255,0.05)";
2439
- });
3290
+ attachHover(addBtn, { bg: v("surface-active") });
2440
3291
  addBtn.addEventListener("click", () => addRow({ text: "", style: "normal" }));
2441
3292
  container.appendChild(addBtn);
2442
3293
  const serialise = () => {
2443
3294
  const editorRows = rows.map((r) => r.read()).filter((r) => r.text.trim() !== "");
2444
- return rowsToPortableText(editorRows);
3295
+ return rowsToPortableText2(editorRows);
2445
3296
  };
2446
3297
  return {
2447
3298
  control: container,
@@ -2449,7 +3300,7 @@ function renderRichTextField(field, initial, setOwnError) {
2449
3300
  validate: () => {
2450
3301
  setOwnError(null);
2451
3302
  const value = serialise();
2452
- const parsed = portableTextSubsetSchema.safeParse(value);
3303
+ const parsed = portableTextSubsetSchema2.safeParse(value);
2453
3304
  if (!parsed.success) {
2454
3305
  setOwnError("This rich-text content is not valid. Check links and formatting.");
2455
3306
  return { value, ok: false };
@@ -2569,77 +3420,106 @@ function openEntryModal(opts) {
2569
3420
  const isEdit = opts.entry !== null;
2570
3421
  const isTranslate = !isEdit && (opts.translateFromEntry ?? null) !== null;
2571
3422
  document.documentElement.style.setProperty("--cancia-accent-border", accentBorder());
2572
- const backdrop = document.createElement("div");
2573
- backdrop.dataset.canciaModalBackdrop = "1";
2574
- backdrop.style.cssText = `
2575
- position: fixed; inset: 0;
2576
- background: rgba(8,8,10,0.55);
2577
- backdrop-filter: blur(3px);
2578
- -webkit-backdrop-filter: blur(3px);
2579
- z-index: ${BACKDROP_Z2};
2580
- opacity: 0;
2581
- transition: opacity 0.2s ease-out;
2582
- `;
2583
- document.body.appendChild(backdrop);
2584
- requestAnimationFrame(() => {
2585
- backdrop.style.opacity = "1";
2586
- });
2587
- backdropEl2 = backdrop;
2588
3423
  const modal = document.createElement("div");
2589
3424
  modal.dataset.canciaModal = "1";
2590
- modal.style.cssText = `
2591
- position: fixed;
2592
- top: 50%; left: 50%;
2593
- transform: translate(-50%, -50%);
2594
- width: min(480px, calc(100vw - 32px));
2595
- max-height: min(680px, calc(100vh - 48px));
2596
- display: flex; flex-direction: column;
2597
- background: rgba(14, 14, 16, 0.97);
2598
- backdrop-filter: blur(24px) saturate(180%);
2599
- -webkit-backdrop-filter: blur(24px) saturate(180%);
2600
- border: 1px solid rgba(255,255,255,0.07);
2601
- border-radius: 14px;
2602
- 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);
2603
- z-index: ${MODAL_Z};
2604
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2605
- color: rgba(255,255,255,0.9);
2606
- animation: cancia-modal-in 0.22s cubic-bezier(0.16, 1, 0.3, 1) forwards;
2607
- overflow: hidden;
2608
- `;
2609
- 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
+ }
2610
3467
  modalEl = modal;
2611
3468
  const header = document.createElement("div");
2612
3469
  header.style.cssText = `
2613
- display: flex; align-items: center; justify-content: space-between;
2614
- padding: 14px 16px 12px;
2615
- 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")};
2616
3473
  flex-shrink: 0;
2617
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
+ }
2618
3493
  const titleWrap = document.createElement("div");
2619
- 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;`;
2620
3495
  const eyebrow = document.createElement("span");
2621
- eyebrow.style.cssText = `
2622
- font-size: 10px; font-weight: 600;
2623
- color: rgba(255,255,255,0.32);
2624
- letter-spacing: 0.08em; text-transform: uppercase;
2625
- `;
3496
+ eyebrow.style.cssText = `${label()} display: inline;`;
2626
3497
  const action = isEdit ? "Edit" : isTranslate ? "Translate" : "New";
2627
3498
  eyebrow.textContent = `${action} ${opts.schema.labelSingular.toLowerCase()} \xB7 ${opts.locale}`;
2628
3499
  const titleEl = document.createElement("span");
2629
3500
  titleEl.style.cssText = `
2630
3501
  font-size: 14px; font-weight: 600;
2631
- color: rgba(255,255,255,0.92);
3502
+ color: ${v("fg-strong")};
2632
3503
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
2633
3504
  `;
2634
3505
  titleEl.textContent = opts.schema.label;
2635
3506
  titleWrap.appendChild(eyebrow);
2636
3507
  titleWrap.appendChild(titleEl);
2637
3508
  header.appendChild(titleWrap);
2638
- header.appendChild(makeCloseButton(() => closeEntryModal()));
3509
+ header.appendChild(
3510
+ makeCloseButton(() => {
3511
+ if (mountedInPanel) {
3512
+ closeEntryModal();
3513
+ closeListPanel();
3514
+ } else {
3515
+ closeEntryModal();
3516
+ }
3517
+ })
3518
+ );
2639
3519
  modal.appendChild(header);
2640
3520
  const body = document.createElement("div");
2641
3521
  body.style.cssText = `
2642
- padding: 14px 16px 4px;
3522
+ padding: 14px ${v("space-4")} ${v("space-1")};
2643
3523
  overflow-y: auto;
2644
3524
  flex: 1 1 auto;
2645
3525
  min-height: 0;
@@ -2648,14 +3528,14 @@ function openEntryModal(opts) {
2648
3528
  const formError = document.createElement("div");
2649
3529
  formError.style.cssText = `
2650
3530
  display: none;
2651
- background: rgba(255,135,134,0.08);
2652
- color: #ff8786;
2653
- border: 1px solid rgba(255,135,134,0.18);
2654
- 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")};
2655
3535
  padding: 9px 11px;
2656
- font-size: 12px;
3536
+ font-size: ${v("text-sm")};
2657
3537
  line-height: 1.4;
2658
- margin-bottom: 12px;
3538
+ margin-bottom: ${v("space-3")};
2659
3539
  `;
2660
3540
  body.appendChild(formError);
2661
3541
  function showFormError(msg) {
@@ -2669,14 +3549,14 @@ function openEntryModal(opts) {
2669
3549
  if (isTranslate) {
2670
3550
  const banner = document.createElement("div");
2671
3551
  banner.style.cssText = `
2672
- background: rgba(245,180,0,0.1);
2673
- color: #f5b400;
2674
- border: 1px solid rgba(245,180,0,0.25);
2675
- 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")};
2676
3556
  padding: 9px 11px;
2677
- font-size: 12px;
3557
+ font-size: ${v("text-sm")};
2678
3558
  line-height: 1.4;
2679
- margin-bottom: 12px;
3559
+ margin-bottom: ${v("space-3")};
2680
3560
  `;
2681
3561
  const src = opts.translateFromEntry;
2682
3562
  banner.textContent = `Translating from ${src.locale} into ${opts.locale}. Fields are pre-filled from the source.`;
@@ -2695,32 +3575,26 @@ function openEntryModal(opts) {
2695
3575
  footer.style.cssText = `
2696
3576
  display: flex; align-items: center; justify-content: space-between;
2697
3577
  gap: 10px;
2698
- padding: 12px 16px;
2699
- border-top: 1px solid rgba(255,255,255,0.06);
2700
- 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")};
2701
3581
  flex-shrink: 0;
2702
3582
  `;
2703
3583
  const leftActions = document.createElement("div");
2704
3584
  const rightActions = document.createElement("div");
2705
- rightActions.style.cssText = `display: flex; gap: 8px;`;
3585
+ rightActions.style.cssText = `display: flex; gap: ${v("space-2")};`;
2706
3586
  if (isEdit) {
2707
3587
  const deleteBtn = document.createElement("button");
2708
3588
  deleteBtn.type = "button";
2709
3589
  deleteBtn.style.cssText = `
2710
- appearance: none; cursor: pointer;
2711
- background: transparent; border: 1px solid transparent;
2712
- color: #ff8786;
2713
- font-size: 12px; font-weight: 500;
2714
- padding: 6px 10px; border-radius: 7px;
2715
- transition: background 0.15s;
3590
+ ${button("danger")}
3591
+ border: 1px solid transparent;
3592
+ height: auto;
3593
+ padding: 6px 10px;
3594
+ cursor: pointer;
2716
3595
  `;
2717
3596
  deleteBtn.textContent = "Delete";
2718
- deleteBtn.addEventListener("mouseenter", () => {
2719
- deleteBtn.style.background = "rgba(255,135,134,0.08)";
2720
- });
2721
- deleteBtn.addEventListener("mouseleave", () => {
2722
- deleteBtn.style.background = "transparent";
2723
- });
3597
+ attachHover(deleteBtn, { bg: v("danger-soft") });
2724
3598
  deleteBtn.addEventListener("click", async () => {
2725
3599
  if (!confirm(`Delete the ${opts.locale} version of this ${opts.schema.labelSingular.toLowerCase()}? This can't be undone.`)) return;
2726
3600
  deleteBtn.disabled = true;
@@ -2738,25 +3612,17 @@ function openEntryModal(opts) {
2738
3612
  const cancelBtn = document.createElement("button");
2739
3613
  cancelBtn.type = "button";
2740
3614
  cancelBtn.style.cssText = `
2741
- appearance: none; cursor: pointer;
2742
- background: rgba(255,255,255,0.04);
2743
- border: 1px solid rgba(255,255,255,0.07);
2744
- color: rgba(255,255,255,0.75);
2745
- font-size: 12px; font-weight: 500;
2746
- padding: 7px 14px; border-radius: 8px;
2747
- 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;
2748
3621
  `;
2749
3622
  cancelBtn.textContent = "Cancel";
2750
- cancelBtn.addEventListener("mouseenter", () => {
2751
- cancelBtn.style.background = "rgba(255,255,255,0.08)";
2752
- cancelBtn.style.color = "rgba(255,255,255,0.9)";
2753
- });
2754
- cancelBtn.addEventListener("mouseleave", () => {
2755
- cancelBtn.style.background = "rgba(255,255,255,0.04)";
2756
- cancelBtn.style.color = "rgba(255,255,255,0.75)";
2757
- });
3623
+ attachHover(cancelBtn, { bg: v("surface-hover"), color: v("fg-strong") });
2758
3624
  cancelBtn.addEventListener("click", () => closeEntryModal());
2759
- const saveBtn = makePrimaryButton(isEdit ? "Save" : "Create", accent4());
3625
+ const saveBtn = makePrimaryButton(isEdit ? "Save" : "Create", accent3());
2760
3626
  saveBtn.addEventListener("click", async () => {
2761
3627
  clearFormError();
2762
3628
  const { data, ok } = preValidate(fieldStates);
@@ -2799,7 +3665,6 @@ function openEntryModal(opts) {
2799
3665
  footer.appendChild(leftActions);
2800
3666
  footer.appendChild(rightActions);
2801
3667
  modal.appendChild(footer);
2802
- backdrop.addEventListener("click", () => closeEntryModal());
2803
3668
  escListener = (e) => {
2804
3669
  if (e.key === "Escape") {
2805
3670
  e.stopPropagation();
@@ -2814,11 +3679,18 @@ function openEntryModal(opts) {
2814
3679
  }
2815
3680
  function closeEntryModal() {
2816
3681
  if (modalEl) {
2817
- modalEl.style.animation = "cancia-modal-out 0.16s cubic-bezier(0.7, 0, 0.84, 0) forwards";
2818
3682
  const el = modalEl;
2819
- setTimeout(() => el.remove(), 160);
2820
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
+ }
2821
3692
  }
3693
+ mountedInPanel = false;
2822
3694
  if (backdropEl2) {
2823
3695
  const el = backdropEl2;
2824
3696
  el.style.opacity = "0";
@@ -2839,13 +3711,12 @@ var toolbarEl = null;
2839
3711
  var pendingPanelEl = null;
2840
3712
  var isExpanded = false;
2841
3713
  var expandedEscListener = null;
2842
- function accent5() {
2843
- return state.config?.accentColor ?? "#6366f1";
2844
- }
3714
+ var revealTimer = null;
2845
3715
  var styleInjected4 = false;
2846
3716
  function injectStyles4() {
2847
3717
  if (styleInjected4) return;
2848
3718
  styleInjected4 = true;
3719
+ injectBaseStyles(state.config?.accentColor, state.config?.toolbarAccent === true);
2849
3720
  const s = document.createElement("style");
2850
3721
  s.textContent = `
2851
3722
  @keyframes cancia-enter {
@@ -2886,20 +3757,30 @@ function injectStyles4() {
2886
3757
  to { opacity: 1; transform: translateX(-50%) translateY(0); }
2887
3758
  }
2888
3759
  [data-cancia-toolbar] * { box-sizing: border-box; }
2889
- [data-cancia-toolbar] button:active:not(:disabled) { transform: scale(0.92) !important; }
2890
3760
  [data-cancia-popup] * { box-sizing: border-box; }
2891
- [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); }
2892
3766
  /* Protect stroke-based icons from host page "svg { fill: currentColor }" rules */
2893
3767
  [data-cancia-toolbar] svg[fill="none"] { fill: none !important; }
2894
3768
  [data-cancia-toolbar] svg[fill="none"] :not([fill]) { fill: none !important; }
2895
3769
  [data-cancia-popup] svg[fill="none"] { fill: none !important; }
2896
3770
  [data-cancia-popup] svg[fill="none"] :not([fill]) { fill: none !important; }
2897
- /* 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. */
2898
3776
  [data-cancia-toolbar] :where(button) {
2899
3777
  background: unset; border: unset; border-radius: unset; padding: unset;
2900
- margin: unset; color: unset; font-family: unset; font-weight: unset;
2901
- font-size: unset; line-height: unset; letter-spacing: unset;
2902
- 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;
2903
3784
  }
2904
3785
  `;
2905
3786
  document.head.appendChild(s);
@@ -2911,27 +3792,28 @@ var tooltipVisible = false;
2911
3792
  function getOrCreateBtnTooltip() {
2912
3793
  if (!btnTooltipEl) {
2913
3794
  btnTooltipEl = document.createElement("div");
3795
+ markUi(btnTooltipEl);
2914
3796
  btnTooltipEl.style.cssText = `
2915
3797
  position: fixed;
2916
3798
  pointer-events: none;
2917
- z-index: 2147483646;
2918
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2919
- font-size: 11px; font-weight: 500; letter-spacing: 0.02em;
2920
- color: #fff;
2921
- background: rgba(10,10,12,0.92);
2922
- backdrop-filter: blur(8px);
2923
- -webkit-backdrop-filter: blur(8px);
2924
- border: 1px solid rgba(255,255,255,0.1);
2925
- 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")};
2926
3808
  white-space: nowrap;
2927
- box-shadow: 0 2px 8px rgba(0,0,0,0.3);
3809
+ box-shadow: ${v("shadow-sm")};
2928
3810
  display: none;
2929
3811
  `;
2930
3812
  document.body.appendChild(btnTooltipEl);
2931
3813
  }
2932
3814
  return btnTooltipEl;
2933
3815
  }
2934
- function showBtnTooltip(btn, label) {
3816
+ function showBtnTooltip(btn, label2) {
2935
3817
  if (tooltipHideTimer) {
2936
3818
  clearTimeout(tooltipHideTimer);
2937
3819
  tooltipHideTimer = null;
@@ -2943,9 +3825,9 @@ function showBtnTooltip(btn, label) {
2943
3825
  const doShow = () => {
2944
3826
  tooltipVisible = true;
2945
3827
  const tooltip = getOrCreateBtnTooltip();
2946
- tooltip.textContent = label;
3828
+ tooltip.textContent = label2;
2947
3829
  tooltip.style.display = "block";
2948
- 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`;
2949
3831
  const rect = btn.getBoundingClientRect();
2950
3832
  const tooltipH = 26;
2951
3833
  const gap = 8;
@@ -2973,37 +3855,39 @@ function buildToolbar() {
2973
3855
  injectStyles4();
2974
3856
  const bar = document.createElement("div");
2975
3857
  bar.dataset.canciaToolbar = "1";
3858
+ markUi(bar);
2976
3859
  bar.style.cssText = `
2977
3860
  position: fixed;
2978
- bottom: 24px;
2979
- right: 24px;
2980
- z-index: 2147483647;
2981
- width: 44px;
2982
- height: 44px;
2983
- border-radius: 22px;
2984
- background: rgba(12, 12, 14, 0.92);
2985
- backdrop-filter: blur(16px) saturate(180%);
2986
- -webkit-backdrop-filter: blur(16px) saturate(180%);
2987
- border: 1px solid rgba(255,255,255,0.08);
2988
- 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);
2989
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2990
- font-size: 13px;
2991
- 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")};
2992
3874
  user-select: none;
2993
3875
  cursor: pointer;
2994
3876
  overflow: hidden;
2995
3877
  display: flex;
2996
3878
  align-items: center;
2997
3879
  justify-content: center;
2998
- transition: width 0.45s cubic-bezier(0.19, 1, 0.22, 1), border-radius 0.45s cubic-bezier(0.19, 1, 0.22, 1);
2999
- 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;
3000
3883
  `;
3001
3884
  const collapseIcon = document.createElement("div");
3002
3885
  collapseIcon.style.cssText = `
3003
3886
  position: absolute;
3004
3887
  display: flex; align-items: center; justify-content: center;
3005
- color: rgba(255,255,255,0.7);
3006
- 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")};
3007
3891
  pointer-events: none;
3008
3892
  `;
3009
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">
@@ -3014,16 +3898,17 @@ function buildToolbar() {
3014
3898
  controls.style.cssText = `
3015
3899
  display: flex;
3016
3900
  align-items: center;
3017
- gap: 0.375rem;
3901
+ gap: ${v("space-1")};
3018
3902
  padding: 5px;
3019
3903
  white-space: nowrap;
3020
3904
  opacity: 0;
3905
+ transform: scale(0.6);
3021
3906
  pointer-events: none;
3022
3907
  transform-origin: right center;
3023
3908
  `;
3024
3909
  let editActive = false;
3025
- const editBtn = makeIconButton(
3026
- `<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">
3027
3912
  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
3028
3913
  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
3029
3914
  </svg>`,
@@ -3032,38 +3917,43 @@ function buildToolbar() {
3032
3917
  );
3033
3918
  controls.appendChild(editBtn);
3034
3919
  const canPublish = state.config?.canPublish ?? true;
3035
- const publishBtn = makeIconButton(
3036
- `<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">
3037
3922
  <path d="M22 2L11 13"/>
3038
3923
  <path d="M22 2L15 22l-4-9-9-4 20-7z"/>
3039
3924
  </svg>`,
3040
- canPublish ? "Publish" : "Publish (no publish method configured)",
3925
+ "Publish",
3041
3926
  () => handlePublish(publishBtn)
3042
3927
  );
3928
+ if (!canPublish) {
3929
+ publishBtn.title = "No publish method is configured for this site.";
3930
+ }
3043
3931
  if (!canPublish) {
3044
3932
  publishBtn.disabled = true;
3045
3933
  publishBtn.style.opacity = "0.3";
3046
3934
  publishBtn.style.cursor = "not-allowed";
3047
3935
  }
3048
3936
  controls.appendChild(publishBtn);
3049
- const logoutBtn = makeIconButton(
3050
- `<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">
3051
3939
  <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
3052
3940
  <path d="M16 17l5-5-5-5"/>
3053
3941
  <path d="M21 12H9"/>
3054
3942
  </svg>`,
3055
- "Log out",
3943
+ "Sign out",
3056
3944
  () => state.onLogout?.()
3057
3945
  );
3058
3946
  controls.appendChild(logoutBtn);
3059
3947
  controls.appendChild(makeDivider());
3060
3948
  const collapseBtn = makeIconButton(
3061
- `<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">
3062
3950
  <path d="M6 6l12 12M18 6L6 18"/>
3063
3951
  </svg>`,
3064
3952
  "Close",
3065
3953
  () => collapse(bar, collapseIcon, controls)
3066
3954
  );
3955
+ collapseBtn.style.width = "38px";
3956
+ collapseBtn.style.height = "38px";
3067
3957
  controls.appendChild(collapseBtn);
3068
3958
  bar.appendChild(collapseIcon);
3069
3959
  bar.appendChild(controls);
@@ -3071,10 +3961,10 @@ function buildToolbar() {
3071
3961
  if (!isExpanded) expand(bar, collapseIcon, controls);
3072
3962
  });
3073
3963
  bar.addEventListener("mouseenter", () => {
3074
- if (!isExpanded) bar.style.background = "rgba(24, 24, 28, 0.96)";
3964
+ if (!isExpanded) bar.style.background = v("surface-2");
3075
3965
  });
3076
3966
  bar.addEventListener("mouseleave", () => {
3077
- bar.style.background = "rgba(12, 12, 14, 0.92)";
3967
+ bar.style.background = v("surface-1");
3078
3968
  });
3079
3969
  onPendingChange(() => {
3080
3970
  const count = state.pending.size;
@@ -3090,9 +3980,10 @@ function buildToolbar() {
3090
3980
  state.editMode = editActive;
3091
3981
  const svgEl = editBtn.querySelector("svg");
3092
3982
  if (editActive) {
3093
- editBtn.style.background = `${accent5()}22`;
3094
- editBtn.style.color = accent5();
3095
- 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");
3096
3987
  editBtn.dataset.canciaTooltip = "Stop editing (Esc)";
3097
3988
  attachHighlight((selection) => {
3098
3989
  if (selection.kind === "field") {
@@ -3156,8 +4047,9 @@ function buildToolbar() {
3156
4047
  };
3157
4048
  document.addEventListener("keydown", editModeEscListener, true);
3158
4049
  } else {
4050
+ delete editBtn.dataset.canciaActive;
3159
4051
  editBtn.style.background = "transparent";
3160
- editBtn.style.color = "rgba(255,255,255,0.85)";
4052
+ editBtn.style.color = v("fg-strong");
3161
4053
  if (svgEl) svgEl.style.stroke = "";
3162
4054
  editBtn.dataset.canciaTooltip = "Edit";
3163
4055
  detachHighlight();
@@ -3184,40 +4076,49 @@ function expand(bar, icon, controls) {
3184
4076
  controls.style.visibility = "hidden";
3185
4077
  controls.style.opacity = "0";
3186
4078
  controls.style.pointerEvents = "none";
3187
- bar.style.width = "max-content";
3188
4079
  bar.style.borderRadius = "100px";
3189
- requestAnimationFrame(() => {
3190
- const naturalW = bar.scrollWidth;
3191
- 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";
3192
4093
  controls.style.visibility = "";
3193
- requestAnimationFrame(() => {
3194
- bar.style.width = `${naturalW}px`;
3195
- bar.style.borderRadius = "100px";
3196
- bar.style.cursor = "default";
3197
- icon.style.opacity = "0";
3198
- icon.style.transform = "scale(0.5) rotate(-90deg)";
3199
- setTimeout(() => {
3200
- controls.style.pointerEvents = "auto";
3201
- controls.style.animation = "cancia-controls-in 0.4s cubic-bezier(0.19, 1, 0.22, 1) both";
3202
- controls.style.opacity = "1";
3203
- }, 80);
3204
- });
3205
- });
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);
3206
4100
  }
3207
4101
  function collapse(bar, icon, controls) {
3208
4102
  if (!isExpanded) return;
3209
4103
  isExpanded = false;
3210
4104
  hideBtnTooltip();
4105
+ if (revealTimer !== null) {
4106
+ clearTimeout(revealTimer);
4107
+ revealTimer = null;
4108
+ }
3211
4109
  if (expandedEscListener) {
3212
4110
  document.removeEventListener("keydown", expandedEscListener, true);
3213
4111
  expandedEscListener = null;
3214
4112
  }
3215
4113
  controls.style.pointerEvents = "none";
3216
- 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)";
3217
4118
  setTimeout(() => {
3218
- controls.style.opacity = "0";
3219
- bar.style.width = "44px";
3220
- bar.style.borderRadius = "22px";
4119
+ if (isExpanded) return;
4120
+ bar.style.width = "52px";
4121
+ bar.style.borderRadius = "26px";
3221
4122
  bar.style.cursor = "pointer";
3222
4123
  icon.style.opacity = "1";
3223
4124
  icon.style.transform = "scale(1) rotate(0deg)";
@@ -3261,42 +4162,87 @@ async function handlePublish(btn) {
3261
4162
  }
3262
4163
  function showToast(message, type) {
3263
4164
  const toast = document.createElement("div");
3264
- const color = type === "success" ? "#4ade80" : "#f87171";
4165
+ markUi(toast);
4166
+ const color = type === "success" ? v("success") : v("danger");
3265
4167
  toast.style.cssText = `
3266
- position: fixed; bottom: 80px; right: 24px; z-index: 2147483647;
3267
- display: flex; align-items: center; gap: 8px;
3268
- background: rgba(12, 12, 14, 0.95);
3269
- backdrop-filter: blur(16px);
3270
- border: 1px solid rgba(255,255,255,0.08);
3271
- border-radius: 10px; padding: 10px 14px;
3272
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3273
- font-size: 13px; font-weight: 500; color: #f0f0f0;
3274
- 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")};
3275
4177
  pointer-events: none;
3276
- 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")};
3277
4180
  `;
3278
4181
  const dot = document.createElement("span");
3279
4182
  dot.style.cssText = `width: 7px; height: 7px; border-radius: 50%; background: ${color}; flex-shrink: 0;`;
3280
- const label = document.createElement("span");
3281
- label.textContent = message;
4183
+ const label2 = document.createElement("span");
4184
+ label2.textContent = message;
3282
4185
  toast.appendChild(dot);
3283
- toast.appendChild(label);
4186
+ toast.appendChild(label2);
3284
4187
  document.body.appendChild(toast);
4188
+ requestAnimationFrame(() => {
4189
+ toast.style.opacity = "1";
4190
+ toast.style.transform = "translateY(0)";
4191
+ });
3285
4192
  setTimeout(() => {
3286
- 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")}`;
3287
4194
  toast.style.opacity = "0";
3288
4195
  toast.style.transform = "translateY(4px)";
3289
4196
  setTimeout(() => toast.remove(), 300);
3290
4197
  }, 2e3);
3291
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
+ }
3292
4236
  function makeIconButton(svg, title, onClick) {
3293
4237
  const btn = document.createElement("button");
3294
4238
  btn.style.cssText = `
3295
- display: flex; align-items: center; justify-content: center;
3296
- width: 34px; height: 34px; border-radius: 50%;
3297
- border: none; background: transparent;
3298
- cursor: pointer; color: rgba(255,255,255,0.85); flex-shrink: 0; padding: 0;
3299
- 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")};
3300
4246
  `;
3301
4247
  btn.innerHTML = svg;
3302
4248
  const svgEl = btn.querySelector("svg");
@@ -3307,15 +4253,12 @@ function makeIconButton(svg, title, onClick) {
3307
4253
  btn.dataset.canciaTooltip = title;
3308
4254
  btn.addEventListener("mouseenter", () => {
3309
4255
  if (!btn.disabled) {
3310
- btn.style.background = "rgba(255,255,255,0.1)";
4256
+ btn.style.background = v("surface-active");
3311
4257
  showBtnTooltip(btn, btn.dataset.canciaTooltip ?? title);
3312
4258
  }
3313
4259
  });
3314
4260
  btn.addEventListener("mouseleave", () => {
3315
- const activeColor = state.config?.accentColor ?? "#6366f1";
3316
- if (!btn.style.background.includes(activeColor.slice(1, 7))) {
3317
- btn.style.background = "transparent";
3318
- }
4261
+ if (btn.dataset.canciaActive !== "1") btn.style.background = "transparent";
3319
4262
  hideBtnTooltip();
3320
4263
  });
3321
4264
  btn.addEventListener("click", (e) => {
@@ -3327,45 +4270,45 @@ function makeIconButton(svg, title, onClick) {
3327
4270
  }
3328
4271
  function makeDivider() {
3329
4272
  const d = document.createElement("span");
3330
- 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;`;
3331
4274
  return d;
3332
4275
  }
3333
4276
  function showPendingPanel(count) {
3334
4277
  if (!pendingPanelEl) {
3335
4278
  pendingPanelEl = document.createElement("div");
4279
+ markUi(pendingPanelEl);
3336
4280
  pendingPanelEl.style.cssText = `
3337
4281
  position: fixed;
3338
4282
  bottom: 80px;
3339
- right: 24px;
3340
- z-index: 2147483646;
4283
+ right: ${v("space-6")};
4284
+ z-index: ${v("z-panel")};
3341
4285
  display: flex;
3342
4286
  align-items: center;
3343
4287
  justify-content: space-between;
3344
- gap: 12px;
3345
- background: rgba(12, 12, 14, 0.92);
3346
- backdrop-filter: blur(16px) saturate(180%);
3347
- -webkit-backdrop-filter: blur(16px) saturate(180%);
3348
- border: 1px solid rgba(255,255,255,0.08);
3349
- 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")};
3350
4294
  padding: 0;
3351
- box-shadow: 0 2px 8px rgba(0,0,0,0.3), 0 8px 32px rgba(0,0,0,0.4);
3352
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3353
- animation: cancia-fade-in 0.25s cubic-bezier(0.16,1,0.3,1) both;
4295
+ box-shadow: ${v("shadow")};
3354
4296
  width: max-content;
3355
4297
  overflow: hidden;
4298
+ opacity: 0; transform: translateY(4px);
4299
+ transition: opacity ${v("duration")} ${v("ease")}, transform ${v("duration")} ${v("ease")};
3356
4300
  `;
3357
- const label2 = document.createElement("span");
3358
- label2.dataset.canciaPendingLabel = "1";
3359
- 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;`;
3360
4304
  const saveBtn = document.createElement("button");
3361
4305
  saveBtn.dataset.canciaSaveBtn = "1";
3362
4306
  saveBtn.title = "Save changes";
3363
4307
  saveBtn.style.cssText = `
3364
- padding: 5px 10px; border-radius: 7px; border: none; cursor: pointer;
3365
- background: #fff; color: #0c0c0e;
3366
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3367
- font-size: 12px; font-weight: 600; letter-spacing: 0.01em;
3368
- 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")};
3369
4312
  flex-shrink: 0;
3370
4313
  `;
3371
4314
  saveBtn.textContent = "Save";
@@ -3375,6 +4318,7 @@ function showPendingPanel(count) {
3375
4318
  saveBtn.addEventListener("mouseleave", () => {
3376
4319
  saveBtn.style.opacity = "1";
3377
4320
  });
4321
+ attachPress(saveBtn);
3378
4322
  saveBtn.addEventListener("click", (e) => {
3379
4323
  e.stopPropagation();
3380
4324
  handleSave(saveBtn);
@@ -3382,22 +4326,22 @@ function showPendingPanel(count) {
3382
4326
  const undoBtn = document.createElement("button");
3383
4327
  undoBtn.title = "Discard changes";
3384
4328
  undoBtn.style.cssText = `
3385
- padding: 5px 10px; border-radius: 7px; border: 1px solid rgba(255,255,255,0.1); cursor: pointer;
3386
- background: transparent; color: rgba(255,255,255,0.5);
3387
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3388
- font-size: 12px; font-weight: 500; letter-spacing: 0.01em;
3389
- 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")};
3390
4333
  flex-shrink: 0;
3391
4334
  `;
3392
4335
  undoBtn.textContent = "Discard";
3393
4336
  undoBtn.addEventListener("mouseenter", () => {
3394
- undoBtn.style.color = "rgba(255,255,255,0.8)";
3395
- undoBtn.style.borderColor = "rgba(255,255,255,0.2)";
4337
+ undoBtn.style.color = v("fg-strong");
4338
+ undoBtn.style.borderColor = v("border-strong");
3396
4339
  });
3397
4340
  undoBtn.addEventListener("mouseleave", () => {
3398
- undoBtn.style.color = "rgba(255,255,255,0.5)";
3399
- undoBtn.style.borderColor = "rgba(255,255,255,0.1)";
4341
+ undoBtn.style.color = v("fg-muted");
4342
+ undoBtn.style.borderColor = v("border");
3400
4343
  });
4344
+ attachPress(undoBtn);
3401
4345
  undoBtn.addEventListener("click", (e) => {
3402
4346
  e.stopPropagation();
3403
4347
  revertPending();
@@ -3412,32 +4356,36 @@ function showPendingPanel(count) {
3412
4356
  row.style.cssText = `
3413
4357
  grid-area: 1/1; display: flex; align-items: center; gap: 6px;
3414
4358
  padding: 7px 7px 7px 12px;
3415
- transition: transform 200ms cubic-bezier(0.785,0.135,0.15,0.86);
4359
+ transition: transform ${v("duration")} ${v("ease-out")};
3416
4360
  `;
3417
- row.appendChild(label2);
4361
+ row.appendChild(label3);
3418
4362
  row.appendChild(undoBtn);
3419
4363
  row.appendChild(saveBtn);
3420
4364
  const flash = document.createElement("div");
3421
4365
  flash.dataset.canciaPanelFlash = "1";
3422
4366
  flash.style.cssText = `
3423
4367
  grid-area: 1/1; display: flex; align-items: center; gap: 7px;
3424
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3425
- font-size: 12px; font-weight: 500; color: rgba(255,255,255,0.8); white-space: nowrap;
3426
- 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")};
3427
4370
  transform: translateY(-150%);
3428
- transition: transform 200ms cubic-bezier(0.785,0.135,0.15,0.86);
4371
+ transition: transform ${v("duration")} ${v("ease-out")};
3429
4372
  `;
3430
4373
  slot.appendChild(row);
3431
4374
  slot.appendChild(flash);
3432
4375
  pendingPanelEl.appendChild(slot);
3433
4376
  document.body.appendChild(pendingPanelEl);
4377
+ const panel = pendingPanelEl;
4378
+ requestAnimationFrame(() => {
4379
+ panel.style.opacity = "1";
4380
+ panel.style.transform = "translateY(0)";
4381
+ });
3434
4382
  }
3435
- const label = pendingPanelEl.querySelector("[data-cancia-pending-label]");
3436
- 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"}`;
3437
4385
  }
3438
4386
  function flashPanelMessage(message, type) {
3439
4387
  if (!pendingPanelEl) return;
3440
- const color = type === "success" ? "#4ade80" : "#f87171";
4388
+ const color = type === "success" ? v("success") : v("danger");
3441
4389
  const row = pendingPanelEl.querySelector("[data-cancia-panel-row]");
3442
4390
  const flash = pendingPanelEl.querySelector("[data-cancia-panel-flash]");
3443
4391
  if (!row || !flash) return;
@@ -3450,7 +4398,7 @@ function hidePendingPanel() {
3450
4398
  if (!pendingPanelEl) return;
3451
4399
  const panel = pendingPanelEl;
3452
4400
  pendingPanelEl = null;
3453
- 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")}`;
3454
4402
  panel.style.opacity = "0";
3455
4403
  panel.style.transform = "translateY(4px)";
3456
4404
  setTimeout(() => panel.remove(), 220);
@@ -3470,7 +4418,7 @@ function unmountToolbar() {
3470
4418
  pendingPanelEl?.remove();
3471
4419
  pendingPanelEl = null;
3472
4420
  if (toolbarEl) {
3473
- 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`;
3474
4422
  setTimeout(() => {
3475
4423
  toolbarEl?.remove();
3476
4424
  toolbarEl = null;