@shadow-garden/bapbong-ui 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,13 +1,23 @@
1
1
  // packages/ui/src/lib/internal.ts
2
+ import { keyLabel } from "@shadow-garden/bapbong-contracts";
3
+ var IS_MAC = typeof navigator !== "undefined" ? /Mac|iP(hone|[oa]d)/.test(navigator.platform) : false;
4
+ function shortcutLabel(command, registries) {
5
+ for (const r of registries) {
6
+ if (!r) continue;
7
+ for (const b of r)
8
+ if (b.command === command) return keyLabel(b.key, IS_MAC);
9
+ }
10
+ return void 0;
11
+ }
2
12
  function injectStyle(id, css) {
3
13
  if (document.getElementById(id)) return;
4
- const el2 = document.createElement("style");
5
- el2.id = id;
6
- el2.textContent = css;
7
- document.head.appendChild(el2);
14
+ const el5 = document.createElement("style");
15
+ el5.id = id;
16
+ el5.textContent = css;
17
+ document.head.appendChild(el5);
8
18
  }
9
- function placeFloating(el2, anchor) {
10
- const r = el2.getBoundingClientRect();
19
+ function placeFloating(el5, anchor) {
20
+ const r = el5.getBoundingClientRect();
11
21
  const pad = 6;
12
22
  const gap = 6;
13
23
  const x = Math.max(
@@ -17,16 +27,75 @@ function placeFloating(el2, anchor) {
17
27
  const below = anchor.y + anchor.height + gap;
18
28
  const above = anchor.y - gap - r.height;
19
29
  const y = below + r.height <= window.innerHeight - pad || above < pad ? Math.min(below, window.innerHeight - r.height - pad) : above;
20
- el2.style.left = `${x}px`;
21
- el2.style.top = `${y}px`;
30
+ el5.style.left = `${x}px`;
31
+ el5.style.top = `${y}px`;
32
+ }
33
+ function rovingGrid(cells) {
34
+ let cur = [0, 0];
35
+ const at = (r, c) => cells[r]?.[c];
36
+ const focusCell = (r, c) => {
37
+ const rows = cells.length;
38
+ if (rows === 0) return;
39
+ const nr = Math.min(rows - 1, Math.max(0, r));
40
+ const nc = Math.min(cells[nr].length - 1, Math.max(0, c));
41
+ const prev = at(cur[0], cur[1]);
42
+ if (prev) prev.tabIndex = -1;
43
+ const next = at(nr, nc);
44
+ if (!next) return;
45
+ next.tabIndex = 0;
46
+ next.focus();
47
+ cur = [nr, nc];
48
+ };
49
+ cells.forEach(
50
+ (row, r) => row.forEach((cell, c) => {
51
+ cell.setAttribute("role", "gridcell");
52
+ cell.tabIndex = r === 0 && c === 0 ? 0 : -1;
53
+ cell.addEventListener("keydown", (e) => {
54
+ const delta = {
55
+ ArrowRight: [0, 1],
56
+ ArrowLeft: [0, -1],
57
+ ArrowDown: [1, 0],
58
+ ArrowUp: [-1, 0],
59
+ Home: [0, -Infinity],
60
+ End: [0, Infinity]
61
+ };
62
+ const d = delta[e.key];
63
+ if (!d) return;
64
+ e.preventDefault();
65
+ focusCell(r + d[0], c + d[1]);
66
+ });
67
+ cell.addEventListener("focus", () => {
68
+ cur = [r, c];
69
+ });
70
+ })
71
+ );
72
+ return { focusCell };
22
73
  }
23
74
 
24
75
  // packages/ui/src/lib/dialog.ts
76
+ function panelAnchor(o) {
77
+ return () => {
78
+ const areaEl = o.area();
79
+ if (!areaEl) return null;
80
+ const a = areaEl.getBoundingClientRect();
81
+ let top = a.top;
82
+ for (const el5 of o.avoid?.() ?? []) {
83
+ if (!el5) continue;
84
+ const b = el5.getBoundingClientRect();
85
+ const overlapsX = b.right > a.left && b.left < a.right;
86
+ const overTop = b.top < a.bottom && b.bottom > top;
87
+ if (overlapsX && overTop) top = Math.max(top, b.bottom);
88
+ }
89
+ if (top >= a.bottom) return null;
90
+ return new DOMRect(a.left, top, a.width, a.bottom - top);
91
+ };
92
+ }
25
93
  var STYLE = `
26
94
  .bb-dialog{position:fixed;inset:auto;z-index:1100;margin:0;padding:0;border:1px solid var(--bb-ui-pop-border,var(--bb-ui-border,#e3e3e0));border-radius:10px;background:var(--bb-ui-dialog-bg,var(--bb-ui-menu-bg,#fff));-webkit-backdrop-filter:var(--bb-ui-dialog-filter,none);backdrop-filter:var(--bb-ui-dialog-filter,none);color:var(--bb-ui-fg,#2c2c2a);box-shadow:0 12px 40px rgba(0,0,0,.18);font-family:var(--bb-ui-font,system-ui,-apple-system,sans-serif);min-width:280px;max-width:min(92vw,440px)}
27
95
  .bb-dialog *{box-sizing:border-box}
28
96
  .bb-dialog::backdrop{background:rgba(0,0,0,.28)}
29
97
  .bb-dialog-modal{top:50%;left:50%;transform:translate(-50%,-50%)}
98
+ .bb-dialog-wide{max-width:min(92vw,640px)}
30
99
  .bb-dialog-float{top:62px;right:24px}
31
100
  .bb-dialog-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 10px 9px 12px;border-bottom:1px solid var(--bb-ui-border,#e3e3e0)}
32
101
  .bb-dialog-title{font-size:13px;font-weight:600}
@@ -60,8 +129,8 @@ var Dialog = class {
60
129
  injectStyle("bb-ui-dialog-styles", STYLE);
61
130
  this.modal = options.modal ?? false;
62
131
  this.anchor = options.anchor;
63
- const el2 = document.createElement("dialog");
64
- el2.className = `bb-dialog ${this.modal ? "bb-dialog-modal" : "bb-dialog-float"}` + (options.className ? ` ${options.className}` : "");
132
+ const el5 = document.createElement("dialog");
133
+ el5.className = `bb-dialog ${this.modal ? "bb-dialog-modal" : "bb-dialog-float"}` + (options.wide ? " bb-dialog-wide" : "") + (options.className ? ` ${options.className}` : "");
65
134
  const header = document.createElement("div");
66
135
  header.className = "bb-dialog-header";
67
136
  this.title = document.createElement("span");
@@ -76,18 +145,18 @@ var Dialog = class {
76
145
  header.append(this.title, close);
77
146
  this.body = document.createElement("div");
78
147
  this.body.className = "bb-dialog-body";
79
- el2.append(header, this.body);
80
- document.body.appendChild(el2);
81
- el2.addEventListener("close", () => {
148
+ el5.append(header, this.body);
149
+ document.body.appendChild(el5);
150
+ el5.addEventListener("close", () => {
82
151
  this.detachReposition();
83
152
  this.closeListeners.forEach((cb) => cb());
84
153
  });
85
154
  if (!this.modal) {
86
- el2.addEventListener("keydown", (e) => {
155
+ el5.addEventListener("keydown", (e) => {
87
156
  if (e.key === "Escape") this.close();
88
157
  });
89
158
  }
90
- this.el = el2;
159
+ this.el = el5;
91
160
  }
92
161
  attachReposition() {
93
162
  window.addEventListener("scroll", this.reposition, true);
@@ -346,45 +415,24 @@ function openColorPicker({
346
415
  grid.className = "bb-cpk-grid";
347
416
  grid.setAttribute("role", "grid");
348
417
  const cells = [];
349
- COLOR_PALETTE.forEach((row, r2) => {
418
+ COLOR_PALETTE.forEach((row) => {
350
419
  const rowCells = [];
351
- row.forEach((color, c) => {
420
+ row.forEach((color) => {
352
421
  const sw = document.createElement("button");
353
422
  sw.type = "button";
354
423
  sw.className = "bb-cpk-sw";
355
424
  sw.style.background = color;
356
425
  sw.title = color;
357
- sw.setAttribute("role", "gridcell");
358
426
  sw.setAttribute("aria-label", color);
359
- sw.tabIndex = -1;
360
427
  if (color.toUpperCase() === value?.toUpperCase()) sw.classList.add("on");
361
428
  sw.addEventListener("click", () => pick(color));
362
- sw.addEventListener("keydown", (e) => onGridKey(e, r2, c));
363
429
  grid.append(sw);
364
430
  rowCells.push(sw);
365
431
  });
366
432
  cells.push(rowCells);
367
433
  });
368
434
  panel.append(grid);
369
- function onGridKey(e, r2, c) {
370
- const delta = {
371
- ArrowRight: [0, 1],
372
- ArrowLeft: [0, -1],
373
- ArrowDown: [1, 0],
374
- ArrowUp: [-1, 0]
375
- };
376
- const d = delta[e.key];
377
- if (!d) return;
378
- e.preventDefault();
379
- const rows = cells.length;
380
- const cols = cells[0].length;
381
- const nr = Math.min(rows - 1, Math.max(0, r2 + d[0]));
382
- const nc = Math.min(cols - 1, Math.max(0, c + d[1]));
383
- cells[r2][c].tabIndex = -1;
384
- const next = cells[nr][nc];
385
- next.tabIndex = 0;
386
- next.focus();
387
- }
435
+ const nav = rovingGrid(cells);
388
436
  panel.append(
389
437
  Object.assign(document.createElement("div"), {
390
438
  className: "bb-cpk-cap",
@@ -440,36 +488,38 @@ function openColorPicker({
440
488
  if (outside) close();
441
489
  });
442
490
  window.addEventListener("scroll", close, true);
443
- const start = cells.flat().find((el2) => el2.classList.contains("on")) ?? cells[0][0];
444
- start.tabIndex = 0;
445
- start.focus();
491
+ const onRow = cells.findIndex(
492
+ (row) => row.some((el5) => el5.classList.contains("on"))
493
+ );
494
+ const onCol = onRow >= 0 ? cells[onRow].findIndex((el5) => el5.classList.contains("on")) : 0;
495
+ nav.focusCell(Math.max(0, onRow), Math.max(0, onCol));
446
496
  return { close };
447
497
  }
448
498
  function colorButton(o) {
449
499
  injectStyle("bb-ui-colorpicker-styles", STYLE2);
450
- const el2 = document.createElement("button");
451
- el2.type = "button";
452
- el2.className = `bb-cpk-btn${o.glyph ? " glyphed" : ""}`;
453
- el2.title = o.title;
454
- el2.setAttribute("aria-label", o.title);
455
- el2.setAttribute("aria-haspopup", "dialog");
500
+ const el5 = document.createElement("button");
501
+ el5.type = "button";
502
+ el5.className = `bb-cpk-btn${o.glyph ? " glyphed" : ""}`;
503
+ el5.title = o.title;
504
+ el5.setAttribute("aria-label", o.title);
505
+ el5.setAttribute("aria-haspopup", "dialog");
456
506
  let chip;
457
507
  if (o.glyph) {
458
- const g = document.createElement("span");
459
- g.className = "bb-cpk-btn-glyph";
460
- g.innerHTML = o.glyph;
508
+ const g2 = document.createElement("span");
509
+ g2.className = "bb-cpk-btn-glyph";
510
+ g2.innerHTML = o.glyph;
461
511
  chip = document.createElement("span");
462
512
  chip.className = "bb-cpk-btn-bar";
463
- el2.append(g, chip);
513
+ el5.append(g2, chip);
464
514
  } else {
465
515
  chip = document.createElement("span");
466
516
  chip.className = "bb-cpk-btn-chip";
467
- el2.append(chip);
468
- if (o.label) el2.append(document.createTextNode(o.label));
517
+ el5.append(chip);
518
+ if (o.label) el5.append(document.createTextNode(o.label));
469
519
  const caret = document.createElement("span");
470
520
  caret.className = "bb-cpk-btn-caret";
471
521
  caret.textContent = "\u25BE";
472
- el2.append(caret);
522
+ el5.append(caret);
473
523
  }
474
524
  function sync() {
475
525
  const v = o.value();
@@ -477,10 +527,10 @@ function colorButton(o) {
477
527
  chip.classList.toggle("none", !v && !o.glyph);
478
528
  chip.textContent = !v && !o.glyph ? "\u29B8" : "";
479
529
  }
480
- el2.addEventListener("mousedown", (e) => e.preventDefault());
481
- el2.addEventListener("click", () => {
530
+ el5.addEventListener("mousedown", (e) => e.preventDefault());
531
+ el5.addEventListener("click", () => {
482
532
  openColorPicker({
483
- anchor: el2,
533
+ anchor: el5,
484
534
  value: o.value(),
485
535
  clearLabel: o.clearLabel,
486
536
  onPick: (c) => {
@@ -490,7 +540,7 @@ function colorButton(o) {
490
540
  });
491
541
  });
492
542
  sync();
493
- return { el: el2, sync };
543
+ return { el: el5, sync };
494
544
  }
495
545
 
496
546
  // packages/ui/src/lib/toolbar.ts
@@ -598,7 +648,7 @@ function defaultToolbarGroups(commands) {
598
648
  const names = [...commands].map((c) => c.name);
599
649
  const aligns = names.filter((n) => n.startsWith("align-"));
600
650
  const rest = names.filter((n) => !n.startsWith("align-"));
601
- return [rest, aligns].filter((g) => g.length > 0);
651
+ return [rest, aligns].filter((g2) => g2.length > 0);
602
652
  }
603
653
  function mountToolbar(host, editor, options = {}) {
604
654
  injectStyle("bb-ui-toolbar-styles", STYLE3);
@@ -643,6 +693,20 @@ function mountToolbar(host, editor, options = {}) {
643
693
  selects.push({ spec: entry, el: sel });
644
694
  continue;
645
695
  }
696
+ if (typeof entry !== "string" && entry.kind === "button") {
697
+ const btn2 = document.createElement("button");
698
+ btn2.type = "button";
699
+ btn2.className = "bb-toolbar-btn";
700
+ btn2.title = entry.title;
701
+ btn2.setAttribute("aria-label", entry.title);
702
+ btn2.setAttribute("aria-haspopup", "dialog");
703
+ if (entry.svg) btn2.innerHTML = entry.svg;
704
+ else btn2.textContent = entry.label ?? entry.title;
705
+ btn2.addEventListener("mousedown", (e) => e.preventDefault());
706
+ btn2.addEventListener("click", () => entry.onClick(btn2));
707
+ groupEl.appendChild(btn2);
708
+ continue;
709
+ }
646
710
  if (typeof entry !== "string" && entry.kind === "color") {
647
711
  const ctl = colorButton({
648
712
  title: entry.title,
@@ -753,6 +817,10 @@ function mountToolbar(host, editor, options = {}) {
753
817
  btn.type = "button";
754
818
  btn.className = "bb-toolbar-btn" + (item.className ? ` ${item.className}` : "");
755
819
  btn.title = item.title;
820
+ btn.addEventListener("mouseenter", () => {
821
+ const key = shortcutLabel(entry, [editor.keybindings]);
822
+ btn.title = key ? `${item.title} (${key})` : item.title;
823
+ });
756
824
  btn.setAttribute("aria-label", item.title);
757
825
  if (item.svg) btn.innerHTML = item.svg;
758
826
  else btn.textContent = item.label ?? entry;
@@ -803,7 +871,7 @@ function mountToolbar(host, editor, options = {}) {
803
871
  if (fits()) return;
804
872
  moreBtn.style.display = "";
805
873
  const groupEls = Array.from(root.children).filter(
806
- (el2) => el2.classList.contains("bb-toolbar-group")
874
+ (el5) => el5.classList.contains("bb-toolbar-group")
807
875
  );
808
876
  for (let i = groupEls.length - 1; i >= 0 && !fits(); i--) {
809
877
  pop.insertBefore(groupEls[i], pop.firstChild);
@@ -818,15 +886,15 @@ function mountToolbar(host, editor, options = {}) {
818
886
  layout();
819
887
  const refresh = (state) => {
820
888
  latest = state;
821
- for (const { name, el: el2 } of buttons) {
889
+ for (const { name, el: el5 } of buttons) {
822
890
  const cmd = editor.commands.get(name);
823
891
  if (!cmd) continue;
824
892
  const active = cmd.isActive?.(state) ?? false;
825
- el2.classList.toggle("is-active", active);
826
- el2.setAttribute("aria-pressed", String(active));
827
- el2.disabled = cmd.isEnabled ? !cmd.isEnabled(state) : false;
893
+ el5.classList.toggle("is-active", active);
894
+ el5.setAttribute("aria-pressed", String(active));
895
+ el5.disabled = cmd.isEnabled ? !cmd.isEnabled(state) : false;
828
896
  }
829
- for (const { spec, el: el2 } of selects) el2.value = spec.value(state);
897
+ for (const { spec, el: el5 } of selects) el5.value = spec.value(state);
830
898
  for (const c of colors) c.sync();
831
899
  for (const { spec, cards } of splits) {
832
900
  const selected = spec.value(state);
@@ -915,19 +983,18 @@ function makeRow(label, opts) {
915
983
  text.className = "bb-menu-label";
916
984
  text.textContent = label;
917
985
  item.append(check, text);
918
- if (opts.shortcut) {
919
- const sc = document.createElement("span");
920
- sc.className = "bb-menu-shortcut";
921
- sc.textContent = opts.shortcut;
922
- item.appendChild(sc);
923
- }
986
+ const sc = document.createElement("span");
987
+ sc.className = "bb-menu-shortcut";
988
+ sc.textContent = opts.shortcut ?? "";
989
+ if (!opts.shortcut) sc.hidden = true;
990
+ item.appendChild(sc);
924
991
  if (opts.hasSub) {
925
992
  const arrow = document.createElement("span");
926
993
  arrow.className = "bb-menu-arrow";
927
994
  arrow.textContent = "\u203A";
928
995
  item.appendChild(arrow);
929
996
  }
930
- return { item, check };
997
+ return { item, check, shortcutEl: sc };
931
998
  }
932
999
  function buildMenuEntries(entries, container, ctx) {
933
1000
  for (const entry of entries) {
@@ -958,10 +1025,11 @@ function buildMenuEntries(entries, container, ctx) {
958
1025
  const editor = ctx.editor;
959
1026
  const cmd = editor?.commands.get(entry.command);
960
1027
  if (!editor || !cmd) continue;
961
- const { item, check } = makeRow(
1028
+ const { item, check, shortcutEl } = makeRow(
962
1029
  entry.label ?? ctx.labels[entry.command] ?? entry.command,
963
- {}
1030
+ { shortcut: ctx.shortcutOf?.(entry.command) }
964
1031
  );
1032
+ ctx.shortcutRef?.(shortcutEl, entry.command);
965
1033
  item.setAttribute("role", "menuitemcheckbox");
966
1034
  item.addEventListener("click", () => {
967
1035
  const s = ctx.state();
@@ -974,9 +1042,10 @@ function buildMenuEntries(entries, container, ctx) {
974
1042
  if (cmd.isEnabled) ctx.enable(item, (s) => cmd.isEnabled(s));
975
1043
  container.appendChild(item);
976
1044
  } else {
977
- const { item, check } = makeRow(entry.label, {
978
- shortcut: entry.shortcut
1045
+ const { item, check, shortcutEl } = makeRow(entry.label, {
1046
+ shortcut: (entry.shortcutOf && ctx.shortcutOf?.(entry.shortcutOf)) ?? entry.shortcut
979
1047
  });
1048
+ if (entry.shortcutOf) ctx.shortcutRef?.(shortcutEl, entry.shortcutOf);
980
1049
  item.setAttribute(
981
1050
  "role",
982
1051
  entry.isActive ? "menuitemcheckbox" : "menuitem"
@@ -1002,15 +1071,15 @@ function closeCurrentPopup() {
1002
1071
  function showPopup(content, at, onClose) {
1003
1072
  injectStyle("bb-ui-menubar-styles", STYLE4);
1004
1073
  closeCurrentPopup();
1005
- const el2 = document.createElement("div");
1006
- el2.className = "bb-menu bb-popup";
1007
- el2.setAttribute("role", "menu");
1008
- el2.appendChild(content);
1009
- document.body.appendChild(el2);
1074
+ const el5 = document.createElement("div");
1075
+ el5.className = "bb-menu bb-popup";
1076
+ el5.setAttribute("role", "menu");
1077
+ el5.appendChild(content);
1078
+ document.body.appendChild(el5);
1010
1079
  const place = () => {
1011
- const r = el2.getBoundingClientRect();
1012
- el2.style.left = `${Math.max(4, Math.min(at.x, window.innerWidth - r.width - 4))}px`;
1013
- el2.style.top = `${Math.max(4, Math.min(at.y, window.innerHeight - r.height - 4))}px`;
1080
+ const r = el5.getBoundingClientRect();
1081
+ el5.style.left = `${Math.max(4, Math.min(at.x, window.innerWidth - r.width - 4))}px`;
1082
+ el5.style.top = `${Math.max(4, Math.min(at.y, window.innerHeight - r.height - 4))}px`;
1014
1083
  };
1015
1084
  place();
1016
1085
  let disposed = false;
@@ -1020,7 +1089,7 @@ function showPopup(content, at, onClose) {
1020
1089
  document.removeEventListener("pointerdown", onPointer, true);
1021
1090
  document.removeEventListener("keydown", onKey, true);
1022
1091
  document.removeEventListener("scroll", onScroll, true);
1023
- el2.remove();
1092
+ el5.remove();
1024
1093
  onClose?.();
1025
1094
  };
1026
1095
  const close = () => {
@@ -1028,7 +1097,7 @@ function showPopup(content, at, onClose) {
1028
1097
  dispose();
1029
1098
  };
1030
1099
  const onPointer = (e) => {
1031
- if (!el2.contains(e.target)) close();
1100
+ if (!el5.contains(e.target)) close();
1032
1101
  };
1033
1102
  const onKey = (e) => {
1034
1103
  if (e.key === "Escape") {
@@ -1037,13 +1106,13 @@ function showPopup(content, at, onClose) {
1037
1106
  }
1038
1107
  };
1039
1108
  const onScroll = (e) => {
1040
- if (!el2.contains(e.target)) close();
1109
+ if (!el5.contains(e.target)) close();
1041
1110
  };
1042
1111
  document.addEventListener("pointerdown", onPointer, true);
1043
1112
  document.addEventListener("keydown", onKey, true);
1044
1113
  document.addEventListener("scroll", onScroll, true);
1045
1114
  currentPopup = dispose;
1046
- return { close, el: el2 };
1115
+ return { close, el: el5 };
1047
1116
  }
1048
1117
  function showMenu(entries, at, options = {}) {
1049
1118
  const labels = { ...DEFAULT_LABELS, ...options.labels ?? {} };
@@ -1067,12 +1136,13 @@ function showMenu(entries, at, options = {}) {
1067
1136
  close: () => handle?.close(),
1068
1137
  labels,
1069
1138
  editor: options.editor,
1139
+ shortcutOf: (command) => shortcutLabel(command, [options.editor?.keybindings]),
1070
1140
  state: stateOf,
1071
- check: (el2, active) => {
1072
- el2.textContent = evalNow(active, false) ? "\u2713" : "";
1141
+ check: (el5, active) => {
1142
+ el5.textContent = evalNow(active, false) ? "\u2713" : "";
1073
1143
  },
1074
- enable: (el2, enabled) => {
1075
- el2.disabled = !evalNow(enabled, true);
1144
+ enable: (el5, enabled) => {
1145
+ el5.disabled = !evalNow(enabled, true);
1076
1146
  }
1077
1147
  });
1078
1148
  handle = showPopup(container, at, options.onClose);
@@ -1116,6 +1186,7 @@ function mountMenubar(host, editor, options = {}) {
1116
1186
  const open = (idx, focusFirst = false) => {
1117
1187
  if (openIdx === idx) return;
1118
1188
  close();
1189
+ refreshShortcuts();
1119
1190
  const p = panels[idx];
1120
1191
  p.dropdown.hidden = false;
1121
1192
  p.title.setAttribute("aria-expanded", "true");
@@ -1124,13 +1195,24 @@ function mountMenubar(host, editor, options = {}) {
1124
1195
  if (focusFirst)
1125
1196
  p.dropdown.querySelector(".bb-menu-item")?.focus();
1126
1197
  };
1198
+ const registries = [editor.keybindings, ...options.keybindings ?? []];
1199
+ const shortcutRefs = [];
1200
+ const refreshShortcuts = () => {
1201
+ for (const { el: el5, command } of shortcutRefs) {
1202
+ const label = shortcutLabel(command, registries);
1203
+ el5.textContent = label ?? "";
1204
+ el5.hidden = !label;
1205
+ }
1206
+ };
1127
1207
  const buildCtx = {
1128
1208
  close: () => close(),
1129
1209
  labels,
1130
1210
  editor,
1211
+ shortcutOf: (command) => shortcutLabel(command, registries),
1212
+ shortcutRef: (el5, command) => shortcutRefs.push({ el: el5, command }),
1131
1213
  state: () => latest,
1132
- check: (el2, active) => checks.push({ el: el2, active }),
1133
- enable: (el2, enabled) => enables.push({ el: el2, enabled })
1214
+ check: (el5, active) => checks.push({ el: el5, active }),
1215
+ enable: (el5, enabled) => enables.push({ el: el5, enabled })
1134
1216
  };
1135
1217
  const buildEntries = (entries, container) => buildMenuEntries(entries, container, buildCtx);
1136
1218
  menus.forEach((menu, idx) => {
@@ -1342,15 +1424,15 @@ function closeCurrent() {
1342
1424
  function showContextMenu(entries, at) {
1343
1425
  injectStyle("bb-ui-contextmenu-styles", STYLE6);
1344
1426
  closeCurrent();
1345
- const el2 = document.createElement("div");
1346
- el2.className = "bb-ctx";
1347
- el2.setAttribute("role", "menu");
1427
+ const el5 = document.createElement("div");
1428
+ el5.className = "bb-ctx";
1429
+ el5.setAttribute("role", "menu");
1348
1430
  for (const entry of entries) {
1349
1431
  if (entry === "separator") {
1350
1432
  const sep = document.createElement("div");
1351
1433
  sep.className = "bb-ctx-sep";
1352
1434
  sep.setAttribute("role", "separator");
1353
- el2.appendChild(sep);
1435
+ el5.appendChild(sep);
1354
1436
  continue;
1355
1437
  }
1356
1438
  const item = document.createElement("button");
@@ -1373,22 +1455,22 @@ function showContextMenu(entries, at) {
1373
1455
  closeCurrent();
1374
1456
  entry.run();
1375
1457
  });
1376
- el2.appendChild(item);
1458
+ el5.appendChild(item);
1377
1459
  }
1378
- document.body.appendChild(el2);
1379
- const r = el2.getBoundingClientRect();
1460
+ document.body.appendChild(el5);
1461
+ const r = el5.getBoundingClientRect();
1380
1462
  const pad = 4;
1381
1463
  const fitsX = (v) => v >= pad && v + r.width <= window.innerWidth - pad;
1382
1464
  const fitsY = (v) => v >= pad && v + r.height <= window.innerHeight - pad;
1383
1465
  const x = fitsX(at.x) ? at.x : fitsX(at.x - r.width) ? at.x - r.width : Math.max(pad, Math.min(at.x, window.innerWidth - r.width - pad));
1384
1466
  const y = fitsY(at.y) ? at.y : fitsY(at.y - r.height) ? at.y - r.height : Math.max(pad, Math.min(at.y, window.innerHeight - r.height - pad));
1385
- el2.style.left = `${x}px`;
1386
- el2.style.top = `${y}px`;
1467
+ el5.style.left = `${x}px`;
1468
+ el5.style.top = `${y}px`;
1387
1469
  const items = () => Array.from(
1388
- el2.querySelectorAll(".bb-ctx-item:not(:disabled)")
1470
+ el5.querySelectorAll(".bb-ctx-item:not(:disabled)")
1389
1471
  );
1390
1472
  const onDown = (e) => {
1391
- if (!el2.contains(e.target)) closeCurrent();
1473
+ if (!el5.contains(e.target)) closeCurrent();
1392
1474
  };
1393
1475
  const onKey = (e) => {
1394
1476
  if (e.key === "Escape") return closeCurrent();
@@ -1410,7 +1492,7 @@ function showContextMenu(entries, at) {
1410
1492
  document.removeEventListener("pointerdown", onDown, true);
1411
1493
  document.removeEventListener("keydown", onKey, true);
1412
1494
  window.removeEventListener("scroll", onScroll, true);
1413
- el2.remove();
1495
+ el5.remove();
1414
1496
  };
1415
1497
  items()[0]?.focus();
1416
1498
  return { close: closeCurrent };
@@ -1471,10 +1553,10 @@ function showLinkPanel(opts) {
1471
1553
  return currentHandle;
1472
1554
  }
1473
1555
  closeCurrent2();
1474
- const el2 = document.createElement("div");
1475
- el2.className = "bb-linkpanel";
1476
- el2.setAttribute("role", "dialog");
1477
- el2.setAttribute("aria-label", "Link");
1556
+ const el5 = document.createElement("div");
1557
+ el5.className = "bb-linkpanel";
1558
+ el5.setAttribute("role", "dialog");
1559
+ el5.setAttribute("aria-label", "Link");
1478
1560
  const iconBtn = (svg, label, onClick) => {
1479
1561
  const b = document.createElement("button");
1480
1562
  b.type = "button";
@@ -1501,7 +1583,7 @@ function showLinkPanel(opts) {
1501
1583
  return { wrap, input };
1502
1584
  };
1503
1585
  const renderView = (existing) => {
1504
- el2.textContent = "";
1586
+ el5.textContent = "";
1505
1587
  const row = document.createElement("div");
1506
1588
  row.className = "bb-linkpanel-row";
1507
1589
  const internal = opts.internal;
@@ -1548,11 +1630,11 @@ function showLinkPanel(opts) {
1548
1630
  })
1549
1631
  );
1550
1632
  }
1551
- el2.appendChild(row);
1552
- placeFloating(el2, opts.anchor);
1633
+ el5.appendChild(row);
1634
+ placeFloating(el5, opts.anchor);
1553
1635
  };
1554
1636
  const renderForm = (prefill) => {
1555
- el2.textContent = "";
1637
+ el5.textContent = "";
1556
1638
  const form = document.createElement("div");
1557
1639
  form.className = "bb-linkpanel-form";
1558
1640
  const withText = !opts.hasSelection;
@@ -1593,16 +1675,16 @@ function showLinkPanel(opts) {
1593
1675
  }
1594
1676
  row.appendChild(apply);
1595
1677
  form.appendChild(row);
1596
- el2.appendChild(form);
1597
- placeFloating(el2, opts.anchor);
1678
+ el5.appendChild(form);
1679
+ placeFloating(el5, opts.anchor);
1598
1680
  u.input.focus();
1599
1681
  u.input.select();
1600
1682
  };
1601
- document.body.appendChild(el2);
1683
+ document.body.appendChild(el5);
1602
1684
  if (opts.existing) renderView(opts.existing);
1603
1685
  else renderForm(null);
1604
1686
  const onDown = (e) => {
1605
- if (el2.contains(e.target)) return;
1687
+ if (el5.contains(e.target)) return;
1606
1688
  if (opts.key) {
1607
1689
  cancelPendingClose();
1608
1690
  pendingClose = setTimeout(closeCurrent2, 80);
@@ -1623,7 +1705,7 @@ function showLinkPanel(opts) {
1623
1705
  document.removeEventListener("pointerdown", onDown, true);
1624
1706
  document.removeEventListener("keydown", onKey, true);
1625
1707
  window.removeEventListener("scroll", onScroll, true);
1626
- el2.remove();
1708
+ el5.remove();
1627
1709
  };
1628
1710
  current2 = dispose;
1629
1711
  currentKey = opts.key ?? null;
@@ -1826,8 +1908,8 @@ function openCellProperties({
1826
1908
  for (const [key, b] of vaBtns) b.classList.toggle("on", vAlign === key);
1827
1909
  }
1828
1910
  function syncPresets() {
1829
- for (const { el: el2, key } of presetEls)
1830
- el2.classList.toggle("on", preset === key);
1911
+ for (const { el: el5, key } of presetEls)
1912
+ el5.classList.toggle("on", preset === key);
1831
1913
  }
1832
1914
  syncVAlign();
1833
1915
  syncPresets();
@@ -1872,7 +1954,7 @@ function tableGridPicker(options) {
1872
1954
  label.className = "bb-grid-label";
1873
1955
  label.textContent = "0 \xD7 0";
1874
1956
  const cells = [];
1875
- const highlight = (r, c) => {
1957
+ const highlight2 = (r, c) => {
1876
1958
  for (let i = 0; i < cells.length; i++) {
1877
1959
  const row = Math.floor(i / maxCols);
1878
1960
  const col = i % maxCols;
@@ -1886,7 +1968,7 @@ function tableGridPicker(options) {
1886
1968
  cell.type = "button";
1887
1969
  cell.className = "bb-grid-cell";
1888
1970
  cell.addEventListener("mousedown", (e) => e.preventDefault());
1889
- cell.addEventListener("mouseenter", () => highlight(r, c));
1971
+ cell.addEventListener("mouseenter", () => highlight2(r, c));
1890
1972
  cell.addEventListener("click", () => options.onPick(r + 1, c + 1));
1891
1973
  cells.push(cell);
1892
1974
  cellsEl.appendChild(cell);
@@ -1935,9 +2017,9 @@ var STYLE10 = `
1935
2017
  `;
1936
2018
  var SVG_NS = "http://www.w3.org/2000/svg";
1937
2019
  function svgEl(name, attrs) {
1938
- const el2 = document.createElementNS(SVG_NS, name);
1939
- for (const [k, v] of Object.entries(attrs)) el2.setAttribute(k, String(v));
1940
- return el2;
2020
+ const el5 = document.createElementNS(SVG_NS, name);
2021
+ for (const [k, v] of Object.entries(attrs)) el5.setAttribute(k, String(v));
2022
+ return el5;
1941
2023
  }
1942
2024
  function pagePreview(ratio) {
1943
2025
  const svg = svgEl("svg", { viewBox: "0 0 34 42", class: "bb-ps-icon" });
@@ -2004,11 +2086,11 @@ function presetRow(icon, name, caption, active, onPick) {
2004
2086
  return row;
2005
2087
  }
2006
2088
  function captionLine(text) {
2007
- const el2 = document.createElement("span");
2008
- el2.className = "bb-ps-dims";
2009
- el2.style.display = "block";
2010
- el2.textContent = text;
2011
- return el2;
2089
+ const el5 = document.createElement("span");
2090
+ el5.className = "bb-ps-dims";
2091
+ el5.style.display = "block";
2092
+ el5.textContent = text;
2093
+ return el5;
2012
2094
  }
2013
2095
  function customRow(icon, title, description, onPick) {
2014
2096
  icon.appendChild(
@@ -2137,8 +2219,8 @@ function cmField(label, valuePx, min) {
2137
2219
  return { row, input };
2138
2220
  }
2139
2221
  function dialogActions() {
2140
- const el2 = document.createElement("div");
2141
- el2.className = "bb-pd-actions";
2222
+ const el5 = document.createElement("div");
2223
+ el5.className = "bb-pd-actions";
2142
2224
  const cancel = document.createElement("button");
2143
2225
  cancel.type = "button";
2144
2226
  cancel.className = "bb-prompt-btn";
@@ -2148,8 +2230,8 @@ function dialogActions() {
2148
2230
  ok.className = "bb-prompt-btn";
2149
2231
  ok.dataset["primary"] = "true";
2150
2232
  ok.textContent = "OK";
2151
- el2.append(cancel, ok);
2152
- return { el: el2, ok, cancel };
2233
+ el5.append(cancel, ok);
2234
+ return { el: el5, ok, cancel };
2153
2235
  }
2154
2236
  function readPx(input, fallbackPx) {
2155
2237
  const n = Number(input.value);
@@ -2346,15 +2428,15 @@ function sectionPaperPanel(options) {
2346
2428
  root.className = "bb-ps";
2347
2429
  root.setAttribute("role", "menu");
2348
2430
  const group = (text) => {
2349
- const el2 = document.createElement("div");
2350
- el2.className = "bb-ps-group";
2351
- el2.textContent = text;
2352
- return el2;
2431
+ const el5 = document.createElement("div");
2432
+ el5.className = "bb-ps-group";
2433
+ el5.textContent = text;
2434
+ return el5;
2353
2435
  };
2354
2436
  const sep = () => {
2355
- const el2 = document.createElement("div");
2356
- el2.className = "bb-ps-sep";
2357
- return el2;
2437
+ const el5 = document.createElement("div");
2438
+ el5.className = "bb-ps-sep";
2439
+ return el5;
2358
2440
  };
2359
2441
  root.appendChild(group(options.labels?.orientation ?? "Orientation"));
2360
2442
  const tiles = document.createElement("div");
@@ -2424,8 +2506,8 @@ var STYLE11 = `
2424
2506
  `;
2425
2507
  function createSectionChip(options) {
2426
2508
  injectStyle("bb-ui-section-chip-styles", STYLE11);
2427
- const el2 = document.createElement("div");
2428
- el2.className = "bb-secchip";
2509
+ const el5 = document.createElement("div");
2510
+ el5.className = "bb-secchip";
2429
2511
  const title = document.createElement("span");
2430
2512
  title.className = "bb-secchip-title";
2431
2513
  title.textContent = options.title;
@@ -2459,16 +2541,16 @@ function createSectionChip(options) {
2459
2541
  x.innerHTML = '<svg viewBox="0 0 16 16" width="9" height="9" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M3 3l10 10M13 3 3 13"/></svg>';
2460
2542
  x.addEventListener("mousedown", (e) => e.preventDefault());
2461
2543
  x.addEventListener("click", () => options.onDelete());
2462
- el2.append(title, divider(), nums.btn, divider(), paper.btn, divider(), x);
2544
+ el5.append(title, divider(), nums.btn, divider(), paper.btn, divider(), x);
2463
2545
  return {
2464
- el: el2,
2546
+ el: el5,
2465
2547
  update(data) {
2466
2548
  nums.label.textContent = data.pageNumbers;
2467
2549
  nums.btn.dataset["muted"] = String(!!data.pageNumbersMuted);
2468
2550
  paper.label.textContent = data.paper;
2469
2551
  },
2470
2552
  destroy() {
2471
- el2.remove();
2553
+ el5.remove();
2472
2554
  }
2473
2555
  };
2474
2556
  }
@@ -2620,7 +2702,7 @@ function openFontDialog({
2620
2702
  const italicBtn = mkSeg("I", initial.italic, "font-style:italic");
2621
2703
  const underBtn = mkSeg("U", initial.underline, "text-decoration:underline");
2622
2704
  let color = initial.color ?? null;
2623
- let highlight = initial.highlight ?? null;
2705
+ let highlight2 = initial.highlight ?? null;
2624
2706
  const colorBtn = colorButton({
2625
2707
  title: "Text color",
2626
2708
  label: "Text color",
@@ -2635,9 +2717,9 @@ function openFontDialog({
2635
2717
  title: "Highlight",
2636
2718
  label: "Highlight",
2637
2719
  clearLabel: "No highlight",
2638
- value: () => highlight,
2720
+ value: () => highlight2,
2639
2721
  onPick: (c) => {
2640
- highlight = c;
2722
+ highlight2 = c;
2641
2723
  paint();
2642
2724
  }
2643
2725
  });
@@ -2757,7 +2839,7 @@ function openFontDialog({
2757
2839
  s.textDecorationStyle = dstrikeCb.checked ? "double" : "solid";
2758
2840
  s.fontVariant = smallCapsCb.checked ? "small-caps" : "normal";
2759
2841
  s.color = color ?? "inherit";
2760
- s.background = highlight ?? "transparent";
2842
+ s.background = highlight2 ?? "transparent";
2761
2843
  s.letterSpacing = `${spacingPt}pt`;
2762
2844
  s.transform = scale === 100 ? "none" : `scaleX(${scale / 100})`;
2763
2845
  s.verticalAlign = supCb.checked ? "super" : subCb.checked ? "sub" : `${positionPt}pt`;
@@ -2786,7 +2868,7 @@ function openFontDialog({
2786
2868
  smallCaps: checkedOrMixed(smallCapsCb),
2787
2869
  vertAlign: supCb.checked ? "super" : subCb.checked ? "sub" : null,
2788
2870
  color,
2789
- highlight,
2871
+ highlight: highlight2,
2790
2872
  scalePercent: scale,
2791
2873
  letterSpacingTwips: ptToTwips(spacingPt),
2792
2874
  positionHalfPoints: ptToHalf(positionPt)
@@ -2801,28 +2883,1216 @@ function openFontDialog({
2801
2883
  paint();
2802
2884
  dialog.open();
2803
2885
  }
2886
+
2887
+ // packages/ui/src/lib/symbol-sets.ts
2888
+ import { AUTOCORRECT_RULES } from "@shadow-garden/bapbong-contracts";
2889
+ var g = (id, label, short, pairs) => ({
2890
+ id,
2891
+ label,
2892
+ short,
2893
+ entries: pairs.map(([char, name]) => ({ char, name }))
2894
+ });
2895
+ var SYMBOL_GROUPS = [
2896
+ g("checks", "Checkboxes & ticks", "Checks", [
2897
+ ["\u2610", "Ballot box"],
2898
+ ["\u2611", "Ballot box with check"],
2899
+ ["\u2612", "Ballot box with X"],
2900
+ ["\u2713", "Check mark"],
2901
+ ["\u2714", "Heavy check mark"],
2902
+ ["\u2717", "Ballot X"],
2903
+ ["\u2718", "Heavy ballot X"],
2904
+ ["\u2715", "Multiplication X"],
2905
+ ["\u2716", "Heavy multiplication X"],
2906
+ ["\u274F", "Lower right drop-shadowed white square"],
2907
+ ["\u2750", "Upper right drop-shadowed white square"],
2908
+ ["\u2751", "Lower right shadowed white square"],
2909
+ ["\u2752", "Upper right shadowed white square"],
2910
+ ["\u25A2", "White square with rounded corners"],
2911
+ ["\u25A3", "White square containing black small square"],
2912
+ ["\u22A0", "Squared times"],
2913
+ ["\u2297", "Circled times"],
2914
+ ["\u29BF", "Circled bullet"],
2915
+ ["\u25C9", "Fisheye"],
2916
+ ["\u25CB", "White circle"],
2917
+ ["\u25CF", "Black circle"],
2918
+ ["\u25EF", "Large circle"],
2919
+ ["\u25FB", "White medium square"],
2920
+ ["\u25FC", "Black medium square"]
2921
+ ]),
2922
+ g("bullets", "Bullets & shapes", "Bullets", [
2923
+ ["\u2022", "Bullet"],
2924
+ ["\u25E6", "White bullet"],
2925
+ ["\u25AA", "Black small square"],
2926
+ ["\u25AB", "White small square"],
2927
+ ["\u25A0", "Black square"],
2928
+ ["\u25A1", "White square"],
2929
+ ["\u25AC", "Black rectangle"],
2930
+ ["\u25C6", "Black diamond"],
2931
+ ["\u25C7", "White diamond"],
2932
+ ["\u25CA", "Lozenge"],
2933
+ ["\u2605", "Black star"],
2934
+ ["\u2606", "White star"],
2935
+ ["\u2726", "Black four pointed star"],
2936
+ ["\u2727", "White four pointed star"],
2937
+ ["\u25BA", "Black right-pointing pointer"],
2938
+ ["\u25B8", "Black right-pointing small triangle"],
2939
+ ["\u25B9", "White right-pointing small triangle"],
2940
+ ["\u25B2", "Black up-pointing triangle"],
2941
+ ["\u25B3", "White up-pointing triangle"],
2942
+ ["\u25BC", "Black down-pointing triangle"],
2943
+ ["\u25BD", "White down-pointing triangle"],
2944
+ ["\u25C4", "Black left-pointing pointer"],
2945
+ ["\u2756", "Black diamond minus white X"],
2946
+ ["\u273D", "Heavy teardrop-spoked asterisk"],
2947
+ ["\u273F", "Black florette"],
2948
+ ["\u2740", "White florette"],
2949
+ ["\u27A2", "Three-D top-lighted rightwards arrowhead"],
2950
+ ["\u27A4", "Black rightwards arrowhead"],
2951
+ ["\u27A3", "Three-D bottom-lighted rightwards arrowhead"],
2952
+ ["\u2713", "Check mark"]
2953
+ ]),
2954
+ g("arrows", "Arrows", "Arrows", [
2955
+ ["\u2190", "Leftwards arrow"],
2956
+ ["\u2191", "Upwards arrow"],
2957
+ ["\u2192", "Rightwards arrow"],
2958
+ ["\u2193", "Downwards arrow"],
2959
+ ["\u2194", "Left right arrow"],
2960
+ ["\u2195", "Up down arrow"],
2961
+ ["\u2196", "North west arrow"],
2962
+ ["\u2197", "North east arrow"],
2963
+ ["\u2198", "South east arrow"],
2964
+ ["\u2199", "South west arrow"],
2965
+ ["\u21D0", "Leftwards double arrow"],
2966
+ ["\u21D1", "Upwards double arrow"],
2967
+ ["\u21D2", "Rightwards double arrow"],
2968
+ ["\u21D3", "Downwards double arrow"],
2969
+ ["\u21D4", "Left right double arrow"],
2970
+ ["\u21D5", "Up down double arrow"],
2971
+ ["\u21A9", "Leftwards arrow with hook"],
2972
+ ["\u21AA", "Rightwards arrow with hook"],
2973
+ ["\u21B5", "Downwards arrow with corner leftwards"],
2974
+ ["\u21C4", "Rightwards arrow over leftwards arrow"],
2975
+ ["\u21C6", "Leftwards arrow over rightwards arrow"],
2976
+ ["\u2794", "Heavy wide-headed rightwards arrow"],
2977
+ ["\u279C", "Heavy round-tipped rightwards arrow"],
2978
+ ["\u279D", "Triangle-headed rightwards arrow"],
2979
+ ["\u279E", "Heavy triangle-headed rightwards arrow"],
2980
+ ["\u27A1", "Black rightwards arrow"],
2981
+ ["\u2B05", "Leftwards black arrow"],
2982
+ ["\u2B06", "Upwards black arrow"],
2983
+ ["\u2B07", "Downwards black arrow"],
2984
+ ["\u21BB", "Clockwise open circle arrow"],
2985
+ ["\u21BA", "Anticlockwise open circle arrow"],
2986
+ ["\u2934", "Arrow pointing rightwards then curving upwards"],
2987
+ ["\u2935", "Arrow pointing rightwards then curving downwards"]
2988
+ ]),
2989
+ g("math", "Math", "Math", [
2990
+ ["\xB1", "Plus-minus sign"],
2991
+ ["\xD7", "Multiplication sign"],
2992
+ ["\xF7", "Division sign"],
2993
+ ["\u2212", "Minus sign"],
2994
+ ["\u2264", "Less-than or equal to"],
2995
+ ["\u2265", "Greater-than or equal to"],
2996
+ ["\u2260", "Not equal to"],
2997
+ ["\u2248", "Almost equal to"],
2998
+ ["\u2261", "Identical to"],
2999
+ ["\u221E", "Infinity"],
3000
+ ["\u221A", "Square root"],
3001
+ ["\u2211", "N-ary summation"],
3002
+ ["\u220F", "N-ary product"],
3003
+ ["\u222B", "Integral"],
3004
+ ["\u2202", "Partial differential"],
3005
+ ["\u2206", "Increment"],
3006
+ ["\u2207", "Nabla"],
3007
+ ["\u2208", "Element of"],
3008
+ ["\u2209", "Not an element of"],
3009
+ ["\u2229", "Intersection"],
3010
+ ["\u222A", "Union"],
3011
+ ["\u2282", "Subset of"],
3012
+ ["\u2283", "Superset of"],
3013
+ ["\u2205", "Empty set"],
3014
+ ["\u2200", "For all"],
3015
+ ["\u2203", "There exists"],
3016
+ ["\xAC", "Not sign"],
3017
+ ["\u2227", "Logical and"],
3018
+ ["\u2228", "Logical or"],
3019
+ ["\xB0", "Degree sign"],
3020
+ ["\u2032", "Prime"],
3021
+ ["\u2033", "Double prime"],
3022
+ ["\u2030", "Per mille sign"],
3023
+ ["\xB5", "Micro sign"],
3024
+ ["\u03C0", "Greek small letter pi"],
3025
+ ["\u221D", "Proportional to"],
3026
+ ["\u2220", "Angle"],
3027
+ ["\u22A5", "Up tack"],
3028
+ ["\u2225", "Parallel to"],
3029
+ ["\xB9", "Superscript one"],
3030
+ ["\xB2", "Superscript two"],
3031
+ ["\xB3", "Superscript three"],
3032
+ ["\xBD", "Vulgar fraction one half"],
3033
+ ["\u2153", "Vulgar fraction one third"],
3034
+ ["\xBC", "Vulgar fraction one quarter"],
3035
+ ["\xBE", "Vulgar fraction three quarters"]
3036
+ ]),
3037
+ g("currency", "Currency", "Currency", [
3038
+ ["\u20AB", "Dong sign"],
3039
+ ["$", "Dollar sign"],
3040
+ ["\u20AC", "Euro sign"],
3041
+ ["\xA3", "Pound sign"],
3042
+ ["\xA5", "Yen sign"],
3043
+ ["\u20A9", "Won sign"],
3044
+ ["\u20B9", "Indian rupee sign"],
3045
+ ["\u20BD", "Ruble sign"],
3046
+ ["\u0E3F", "Thai currency symbol baht"],
3047
+ ["\u20B1", "Peso sign"],
3048
+ ["\xA2", "Cent sign"],
3049
+ ["\xA4", "Currency sign"],
3050
+ ["\u20BF", "Bitcoin sign"]
3051
+ ]),
3052
+ g("punct", "Punctuation & typography", "Punct.", [
3053
+ ["\u2013", "En dash"],
3054
+ ["\u2014", "Em dash"],
3055
+ ["\u2026", "Horizontal ellipsis"],
3056
+ ["\u2018", "Left single quotation mark"],
3057
+ ["\u2019", "Right single quotation mark"],
3058
+ ["\u201C", "Left double quotation mark"],
3059
+ ["\u201D", "Right double quotation mark"],
3060
+ ["\u201A", "Single low-9 quotation mark"],
3061
+ ["\u201E", "Double low-9 quotation mark"],
3062
+ ["\xAB", "Left-pointing double angle quotation mark"],
3063
+ ["\xBB", "Right-pointing double angle quotation mark"],
3064
+ ["\u2039", "Single left-pointing angle quotation mark"],
3065
+ ["\u203A", "Single right-pointing angle quotation mark"],
3066
+ ["\xA7", "Section sign"],
3067
+ ["\xB6", "Pilcrow sign"],
3068
+ ["\u2020", "Dagger"],
3069
+ ["\u2021", "Double dagger"],
3070
+ ["\u2022", "Bullet"],
3071
+ ["\xB7", "Middle dot"],
3072
+ ["\u2116", "Numero sign"],
3073
+ ["\xA9", "Copyright sign"],
3074
+ ["\xAE", "Registered sign"],
3075
+ ["\u2122", "Trade mark sign"],
3076
+ ["\u2120", "Service mark"],
3077
+ ["\xA1", "Inverted exclamation mark"],
3078
+ ["\xBF", "Inverted question mark"],
3079
+ ["\u203D", "Interrobang"],
3080
+ ["\u02C6", "Modifier letter circumflex accent"],
3081
+ ["\u02DC", "Small tilde"],
3082
+ ["\xA6", "Broken bar"]
3083
+ ]),
3084
+ g("greek", "Greek", "Greek", [
3085
+ ["\u03B1", "Alpha"],
3086
+ ["\u03B2", "Beta"],
3087
+ ["\u03B3", "Gamma"],
3088
+ ["\u03B4", "Delta"],
3089
+ ["\u03B5", "Epsilon"],
3090
+ ["\u03B6", "Zeta"],
3091
+ ["\u03B7", "Eta"],
3092
+ ["\u03B8", "Theta"],
3093
+ ["\u03B9", "Iota"],
3094
+ ["\u03BA", "Kappa"],
3095
+ ["\u03BB", "Lambda"],
3096
+ ["\u03BC", "Mu"],
3097
+ ["\u03BD", "Nu"],
3098
+ ["\u03BE", "Xi"],
3099
+ ["\u03BF", "Omicron"],
3100
+ ["\u03C0", "Pi"],
3101
+ ["\u03C1", "Rho"],
3102
+ ["\u03C3", "Sigma"],
3103
+ ["\u03C2", "Final sigma"],
3104
+ ["\u03C4", "Tau"],
3105
+ ["\u03C5", "Upsilon"],
3106
+ ["\u03C6", "Phi"],
3107
+ ["\u03C7", "Chi"],
3108
+ ["\u03C8", "Psi"],
3109
+ ["\u03C9", "Omega"],
3110
+ ["\u0391", "Capital alpha"],
3111
+ ["\u0392", "Capital beta"],
3112
+ ["\u0393", "Capital gamma"],
3113
+ ["\u0394", "Capital delta"],
3114
+ ["\u0395", "Capital epsilon"],
3115
+ ["\u0396", "Capital zeta"],
3116
+ ["\u0397", "Capital eta"],
3117
+ ["\u0398", "Capital theta"],
3118
+ ["\u0399", "Capital iota"],
3119
+ ["\u039A", "Capital kappa"],
3120
+ ["\u039B", "Capital lambda"],
3121
+ ["\u039C", "Capital mu"],
3122
+ ["\u039D", "Capital nu"],
3123
+ ["\u039E", "Capital xi"],
3124
+ ["\u039F", "Capital omicron"],
3125
+ ["\u03A0", "Capital pi"],
3126
+ ["\u03A1", "Capital rho"],
3127
+ ["\u03A3", "Capital sigma"],
3128
+ ["\u03A4", "Capital tau"],
3129
+ ["\u03A5", "Capital upsilon"],
3130
+ ["\u03A6", "Capital phi"],
3131
+ ["\u03A7", "Capital chi"],
3132
+ ["\u03A8", "Capital psi"],
3133
+ ["\u03A9", "Capital omega"]
3134
+ ]),
3135
+ g("latin", "Latin (Vietnamese & accents)", "Latin", [
3136
+ ["\u0110", "Capital D with stroke"],
3137
+ ["\u0111", "Small d with stroke"],
3138
+ ["\u0102", "Capital A with breve"],
3139
+ ["\u0103", "Small a with breve"],
3140
+ ["\xC2", "Capital A with circumflex"],
3141
+ ["\xE2", "Small a with circumflex"],
3142
+ ["\xCA", "Capital E with circumflex"],
3143
+ ["\xEA", "Small e with circumflex"],
3144
+ ["\xD4", "Capital O with circumflex"],
3145
+ ["\xF4", "Small o with circumflex"],
3146
+ ["\u01A0", "Capital O with horn"],
3147
+ ["\u01A1", "Small o with horn"],
3148
+ ["\u01AF", "Capital U with horn"],
3149
+ ["\u01B0", "Small u with horn"],
3150
+ ["\xC0", "Capital A with grave"],
3151
+ ["\xE0", "Small a with grave"],
3152
+ ["\xC1", "Capital A with acute"],
3153
+ ["\xE1", "Small a with acute"],
3154
+ ["\xC3", "Capital A with tilde"],
3155
+ ["\xE3", "Small a with tilde"],
3156
+ ["\u1EA2", "Capital A with hook above"],
3157
+ ["\u1EA3", "Small a with hook above"],
3158
+ ["\u1EA0", "Capital A with dot below"],
3159
+ ["\u1EA1", "Small a with dot below"],
3160
+ ["\xC8", "Capital E with grave"],
3161
+ ["\xE9", "Small e with acute"],
3162
+ ["\xCC", "Capital I with grave"],
3163
+ ["\xED", "Small i with acute"],
3164
+ ["\xD2", "Capital O with grave"],
3165
+ ["\xF3", "Small o with acute"],
3166
+ ["\xD9", "Capital U with grave"],
3167
+ ["\xFA", "Small u with acute"],
3168
+ ["\xDD", "Capital Y with acute"],
3169
+ ["\u1EF9", "Small y with tilde"],
3170
+ ["\xC7", "Capital C with cedilla"],
3171
+ ["\xE7", "Small c with cedilla"],
3172
+ ["\xD1", "Capital N with tilde"],
3173
+ ["\xF1", "Small n with tilde"],
3174
+ ["\xD6", "Capital O with diaeresis"],
3175
+ ["\xF6", "Small o with diaeresis"],
3176
+ ["\xDC", "Capital U with diaeresis"],
3177
+ ["\xFC", "Small u with diaeresis"],
3178
+ ["\xDF", "Small sharp s"],
3179
+ ["\xC6", "Capital AE"],
3180
+ ["\xE6", "Small ae"],
3181
+ ["\xD8", "Capital O with stroke"],
3182
+ ["\xF8", "Small o with stroke"],
3183
+ ["\u0152", "Capital ligature OE"],
3184
+ ["\u0153", "Small ligature oe"]
3185
+ ]),
3186
+ g("misc", "Misc", "Misc", [
3187
+ ["\u260E", "Black telephone"],
3188
+ ["\u2706", "Telephone location sign"],
3189
+ ["\u2709", "Envelope"],
3190
+ ["\u2702", "Black scissors"],
3191
+ ["\u270E", "Lower right pencil"],
3192
+ ["\u270F", "Pencil"],
3193
+ ["\u270D", "Writing hand"],
3194
+ ["\u263A", "White smiling face"],
3195
+ ["\u2639", "White frowning face"],
3196
+ ["\u2660", "Black spade suit"],
3197
+ ["\u2663", "Black club suit"],
3198
+ ["\u2665", "Black heart suit"],
3199
+ ["\u2666", "Black diamond suit"],
3200
+ ["\u266A", "Eighth note"],
3201
+ ["\u266B", "Beamed eighth notes"],
3202
+ ["\u2600", "Black sun with rays"],
3203
+ ["\u2601", "Cloud"],
3204
+ ["\u2602", "Umbrella"],
3205
+ ["\u2691", "Black flag"],
3206
+ ["\u2690", "White flag"],
3207
+ ["\u26A0", "Warning sign"],
3208
+ ["\u26D4", "No entry"],
3209
+ ["\u2640", "Female sign"],
3210
+ ["\u2642", "Male sign"],
3211
+ ["\u2318", "Place of interest sign"],
3212
+ ["\u2325", "Option key"],
3213
+ ["\u21E7", "Upwards white arrow"],
3214
+ ["\u23CE", "Return symbol"],
3215
+ ["\u232B", "Erase to the left"],
3216
+ ["\u23CF", "Eject symbol"],
3217
+ ["\u267B", "Black universal recycling symbol"],
3218
+ ["\u2708", "Airplane"],
3219
+ ["\u2696", "Scales"],
3220
+ ["\u2695", "Staff of aesculapius"]
3221
+ ])
3222
+ ];
3223
+ function autoCorrectHint(char) {
3224
+ return AUTOCORRECT_RULES.find((r) => r.to === char)?.seq ?? "";
3225
+ }
3226
+ var SPECIAL_CHARACTERS = [
3227
+ { char: "\u2014", name: "Em dash", hint: "" },
3228
+ { char: "\u2013", name: "En dash", hint: "" },
3229
+ { char: "\u2011", name: "Nonbreaking hyphen", hint: "" },
3230
+ { char: "\xAD", name: "Optional hyphen", hint: "" },
3231
+ { char: "\xA0", name: "Nonbreaking space", hint: "" },
3232
+ { char: "\u2003", name: "Em space", hint: "" },
3233
+ { char: "\u2002", name: "En space", hint: "" },
3234
+ { char: "\u2009", name: "Thin space", hint: "" },
3235
+ { char: "\xA9", name: "Copyright", hint: autoCorrectHint("\xA9") },
3236
+ { char: "\xAE", name: "Registered", hint: autoCorrectHint("\xAE") },
3237
+ { char: "\u2122", name: "Trademark", hint: autoCorrectHint("\u2122") },
3238
+ { char: "\xA7", name: "Section", hint: "" },
3239
+ { char: "\xB6", name: "Paragraph", hint: "" },
3240
+ { char: "\u2026", name: "Ellipsis", hint: autoCorrectHint("\u2026") },
3241
+ { char: "\u2018", name: "Single opening quote", hint: "" },
3242
+ { char: "\u2019", name: "Single closing quote", hint: "" },
3243
+ { char: "\u201C", name: "Double opening quote", hint: "" },
3244
+ { char: "\u201D", name: "Double closing quote", hint: "" },
3245
+ { char: "\u20AB", name: "Dong sign", hint: "" },
3246
+ { char: "\u2116", name: "Numero sign", hint: "" }
3247
+ ];
3248
+ var SYMBOL_NAMES = (() => {
3249
+ const m = /* @__PURE__ */ new Map();
3250
+ for (const grp of SYMBOL_GROUPS)
3251
+ for (const e of grp.entries) if (!m.has(e.char)) m.set(e.char, e.name);
3252
+ for (const e of SPECIAL_CHARACTERS) if (!m.has(e.char)) m.set(e.char, e.name);
3253
+ return m;
3254
+ })();
3255
+ function codePointLabel(char) {
3256
+ const cp = char.codePointAt(0) ?? 0;
3257
+ return `U+${cp.toString(16).toUpperCase().padStart(4, "0")}`;
3258
+ }
3259
+ function parseCodePoint(input) {
3260
+ const m = /^\s*(?:u\+|0x)?([0-9a-f]{1,6})\s*$/i.exec(input);
3261
+ if (!m) return null;
3262
+ const cp = parseInt(m[1], 16);
3263
+ if (cp > 1114111 || cp >= 55296 && cp <= 57343) return null;
3264
+ if (cp < 32 || cp >= 127 && cp < 160) return null;
3265
+ return String.fromCodePoint(cp);
3266
+ }
3267
+ var RECENT_MAX = 16;
3268
+ function pushRecent(recent, char, max = RECENT_MAX) {
3269
+ return [char, ...recent.filter((c) => c !== char)].slice(0, max);
3270
+ }
3271
+ function searchSymbols(query) {
3272
+ const q = query.trim().toLowerCase();
3273
+ if (!q) return [];
3274
+ const seen = /* @__PURE__ */ new Set();
3275
+ const ranked = [];
3276
+ for (const grp of SYMBOL_GROUPS) {
3277
+ const groupHit = grp.label.toLowerCase().includes(q);
3278
+ for (const e of grp.entries) {
3279
+ const name = e.name.toLowerCase();
3280
+ if (seen.has(e.char) || !name.includes(q)) continue;
3281
+ seen.add(e.char);
3282
+ ranked.push({ e, rank: groupHit ? 0 : name.startsWith(q) ? 1 : 2 });
3283
+ }
3284
+ }
3285
+ return ranked.sort((a, b) => a.rank - b.rank).map((r) => r.e);
3286
+ }
3287
+
3288
+ // packages/ui/src/lib/symbol-dialog.ts
3289
+ var STYLE13 = `
3290
+ .bb-sd{display:flex;flex-direction:column;gap:12px;width:412px;max-width:100%;color:var(--bb-ui-fg,#2c2c2a)}
3291
+ .bb-sd *{box-sizing:border-box}
3292
+ .bb-sd-tabs{display:inline-flex;border:0.5px solid var(--bb-ui-control-border,var(--bb-ui-border,#d8d6cf));border-radius:6px;overflow:hidden;align-self:flex-start}
3293
+ .bb-sd-tab{height:30px;padding:0 18px;border:0;border-right:0.5px solid var(--bb-ui-border,#e3e3e0);background:transparent;color:inherit;opacity:.65;font:inherit;font-size:13px;cursor:pointer}
3294
+ .bb-sd-tab:last-child{border-right:0}
3295
+ .bb-sd-tab[aria-selected="true"]{background:var(--bb-ui-active-bg,#e6f1fb);color:var(--bb-ui-active-fg,#0c447c);opacity:1}
3296
+ .bb-sd-pane{display:flex;flex-direction:column;gap:12px}
3297
+ .bb-sd-pane[hidden]{display:none}
3298
+ .bb-sd-lbl{font-size:12px;opacity:.7;margin-bottom:5px}
3299
+ .bb-sd-row{display:flex;gap:10px;align-items:flex-end}
3300
+ .bb-sd-ctl::placeholder{color:inherit;opacity:.45}
3301
+ .bb-sd-ctl{width:100%;height:32px;border:0.5px solid var(--bb-ui-control-border,var(--bb-ui-border,#d8d6cf));border-radius:6px;background:var(--bb-ui-control-bg,var(--bb-ui-bg,#fff));color:inherit;font:inherit;font-size:13px;padding:0 8px}
3302
+ .bb-sd-grid{display:grid;grid-template-columns:repeat(12,1fr);gap:2px;border:0.5px solid var(--bb-ui-control-border,var(--bb-ui-border,#d8d6cf));border-radius:8px;padding:6px;background:var(--bb-ui-control-bg,var(--bb-ui-bg,#fff));max-height:196px;overflow:auto}
3303
+ .bb-sd-grid.recent{grid-template-columns:repeat(16,1fr);max-height:none;border-color:transparent;padding:0;background:transparent;min-height:26px}
3304
+ .bb-sd-cell{aspect-ratio:1;display:grid;place-items:center;border:0;border-radius:5px;background:transparent;color:inherit;font-size:18px;line-height:1;cursor:pointer;padding:0;font-family:inherit,"Noto Sans Symbols 2"}
3305
+ .bb-sd-grid.recent .bb-sd-cell{border:0.5px solid var(--bb-ui-control-border,var(--bb-ui-border,#d8d6cf));background:var(--bb-ui-control-bg,var(--bb-ui-bg,#fff));font-size:15px}
3306
+ .bb-sd-cell:hover{background:var(--bb-ui-hover,#f1efe8)}
3307
+ .bb-sd-cell:focus{outline:none}
3308
+ .bb-sd-cell[aria-selected="true"]{background:var(--bb-ui-active-bg,#e6f1fb);box-shadow:inset 0 0 0 1.5px var(--bb-ui-active-border,#7fb2ec);color:var(--bb-ui-active-fg,#0c447c)}
3309
+ .bb-sd-empty{grid-column:1/-1;font-size:12px;opacity:.55;padding:6px 2px}
3310
+ .bb-sd-meta{display:grid;grid-template-columns:minmax(0,1fr) 140px;gap:10px;align-items:end}
3311
+ .bb-sd-info{min-width:0;border:0.5px solid var(--bb-ui-control-border,var(--bb-ui-border,#d8d6cf));border-radius:8px;padding:8px 10px;display:flex;gap:12px;align-items:center;min-height:56px}
3312
+ .bb-sd-big{font-size:30px;line-height:1;width:44px;text-align:center;font-family:inherit,"Noto Sans Symbols 2"}
3313
+ .bb-sd-name{font-size:13px}
3314
+ .bb-sd-cp{font-size:11px;opacity:.55;font-family:ui-monospace,Menlo,Consolas,monospace}
3315
+ .bb-sd-code{font-family:ui-monospace,Menlo,Consolas,monospace}
3316
+ .bb-sd-code[aria-invalid="true"]{border-color:#c0392b}
3317
+ .bb-sd-list{border:0.5px solid var(--bb-ui-control-border,var(--bb-ui-border,#d8d6cf));border-radius:8px;max-height:280px;overflow:auto}
3318
+ .bb-sd-list table{width:100%;border-collapse:collapse;font-size:13px}
3319
+ .bb-sd-list td{padding:6px 8px;border-bottom:0.5px solid var(--bb-ui-border,#e3e3e0)}
3320
+ .bb-sd-list tr:last-child td{border-bottom:0}
3321
+ .bb-sd-list tr{cursor:pointer}
3322
+ .bb-sd-list tr:hover td{background:var(--bb-ui-hover,#f1efe8)}
3323
+ .bb-sd-list tr[aria-selected="true"] td{background:var(--bb-ui-active-bg,#e6f1fb);color:var(--bb-ui-active-fg,#0c447c)}
3324
+ .bb-sd-list td.ch{width:36px;text-align:center;font-size:16px;font-family:inherit,"Noto Sans Symbols 2"}
3325
+ .bb-sd-list td.k{width:120px;font-size:11px;opacity:.6;font-family:ui-monospace,Menlo,Consolas,monospace;text-align:right}
3326
+ .bb-sd-foot{display:flex;justify-content:space-between;align-items:center;gap:8px}
3327
+ .bb-sd-hint{font-size:11px;opacity:.55}
3328
+ .bb-sd-btns{display:flex;gap:8px}
3329
+ .bb-sd-btn{height:31px;padding:0 17px;border:0.5px solid var(--bb-ui-control-border,var(--bb-ui-border,#d8d6cf));border-radius:6px;background:var(--bb-ui-control-bg,var(--bb-ui-bg,#fff));color:inherit;font:inherit;font-size:13px;cursor:pointer}
3330
+ .bb-sd-btn.primary{background:var(--bb-ui-active-bg,#e6f1fb);border-color:var(--bb-ui-active-border,#7fb2ec);color:var(--bb-ui-active-fg,#0c447c)}
3331
+ .bb-sd-btn:disabled{opacity:.5;cursor:default}
3332
+ `;
3333
+ var GRID_COLS = 12;
3334
+ var RECENT_COLS = 16;
3335
+ function el2(tag, cls, text) {
3336
+ const n = document.createElement(tag);
3337
+ if (cls) n.className = cls;
3338
+ if (text !== void 0) n.textContent = text;
3339
+ return n;
3340
+ }
3341
+ function glyphFor(char) {
3342
+ const cp = char.codePointAt(0) ?? 0;
3343
+ if (char === " " || char === "\xA0") return "\u2423";
3344
+ if (cp === 8194 || cp === 8195 || cp === 8201) return "\u2423";
3345
+ if (cp === 173) return "-";
3346
+ if (cp === 8209) return "\u2011";
3347
+ return char;
3348
+ }
3349
+ function createSymbolDialog(options) {
3350
+ injectStyle("bb-symbol-dialog", STYLE13);
3351
+ let recent = [...options.recent ?? []].slice(0, RECENT_MAX);
3352
+ let selected = null;
3353
+ const root = el2("div", "bb-sd");
3354
+ const tabs = el2("div", "bb-sd-tabs");
3355
+ tabs.setAttribute("role", "tablist");
3356
+ const symTab = el2("button", "bb-sd-tab", "Symbols");
3357
+ const spcTab = el2("button", "bb-sd-tab", "Special characters");
3358
+ for (const t of [symTab, spcTab]) {
3359
+ t.type = "button";
3360
+ t.setAttribute("role", "tab");
3361
+ tabs.append(t);
3362
+ }
3363
+ const symPane = el2("div", "bb-sd-pane");
3364
+ const spcPane = el2("div", "bb-sd-pane");
3365
+ const showTab = (special) => {
3366
+ symTab.setAttribute("aria-selected", String(!special));
3367
+ spcTab.setAttribute("aria-selected", String(special));
3368
+ symPane.hidden = special;
3369
+ spcPane.hidden = !special;
3370
+ };
3371
+ symTab.addEventListener("click", () => showTab(false));
3372
+ spcTab.addEventListener("click", () => showTab(true));
3373
+ const recentWrap = el2("div");
3374
+ recentWrap.append(el2("div", "bb-sd-lbl", "Recently used"));
3375
+ const recentGrid = el2("div", "bb-sd-grid recent");
3376
+ recentGrid.setAttribute("role", "grid");
3377
+ recentGrid.setAttribute("aria-label", "Recently used symbols");
3378
+ recentWrap.append(recentGrid);
3379
+ const controls = el2("div", "bb-sd-row");
3380
+ const groupCell = el2("div");
3381
+ groupCell.style.flex = "1";
3382
+ groupCell.append(el2("div", "bb-sd-lbl", "Group"));
3383
+ const groupSel = el2("select", "bb-sd-ctl");
3384
+ groupSel.setAttribute("aria-label", "Group");
3385
+ for (const grp of SYMBOL_GROUPS) {
3386
+ const o = el2("option", void 0, grp.label);
3387
+ o.value = grp.id;
3388
+ groupSel.append(o);
3389
+ }
3390
+ groupCell.append(groupSel);
3391
+ const searchCell = el2("div");
3392
+ searchCell.style.width = "160px";
3393
+ searchCell.append(el2("div", "bb-sd-lbl", "Search name"));
3394
+ const search = el2("input", "bb-sd-ctl");
3395
+ search.type = "search";
3396
+ search.placeholder = "e.g. ballot, arrow";
3397
+ search.setAttribute("aria-label", "Search symbols by name");
3398
+ searchCell.append(search);
3399
+ controls.append(groupCell, searchCell);
3400
+ const grid = el2("div", "bb-sd-grid");
3401
+ grid.setAttribute("role", "grid");
3402
+ grid.setAttribute("aria-label", "Symbols");
3403
+ const meta = el2("div", "bb-sd-meta");
3404
+ const info = el2("div", "bb-sd-info");
3405
+ const big = el2("div", "bb-sd-big", "");
3406
+ const infoText = el2("div");
3407
+ const nameEl = el2("div", "bb-sd-name", "Select a symbol");
3408
+ const cpEl = el2("div", "bb-sd-cp", "");
3409
+ infoText.append(nameEl, cpEl);
3410
+ info.append(big, infoText);
3411
+ const codeCell = el2("div");
3412
+ codeCell.append(el2("div", "bb-sd-lbl", "Character code (hex)"));
3413
+ const code = el2("input", "bb-sd-ctl bb-sd-code");
3414
+ code.placeholder = "e.g. 2611";
3415
+ code.setAttribute("aria-label", "Character code, hexadecimal");
3416
+ code.spellcheck = false;
3417
+ codeCell.append(code);
3418
+ meta.append(info, codeCell);
3419
+ symPane.append(recentWrap, controls, grid, meta);
3420
+ const list = el2("div", "bb-sd-list");
3421
+ const table = el2("table");
3422
+ const tbody = el2("tbody");
3423
+ const rows = [];
3424
+ for (const sc of SPECIAL_CHARACTERS) {
3425
+ const tr = el2("tr");
3426
+ tr.tabIndex = -1;
3427
+ tr.setAttribute("role", "option");
3428
+ const ch = el2("td", "ch", glyphFor(sc.char));
3429
+ const nm = el2("td", void 0, sc.name);
3430
+ const k = el2("td", "k", sc.hint);
3431
+ tr.append(ch, nm, k);
3432
+ tr.addEventListener("click", () => select2(sc.char, tr));
3433
+ tr.addEventListener("dblclick", () => insert(sc.char));
3434
+ tr.addEventListener("keydown", (e) => {
3435
+ const i = rows.indexOf(tr);
3436
+ if (e.key === "ArrowDown" && rows[i + 1]) {
3437
+ e.preventDefault();
3438
+ rows[i + 1].focus();
3439
+ select2(SPECIAL_CHARACTERS[i + 1].char, rows[i + 1]);
3440
+ } else if (e.key === "ArrowUp" && rows[i - 1]) {
3441
+ e.preventDefault();
3442
+ rows[i - 1].focus();
3443
+ select2(SPECIAL_CHARACTERS[i - 1].char, rows[i - 1]);
3444
+ } else if (e.key === "Enter") {
3445
+ e.preventDefault();
3446
+ insert(sc.char);
3447
+ }
3448
+ });
3449
+ tbody.append(tr);
3450
+ rows.push(tr);
3451
+ }
3452
+ table.append(tbody);
3453
+ list.append(table);
3454
+ list.setAttribute("role", "listbox");
3455
+ spcPane.append(list);
3456
+ const foot = el2("div", "bb-sd-foot");
3457
+ const hint = el2(
3458
+ "span",
3459
+ "bb-sd-hint",
3460
+ "Double-click a cell to insert \xB7 Enter inserts the selection"
3461
+ );
3462
+ const btns = el2("div", "bb-sd-btns");
3463
+ const closeBtn = el2("button", "bb-sd-btn", "Close");
3464
+ closeBtn.type = "button";
3465
+ const insertBtn = el2("button", "bb-sd-btn primary", "Insert");
3466
+ insertBtn.type = "button";
3467
+ insertBtn.disabled = true;
3468
+ btns.append(closeBtn, insertBtn);
3469
+ foot.append(hint, btns);
3470
+ root.append(tabs, symPane, spcPane, foot);
3471
+ const dialog = new Dialog({
3472
+ title: "Symbol",
3473
+ modal: false,
3474
+ anchor: options.anchor,
3475
+ className: "bb-symbol-dialog"
3476
+ });
3477
+ dialog.setContent(root);
3478
+ let selectedEl = null;
3479
+ function select2(char, cell) {
3480
+ selected = char;
3481
+ if (selectedEl) selectedEl.removeAttribute("aria-selected");
3482
+ selectedEl = cell ?? null;
3483
+ if (selectedEl) selectedEl.setAttribute("aria-selected", "true");
3484
+ big.textContent = glyphFor(char);
3485
+ nameEl.textContent = SYMBOL_NAMES.get(char) ?? "Character";
3486
+ cpEl.textContent = codePointLabel(char);
3487
+ if (document.activeElement !== code)
3488
+ code.value = codePointLabel(char).slice(2);
3489
+ code.removeAttribute("aria-invalid");
3490
+ insertBtn.disabled = false;
3491
+ }
3492
+ function insert(char) {
3493
+ const active = document.activeElement;
3494
+ options.onInsert(char);
3495
+ recent = pushRecent(recent, char);
3496
+ options.onRecentChange?.([...recent]);
3497
+ renderRecent();
3498
+ if (active instanceof HTMLElement && root.contains(active))
3499
+ queueMicrotask(() => {
3500
+ if (active.isConnected) active.focus();
3501
+ else search.focus();
3502
+ });
3503
+ }
3504
+ function cellFor(e) {
3505
+ const b = el2("button", "bb-sd-cell", glyphFor(e.char));
3506
+ b.type = "button";
3507
+ b.title = `${e.name} \xB7 ${codePointLabel(e.char)}`;
3508
+ b.setAttribute("aria-label", e.name);
3509
+ b.addEventListener("click", () => select2(e.char, b));
3510
+ b.addEventListener("dblclick", () => insert(e.char));
3511
+ b.addEventListener("keydown", (ev) => {
3512
+ if (ev.key === "Enter" || ev.key === " ") {
3513
+ ev.preventDefault();
3514
+ select2(e.char, b);
3515
+ insert(e.char);
3516
+ }
3517
+ });
3518
+ b.addEventListener("focus", () => select2(e.char, b));
3519
+ return b;
3520
+ }
3521
+ function fillGrid(target, entries, cols, empty) {
3522
+ target.replaceChildren();
3523
+ if (entries.length === 0) {
3524
+ target.append(el2("div", "bb-sd-empty", empty));
3525
+ return;
3526
+ }
3527
+ const cells = [];
3528
+ entries.forEach((e, i) => {
3529
+ const b = cellFor(e);
3530
+ target.append(b);
3531
+ if (i % cols === 0) cells.push([]);
3532
+ cells[cells.length - 1].push(b);
3533
+ });
3534
+ rovingGrid(cells);
3535
+ }
3536
+ function renderRecent() {
3537
+ fillGrid(
3538
+ recentGrid,
3539
+ recent.map((c) => ({
3540
+ char: c,
3541
+ name: SYMBOL_NAMES.get(c) ?? "Character"
3542
+ })),
3543
+ RECENT_COLS,
3544
+ "Nothing yet \u2014 inserted symbols appear here."
3545
+ );
3546
+ }
3547
+ function renderGroup() {
3548
+ const q = search.value.trim();
3549
+ const entries = q ? searchSymbols(q) : SYMBOL_GROUPS.find((g2) => g2.id === groupSel.value)?.entries ?? [];
3550
+ fillGrid(grid, entries, GRID_COLS, "No symbol matches that name.");
3551
+ const first = grid.querySelector(".bb-sd-cell");
3552
+ if (q && first && entries[0]) select2(entries[0].char, first);
3553
+ else clearSelection();
3554
+ }
3555
+ function clearSelection() {
3556
+ selected = null;
3557
+ selectedEl?.removeAttribute("aria-selected");
3558
+ selectedEl = null;
3559
+ big.textContent = "";
3560
+ nameEl.textContent = "Select a symbol";
3561
+ cpEl.textContent = "";
3562
+ if (document.activeElement !== code) code.value = "";
3563
+ insertBtn.disabled = true;
3564
+ }
3565
+ groupSel.addEventListener("change", () => {
3566
+ search.value = "";
3567
+ renderGroup();
3568
+ });
3569
+ search.addEventListener("input", renderGroup);
3570
+ search.addEventListener("keydown", (e) => {
3571
+ if (e.key === "Enter" && selected) {
3572
+ e.preventDefault();
3573
+ insert(selected);
3574
+ } else if (e.key === "ArrowDown") {
3575
+ e.preventDefault();
3576
+ grid.querySelector(".bb-sd-cell")?.focus();
3577
+ }
3578
+ });
3579
+ code.addEventListener("input", () => {
3580
+ const ch = parseCodePoint(code.value);
3581
+ if (ch) {
3582
+ code.removeAttribute("aria-invalid");
3583
+ const cell = Array.from(
3584
+ grid.querySelectorAll(".bb-sd-cell")
3585
+ ).find((c) => c.textContent === glyphFor(ch));
3586
+ select2(ch, cell ?? null);
3587
+ } else {
3588
+ code.setAttribute("aria-invalid", code.value.trim() ? "true" : "false");
3589
+ insertBtn.disabled = true;
3590
+ selected = null;
3591
+ }
3592
+ });
3593
+ code.addEventListener("keydown", (e) => {
3594
+ if (e.key === "Enter" && selected) {
3595
+ e.preventDefault();
3596
+ insert(selected);
3597
+ }
3598
+ });
3599
+ insertBtn.addEventListener("click", () => {
3600
+ if (selected) insert(selected);
3601
+ });
3602
+ closeBtn.addEventListener("click", () => dialog.close());
3603
+ showTab(false);
3604
+ renderRecent();
3605
+ renderGroup();
3606
+ return {
3607
+ open() {
3608
+ dialog.open();
3609
+ showTab(false);
3610
+ queueMicrotask(() => {
3611
+ search.focus();
3612
+ search.select();
3613
+ });
3614
+ },
3615
+ close() {
3616
+ dialog.close();
3617
+ },
3618
+ destroy() {
3619
+ dialog.destroy();
3620
+ },
3621
+ get isOpen() {
3622
+ return dialog.isOpen;
3623
+ }
3624
+ };
3625
+ }
3626
+
3627
+ // packages/ui/src/lib/symbol-popover.ts
3628
+ var STYLE14 = `
3629
+ .bb-spk{position:fixed;margin:0;max-width:none;max-height:none;z-index:1300;width:276px;padding:10px;background:var(--bb-ui-menu-bg,#fff);-webkit-backdrop-filter:var(--bb-ui-pop-filter,none);backdrop-filter:var(--bb-ui-pop-filter,none);color:var(--bb-ui-fg,#2c2c2a);border:1px solid var(--bb-ui-pop-border,var(--bb-ui-border,#e3e3e0));border-radius:10px;box-shadow:0 8px 28px rgba(0,0,0,.16);font-family:var(--bb-ui-font,system-ui,-apple-system,sans-serif);font-size:12px;box-sizing:border-box;display:flex;flex-direction:column;gap:8px}
3630
+ .bb-spk *{box-sizing:border-box}
3631
+ .bb-spk::backdrop{background:transparent}
3632
+ .bb-spk-search{width:100%;height:28px;border:0.5px solid var(--bb-ui-control-border,var(--bb-ui-border,#d8d6cf));border-radius:6px;background:var(--bb-ui-control-bg,var(--bb-ui-bg,#fff));color:inherit;font:inherit;font-size:12px;padding:0 8px}
3633
+ .bb-spk-search::placeholder{color:inherit;opacity:.45}
3634
+ .bb-spk-lbl{font-size:10.5px;opacity:.65;margin:0 0 4px}
3635
+ .bb-spk-recent{display:grid;grid-template-columns:repeat(8,1fr);gap:3px;min-height:26px}
3636
+ .bb-spk-recent .bb-spk-cell{border:0.5px solid var(--bb-ui-control-border,var(--bb-ui-border,#d8d6cf));background:var(--bb-ui-control-bg,var(--bb-ui-bg,#fff));font-size:14px}
3637
+ .bb-spk-empty{grid-column:1/-1;font-size:11px;opacity:.5;padding:4px 2px}
3638
+ .bb-spk-chips{display:flex;flex-wrap:wrap;gap:3px}
3639
+ .bb-spk-chip{padding:2px 6px;border-radius:10px;border:0.5px solid var(--bb-ui-control-border,var(--bb-ui-border,#d8d6cf));background:var(--bb-ui-control-bg,var(--bb-ui-bg,#fff));color:inherit;font:inherit;font-size:10px;opacity:.8;cursor:pointer}
3640
+ .bb-spk-chip:hover{background:var(--bb-ui-hover,#f1efe8)}
3641
+ .bb-spk-chip[aria-pressed="true"]{background:var(--bb-ui-active-bg,#e6f1fb);color:var(--bb-ui-active-fg,#0c447c);border-color:var(--bb-ui-active-border,#7fb2ec);opacity:1}
3642
+ .bb-spk-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:2px;border:0.5px solid var(--bb-ui-control-border,var(--bb-ui-border,#d8d6cf));border-radius:7px;padding:4px;background:var(--bb-ui-control-bg,var(--bb-ui-bg,#fff));max-height:150px;overflow:auto}
3643
+ .bb-spk-cell{aspect-ratio:1;display:grid;place-items:center;border:0;border-radius:5px;background:transparent;color:inherit;font-size:16px;line-height:1;padding:0;cursor:pointer;font-family:inherit,"Noto Sans Symbols 2"}
3644
+ .bb-spk-cell:hover{background:var(--bb-ui-hover,#f1efe8)}
3645
+ .bb-spk-cell:focus{outline:none;box-shadow:inset 0 0 0 1.5px var(--bb-ui-active-border,#7fb2ec)}
3646
+ .bb-spk-cell[aria-selected="true"]{background:var(--bb-ui-active-bg,#e6f1fb);color:var(--bb-ui-active-fg,#0c447c)}
3647
+ .bb-spk-foot{display:flex;justify-content:space-between;align-items:center;gap:8px;font-size:11px}
3648
+ .bb-spk-name{opacity:.75;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3649
+ .bb-spk-more{border:0;background:transparent;color:var(--bb-ui-active-fg,#0c447c);font:inherit;font-size:11px;cursor:pointer;padding:2px 4px;border-radius:4px;white-space:nowrap}
3650
+ .bb-spk-more:hover{background:var(--bb-ui-hover,#f1efe8)}
3651
+ `;
3652
+ var COLS = 8;
3653
+ var RECENT_ROW = COLS;
3654
+ function el3(tag, cls, text) {
3655
+ const n = document.createElement(tag);
3656
+ if (cls) n.className = cls;
3657
+ if (text !== void 0) n.textContent = text;
3658
+ return n;
3659
+ }
3660
+ function openSymbolPopover(options) {
3661
+ injectStyle("bb-ui-symbol-popover", STYLE14);
3662
+ const { anchor } = options;
3663
+ let recent = [...options.recent ?? []].slice(0, RECENT_MAX);
3664
+ let groupId = SYMBOL_GROUPS[0].id;
3665
+ let selected = null;
3666
+ const panel = document.createElement("dialog");
3667
+ panel.className = "bb-spk";
3668
+ panel.setAttribute("aria-label", "Insert symbol");
3669
+ let closed = false;
3670
+ const close = () => {
3671
+ if (closed) return;
3672
+ closed = true;
3673
+ window.removeEventListener("scroll", close, true);
3674
+ if (panel.open) panel.close();
3675
+ panel.remove();
3676
+ options.onClose?.();
3677
+ };
3678
+ const search = el3("input", "bb-spk-search");
3679
+ search.type = "search";
3680
+ search.placeholder = "Search name or code (U+2611)";
3681
+ search.setAttribute("aria-label", "Search symbols by name or code");
3682
+ search.spellcheck = false;
3683
+ const recentWrap = el3("div");
3684
+ recentWrap.append(el3("div", "bb-spk-lbl", "Recent"));
3685
+ const recentGrid = el3("div", "bb-spk-recent");
3686
+ recentGrid.setAttribute("role", "grid");
3687
+ recentGrid.setAttribute("aria-label", "Recently used symbols");
3688
+ recentWrap.append(recentGrid);
3689
+ const chips = el3("div", "bb-spk-chips");
3690
+ const chipEls = /* @__PURE__ */ new Map();
3691
+ for (const grp of SYMBOL_GROUPS) {
3692
+ const c = el3("button", "bb-spk-chip", grp.short);
3693
+ c.type = "button";
3694
+ c.title = grp.label;
3695
+ c.addEventListener("click", () => {
3696
+ groupId = grp.id;
3697
+ search.value = "";
3698
+ render();
3699
+ });
3700
+ chips.append(c);
3701
+ chipEls.set(grp.id, c);
3702
+ }
3703
+ const grid = el3("div", "bb-spk-grid");
3704
+ grid.setAttribute("role", "grid");
3705
+ grid.setAttribute("aria-label", "Symbols");
3706
+ const foot = el3("div", "bb-spk-foot");
3707
+ const nameEl = el3("span", "bb-spk-name", "");
3708
+ const more = el3("button", "bb-spk-more", "More symbols\u2026");
3709
+ more.type = "button";
3710
+ more.addEventListener("click", () => {
3711
+ close();
3712
+ options.onMore?.();
3713
+ });
3714
+ foot.append(nameEl, more);
3715
+ if (!options.onMore) more.hidden = true;
3716
+ panel.append(search, recentWrap, chips, grid, foot);
3717
+ let selectedEl = null;
3718
+ const select2 = (char, cell) => {
3719
+ selected = char;
3720
+ selectedEl?.removeAttribute("aria-selected");
3721
+ selectedEl = cell;
3722
+ selectedEl?.setAttribute("aria-selected", "true");
3723
+ nameEl.textContent = `${SYMBOL_NAMES.get(char) ?? "Character"} \xB7 ${codePointLabel(char)}`;
3724
+ };
3725
+ const insert = (char) => {
3726
+ const active = document.activeElement;
3727
+ options.onInsert(char);
3728
+ recent = pushRecent(recent, char);
3729
+ options.onRecentChange?.([...recent]);
3730
+ renderRecent();
3731
+ queueMicrotask(() => {
3732
+ if (active instanceof HTMLElement && active.isConnected) active.focus();
3733
+ else search.focus();
3734
+ });
3735
+ };
3736
+ const cellFor = (e) => {
3737
+ const b = el3("button", "bb-spk-cell", e.char);
3738
+ b.type = "button";
3739
+ b.title = `${e.name} \xB7 ${codePointLabel(e.char)}`;
3740
+ b.setAttribute("aria-label", e.name);
3741
+ b.addEventListener("focus", () => select2(e.char, b));
3742
+ b.addEventListener("click", () => {
3743
+ select2(e.char, b);
3744
+ insert(e.char);
3745
+ });
3746
+ b.addEventListener("keydown", (ev) => {
3747
+ if (ev.key === "Enter" || ev.key === " ") {
3748
+ ev.preventDefault();
3749
+ select2(e.char, b);
3750
+ insert(e.char);
3751
+ }
3752
+ });
3753
+ return b;
3754
+ };
3755
+ const fill = (target, entries, empty) => {
3756
+ target.replaceChildren();
3757
+ if (entries.length === 0) {
3758
+ target.append(el3("div", "bb-spk-empty", empty));
3759
+ return;
3760
+ }
3761
+ const cells = [];
3762
+ entries.forEach((e, i) => {
3763
+ const b = cellFor(e);
3764
+ target.append(b);
3765
+ if (i % COLS === 0) cells.push([]);
3766
+ cells[cells.length - 1].push(b);
3767
+ });
3768
+ rovingGrid(cells);
3769
+ };
3770
+ const renderRecent = () => fill(
3771
+ recentGrid,
3772
+ recent.slice(0, RECENT_ROW).map((c) => ({
3773
+ char: c,
3774
+ name: SYMBOL_NAMES.get(c) ?? "Character"
3775
+ })),
3776
+ "Nothing yet."
3777
+ );
3778
+ const render = () => {
3779
+ const q = search.value.trim();
3780
+ const byCode = q ? parseCodePoint(q) : null;
3781
+ const entries = q ? byCode && !searchSymbols(q).length ? [{ char: byCode, name: SYMBOL_NAMES.get(byCode) ?? "Character" }] : searchSymbols(q) : SYMBOL_GROUPS.find((g2) => g2.id === groupId)?.entries ?? [];
3782
+ for (const [id, c] of chipEls)
3783
+ c.setAttribute("aria-pressed", String(!q && id === groupId));
3784
+ fill(grid, entries, "No match.");
3785
+ const first = grid.querySelector(".bb-spk-cell");
3786
+ if (q && first && entries[0]) select2(entries[0].char, first);
3787
+ else {
3788
+ selected = null;
3789
+ selectedEl?.removeAttribute("aria-selected");
3790
+ selectedEl = null;
3791
+ nameEl.textContent = "";
3792
+ }
3793
+ };
3794
+ search.addEventListener("input", render);
3795
+ search.addEventListener("keydown", (e) => {
3796
+ if (e.key === "Enter" && selected) {
3797
+ e.preventDefault();
3798
+ insert(selected);
3799
+ } else if (e.key === "ArrowDown") {
3800
+ e.preventDefault();
3801
+ grid.querySelector(".bb-spk-cell")?.focus();
3802
+ }
3803
+ });
3804
+ document.body.append(panel);
3805
+ panel.showModal();
3806
+ const r = anchor.getBoundingClientRect();
3807
+ placeFloating(panel, { x: r.left, y: r.top, height: r.height });
3808
+ panel.addEventListener("close", close);
3809
+ panel.addEventListener("pointerdown", (e) => {
3810
+ const b = panel.getBoundingClientRect();
3811
+ const outside = e.clientX < b.left || e.clientX > b.right || e.clientY < b.top || e.clientY > b.bottom;
3812
+ if (outside) close();
3813
+ });
3814
+ window.addEventListener("scroll", close, true);
3815
+ renderRecent();
3816
+ render();
3817
+ search.focus();
3818
+ return { close };
3819
+ }
3820
+
3821
+ // packages/ui/src/lib/shortcuts-dialog.ts
3822
+ import {
3823
+ filterKeybindingRows,
3824
+ formatKey,
3825
+ keybindingRows
3826
+ } from "@shadow-garden/bapbong-contracts";
3827
+ var STYLE15 = `
3828
+ .bb-ks{display:flex;flex-direction:column;gap:10px;width:600px;max-width:100%;color:var(--bb-ui-fg,#2c2c2a);font-size:12px}
3829
+ .bb-ks *{box-sizing:border-box}
3830
+ .bb-ks-top{display:flex;gap:8px;align-items:center}
3831
+ .bb-ks-search{flex:1;height:30px;border:0.5px solid var(--bb-ui-control-border,var(--bb-ui-border,#d8d6cf));border-radius:6px;background:var(--bb-ui-control-bg,var(--bb-ui-bg,#fff));color:inherit;font:inherit;font-size:12px;padding:0 8px}
3832
+ .bb-ks-search::placeholder{color:inherit;opacity:.45}
3833
+ .bb-ks-count{font-size:11px;opacity:.6;white-space:nowrap}
3834
+ /* No box around the list: a bordered, tinted scroller turns the scrollbar's
3835
+ gutter into a blank column at its edge. Flat on the dialog surface, the
3836
+ rail reads like the app's own (idle-hidden overlay on a plain ground). */
3837
+ .bb-ks-wrap{overflow:auto;max-height:min(60vh,420px);margin:0 -12px;padding:0 12px;scrollbar-gutter:stable}
3838
+ .bb-ks table{width:100%;border-collapse:collapse}
3839
+ .bb-ks th{position:sticky;top:0;z-index:1;text-align:left;font-size:11px;font-weight:600;padding:7px 10px;border-bottom:0.5px solid var(--bb-ui-control-border,var(--bb-ui-border,#d8d6cf));cursor:pointer;user-select:none;white-space:nowrap;background-color:var(--bb-ui-bg,#fff);background-image:linear-gradient(var(--bb-ui-dialog-bg,transparent),var(--bb-ui-dialog-bg,transparent))}
3840
+ /* Opaque surface UNDER the dialog's own tint: a sticky header on the glass
3841
+ alone let the rows scrolling beneath show through it. The dimming goes on
3842
+ the text, not the cell, for the same reason. */
3843
+ .bb-ks th span{opacity:.75}
3844
+ .bb-ks th[aria-sort]{color:var(--bb-ui-active-fg,#0c447c)}
3845
+ .bb-ks th[aria-sort] span{opacity:1}
3846
+ .bb-ks td{padding:7px 10px;border-bottom:0.5px solid var(--bb-ui-border,#e3e3e0);vertical-align:middle}
3847
+ .bb-ks tr:last-child td{border-bottom:0}
3848
+ .bb-ks tbody tr:hover td{background:var(--bb-ui-hover,#f1efe8)}
3849
+ .bb-ks tbody tr:hover td:first-child{border-radius:6px 0 0 6px}
3850
+ .bb-ks tbody tr:hover td:last-child{border-radius:0 6px 6px 0}
3851
+ .bb-ks td.cmd{width:42%}
3852
+ .bb-ks td.cmd small{display:block;font-size:10px;opacity:.5;font-family:ui-monospace,Menlo,Consolas,monospace}
3853
+ .bb-ks kbd{display:inline-block;min-width:20px;padding:1px 6px;margin-right:3px;border:0.5px solid var(--bb-ui-control-border,var(--bb-ui-border,#d8d6cf));border-bottom-width:1.5px;border-radius:5px;background:var(--bb-ui-bg,#fff);font:inherit;font-size:11px;text-align:center}
3854
+ .bb-ks .or{opacity:.5;margin:0 4px 0 1px;font-size:11px}
3855
+ .bb-ks td.when{font-size:11px;opacity:.7;width:24%}
3856
+ .bb-ks td.src{width:12%}
3857
+ .bb-ks-chip{display:inline-block;padding:1px 7px;border-radius:9px;font-size:10px;border:0.5px solid var(--bb-ui-control-border,var(--bb-ui-border,#d8d6cf));background:var(--bb-ui-control-bg,var(--bb-ui-bg,#fff));opacity:.85;white-space:nowrap}
3858
+ .bb-ks-chip.core{background:var(--bb-ui-active-bg,#e6f1fb);border-color:var(--bb-ui-active-border,#7fb2ec);color:var(--bb-ui-active-fg,#0c447c);opacity:1}
3859
+ .bb-ks-empty{padding:16px;text-align:center;font-size:12px;opacity:.55}
3860
+ .bb-ks-foot{display:flex;justify-content:space-between;font-size:11px;opacity:.6}
3861
+ .bb-ks mark{background:transparent;color:inherit;font-weight:600;text-decoration:underline;text-decoration-color:var(--bb-ui-active-border,#7fb2ec);text-underline-offset:2px}
3862
+ `;
3863
+ function el4(tag, cls, text) {
3864
+ const n = document.createElement(tag);
3865
+ if (cls) n.className = cls;
3866
+ if (text !== void 0) n.textContent = text;
3867
+ return n;
3868
+ }
3869
+ function highlight(text, q) {
3870
+ if (!q) return [text];
3871
+ const out = [];
3872
+ const lower = text.toLowerCase();
3873
+ let i = 0;
3874
+ for (; ; ) {
3875
+ const j = lower.indexOf(q, i);
3876
+ if (j < 0) break;
3877
+ if (j > i) out.push(text.slice(i, j));
3878
+ out.push(el4("mark", void 0, text.slice(j, j + q.length)));
3879
+ i = j + q.length;
3880
+ }
3881
+ if (i < text.length) out.push(text.slice(i));
3882
+ return out;
3883
+ }
3884
+ function openKeyboardShortcutsDialog(options) {
3885
+ injectStyle("bb-ui-shortcuts-dialog", STYLE15);
3886
+ const dialog = new Dialog({
3887
+ title: options.title ?? "Keyboard shortcuts",
3888
+ modal: true,
3889
+ wide: true
3890
+ });
3891
+ const root = el4("div", "bb-ks");
3892
+ const top = el4("div", "bb-ks-top");
3893
+ const search = el4("input", "bb-ks-search");
3894
+ search.type = "search";
3895
+ search.placeholder = "Search command, key, when or source\u2026";
3896
+ search.setAttribute("aria-label", "Search shortcuts");
3897
+ const count = el4("span", "bb-ks-count", "");
3898
+ top.append(search, count);
3899
+ const wrap = el4("div", "bb-ks-wrap");
3900
+ const table = el4("table");
3901
+ const thead = el4("thead");
3902
+ const headRow = el4("tr");
3903
+ const columns = [
3904
+ { key: "title", label: "Command" },
3905
+ { key: "keys", label: "Keybinding" },
3906
+ { key: "when", label: "When" },
3907
+ { key: "source", label: "Source" }
3908
+ ];
3909
+ let sortKey = "title";
3910
+ let sortAsc = true;
3911
+ const ths = /* @__PURE__ */ new Map();
3912
+ for (const c of columns) {
3913
+ const th = el4("th");
3914
+ th.append(el4("span", void 0, c.label));
3915
+ th.addEventListener("click", () => {
3916
+ if (sortKey === c.key) sortAsc = !sortAsc;
3917
+ else {
3918
+ sortKey = c.key;
3919
+ sortAsc = true;
3920
+ }
3921
+ render();
3922
+ });
3923
+ headRow.append(th);
3924
+ ths.set(c.key, th);
3925
+ }
3926
+ thead.append(headRow);
3927
+ const tbody = el4("tbody");
3928
+ table.append(thead, tbody);
3929
+ wrap.append(table);
3930
+ const foot = el4("div", "bb-ks-foot");
3931
+ const footL = el4(
3932
+ "span",
3933
+ void 0,
3934
+ "Sorted by command \xB7 click a header to sort"
3935
+ );
3936
+ const footR = el4("span", void 0, "Esc closes");
3937
+ foot.append(footL, footR);
3938
+ root.append(top, wrap, foot);
3939
+ dialog.setContent(root);
3940
+ const allRows = () => {
3941
+ const rows = [];
3942
+ for (const src of options.sources)
3943
+ rows.push(
3944
+ ...keybindingRows(src.keybindings, (c) => src.commands.get(c)?.title)
3945
+ );
3946
+ return rows;
3947
+ };
3948
+ const kbdCell = (keys) => {
3949
+ const td = el4("td");
3950
+ keys.forEach((k, i) => {
3951
+ if (i > 0) td.append(el4("span", "or", "or"));
3952
+ for (const part of formatKey(k, IS_MAC))
3953
+ td.append(el4("kbd", void 0, part));
3954
+ });
3955
+ return td;
3956
+ };
3957
+ const sortRows = (rows) => {
3958
+ const val = (r) => sortKey === "title" ? r.title : sortKey === "keys" ? r.keys.map((k) => formatKey(k, IS_MAC).join("")).join(" ") : sortKey === "when" ? r.when ?? "" : r.source;
3959
+ const sorted = [...rows].sort(
3960
+ (a, b) => val(a).localeCompare(val(b), void 0, { sensitivity: "base" })
3961
+ );
3962
+ return sortAsc ? sorted : sorted.reverse();
3963
+ };
3964
+ const render = () => {
3965
+ const q = search.value.trim().toLowerCase();
3966
+ const rows = allRows();
3967
+ const shown = sortRows(filterKeybindingRows(rows, q, IS_MAC));
3968
+ for (const [key, th] of ths) {
3969
+ if (key === sortKey)
3970
+ th.setAttribute("aria-sort", sortAsc ? "ascending" : "descending");
3971
+ else th.removeAttribute("aria-sort");
3972
+ }
3973
+ tbody.replaceChildren();
3974
+ if (shown.length === 0) {
3975
+ const tr = el4("tr");
3976
+ const td = el4("td", "bb-ks-empty", "No shortcut matches.");
3977
+ td.colSpan = 4;
3978
+ tr.append(td);
3979
+ tbody.append(tr);
3980
+ }
3981
+ for (const r of shown) {
3982
+ const tr = el4("tr");
3983
+ const cmd = el4("td", "cmd");
3984
+ cmd.append(...highlight(r.title, q));
3985
+ const small = el4("small");
3986
+ small.append(...highlight(r.command, q));
3987
+ cmd.append(small);
3988
+ const when = el4("td", "when");
3989
+ when.append(...highlight(r.when ?? "", q));
3990
+ const src = el4("td", "src");
3991
+ const chip = el4(
3992
+ "span",
3993
+ "bb-ks-chip" + (r.source === "core" ? " core" : "")
3994
+ );
3995
+ chip.append(...highlight(r.source, q));
3996
+ src.append(chip);
3997
+ tr.append(cmd, kbdCell(r.keys), when, src);
3998
+ tbody.append(tr);
3999
+ }
4000
+ count.textContent = shown.length === rows.length ? `${rows.length} shortcuts` : `${shown.length} of ${rows.length}`;
4001
+ footL.textContent = `Sorted by ${columns.find((c) => c.key === sortKey)?.label.toLowerCase()} \xB7 click a header to sort`;
4002
+ };
4003
+ search.addEventListener("input", render);
4004
+ dialog.onClose(() => dialog.destroy());
4005
+ render();
4006
+ dialog.open();
4007
+ search.focus();
4008
+ }
4009
+
4010
+ // packages/ui/src/lib/scrollbars.ts
4011
+ var STYLE16 = `
4012
+ @property --bb-scroll-thumb-now {
4013
+ syntax: '<color>';
4014
+ /* MUST inherit: the thumb pseudo-element never matches .bb-scrolling
4015
+ itself \u2014 it only ever INHERITS the value from its owner. */
4016
+ inherits: true;
4017
+ initial-value: transparent;
4018
+ }
4019
+ * { transition: --bb-scroll-thumb-now 0.35s ease; }
4020
+ .bb-scrolling { --bb-scroll-thumb-now: var(--bb-ui-scroll-thumb, rgba(64,52,28,.28)); }
4021
+ ::-webkit-scrollbar { width: 13px; height: 13px; background: transparent; }
4022
+ ::-webkit-scrollbar-track { background: transparent; }
4023
+ ::-webkit-scrollbar-corner { background: transparent; }
4024
+ ::-webkit-scrollbar-thumb {
4025
+ background-color: var(--bb-scroll-thumb-now);
4026
+ background-clip: content-box;
4027
+ border: 3px solid transparent;
4028
+ border-radius: 99px;
4029
+ min-height: 40px; /* a 277-page doc must not shrink the thumb to a dot */
4030
+ }
4031
+ ::-webkit-scrollbar-thumb:hover,
4032
+ ::-webkit-scrollbar-thumb:active {
4033
+ background-color: var(--bb-ui-scroll-thumb-hover, rgba(64,52,28,.45));
4034
+ }
4035
+ @supports not selector(::-webkit-scrollbar) {
4036
+ /* non-WebKit (Firefox): native thin overlay, same colours */
4037
+ * { scrollbar-width: thin; scrollbar-color: var(--bb-ui-scroll-thumb, rgba(64,52,28,.28)) transparent; }
4038
+ }
4039
+ `;
4040
+ var installed = null;
4041
+ function installScrollbars(root = document) {
4042
+ if (installed) return installed;
4043
+ injectStyle("bb-ui-scrollbars", STYLE16);
4044
+ const quiet = /* @__PURE__ */ new WeakMap();
4045
+ const onScroll = (ev) => {
4046
+ const el5 = ev.target instanceof HTMLElement ? ev.target : root.documentElement;
4047
+ el5.classList.add("bb-scrolling");
4048
+ clearTimeout(quiet.get(el5));
4049
+ quiet.set(
4050
+ el5,
4051
+ window.setTimeout(() => el5.classList.remove("bb-scrolling"), 1e3)
4052
+ );
4053
+ };
4054
+ root.addEventListener("scroll", onScroll, { capture: true, passive: true });
4055
+ installed = () => {
4056
+ root.removeEventListener("scroll", onScroll, { capture: true });
4057
+ installed = null;
4058
+ };
4059
+ return installed;
4060
+ }
2804
4061
  export {
2805
4062
  COLOR_PALETTE,
2806
4063
  Dialog,
4064
+ RECENT_MAX,
4065
+ SPECIAL_CHARACTERS,
4066
+ SYMBOL_GROUPS,
4067
+ SYMBOL_NAMES,
2807
4068
  cmToPx,
4069
+ codePointLabel,
2808
4070
  colorButton,
2809
4071
  createFindDialog,
2810
4072
  createSectionChip,
4073
+ createSymbolDialog,
2811
4074
  defaultMenus,
2812
4075
  defaultToolbarGroups,
4076
+ installScrollbars,
2813
4077
  marginPresetPicker,
2814
4078
  mountMenubar,
2815
4079
  mountToolbar,
2816
4080
  openCellProperties,
2817
4081
  openColorPicker,
2818
4082
  openFontDialog,
4083
+ openKeyboardShortcutsDialog,
2819
4084
  openMarginsDialog,
2820
4085
  openPageSizeDialog,
4086
+ openSymbolPopover,
2821
4087
  orientationPicker,
2822
4088
  pageNumberPicker,
2823
4089
  pageSizePicker,
4090
+ panelAnchor,
4091
+ parseCodePoint,
2824
4092
  promptDialog,
4093
+ pushRecent,
2825
4094
  pxToCm,
4095
+ searchSymbols,
2826
4096
  sectionPaperPanel,
2827
4097
  showContextMenu,
2828
4098
  showLinkPanel,