@cancia/toolbar 0.5.2 → 0.12.1

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