@shadow-garden/bapbong-ui
Framework-agnostic editor UI (L3). Vanilla DOM widgets that mount into a
host element and bind to editor.commands — a host framework only supplies the
element and calls mount/destroy. The same lib works in Angular, React,
Svelte, or plain HTML.
- Scope:
scope:app - Depends on:
@shadow-garden/bapbong-contractsonly. The editor surface is a structuralEditorHandleinterface (whichBapbongEditorsatisfies), and the editor-state type is derived from theCommandcontract — so this package imports neither the editor nor ProseMirror (same decoupling as the plugin contract).
Toolbar
import { mountToolbar } from '@shadow-garden/bapbong-ui';
const handle = mountToolbar(hostEl, editor); // renders + wires everything
// …
handle.destroy(); // removes DOM + listeners
- Renders a button per command from the registry; the lib owns labels/icons
(the headless
Commandcarries none), groups, styling and active state. - Click →
command.run(state, dispatch); active/disabled tracked viaeditor.onChange+command.isActive/isEnabled. - Customise with
{ groups, items }— groups reference command names; default is marks then alignments, derived from the registry. - Theme via CSS variables:
--bb-ui-bg,--bb-ui-fg,--bb-ui-border,--bb-ui-hover,--bb-ui-active-bg,--bb-ui-active-fg.
Menubar
import { mountMenubar } from '@shadow-garden/bapbong-ui';
const handle = mountMenubar(hostEl, editor); // top-level titles → dropdowns
handle.destroy();
- Top-level titles open dropdowns; active toggles show a check. Click-outside / Escape close; hovering another title switches; submenus open on hover.
- Entries are a declarative tree (
{ menus, labels }). AMenuEntryis one of:'separator'{ command, label? }— runs a registry command (active check + enabled).{ label, run, isActive?, isEnabled?, shortcut? }— a host action (File > Open, View > comment mode, Help…).{ label, submenu: MenuEntry[] }— a nested dropdown.{ label, widget: (close) => HTMLElement }— custom flyout content.
tableGridPicker({ maxRows, maxCols, onPick })— a Word/Docs-style size grid to drop into awidgetentry (Insert > Table).
Context menu
import { showContextMenu } from '@shadow-garden/bapbong-ui';
showContextMenu(
[{ label: 'Cut', run, shortcut: '⌘X' }, 'separator', { label: 'Delete', run, enabled: false }],
{ x: ev.clientX, y: ev.clientY },
);
- A floating menu at a viewport point (one-shot rows + separators). Viewport-
clamped; closes on select / outside click / Escape / scroll; only one open at
a time. The host builds the entries from the editor's
contextmenupointer event (position + target).
Dialog
A generic overlay primitive (built on the native <dialog> — modal mode gets
focus-trap / Esc / backdrop for free). Reused by find, prompts, shortcuts, and
the cell-properties dialog — one primitive, not N bespoke widgets.
import { Dialog, promptDialog } from '@shadow-garden/bapbong-ui';
const dlg = new Dialog({ title: 'Cell properties', modal: true });
dlg.setContent(myFormElement);
dlg.open(); // open() / close() / onClose() / destroy()
const url = await promptDialog({ title: 'Insert link', placeholder: 'https://…' });
Pass { anchor: () => el.getBoundingClientRect() } to pin a non-modal dialog's
top-right just inside a rect (e.g. the canvas viewport) instead of the screen
corner; it re-positions on scroll/resize.
Find dialog
import { createFindDialog } from '@shadow-garden/bapbong-ui';
const find = createFindDialog(editor.find); // bound to the find plugin
find.open(); // e.g. from an Edit ▸ Find menu item / Ctrl+F
find.destroy();
- Find/replace inside a (non-modal)
Dialog, bound to a structuralFindHandle(the editor'sfindplugin satisfies it), so the package never imports the editor. Query input, match count, prev/next, replace + replace-all; closing clears the search. i18n via{ labels }. - Opens on Ctrl/Cmd+F (pre-empting the browser's native find); disable with
{ shortcut: false }. Pin it with{ anchor }(see Dialog).
Build / test
pnpm nx build @shadow-garden/bapbong-ui
pnpm nx test @shadow-garden/bapbong-ui # Node (DOM behaviour verified in-browser)