@shadow-garden/bapbong-ui 0.1.2 → 0.3.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.cjs CHANGED
@@ -29,6 +29,7 @@ __export(index_exports, {
29
29
  openCellProperties: () => openCellProperties,
30
30
  promptDialog: () => promptDialog,
31
31
  showContextMenu: () => showContextMenu,
32
+ showLinkPanel: () => showLinkPanel,
32
33
  tableGridPicker: () => tableGridPicker
33
34
  });
34
35
  module.exports = __toCommonJS(index_exports);
@@ -287,6 +288,20 @@ var STYLE2 = `
287
288
  .bb-color-none{grid-column:1/-1;font:inherit;font-size:12px;padding:3px 0;border:1px solid var(--bb-ui-border,#e3e3e0);border-radius:5px;background:var(--bb-ui-bg,#fff);cursor:pointer}
288
289
  .bb-color-none:hover{background:var(--bb-ui-hover,#f1efe8)}
289
290
  .bb-i-bold{font-weight:700}.bb-i-italic{font-style:italic}.bb-i-underline{text-decoration:underline}.bb-i-strike{text-decoration:line-through}
291
+ .bb-toolbar-split{display:flex;align-items:center;gap:0}
292
+ .bb-toolbar-split .bb-toolbar-btn{min-width:26px;padding:0 5px}
293
+ .bb-split-arrow{min-width:14px !important;width:15px;padding:0 !important}
294
+ .bb-split-arrow svg{display:block}
295
+ .bb-split-pop{position:absolute;z-index:1200;display:grid;grid-template-columns:repeat(3,auto);gap:8px;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);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)}
296
+ .bb-split-pop[hidden]{display:none}
297
+ .bb-split-card{display:flex;flex-direction:column;gap:6px;width:76px;padding:9px 8px;border:1px solid var(--bb-ui-border,#e3e3e0);border-radius:6px;background:transparent;cursor:pointer;font-family:inherit}
298
+ .bb-split-card:hover{background:var(--bb-ui-hover,#f1efe8)}
299
+ .bb-split-card.is-selected{background:var(--bb-ui-active-bg,#e6f1fb);border-color:var(--bb-ui-active-border,#b5d4f4)}
300
+ .bb-split-row{display:flex;align-items:center;gap:4px}
301
+ .bb-split-row[data-lvl="1"]{padding-left:9px}
302
+ .bb-split-row[data-lvl="2"]{padding-left:18px}
303
+ .bb-split-marker{flex:none;min-width:14px;font-size:9px;line-height:1.1;text-align:right;color:var(--bb-ui-fg,#2c2c2a);white-space:nowrap}
304
+ .bb-split-bar{flex:1;height:3px;border-radius:2px;background:var(--bb-ui-border,#d9d9d4)}
290
305
  `;
291
306
  function defaultToolbarGroups(commands) {
292
307
  const names = [...commands].map((c) => c.name);
@@ -306,6 +321,7 @@ function mountToolbar(host, editor, options = {}) {
306
321
  const buttons = [];
307
322
  const selects = [];
308
323
  const colors = [];
324
+ const splits = [];
309
325
  let latest = null;
310
326
  for (const group of groups) {
311
327
  const entries = group.filter(
@@ -381,7 +397,7 @@ function mountToolbar(host, editor, options = {}) {
381
397
  btn2.addEventListener("mousedown", (e) => e.preventDefault());
382
398
  btn2.addEventListener("click", () => {
383
399
  const open = pop2.hidden;
384
- wrap.querySelectorAll(".bb-color-pop").forEach((p) => p.hidden = true);
400
+ wrap.querySelectorAll(".bb-color-pop,.bb-split-pop").forEach((p) => p.hidden = true);
385
401
  pop2.hidden = !open;
386
402
  if (!pop2.hidden) {
387
403
  const wrapRect = wrap.getBoundingClientRect();
@@ -410,6 +426,94 @@ function mountToolbar(host, editor, options = {}) {
410
426
  colors.push({ spec: entry, bar });
411
427
  continue;
412
428
  }
429
+ if (typeof entry !== "string" && entry.kind === "split") {
430
+ const splitWrap = document.createElement("div");
431
+ splitWrap.className = "bb-toolbar-split";
432
+ const item2 = items[entry.name] ?? { title: entry.name };
433
+ const main = document.createElement("button");
434
+ main.type = "button";
435
+ main.className = "bb-toolbar-btn";
436
+ main.title = item2.title;
437
+ main.setAttribute("aria-label", item2.title);
438
+ if (item2.svg) main.innerHTML = item2.svg;
439
+ else main.textContent = item2.label ?? entry.name;
440
+ main.addEventListener("mousedown", (e) => e.preventDefault());
441
+ main.addEventListener("click", () => {
442
+ if (!latest) return;
443
+ editor.commands.get(entry.name)?.run(latest, (tr) => editor.dispatch(tr));
444
+ editor.focus();
445
+ });
446
+ buttons.push({ name: entry.name, el: main });
447
+ const arrow = document.createElement("button");
448
+ arrow.type = "button";
449
+ arrow.className = "bb-toolbar-btn bb-split-arrow";
450
+ arrow.title = `${item2.title} styles`;
451
+ arrow.setAttribute("aria-label", `${item2.title} styles`);
452
+ arrow.setAttribute("aria-haspopup", "true");
453
+ arrow.innerHTML = '<svg viewBox="0 0 8 8" width="8" height="8" fill="currentColor" aria-hidden="true"><path d="M1 2.5h6L4 6z"/></svg>';
454
+ const pop2 = document.createElement("div");
455
+ pop2.className = "bb-split-pop";
456
+ pop2.hidden = true;
457
+ const cards = [];
458
+ for (const opt of entry.options) {
459
+ const card = document.createElement("button");
460
+ card.type = "button";
461
+ card.className = "bb-split-card";
462
+ card.dataset["value"] = opt.value;
463
+ if (opt.title) card.title = opt.title;
464
+ opt.rows.forEach((marker, i) => {
465
+ const row = document.createElement("span");
466
+ row.className = "bb-split-row";
467
+ row.dataset["lvl"] = String(i);
468
+ const m = document.createElement("span");
469
+ m.className = "bb-split-marker";
470
+ m.textContent = marker;
471
+ const bar = document.createElement("span");
472
+ bar.className = "bb-split-bar";
473
+ row.append(m, bar);
474
+ card.appendChild(row);
475
+ });
476
+ card.addEventListener("mousedown", (e) => e.preventDefault());
477
+ card.addEventListener("click", () => {
478
+ entry.onSelect(opt.value);
479
+ pop2.hidden = true;
480
+ editor.focus();
481
+ });
482
+ pop2.appendChild(card);
483
+ cards.push(card);
484
+ }
485
+ arrow.addEventListener("mousedown", (e) => e.preventDefault());
486
+ arrow.addEventListener("click", () => {
487
+ const open = pop2.hidden;
488
+ wrap.querySelectorAll(".bb-split-pop,.bb-color-pop").forEach((p) => p.hidden = true);
489
+ pop2.hidden = !open;
490
+ if (!pop2.hidden) {
491
+ const wrapRect = wrap.getBoundingClientRect();
492
+ const btnRect = arrow.getBoundingClientRect();
493
+ pop2.style.top = `${btnRect.bottom - wrapRect.top + 4}px`;
494
+ const left = Math.max(
495
+ 0,
496
+ Math.min(
497
+ btnRect.left - wrapRect.left,
498
+ wrap.clientWidth - pop2.offsetWidth - 4
499
+ )
500
+ );
501
+ pop2.style.left = `${left}px`;
502
+ const onDoc = (ev) => {
503
+ if (!splitWrap.contains(ev.target) && !pop2.contains(ev.target)) {
504
+ pop2.hidden = true;
505
+ document.removeEventListener("pointerdown", onDoc, true);
506
+ }
507
+ };
508
+ document.addEventListener("pointerdown", onDoc, true);
509
+ }
510
+ });
511
+ splitWrap.append(main, arrow);
512
+ wrap.appendChild(pop2);
513
+ groupEl.appendChild(splitWrap);
514
+ splits.push({ spec: entry, cards });
515
+ continue;
516
+ }
413
517
  const item = items[entry] ?? { title: entry };
414
518
  const btn = document.createElement("button");
415
519
  btn.type = "button";
@@ -461,7 +565,7 @@ function mountToolbar(host, editor, options = {}) {
461
565
  while (pop.firstChild) root.insertBefore(pop.firstChild, moreBtn);
462
566
  moreBtn.style.display = "none";
463
567
  closePop();
464
- wrap.querySelectorAll(".bb-color-pop").forEach((p) => p.hidden = true);
568
+ wrap.querySelectorAll(".bb-color-pop,.bb-split-pop").forEach((p) => p.hidden = true);
465
569
  if (fits()) return;
466
570
  moreBtn.style.display = "";
467
571
  const groupEls = Array.from(root.children).filter(
@@ -491,6 +595,14 @@ function mountToolbar(host, editor, options = {}) {
491
595
  for (const { spec, el } of selects) el.value = spec.value(state);
492
596
  for (const { spec, bar } of colors)
493
597
  bar.style.background = spec.value(state) ?? "";
598
+ for (const { spec, cards } of splits) {
599
+ const selected = spec.value(state);
600
+ for (const card of cards)
601
+ card.classList.toggle(
602
+ "is-selected",
603
+ selected != null && card.dataset["value"] === selected
604
+ );
605
+ }
494
606
  };
495
607
  const off = editor.onChange((c) => refresh(c.state));
496
608
  try {
@@ -904,9 +1016,16 @@ function showContextMenu(entries, at) {
904
1016
  }
905
1017
  document.body.appendChild(el);
906
1018
  const r = el.getBoundingClientRect();
907
- el.style.left = `${Math.max(4, Math.min(at.x, window.innerWidth - r.width - 4))}px`;
908
- el.style.top = `${Math.max(4, Math.min(at.y, window.innerHeight - r.height - 4))}px`;
909
- const items = () => Array.from(el.querySelectorAll(".bb-ctx-item:not(:disabled)"));
1019
+ const pad = 4;
1020
+ const fitsX = (v) => v >= pad && v + r.width <= window.innerWidth - pad;
1021
+ const fitsY = (v) => v >= pad && v + r.height <= window.innerHeight - pad;
1022
+ 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));
1023
+ 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));
1024
+ el.style.left = `${x}px`;
1025
+ el.style.top = `${y}px`;
1026
+ const items = () => Array.from(
1027
+ el.querySelectorAll(".bb-ctx-item:not(:disabled)")
1028
+ );
910
1029
  const onDown = (e) => {
911
1030
  if (!el.contains(e.target)) closeCurrent();
912
1031
  };
@@ -936,6 +1055,213 @@ function showContextMenu(entries, at) {
936
1055
  return { close: closeCurrent };
937
1056
  }
938
1057
 
1058
+ // packages/ui/src/lib/link-panel.ts
1059
+ var STYLE6 = `
1060
+ .bb-linkpanel{position:fixed;z-index:1200;width:300px;padding:8px;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);box-sizing:border-box}
1061
+ .bb-linkpanel *{box-sizing:border-box}
1062
+ .bb-linkpanel-row{display:flex;align-items:center;gap:8px}
1063
+ .bb-linkpanel-form{display:flex;flex-direction:column;gap:8px}
1064
+ .bb-linkpanel-field{display:flex;align-items:center;gap:7px;border:1px solid var(--bb-ui-border,#e3e3e0);border-radius:7px;padding:0 9px;background:var(--bb-ui-bg,#fff)}
1065
+ .bb-linkpanel-field:focus-within{border-color:var(--bb-ui-accent,#d85a30)}
1066
+ .bb-linkpanel-field svg{flex:none;opacity:.55}
1067
+ .bb-linkpanel-input{flex:1;min-width:0;height:30px;border:0;background:transparent;color:inherit;font:inherit;font-size:13px;outline:none}
1068
+ .bb-linkpanel-href{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;color:var(--bb-ui-accent,#d85a30)}
1069
+ .bb-linkpanel-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:6px;background:transparent;color:inherit;cursor:pointer;flex:none}
1070
+ .bb-linkpanel-btn:hover{background:var(--bb-ui-hover,#f1efe8)}
1071
+ .bb-linkpanel-apply{font:inherit;font-size:13px;font-weight:600;color:var(--bb-ui-accent,#d85a30);background:transparent;border:0;border-radius:6px;padding:6px 8px;cursor:pointer;flex:none}
1072
+ .bb-linkpanel-apply:hover{background:var(--bb-ui-hover,#f1efe8)}
1073
+ .bb-linkpanel-apply:disabled{opacity:.4;cursor:default}
1074
+ `;
1075
+ var ICONS = {
1076
+ link: '<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 9.5 9.5 6.5"/><path d="M7.5 4.5 9 3a2.47 2.47 0 0 1 3.5 0L13 3.5a2.47 2.47 0 0 1 0 3.5l-1.5 1.5"/><path d="M8.5 11.5 7 13a2.47 2.47 0 0 1-3.5 0L3 12.5a2.47 2.47 0 0 1 0-3.5l1.5-1.5"/></svg>',
1077
+ text: '<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 4V2.5h10V4M8 2.5v11M6 13.5h4"/></svg>',
1078
+ copy: '<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5"/><path d="M10.5 5.5v-2a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2"/></svg>',
1079
+ edit: '<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9.5 3.5 3 3L6 13l-3.5.5L3 10z"/><path d="m11 2 .9-.9a1.55 1.55 0 0 1 2.2 0l.8.8a1.55 1.55 0 0 1 0 2.2L14 5"/></svg>',
1080
+ unlink: '<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7.5 4.5 9 3a2.47 2.47 0 0 1 3.5 0L13 3.5a2.47 2.47 0 0 1 0 3.5l-1.5 1.5"/><path d="M8.5 11.5 7 13a2.47 2.47 0 0 1-3.5 0L3 12.5a2.47 2.47 0 0 1 0-3.5l1.5-1.5"/><path d="m3 3 10 10"/></svg>'
1081
+ };
1082
+ var current2 = null;
1083
+ var currentKey = null;
1084
+ var currentHandle = null;
1085
+ var pendingClose = null;
1086
+ function cancelPendingClose() {
1087
+ if (pendingClose !== null) {
1088
+ clearTimeout(pendingClose);
1089
+ pendingClose = null;
1090
+ }
1091
+ }
1092
+ function closeCurrent2() {
1093
+ cancelPendingClose();
1094
+ if (current2) {
1095
+ const dispose = current2;
1096
+ current2 = null;
1097
+ currentKey = null;
1098
+ currentHandle = null;
1099
+ dispose();
1100
+ }
1101
+ }
1102
+ function place(el, anchor) {
1103
+ const r = el.getBoundingClientRect();
1104
+ const pad = 6;
1105
+ const gap = 6;
1106
+ const x = Math.max(
1107
+ pad,
1108
+ Math.min(anchor.x, window.innerWidth - r.width - pad)
1109
+ );
1110
+ const below = anchor.y + anchor.height + gap;
1111
+ const above = anchor.y - gap - r.height;
1112
+ const y = below + r.height <= window.innerHeight - pad || above < pad ? Math.min(below, window.innerHeight - r.height - pad) : above;
1113
+ el.style.left = `${x}px`;
1114
+ el.style.top = `${y}px`;
1115
+ }
1116
+ function showLinkPanel(opts) {
1117
+ injectStyle("bb-ui-linkpanel-styles", STYLE6);
1118
+ if (opts.key && current2 && currentKey === opts.key && currentHandle) {
1119
+ cancelPendingClose();
1120
+ return currentHandle;
1121
+ }
1122
+ closeCurrent2();
1123
+ const el = document.createElement("div");
1124
+ el.className = "bb-linkpanel";
1125
+ el.setAttribute("role", "dialog");
1126
+ el.setAttribute("aria-label", "Link");
1127
+ const iconBtn = (svg, label, onClick) => {
1128
+ const b = document.createElement("button");
1129
+ b.type = "button";
1130
+ b.className = "bb-linkpanel-btn";
1131
+ b.title = label;
1132
+ b.setAttribute("aria-label", label);
1133
+ b.innerHTML = svg;
1134
+ b.addEventListener("mousedown", (e) => e.preventDefault());
1135
+ b.addEventListener("click", onClick);
1136
+ return b;
1137
+ };
1138
+ const field = (icon, placeholder, value) => {
1139
+ const wrap = document.createElement("div");
1140
+ wrap.className = "bb-linkpanel-field";
1141
+ wrap.innerHTML = icon;
1142
+ const input = document.createElement("input");
1143
+ input.className = "bb-linkpanel-input";
1144
+ input.type = "text";
1145
+ input.placeholder = placeholder;
1146
+ input.value = value;
1147
+ input.spellcheck = false;
1148
+ input.autocomplete = "off";
1149
+ wrap.appendChild(input);
1150
+ return { wrap, input };
1151
+ };
1152
+ const renderView = (existing) => {
1153
+ el.textContent = "";
1154
+ const row = document.createElement("div");
1155
+ row.className = "bb-linkpanel-row";
1156
+ const href = document.createElement("span");
1157
+ href.className = "bb-linkpanel-href";
1158
+ href.textContent = existing.href;
1159
+ href.title = existing.href;
1160
+ row.appendChild(href);
1161
+ row.appendChild(
1162
+ iconBtn(ICONS.copy, "Copy link", () => {
1163
+ void navigator.clipboard?.writeText(existing.href).catch(() => void 0);
1164
+ closeCurrent2();
1165
+ })
1166
+ );
1167
+ row.appendChild(
1168
+ iconBtn(ICONS.edit, "Edit link", () => renderForm(existing))
1169
+ );
1170
+ row.appendChild(
1171
+ iconBtn(ICONS.unlink, "Remove link", () => {
1172
+ closeCurrent2();
1173
+ opts.onUnlink?.();
1174
+ })
1175
+ );
1176
+ el.appendChild(row);
1177
+ place(el, opts.anchor);
1178
+ };
1179
+ const renderForm = (prefill) => {
1180
+ el.textContent = "";
1181
+ const form = document.createElement("div");
1182
+ form.className = "bb-linkpanel-form";
1183
+ const withText = !opts.hasSelection;
1184
+ let textInput = null;
1185
+ if (withText) {
1186
+ const t = field(ICONS.text, "Text (optional)", prefill?.text ?? "");
1187
+ textInput = t.input;
1188
+ form.appendChild(t.wrap);
1189
+ }
1190
+ const u = field(ICONS.link, "Paste or type a link", prefill?.href ?? "");
1191
+ const row = document.createElement("div");
1192
+ row.className = "bb-linkpanel-row";
1193
+ row.appendChild(u.wrap);
1194
+ u.wrap.style.flex = "1";
1195
+ const apply = document.createElement("button");
1196
+ apply.type = "button";
1197
+ apply.className = "bb-linkpanel-apply";
1198
+ apply.textContent = "Apply";
1199
+ apply.disabled = !u.input.value.trim();
1200
+ u.input.addEventListener("input", () => {
1201
+ apply.disabled = !u.input.value.trim();
1202
+ });
1203
+ const submit = () => {
1204
+ const href = u.input.value.trim();
1205
+ if (!href) return;
1206
+ const text = textInput?.value ?? void 0;
1207
+ closeCurrent2();
1208
+ opts.onApply(href, text);
1209
+ };
1210
+ apply.addEventListener("click", submit);
1211
+ for (const input of [textInput, u.input]) {
1212
+ input?.addEventListener("keydown", (e) => {
1213
+ if (e.key === "Enter") {
1214
+ e.preventDefault();
1215
+ submit();
1216
+ }
1217
+ });
1218
+ }
1219
+ row.appendChild(apply);
1220
+ form.appendChild(row);
1221
+ el.appendChild(form);
1222
+ place(el, opts.anchor);
1223
+ u.input.focus();
1224
+ u.input.select();
1225
+ };
1226
+ document.body.appendChild(el);
1227
+ if (opts.existing) renderView(opts.existing);
1228
+ else renderForm(null);
1229
+ const onDown = (e) => {
1230
+ if (el.contains(e.target)) return;
1231
+ if (opts.key) {
1232
+ cancelPendingClose();
1233
+ pendingClose = setTimeout(closeCurrent2, 80);
1234
+ } else {
1235
+ closeCurrent2();
1236
+ }
1237
+ };
1238
+ const onKey = (e) => {
1239
+ if (e.key === "Escape") closeCurrent2();
1240
+ };
1241
+ const onScroll = () => closeCurrent2();
1242
+ setTimeout(() => {
1243
+ document.addEventListener("pointerdown", onDown, true);
1244
+ document.addEventListener("keydown", onKey, true);
1245
+ window.addEventListener("scroll", onScroll, true);
1246
+ }, 0);
1247
+ const dispose = () => {
1248
+ document.removeEventListener("pointerdown", onDown, true);
1249
+ document.removeEventListener("keydown", onKey, true);
1250
+ window.removeEventListener("scroll", onScroll, true);
1251
+ el.remove();
1252
+ };
1253
+ current2 = dispose;
1254
+ currentKey = opts.key ?? null;
1255
+ const handle = {
1256
+ close: () => {
1257
+ if (current2 === dispose) closeCurrent2();
1258
+ },
1259
+ isOpen: () => current2 === dispose
1260
+ };
1261
+ currentHandle = handle;
1262
+ return handle;
1263
+ }
1264
+
939
1265
  // packages/ui/src/lib/cell-properties.ts
940
1266
  var FILL_PRESETS = ["#FCEBEB", "#FAEEDA", "#EAF3DE", "#E6F1FB", "#EEEDFE", "#FBEAF0", "#E1F5EE", "#F1EFE8", "#D3D1C7"];
941
1267
  var PEN_COLORS = ["#000000", "#5F5E5A", "#B0B0B0", "#E24B4A", "#185FA5", "#1D9E75", "#BA7517"];
@@ -963,7 +1289,7 @@ var PRESETS = [
963
1289
  { key: "insideH", label: "Inside horizontal", sides: [0, 0, 0, 0, 1, 0], inside: true },
964
1290
  { key: "insideV", label: "Inside vertical", sides: [0, 0, 0, 0, 0, 1], inside: true }
965
1291
  ];
966
- var STYLE6 = `
1292
+ var STYLE7 = `
967
1293
  .bb-cp{display:flex;flex-direction:column;gap:16px;min-width:320px;color:var(--bb-ui-fg,#2c2c2a)}
968
1294
  .bb-cp *{box-sizing:border-box}
969
1295
  .bb-cp-label{font-size:12px;opacity:.7;margin-bottom:8px}
@@ -1004,7 +1330,7 @@ var section = (label) => {
1004
1330
  return sec;
1005
1331
  };
1006
1332
  function openCellProperties({ initial, singleCell, onApply }) {
1007
- injectStyle("bb-ui-cellprops-styles", STYLE6);
1333
+ injectStyle("bb-ui-cellprops-styles", STYLE7);
1008
1334
  let background = initial.background;
1009
1335
  let vAlign = initial.vAlign;
1010
1336
  let penWidthPt = 1;
@@ -1177,7 +1503,7 @@ function openCellProperties({ initial, singleCell, onApply }) {
1177
1503
  }
1178
1504
 
1179
1505
  // packages/ui/src/lib/table-grid.ts
1180
- var STYLE7 = `
1506
+ var STYLE8 = `
1181
1507
  .bb-grid{display:flex;flex-direction:column;gap:6px;user-select:none}
1182
1508
  .bb-grid-cells{display:grid;gap:2px}
1183
1509
  .bb-grid-cell{width:15px;height:15px;border:1px solid var(--bb-ui-border,#d8d6cf);border-radius:2px;background:var(--bb-ui-bg,#fff);cursor:pointer;padding:0}
@@ -1185,7 +1511,7 @@ var STYLE7 = `
1185
1511
  .bb-grid-label{font-size:12px;text-align:center;opacity:.7}
1186
1512
  `;
1187
1513
  function tableGridPicker(options) {
1188
- injectStyle("bb-ui-grid-styles", STYLE7);
1514
+ injectStyle("bb-ui-grid-styles", STYLE8);
1189
1515
  const maxRows = options.maxRows ?? 8;
1190
1516
  const maxCols = options.maxCols ?? 10;
1191
1517
  const root = document.createElement("div");
@@ -1231,5 +1557,6 @@ function tableGridPicker(options) {
1231
1557
  openCellProperties,
1232
1558
  promptDialog,
1233
1559
  showContextMenu,
1560
+ showLinkPanel,
1234
1561
  tableGridPicker
1235
1562
  });
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export * from './lib/toolbar.js';
4
4
  export * from './lib/menubar.js';
5
5
  export * from './lib/find.js';
6
6
  export * from './lib/context-menu.js';
7
+ export * from './lib/link-panel.js';
7
8
  export * from './lib/cell-properties.js';
8
9
  export * from './lib/table-grid.js';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACrE,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACrE,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -252,6 +252,20 @@ var STYLE2 = `
252
252
  .bb-color-none{grid-column:1/-1;font:inherit;font-size:12px;padding:3px 0;border:1px solid var(--bb-ui-border,#e3e3e0);border-radius:5px;background:var(--bb-ui-bg,#fff);cursor:pointer}
253
253
  .bb-color-none:hover{background:var(--bb-ui-hover,#f1efe8)}
254
254
  .bb-i-bold{font-weight:700}.bb-i-italic{font-style:italic}.bb-i-underline{text-decoration:underline}.bb-i-strike{text-decoration:line-through}
255
+ .bb-toolbar-split{display:flex;align-items:center;gap:0}
256
+ .bb-toolbar-split .bb-toolbar-btn{min-width:26px;padding:0 5px}
257
+ .bb-split-arrow{min-width:14px !important;width:15px;padding:0 !important}
258
+ .bb-split-arrow svg{display:block}
259
+ .bb-split-pop{position:absolute;z-index:1200;display:grid;grid-template-columns:repeat(3,auto);gap:8px;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);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)}
260
+ .bb-split-pop[hidden]{display:none}
261
+ .bb-split-card{display:flex;flex-direction:column;gap:6px;width:76px;padding:9px 8px;border:1px solid var(--bb-ui-border,#e3e3e0);border-radius:6px;background:transparent;cursor:pointer;font-family:inherit}
262
+ .bb-split-card:hover{background:var(--bb-ui-hover,#f1efe8)}
263
+ .bb-split-card.is-selected{background:var(--bb-ui-active-bg,#e6f1fb);border-color:var(--bb-ui-active-border,#b5d4f4)}
264
+ .bb-split-row{display:flex;align-items:center;gap:4px}
265
+ .bb-split-row[data-lvl="1"]{padding-left:9px}
266
+ .bb-split-row[data-lvl="2"]{padding-left:18px}
267
+ .bb-split-marker{flex:none;min-width:14px;font-size:9px;line-height:1.1;text-align:right;color:var(--bb-ui-fg,#2c2c2a);white-space:nowrap}
268
+ .bb-split-bar{flex:1;height:3px;border-radius:2px;background:var(--bb-ui-border,#d9d9d4)}
255
269
  `;
256
270
  function defaultToolbarGroups(commands) {
257
271
  const names = [...commands].map((c) => c.name);
@@ -271,6 +285,7 @@ function mountToolbar(host, editor, options = {}) {
271
285
  const buttons = [];
272
286
  const selects = [];
273
287
  const colors = [];
288
+ const splits = [];
274
289
  let latest = null;
275
290
  for (const group of groups) {
276
291
  const entries = group.filter(
@@ -346,7 +361,7 @@ function mountToolbar(host, editor, options = {}) {
346
361
  btn2.addEventListener("mousedown", (e) => e.preventDefault());
347
362
  btn2.addEventListener("click", () => {
348
363
  const open = pop2.hidden;
349
- wrap.querySelectorAll(".bb-color-pop").forEach((p) => p.hidden = true);
364
+ wrap.querySelectorAll(".bb-color-pop,.bb-split-pop").forEach((p) => p.hidden = true);
350
365
  pop2.hidden = !open;
351
366
  if (!pop2.hidden) {
352
367
  const wrapRect = wrap.getBoundingClientRect();
@@ -375,6 +390,94 @@ function mountToolbar(host, editor, options = {}) {
375
390
  colors.push({ spec: entry, bar });
376
391
  continue;
377
392
  }
393
+ if (typeof entry !== "string" && entry.kind === "split") {
394
+ const splitWrap = document.createElement("div");
395
+ splitWrap.className = "bb-toolbar-split";
396
+ const item2 = items[entry.name] ?? { title: entry.name };
397
+ const main = document.createElement("button");
398
+ main.type = "button";
399
+ main.className = "bb-toolbar-btn";
400
+ main.title = item2.title;
401
+ main.setAttribute("aria-label", item2.title);
402
+ if (item2.svg) main.innerHTML = item2.svg;
403
+ else main.textContent = item2.label ?? entry.name;
404
+ main.addEventListener("mousedown", (e) => e.preventDefault());
405
+ main.addEventListener("click", () => {
406
+ if (!latest) return;
407
+ editor.commands.get(entry.name)?.run(latest, (tr) => editor.dispatch(tr));
408
+ editor.focus();
409
+ });
410
+ buttons.push({ name: entry.name, el: main });
411
+ const arrow = document.createElement("button");
412
+ arrow.type = "button";
413
+ arrow.className = "bb-toolbar-btn bb-split-arrow";
414
+ arrow.title = `${item2.title} styles`;
415
+ arrow.setAttribute("aria-label", `${item2.title} styles`);
416
+ arrow.setAttribute("aria-haspopup", "true");
417
+ arrow.innerHTML = '<svg viewBox="0 0 8 8" width="8" height="8" fill="currentColor" aria-hidden="true"><path d="M1 2.5h6L4 6z"/></svg>';
418
+ const pop2 = document.createElement("div");
419
+ pop2.className = "bb-split-pop";
420
+ pop2.hidden = true;
421
+ const cards = [];
422
+ for (const opt of entry.options) {
423
+ const card = document.createElement("button");
424
+ card.type = "button";
425
+ card.className = "bb-split-card";
426
+ card.dataset["value"] = opt.value;
427
+ if (opt.title) card.title = opt.title;
428
+ opt.rows.forEach((marker, i) => {
429
+ const row = document.createElement("span");
430
+ row.className = "bb-split-row";
431
+ row.dataset["lvl"] = String(i);
432
+ const m = document.createElement("span");
433
+ m.className = "bb-split-marker";
434
+ m.textContent = marker;
435
+ const bar = document.createElement("span");
436
+ bar.className = "bb-split-bar";
437
+ row.append(m, bar);
438
+ card.appendChild(row);
439
+ });
440
+ card.addEventListener("mousedown", (e) => e.preventDefault());
441
+ card.addEventListener("click", () => {
442
+ entry.onSelect(opt.value);
443
+ pop2.hidden = true;
444
+ editor.focus();
445
+ });
446
+ pop2.appendChild(card);
447
+ cards.push(card);
448
+ }
449
+ arrow.addEventListener("mousedown", (e) => e.preventDefault());
450
+ arrow.addEventListener("click", () => {
451
+ const open = pop2.hidden;
452
+ wrap.querySelectorAll(".bb-split-pop,.bb-color-pop").forEach((p) => p.hidden = true);
453
+ pop2.hidden = !open;
454
+ if (!pop2.hidden) {
455
+ const wrapRect = wrap.getBoundingClientRect();
456
+ const btnRect = arrow.getBoundingClientRect();
457
+ pop2.style.top = `${btnRect.bottom - wrapRect.top + 4}px`;
458
+ const left = Math.max(
459
+ 0,
460
+ Math.min(
461
+ btnRect.left - wrapRect.left,
462
+ wrap.clientWidth - pop2.offsetWidth - 4
463
+ )
464
+ );
465
+ pop2.style.left = `${left}px`;
466
+ const onDoc = (ev) => {
467
+ if (!splitWrap.contains(ev.target) && !pop2.contains(ev.target)) {
468
+ pop2.hidden = true;
469
+ document.removeEventListener("pointerdown", onDoc, true);
470
+ }
471
+ };
472
+ document.addEventListener("pointerdown", onDoc, true);
473
+ }
474
+ });
475
+ splitWrap.append(main, arrow);
476
+ wrap.appendChild(pop2);
477
+ groupEl.appendChild(splitWrap);
478
+ splits.push({ spec: entry, cards });
479
+ continue;
480
+ }
378
481
  const item = items[entry] ?? { title: entry };
379
482
  const btn = document.createElement("button");
380
483
  btn.type = "button";
@@ -426,7 +529,7 @@ function mountToolbar(host, editor, options = {}) {
426
529
  while (pop.firstChild) root.insertBefore(pop.firstChild, moreBtn);
427
530
  moreBtn.style.display = "none";
428
531
  closePop();
429
- wrap.querySelectorAll(".bb-color-pop").forEach((p) => p.hidden = true);
532
+ wrap.querySelectorAll(".bb-color-pop,.bb-split-pop").forEach((p) => p.hidden = true);
430
533
  if (fits()) return;
431
534
  moreBtn.style.display = "";
432
535
  const groupEls = Array.from(root.children).filter(
@@ -456,6 +559,14 @@ function mountToolbar(host, editor, options = {}) {
456
559
  for (const { spec, el } of selects) el.value = spec.value(state);
457
560
  for (const { spec, bar } of colors)
458
561
  bar.style.background = spec.value(state) ?? "";
562
+ for (const { spec, cards } of splits) {
563
+ const selected = spec.value(state);
564
+ for (const card of cards)
565
+ card.classList.toggle(
566
+ "is-selected",
567
+ selected != null && card.dataset["value"] === selected
568
+ );
569
+ }
459
570
  };
460
571
  const off = editor.onChange((c) => refresh(c.state));
461
572
  try {
@@ -869,9 +980,16 @@ function showContextMenu(entries, at) {
869
980
  }
870
981
  document.body.appendChild(el);
871
982
  const r = el.getBoundingClientRect();
872
- el.style.left = `${Math.max(4, Math.min(at.x, window.innerWidth - r.width - 4))}px`;
873
- el.style.top = `${Math.max(4, Math.min(at.y, window.innerHeight - r.height - 4))}px`;
874
- const items = () => Array.from(el.querySelectorAll(".bb-ctx-item:not(:disabled)"));
983
+ const pad = 4;
984
+ const fitsX = (v) => v >= pad && v + r.width <= window.innerWidth - pad;
985
+ const fitsY = (v) => v >= pad && v + r.height <= window.innerHeight - pad;
986
+ 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));
987
+ 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));
988
+ el.style.left = `${x}px`;
989
+ el.style.top = `${y}px`;
990
+ const items = () => Array.from(
991
+ el.querySelectorAll(".bb-ctx-item:not(:disabled)")
992
+ );
875
993
  const onDown = (e) => {
876
994
  if (!el.contains(e.target)) closeCurrent();
877
995
  };
@@ -901,6 +1019,213 @@ function showContextMenu(entries, at) {
901
1019
  return { close: closeCurrent };
902
1020
  }
903
1021
 
1022
+ // packages/ui/src/lib/link-panel.ts
1023
+ var STYLE6 = `
1024
+ .bb-linkpanel{position:fixed;z-index:1200;width:300px;padding:8px;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);box-sizing:border-box}
1025
+ .bb-linkpanel *{box-sizing:border-box}
1026
+ .bb-linkpanel-row{display:flex;align-items:center;gap:8px}
1027
+ .bb-linkpanel-form{display:flex;flex-direction:column;gap:8px}
1028
+ .bb-linkpanel-field{display:flex;align-items:center;gap:7px;border:1px solid var(--bb-ui-border,#e3e3e0);border-radius:7px;padding:0 9px;background:var(--bb-ui-bg,#fff)}
1029
+ .bb-linkpanel-field:focus-within{border-color:var(--bb-ui-accent,#d85a30)}
1030
+ .bb-linkpanel-field svg{flex:none;opacity:.55}
1031
+ .bb-linkpanel-input{flex:1;min-width:0;height:30px;border:0;background:transparent;color:inherit;font:inherit;font-size:13px;outline:none}
1032
+ .bb-linkpanel-href{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;color:var(--bb-ui-accent,#d85a30)}
1033
+ .bb-linkpanel-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:6px;background:transparent;color:inherit;cursor:pointer;flex:none}
1034
+ .bb-linkpanel-btn:hover{background:var(--bb-ui-hover,#f1efe8)}
1035
+ .bb-linkpanel-apply{font:inherit;font-size:13px;font-weight:600;color:var(--bb-ui-accent,#d85a30);background:transparent;border:0;border-radius:6px;padding:6px 8px;cursor:pointer;flex:none}
1036
+ .bb-linkpanel-apply:hover{background:var(--bb-ui-hover,#f1efe8)}
1037
+ .bb-linkpanel-apply:disabled{opacity:.4;cursor:default}
1038
+ `;
1039
+ var ICONS = {
1040
+ link: '<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 9.5 9.5 6.5"/><path d="M7.5 4.5 9 3a2.47 2.47 0 0 1 3.5 0L13 3.5a2.47 2.47 0 0 1 0 3.5l-1.5 1.5"/><path d="M8.5 11.5 7 13a2.47 2.47 0 0 1-3.5 0L3 12.5a2.47 2.47 0 0 1 0-3.5l1.5-1.5"/></svg>',
1041
+ text: '<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 4V2.5h10V4M8 2.5v11M6 13.5h4"/></svg>',
1042
+ copy: '<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5"/><path d="M10.5 5.5v-2a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2"/></svg>',
1043
+ edit: '<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9.5 3.5 3 3L6 13l-3.5.5L3 10z"/><path d="m11 2 .9-.9a1.55 1.55 0 0 1 2.2 0l.8.8a1.55 1.55 0 0 1 0 2.2L14 5"/></svg>',
1044
+ unlink: '<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7.5 4.5 9 3a2.47 2.47 0 0 1 3.5 0L13 3.5a2.47 2.47 0 0 1 0 3.5l-1.5 1.5"/><path d="M8.5 11.5 7 13a2.47 2.47 0 0 1-3.5 0L3 12.5a2.47 2.47 0 0 1 0-3.5l1.5-1.5"/><path d="m3 3 10 10"/></svg>'
1045
+ };
1046
+ var current2 = null;
1047
+ var currentKey = null;
1048
+ var currentHandle = null;
1049
+ var pendingClose = null;
1050
+ function cancelPendingClose() {
1051
+ if (pendingClose !== null) {
1052
+ clearTimeout(pendingClose);
1053
+ pendingClose = null;
1054
+ }
1055
+ }
1056
+ function closeCurrent2() {
1057
+ cancelPendingClose();
1058
+ if (current2) {
1059
+ const dispose = current2;
1060
+ current2 = null;
1061
+ currentKey = null;
1062
+ currentHandle = null;
1063
+ dispose();
1064
+ }
1065
+ }
1066
+ function place(el, anchor) {
1067
+ const r = el.getBoundingClientRect();
1068
+ const pad = 6;
1069
+ const gap = 6;
1070
+ const x = Math.max(
1071
+ pad,
1072
+ Math.min(anchor.x, window.innerWidth - r.width - pad)
1073
+ );
1074
+ const below = anchor.y + anchor.height + gap;
1075
+ const above = anchor.y - gap - r.height;
1076
+ const y = below + r.height <= window.innerHeight - pad || above < pad ? Math.min(below, window.innerHeight - r.height - pad) : above;
1077
+ el.style.left = `${x}px`;
1078
+ el.style.top = `${y}px`;
1079
+ }
1080
+ function showLinkPanel(opts) {
1081
+ injectStyle("bb-ui-linkpanel-styles", STYLE6);
1082
+ if (opts.key && current2 && currentKey === opts.key && currentHandle) {
1083
+ cancelPendingClose();
1084
+ return currentHandle;
1085
+ }
1086
+ closeCurrent2();
1087
+ const el = document.createElement("div");
1088
+ el.className = "bb-linkpanel";
1089
+ el.setAttribute("role", "dialog");
1090
+ el.setAttribute("aria-label", "Link");
1091
+ const iconBtn = (svg, label, onClick) => {
1092
+ const b = document.createElement("button");
1093
+ b.type = "button";
1094
+ b.className = "bb-linkpanel-btn";
1095
+ b.title = label;
1096
+ b.setAttribute("aria-label", label);
1097
+ b.innerHTML = svg;
1098
+ b.addEventListener("mousedown", (e) => e.preventDefault());
1099
+ b.addEventListener("click", onClick);
1100
+ return b;
1101
+ };
1102
+ const field = (icon, placeholder, value) => {
1103
+ const wrap = document.createElement("div");
1104
+ wrap.className = "bb-linkpanel-field";
1105
+ wrap.innerHTML = icon;
1106
+ const input = document.createElement("input");
1107
+ input.className = "bb-linkpanel-input";
1108
+ input.type = "text";
1109
+ input.placeholder = placeholder;
1110
+ input.value = value;
1111
+ input.spellcheck = false;
1112
+ input.autocomplete = "off";
1113
+ wrap.appendChild(input);
1114
+ return { wrap, input };
1115
+ };
1116
+ const renderView = (existing) => {
1117
+ el.textContent = "";
1118
+ const row = document.createElement("div");
1119
+ row.className = "bb-linkpanel-row";
1120
+ const href = document.createElement("span");
1121
+ href.className = "bb-linkpanel-href";
1122
+ href.textContent = existing.href;
1123
+ href.title = existing.href;
1124
+ row.appendChild(href);
1125
+ row.appendChild(
1126
+ iconBtn(ICONS.copy, "Copy link", () => {
1127
+ void navigator.clipboard?.writeText(existing.href).catch(() => void 0);
1128
+ closeCurrent2();
1129
+ })
1130
+ );
1131
+ row.appendChild(
1132
+ iconBtn(ICONS.edit, "Edit link", () => renderForm(existing))
1133
+ );
1134
+ row.appendChild(
1135
+ iconBtn(ICONS.unlink, "Remove link", () => {
1136
+ closeCurrent2();
1137
+ opts.onUnlink?.();
1138
+ })
1139
+ );
1140
+ el.appendChild(row);
1141
+ place(el, opts.anchor);
1142
+ };
1143
+ const renderForm = (prefill) => {
1144
+ el.textContent = "";
1145
+ const form = document.createElement("div");
1146
+ form.className = "bb-linkpanel-form";
1147
+ const withText = !opts.hasSelection;
1148
+ let textInput = null;
1149
+ if (withText) {
1150
+ const t = field(ICONS.text, "Text (optional)", prefill?.text ?? "");
1151
+ textInput = t.input;
1152
+ form.appendChild(t.wrap);
1153
+ }
1154
+ const u = field(ICONS.link, "Paste or type a link", prefill?.href ?? "");
1155
+ const row = document.createElement("div");
1156
+ row.className = "bb-linkpanel-row";
1157
+ row.appendChild(u.wrap);
1158
+ u.wrap.style.flex = "1";
1159
+ const apply = document.createElement("button");
1160
+ apply.type = "button";
1161
+ apply.className = "bb-linkpanel-apply";
1162
+ apply.textContent = "Apply";
1163
+ apply.disabled = !u.input.value.trim();
1164
+ u.input.addEventListener("input", () => {
1165
+ apply.disabled = !u.input.value.trim();
1166
+ });
1167
+ const submit = () => {
1168
+ const href = u.input.value.trim();
1169
+ if (!href) return;
1170
+ const text = textInput?.value ?? void 0;
1171
+ closeCurrent2();
1172
+ opts.onApply(href, text);
1173
+ };
1174
+ apply.addEventListener("click", submit);
1175
+ for (const input of [textInput, u.input]) {
1176
+ input?.addEventListener("keydown", (e) => {
1177
+ if (e.key === "Enter") {
1178
+ e.preventDefault();
1179
+ submit();
1180
+ }
1181
+ });
1182
+ }
1183
+ row.appendChild(apply);
1184
+ form.appendChild(row);
1185
+ el.appendChild(form);
1186
+ place(el, opts.anchor);
1187
+ u.input.focus();
1188
+ u.input.select();
1189
+ };
1190
+ document.body.appendChild(el);
1191
+ if (opts.existing) renderView(opts.existing);
1192
+ else renderForm(null);
1193
+ const onDown = (e) => {
1194
+ if (el.contains(e.target)) return;
1195
+ if (opts.key) {
1196
+ cancelPendingClose();
1197
+ pendingClose = setTimeout(closeCurrent2, 80);
1198
+ } else {
1199
+ closeCurrent2();
1200
+ }
1201
+ };
1202
+ const onKey = (e) => {
1203
+ if (e.key === "Escape") closeCurrent2();
1204
+ };
1205
+ const onScroll = () => closeCurrent2();
1206
+ setTimeout(() => {
1207
+ document.addEventListener("pointerdown", onDown, true);
1208
+ document.addEventListener("keydown", onKey, true);
1209
+ window.addEventListener("scroll", onScroll, true);
1210
+ }, 0);
1211
+ const dispose = () => {
1212
+ document.removeEventListener("pointerdown", onDown, true);
1213
+ document.removeEventListener("keydown", onKey, true);
1214
+ window.removeEventListener("scroll", onScroll, true);
1215
+ el.remove();
1216
+ };
1217
+ current2 = dispose;
1218
+ currentKey = opts.key ?? null;
1219
+ const handle = {
1220
+ close: () => {
1221
+ if (current2 === dispose) closeCurrent2();
1222
+ },
1223
+ isOpen: () => current2 === dispose
1224
+ };
1225
+ currentHandle = handle;
1226
+ return handle;
1227
+ }
1228
+
904
1229
  // packages/ui/src/lib/cell-properties.ts
905
1230
  var FILL_PRESETS = ["#FCEBEB", "#FAEEDA", "#EAF3DE", "#E6F1FB", "#EEEDFE", "#FBEAF0", "#E1F5EE", "#F1EFE8", "#D3D1C7"];
906
1231
  var PEN_COLORS = ["#000000", "#5F5E5A", "#B0B0B0", "#E24B4A", "#185FA5", "#1D9E75", "#BA7517"];
@@ -928,7 +1253,7 @@ var PRESETS = [
928
1253
  { key: "insideH", label: "Inside horizontal", sides: [0, 0, 0, 0, 1, 0], inside: true },
929
1254
  { key: "insideV", label: "Inside vertical", sides: [0, 0, 0, 0, 0, 1], inside: true }
930
1255
  ];
931
- var STYLE6 = `
1256
+ var STYLE7 = `
932
1257
  .bb-cp{display:flex;flex-direction:column;gap:16px;min-width:320px;color:var(--bb-ui-fg,#2c2c2a)}
933
1258
  .bb-cp *{box-sizing:border-box}
934
1259
  .bb-cp-label{font-size:12px;opacity:.7;margin-bottom:8px}
@@ -969,7 +1294,7 @@ var section = (label) => {
969
1294
  return sec;
970
1295
  };
971
1296
  function openCellProperties({ initial, singleCell, onApply }) {
972
- injectStyle("bb-ui-cellprops-styles", STYLE6);
1297
+ injectStyle("bb-ui-cellprops-styles", STYLE7);
973
1298
  let background = initial.background;
974
1299
  let vAlign = initial.vAlign;
975
1300
  let penWidthPt = 1;
@@ -1142,7 +1467,7 @@ function openCellProperties({ initial, singleCell, onApply }) {
1142
1467
  }
1143
1468
 
1144
1469
  // packages/ui/src/lib/table-grid.ts
1145
- var STYLE7 = `
1470
+ var STYLE8 = `
1146
1471
  .bb-grid{display:flex;flex-direction:column;gap:6px;user-select:none}
1147
1472
  .bb-grid-cells{display:grid;gap:2px}
1148
1473
  .bb-grid-cell{width:15px;height:15px;border:1px solid var(--bb-ui-border,#d8d6cf);border-radius:2px;background:var(--bb-ui-bg,#fff);cursor:pointer;padding:0}
@@ -1150,7 +1475,7 @@ var STYLE7 = `
1150
1475
  .bb-grid-label{font-size:12px;text-align:center;opacity:.7}
1151
1476
  `;
1152
1477
  function tableGridPicker(options) {
1153
- injectStyle("bb-ui-grid-styles", STYLE7);
1478
+ injectStyle("bb-ui-grid-styles", STYLE8);
1154
1479
  const maxRows = options.maxRows ?? 8;
1155
1480
  const maxCols = options.maxCols ?? 10;
1156
1481
  const root = document.createElement("div");
@@ -1195,5 +1520,6 @@ export {
1195
1520
  openCellProperties,
1196
1521
  promptDialog,
1197
1522
  showContextMenu,
1523
+ showLinkPanel,
1198
1524
  tableGridPicker
1199
1525
  };
@@ -1 +1 @@
1
- {"version":3,"file":"context-menu.d.ts","sourceRoot":"","sources":["../../src/lib/context-menu.ts"],"names":[],"mappings":"AAEA,mCAAmC;AACnC,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,IAAI,CAAC;IAChB,kCAAkC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AACD,MAAM,MAAM,gBAAgB,GAAG,eAAe,GAAG,WAAW,CAAC;AAE7D,MAAM,WAAW,iBAAiB;IAChC,KAAK,IAAI,IAAI,CAAC;CACf;AAuBD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,EAAE,EAAE,EAAE,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,iBAAiB,CA2E5G"}
1
+ {"version":3,"file":"context-menu.d.ts","sourceRoot":"","sources":["../../src/lib/context-menu.ts"],"names":[],"mappings":"AAEA,mCAAmC;AACnC,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,IAAI,CAAC;IAChB,kCAAkC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AACD,MAAM,MAAM,gBAAgB,GAAG,eAAe,GAAG,WAAW,CAAC;AAE7D,MAAM,WAAW,iBAAiB;IAChC,KAAK,IAAI,IAAI,CAAC;CACf;AAuBD;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,gBAAgB,EAAE,EAC3B,EAAE,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3B,iBAAiB,CAmGnB"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Floating link panel, anchored at the caret / selection (Google Docs-style).
3
+ *
4
+ * Three modes:
5
+ * - **view** — the caret sits inside an existing link: the href plus three
6
+ * icon actions (copy / edit / remove). Edit switches to the form.
7
+ * - **form with selection** — one URL field; Apply wraps the selection.
8
+ * - **form at a bare caret** — an optional "Text" field above the URL; left
9
+ * empty, the URL itself becomes the inserted text.
10
+ *
11
+ * The panel is one-shot DOM (no framework), closes on Escape / outside click /
12
+ * scroll, and positions with connected pairs: below the anchor first, flipped
13
+ * above when the viewport runs out.
14
+ */
15
+ export interface LinkPanelAnchor {
16
+ /** Caret top-left in viewport CSS px + the caret's rendered height. */
17
+ x: number;
18
+ y: number;
19
+ height: number;
20
+ }
21
+ export interface LinkPanelOptions {
22
+ anchor: LinkPanelAnchor;
23
+ /** Existing link under the caret → open in view mode (prefills the form). */
24
+ existing?: {
25
+ href: string;
26
+ text: string;
27
+ } | null;
28
+ /** A text range is selected → the Text field is hidden (the selection is
29
+ * the text). */
30
+ hasSelection?: boolean;
31
+ /** Identity for keep-alive: with a key, an outside click defers the close
32
+ * briefly, and re-showing the SAME key within that window keeps the
33
+ * existing panel untouched — so a caret moving within one link never
34
+ * re-creates it. Hosts pass e.g. the link's doc range. */
35
+ key?: string;
36
+ /** Apply: `text` is present only when the Text field was shown. */
37
+ onApply(href: string, text?: string): void;
38
+ onUnlink?(): void;
39
+ }
40
+ export interface LinkPanelHandle {
41
+ close(): void;
42
+ /** Whether this panel is still on screen (hosts skip redundant reopens —
43
+ * e.g. the caret moving WITHIN the same link should not repaint). */
44
+ isOpen(): boolean;
45
+ }
46
+ export declare function showLinkPanel(opts: LinkPanelOptions): LinkPanelHandle;
47
+ //# sourceMappingURL=link-panel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-panel.d.ts","sourceRoot":"","sources":["../../src/lib/link-panel.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,eAAe,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjD;qBACiB;IACjB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;+DAG2D;IAC3D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,QAAQ,CAAC,IAAI,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,IAAI,IAAI,CAAC;IACd;0EACsE;IACtE,MAAM,IAAI,OAAO,CAAC;CACnB;AAwED,wBAAgB,aAAa,CAAC,IAAI,EAAE,gBAAgB,GAAG,eAAe,CAgLrE"}
@@ -46,8 +46,28 @@ export interface ToolbarColor {
46
46
  /** Called with the picked colour (null = cleared). */
47
47
  onSelect: (color: string | null) => void;
48
48
  }
49
- /** A toolbar group entry: a command name (button) or a control (select/colour). */
50
- export type ToolbarEntry = string | ToolbarSelect | ToolbarColor;
49
+ /** One style card in a split button's dropdown: three preview markers, one per
50
+ * nesting level (e.g. ['1.', 'a.', 'i.']), rendered beside placeholder bars. */
51
+ export interface ToolbarSplitOption {
52
+ value: string;
53
+ rows: [string, string, string];
54
+ title?: string;
55
+ }
56
+ /** A split button: the main button runs the command `name` (toggle, with its
57
+ * active/enabled tracking and icon), and a narrow ▾ opens a dropdown of style
58
+ * cards. The host owns the parameterized command each card runs. */
59
+ export interface ToolbarSplit {
60
+ kind: 'split';
61
+ /** Command for the main button — also the icon/tooltip lookup key. */
62
+ name: string;
63
+ options: ToolbarSplitOption[];
64
+ /** Currently applied option value (highlights its card), or null. */
65
+ value: (state: EditorStateOf) => string | null;
66
+ /** Called when the user picks a card. */
67
+ onSelect: (value: string) => void;
68
+ }
69
+ /** A toolbar group entry: a command name (button) or a control (select/colour/split). */
70
+ export type ToolbarEntry = string | ToolbarSelect | ToolbarColor | ToolbarSplit;
51
71
  export interface ToolbarOptions {
52
72
  /** Groups rendered as separated clusters — command names (buttons) and/or
53
73
  * controls (selects). Defaults to marks then alignments, from the registry. */
@@ -1 +1 @@
1
- {"version":3,"file":"toolbar.d.ts","sourceRoot":"","sources":["../../src/lib/toolbar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,aAAa,EAEnB,MAAM,eAAe,CAAC;AAEvB;wCACwC;AACxC,MAAM,WAAW,WAAW;IAC1B,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;wEAEwE;AACxE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,QAAQ,CAAC;IACf,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC5C,qEAAqE;IACrE,KAAK,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,MAAM,CAAC;IACxC,0CAA0C;IAC1C,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;8EAC8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6DAA6D;IAC7D,KAAK,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,MAAM,GAAG,IAAI,CAAC;IAC/C,sDAAsD;IACtD,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CAC1C;AAED,mFAAmF;AACnF,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,aAAa,GAAG,YAAY,CAAC;AAEjE,MAAM,WAAW,cAAc;IAC7B;oFACgF;IAChF,MAAM,CAAC,EAAE,YAAY,EAAE,EAAE,CAAC;IAC1B,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACrC;AAED,+CAA+C;AAC/C,MAAM,WAAW,aAAa;IAC5B,OAAO,IAAI,IAAI,CAAC;CACjB;AA2GD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,UAAU,CAAC,OAAO,CAAC,GAC5B,MAAM,EAAE,EAAE,CAKZ;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,cAAmB,GAC3B,aAAa,CA4Pf"}
1
+ {"version":3,"file":"toolbar.d.ts","sourceRoot":"","sources":["../../src/lib/toolbar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,aAAa,EAEnB,MAAM,eAAe,CAAC;AAEvB;wCACwC;AACxC,MAAM,WAAW,WAAW;IAC1B,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;wEAEwE;AACxE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,QAAQ,CAAC;IACf,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC5C,qEAAqE;IACrE,KAAK,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,MAAM,CAAC;IACxC,0CAA0C;IAC1C,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;8EAC8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6DAA6D;IAC7D,KAAK,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,MAAM,GAAG,IAAI,CAAC;IAC/C,sDAAsD;IACtD,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CAC1C;AAED;iFACiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;qEAEqE;AACrE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,qEAAqE;IACrE,KAAK,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,MAAM,GAAG,IAAI,CAAC;IAC/C,yCAAyC;IACzC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,yFAAyF;AACzF,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,aAAa,GAAG,YAAY,GAAG,YAAY,CAAC;AAEhF,MAAM,WAAW,cAAc;IAC7B;oFACgF;IAChF,MAAM,CAAC,EAAE,YAAY,EAAE,EAAE,CAAC;IAC1B,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACrC;AAED,+CAA+C;AAC/C,MAAM,WAAW,aAAa;IAC5B,OAAO,IAAI,IAAI,CAAC;CACjB;AAyHD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,UAAU,CAAC,OAAO,CAAC,GAC5B,MAAM,EAAE,EAAE,CAKZ;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,cAAmB,GAC3B,aAAa,CAwWf"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shadow-garden/bapbong-ui",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "bapbong — framework-agnostic editor UI (toolbar/menubar) that mounts into a host element and binds to editor.commands",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -56,6 +56,6 @@
56
56
  }
57
57
  },
58
58
  "dependencies": {
59
- "@shadow-garden/bapbong-contracts": "^0.1.1"
59
+ "@shadow-garden/bapbong-contracts": "^0.3.0"
60
60
  }
61
61
  }