Docs / Component Reference

Component Reference

Complete reference for all custom components shipped with the enterprise theme.

Components

ComponentCSSJS
actionitemscomponents/actionitems/actionitems.csscomponents/actionitems/actionitems.js
activityfeedcomponents/activityfeed/activityfeed.csscomponents/activityfeed/activityfeed.js
anchorlayoutcomponents/anchorlayout/anchorlayout.csscomponents/anchorlayout/anchorlayout.js
anglepickercomponents/anglepicker/anglepicker.csscomponents/anglepicker/anglepicker.js
applaunchercomponents/applauncher/applauncher.csscomponents/applauncher/applauncher.js
auditlogviewercomponents/auditlogviewer/auditlogviewer.csscomponents/auditlogviewer/auditlogviewer.js
bannerbarcomponents/bannerbar/bannerbar.csscomponents/bannerbar/bannerbar.js
borderlayoutcomponents/borderlayout/borderlayout.csscomponents/borderlayout/borderlayout.js
boxlayoutcomponents/boxlayout/boxlayout.csscomponents/boxlayout/boxlayout.js
breadcrumbcomponents/breadcrumb/breadcrumb.csscomponents/breadcrumb/breadcrumb.js
cardlayoutcomponents/cardlayout/cardlayout.csscomponents/cardlayout/cardlayout.js
codeeditorcomponents/codeeditor/codeeditor.csscomponents/codeeditor/codeeditor.js
colorpickercomponents/colorpicker/colorpicker.csscomponents/colorpicker/colorpicker.js
columnspickercomponents/columnspicker/columnspicker.csscomponents/columnspicker/columnspicker.js
commandpalettecomponents/commandpalette/commandpalette.csscomponents/commandpalette/commandpalette.js
commentoverlaycomponents/commentoverlay/commentoverlay.csscomponents/commentoverlay/commentoverlay.js
confirmdialogcomponents/confirmdialog/confirmdialog.csscomponents/confirmdialog/confirmdialog.js
contextmenucomponents/contextmenu/contextmenu.csscomponents/contextmenu/contextmenu.js
conversationcomponents/conversation/conversation.csscomponents/conversation/conversation.js
cronpickercomponents/cronpicker/cronpicker.csscomponents/cronpicker/cronpicker.js
datagridcomponents/datagrid/datagrid.csscomponents/datagrid/datagrid.js
datepickercomponents/datepicker/datepicker.csscomponents/datepicker/datepicker.js
diagramenginecomponents/diagramengine/diagramengine.csscomponents/diagramengine/diagramengine.js
docklayoutcomponents/docklayout/docklayout.csscomponents/docklayout/docklayout.js
docviewercomponents/docviewer/docviewer.csscomponents/docviewer/docviewer.js
durationpickercomponents/durationpicker/durationpicker.csscomponents/durationpicker/durationpicker.js
editablecomboboxcomponents/editablecombobox/editablecombobox.csscomponents/editablecombobox/editablecombobox.js
emptystatecomponents/emptystate/emptystate.csscomponents/emptystate/emptystate.js
errordialogcomponents/errordialog/errordialog.csscomponents/errordialog/errordialog.js
explorerpickercomponents/explorerpicker/explorerpicker.csscomponents/explorerpicker/explorerpicker.js
facetsearchcomponents/facetsearch/facetsearch.csscomponents/facetsearch/facetsearch.js
fileexplorercomponents/fileexplorer/fileexplorer.csscomponents/fileexplorer/fileexplorer.js
fileuploadcomponents/fileupload/fileupload.csscomponents/fileupload/fileupload.js
flexgridlayoutcomponents/flexgridlayout/flexgridlayout.csscomponents/flexgridlayout/flexgridlayout.js
flowlayoutcomponents/flowlayout/flowlayout.csscomponents/flowlayout/flowlayout.js
fontdropdowncomponents/fontdropdown/fontdropdown.csscomponents/fontdropdown/fontdropdown.js
formdialogcomponents/formdialog/formdialog.csscomponents/formdialog/formdialog.js
gaugecomponents/gauge/gauge.csscomponents/gauge/gauge.js
gradientpickercomponents/gradientpicker/gradientpicker.csscomponents/gradientpicker/gradientpicker.js
graphcanvascomponents/graphcanvas/graphcanvas.csscomponents/graphcanvas/graphcanvas.js
graphlegendcomponents/graphlegend/graphlegend.csscomponents/graphlegend/graphlegend.js
graphminimapcomponents/graphminimap/graphminimap.csscomponents/graphminimap/graphminimap.js
graphtoolbarcomponents/graphtoolbar/graphtoolbar.csscomponents/graphtoolbar/graphtoolbar.js
gridlayoutcomponents/gridlayout/gridlayout.csscomponents/gridlayout/gridlayout.js
guidedtourcomponents/guidedtour/guidedtour.csscomponents/guidedtour/guidedtour.js
helpdrawercomponents/helpdrawer/helpdrawer.csscomponents/helpdrawer/helpdrawer.js
helptooltipcomponents/helptooltip/helptooltip.csscomponents/helptooltip/helptooltip.js
inlinetoolbarcomponents/inlinetoolbar/inlinetoolbar.csscomponents/inlinetoolbar/inlinetoolbar.js
latexeditorcomponents/latexeditor/latexeditor.csscomponents/latexeditor/latexeditor.js
layerlayoutcomponents/layerlayout/layerlayout.csscomponents/layerlayout/layerlayout.js
lineendingpickercomponents/lineendingpicker/lineendingpicker.csscomponents/lineendingpicker/lineendingpicker.js
lineshapepickercomponents/lineshapepicker/lineshapepicker.csscomponents/lineshapepicker/lineshapepicker.js
linetypepickercomponents/linetypepicker/linetypepicker.csscomponents/linetypepicker/linetypepicker.js
linewidthpickercomponents/linewidthpicker/linewidthpicker.csscomponents/linewidthpicker/linewidthpicker.js
logconsolecomponents/logconsole/logconsole.csscomponents/logconsole/logconsole.js
logutilitycomponents/logutility/logutility.csscomponents/logutility/logutility.js
magnifiercomponents/magnifier/magnifier.csscomponents/magnifier/magnifier.js
marginspickercomponents/marginspicker/marginspicker.csscomponents/marginspicker/marginspicker.js
markdowneditorcomponents/markdowneditor/markdowneditor.csscomponents/markdowneditor/markdowneditor.js
markdownrenderercomponents/markdownrenderer/markdownrenderer.csscomponents/markdownrenderer/markdownrenderer.js
maskedentrycomponents/maskedentry/maskedentry.csscomponents/maskedentry/maskedentry.js
multiselectcombocomponents/multiselectcombo/multiselectcombo.csscomponents/multiselectcombo/multiselectcombo.js
notificationcentercomponents/notificationcenter/notificationcenter.csscomponents/notificationcenter/notificationcenter.js
orientationpickercomponents/orientationpicker/orientationpicker.csscomponents/orientationpicker/orientationpicker.js
peoplepickercomponents/peoplepicker/peoplepicker.csscomponents/peoplepicker/peoplepicker.js
periodpickercomponents/periodpicker/periodpicker.csscomponents/periodpicker/periodpicker.js
permissionmatrixcomponents/permissionmatrix/permissionmatrix.csscomponents/permissionmatrix/permissionmatrix.js
personchipcomponents/personchip/personchip.csscomponents/personchip/personchip.js
pillcomponents/pill/pill.csscomponents/pill/pill.js
presenceindicatorcomponents/presenceindicator/presenceindicator.csscomponents/presenceindicator/presenceindicator.js
progressmodalcomponents/progressmodal/progressmodal.csscomponents/progressmodal/progressmodal.js
prompttemplatemanagercomponents/prompttemplatemanager/prompttemplatemanager.csscomponents/prompttemplatemanager/prompttemplatemanager.js
propertyinspectorcomponents/propertyinspector/propertyinspector.csscomponents/propertyinspector/propertyinspector.js
reasoningaccordioncomponents/reasoningaccordion/reasoningaccordion.csscomponents/reasoningaccordion/reasoningaccordion.js
relationshipmanagercomponents/relationshipmanager/relationshipmanager.csscomponents/relationshipmanager/relationshipmanager.js
ribboncomponents/ribbon/ribbon.csscomponents/ribbon/ribbon.js
ribbonbuildercomponents/ribbonbuilder/ribbonbuilder.csscomponents/ribbonbuilder/ribbonbuilder.js
richtextinputcomponents/richtextinput/richtextinput.csscomponents/richtextinput/richtextinput.js
rulercomponents/ruler/ruler.csscomponents/ruler/ruler.js
searchboxcomponents/searchbox/searchbox.csscomponents/searchbox/searchbox.js
sharedialogcomponents/sharedialog/sharedialog.csscomponents/sharedialog/sharedialog.js
sidebarcomponents/sidebar/sidebar.csscomponents/sidebar/sidebar.js
sizespickercomponents/sizespicker/sizespicker.csscomponents/sizespicker/sizespicker.js
skeletonloadercomponents/skeletonloader/skeletonloader.csscomponents/skeletonloader/skeletonloader.js
slidercomponents/slider/slider.csscomponents/slider/slider.js
smarttextinputcomponents/smarttextinput/smarttextinput.csscomponents/smarttextinput/smarttextinput.js
spacingpickercomponents/spacingpicker/spacingpicker.csscomponents/spacingpicker/spacingpicker.js
spinemapcomponents/spinemap/spinemap.csscomponents/spinemap/spinemap.js
splitlayoutcomponents/splitlayout/splitlayout.csscomponents/splitlayout/splitlayout.js
sprintpickercomponents/sprintpicker/sprintpicker.csscomponents/sprintpicker/sprintpicker.js
stacklayoutcomponents/stacklayout/stacklayout.csscomponents/stacklayout/stacklayout.js
statusbadgecomponents/statusbadge/statusbadge.csscomponents/statusbadge/statusbadge.js
statusbarcomponents/statusbar/statusbar.csscomponents/statusbar/statusbar.js
steppercomponents/stepper/stepper.csscomponents/stepper/stepper.js
symbolpickercomponents/symbolpicker/symbolpicker.csscomponents/symbolpicker/symbolpicker.js
tabbedpanelcomponents/tabbedpanel/tabbedpanel.csscomponents/tabbedpanel/tabbedpanel.js
taggercomponents/tagger/tagger.csscomponents/tagger/tagger.js
themetogglecomponents/themetoggle/themetoggle.csscomponents/themetoggle/themetoggle.js
timelinecomponents/timeline/timeline.csscomponents/timeline/timeline.js
timepickercomponents/timepicker/timepicker.csscomponents/timepicker/timepicker.js
timezonepickercomponents/timezonepicker/timezonepicker.csscomponents/timezonepicker/timezonepicker.js
toastcomponents/toast/toast.csscomponents/toast/toast.js
toolbarcomponents/toolbar/toolbar.csscomponents/toolbar/toolbar.js
toolcolorpickercomponents/toolcolorpicker/toolcolorpicker.csscomponents/toolcolorpicker/toolcolorpicker.js
treegridcomponents/treegrid/treegrid.csscomponents/treegrid/treegrid.js
treeviewcomponents/treeview/treeview.csscomponents/treeview/treeview.js
typebadgecomponents/typebadge/typebadge.csscomponents/typebadge/typebadge.js
usermenucomponents/usermenu/usermenu.csscomponents/usermenu/usermenu.js
visualtableeditorcomponents/visualtableeditor/visualtableeditor.csscomponents/visualtableeditor/visualtableeditor.js
workspaceswitchercomponents/workspaceswitcher/workspaceswitcher.csscomponents/workspaceswitcher/workspaceswitcher.js

ActionItems

A rich, stateful action item list with status lifecycle tracking, person assignments, priority badges, due dates, comment slots, tags, hierarchical numbering, inline editing, section-based grouping, drag-and-drop reordering, nesting, keyboard navigation, multi-select, bulk operations, faceted filtering, sorting, clipboard, and export.

Quick Start

<link rel="stylesheet" href="components/actionitems/actionitems.css">
<script src="components/actionitems/actionitems.js"></script>
const list = createActionItems({
    container: "my-container",
    items: [
        {
            id: "1",
            index: 1,
            order: 1,
            content: "Review Q3 budget",
            status: "not-started",
            priority: "high",
            dueDate: "2026-03-20",
            tags: [],
            commentCount: 0,
            createdAt: new Date().toISOString(),
            updatedAt: new Date().toISOString()
        }
    ],
    onStatusChange: (id, oldS, newS) => console.log(id, oldS, "->", newS)
});

Features

Configuration Options

OptionTypeDefaultDescription
containerstring | HTMLElementrequiredContainer element or ID string
itemsActionItem[][]Initial items to render
mode"full" | "compact""full"Display mode
groupByStatusbooleantrueGroup items by status with section headers
showPrioritybooleantrueShow priority badges
showDueDatesbooleantrueShow due date display
showCommentsbooleantrueShow comment count and comment slot
showTagsbooleantrueShow tags/labels on items
allowNestingbooleantrueAllow sub-item nesting
allowCreatebooleantrueAllow inline creation of new items
allowReorderbooleantrueAllow drag-and-drop reordering
defaultSortSortOption"order"Default sort order
placeholderstring"Add a new item..."Placeholder text for new items
emptyMessagestring"No items yet"Message for empty state

Public API

MethodSignatureDescription
addItem(item: Partial<ActionItem>) => ActionItemAdd a new item; returns the created item with generated ID and timestamps
removeItem(itemId: string) => voidRemove an item by ID
updateItem(itemId: string, changes: Partial<ActionItem>) => voidUpdate an item's properties
setAssignee(itemId: string, person?: ActionItemPerson) => voidSet or clear the assignee
setCommentCount(itemId: string, count: number) => voidSet the comment count badge value
getItems() => ActionItem[]Get all items
getItem(itemId: string) => ActionItem | nullGet a single item by ID
getItemsByStatus(status: ActionItemStatus) => ActionItem[]Get items filtered by status
getSelectedIds() => string[]Get selected item IDs
setSelection(ids: string[]) => voidSet selection programmatically
clearSelection() => voidClear all selections
toggleSection(status: ActionItemStatus, expanded: boolean) => voidExpand or collapse a status section
setFilter(filter: ActionItemFilter) => voidApply a faceted filter
clearFilters() => voidClear all filters
setSort(sort: SortOption) => voidSet the sort order
export(format: "json" | "markdown") => stringExport items in the specified format
importMarkdown(markdown: string) => ActionItem[]Import items from markdown checklist format
scrollToItem(itemId: string) => voidScroll to a specific item
getElement() => HTMLElementGet the root DOM element
destroy() => voidDestroy the component and clean up

Event Callbacks

CallbackSignatureDescription
onItemCreate(item: ActionItem) => voidFired when a new item is created
onItemUpdate(itemId: string, changes: Partial<ActionItem>) => voidFired when any item property changes
onItemDelete(itemId: string) => voidFired when an item is deleted
onStatusChange(itemId, oldStatus, newStatus) => voidFired when an item's status changes
onContentEdit(itemId: string, newContent: string) => voidFired when item content is edited
onCommentToggle(itemId: string, expanded: boolean) => voidFired when a comment slot is toggled
onRenderCommentSlot(itemId: string, container: HTMLElement) => voidFired to render comments into the slot
onAssignmentRequest(itemId, currentAssignee?) => voidFired when the user clicks the assignee chip
onPriorityChange(itemId: string, priority?: ActionItemPriority) => voidFired when priority changes
onDueDateChange(itemId: string, dueDate?: string) => voidFired when due date changes
onSelectionChange(selectedIds: string[]) => voidFired on selection changes
onTagChange(itemId: string, tags: ActionItemTag[]) => voidFired on tag add/remove
onItemReorder(itemId, newOrder, newParentId?) => voidFired when an item is reordered via drag-and-drop
onBulkStatusChange(itemIds: string[], newStatus) => voidFired on bulk status change
onBulkDelete(itemIds: string[]) => voidFired on bulk delete request
onBulkAssign(itemIds: string[], currentAssignee?) => voidFired on bulk assign request
onExport(format, items: ActionItem[]) => voidFired on export request

Keyboard Shortcuts

KeyAction
Arrow Up / DownNavigate between items
Ctrl+Shift+Arrow Up / DownMove item up/down in order
SpaceCycle focused item status (Not Started -> In Progress -> Done)
EnterBegin inline editing on focused item
EscapeCancel editing or clear selection
TabIndent item (nest as sub-item)
Shift+TabOutdent item (un-nest)
Delete / BackspaceDelete focused or selected items
Ctrl+CCopy selected items as markdown checklist
Ctrl+XCut selected items
Ctrl+VPaste items from clipboard
Ctrl+ASelect all items

Compact Mode

createActionItems({
    container: "sidebar-tasks",
    items: myItems,
    mode: "compact",
    groupByStatus: false,
    showComments: false,
    showTags: false,
    allowNesting: false,
    placeholder: "Quick add..."
});

Compact mode reduces padding and hides comment slots and tag displays for use in space-constrained contexts such as sidebar panels and dashboard widgets.

Sort Options

ValueLabel
"order"Manual order
"created-asc"Created (oldest)
"created-desc"Created (newest)
"modified"Last modified
"priority-desc"Priority (high first)
"priority-asc"Priority (low first)
"due-date-asc"Due date (soonest)
"due-date-desc"Due date (latest)
"assignee-asc"Assignee (A-Z)
"assignee-desc"Assignee (Z-A)

Integration Notes

Status Lifecycle

Not Started  ──>  In Progress  ──>  Done
    ⬡                ◐              ✓

Items cycle through statuses via click on the status indicator or Space key. The "archived" status is available programmatically via updateItem() but does not appear in the cycle.


ActivityFeed

Social-style activity feed with date grouping, infinite scroll, real-time additions, and compact mode.

Usage

<link rel="stylesheet" href="components/activityfeed/activityfeed.css">
<script src="components/activityfeed/activityfeed.js"></script>
const feed = createActivityFeed({
    events: [
        {
            id: "1",
            actor: { id: "u1", name: "Jane Doe" },
            action: "commented on",
            target: "Project Alpha",
            targetUrl: "/projects/alpha",
            timestamp: new Date(),
            eventType: "comment",
            content: "Looking great! Let's ship this week."
        }
    ],
    height: "400px",
    onLoadMore: async () => fetchMoreEvents(),
    onEventClick: (ev) => console.log("Clicked:", ev.id),
}, "my-container");

Options

OptionTypeDefaultDescription
eventsActivityEvent[][]Initial events
groupByDatebooleantrueGroup by Today/Yesterday/This Week/Earlier
showAvatarsbooleantrueShow actor avatars
compactbooleanfalseCompact mode
maxInitialEventsnumber20Max initial render
heightstring"auto"Container height
onLoadMore() => Promise<ActivityEvent[]>-Infinite scroll callback
onEventClick(ev) => void-Event card click
onActorClick(actor) => void-Actor name click
onTargetClick(ev) => void-Target link click

API

MethodDescription
show(containerId)Mount to container
hide()Remove from DOM
destroy()Full cleanup
addEvent(ev)Prepend real-time event
addEvents(evs)Append events (pagination)
setEvents(evs)Replace all events
getEvents()Get current events
clear()Remove all events
refresh()Re-render
scrollToTop()Scroll to top

Event Types

TypeIconColour
commentbi-chat-left-textBlue
status_changebi-arrow-repeatPurple
assignmentbi-person-plusTeal
creationbi-plus-circleGreen
deletionbi-trashRed
uploadbi-cloud-uploadCyan
custombi-circleGray

AnchorLayout

A constraint-based layout container that positions children by declaring anchor relationships between child edges and container edges. Children stretch or float based on which edges are anchored. Uses CSS position: relative on the container and position: absolute on children. Inspired by Android ConstraintLayout, Qt AnchorLayout, and WPF Canvas with anchoring.

Assets

AssetPath
CSScomponents/anchorlayout/anchorlayout.css
JScomponents/anchorlayout/anchorlayout.js
Typescomponents/anchorlayout/anchorlayout.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/anchorlayout/anchorlayout.css">
<script src="components/anchorlayout/anchorlayout.js"></script>

<script>
    // Pin a button to the bottom-right corner
    var saveBtn = document.createElement("button");
    saveBtn.textContent = "Save";
    saveBtn.className = "btn btn-primary";

    var layout = createAnchorLayout({
        height: "400px",
        width: "100%",
        children: [
            {
                child: saveBtn,
                anchorBottom: 16,
                anchorRight: 16
            }
        ]
    });
</script>

How It Works

AnchorLayout creates a position: relative container. Each child is wrapped in a position: absolute div whose top, bottom, left, right, and transform properties are set based on anchor declarations.

Container (position: relative)
┌──────────────────────────────────────────────────────────┐
│                                                          │
│  anchorTop: 10 ──────────────────── anchorTop: 10        │
│  anchorLeft: 10                     anchorRight: 10      │
│  ┌──────────┐                       ┌──────────┐        │
│  │ Child A  │                       │ Child B  │        │
│  │ (floats  │                       │ (floats  │        │
│  │  top-left│                       │ top-right│        │
│  └──────────┘                       └──────────┘        │
│                                                          │
│                  ┌──────────┐                            │
│                  │ Child C  │ anchorCenterH: 0           │
│                  │ (centered│ anchorCenterV: 0           │
│                  │  both)   │                            │
│                  └──────────┘                            │
│                                                          │
│  anchorTop: 0    ┌────────────────────────────┐          │
│  anchorBottom: 0 │ Child D (stretches         │          │
│  anchorLeft: 0   │  to fill — all 4 edges     │          │
│                  │  anchored)                  │          │
│                  └────────────────────────────┘          │
│                                                          │
└──────────────────────────────────────────────────────────┘

Options

AnchorLayoutOptions

OptionTypeDefaultDescription
idstringautoCustom element ID
childrenAnchorChildConfig[][]Initial children with anchor constraints
paddingstringContainer padding (CSS value)
cssClassstringAdditional CSS classes
heightstringHeight CSS value
widthstringWidth CSS value
onLayoutChange(state) => voidFired on resize events

AnchorChildConfig

PropertyTypeDefaultDescription
childHTMLElement | ComponentChild element or component
anchorTopnumber | stringOffset from top edge (px or CSS value)
anchorBottomnumber | stringOffset from bottom edge
anchorLeftnumber | stringOffset from left edge
anchorRightnumber | stringOffset from right edge
anchorCenterHnumber | stringCenter horizontally with optional offset
anchorCenterVnumber | stringCenter vertically with optional offset
minWidthnumber | stringMinimum width constraint
maxWidthnumber | stringMaximum width constraint
minHeightnumber | stringMinimum height constraint
maxHeightnumber | stringMaximum height constraint

Anchor Rules

Dual-Edge Stretching

When both opposing edges are anchored, the child stretches to fill the space between them.

// Stretches horizontally between 10px from left and 10px from right
{ child: panel, anchorLeft: 10, anchorRight: 10 }

// Stretches both directions — fills the entire container with 20px margin
{ child: overlay, anchorTop: 20, anchorBottom: 20, anchorLeft: 20, anchorRight: 20 }

Single-Edge Floating

When only one edge is anchored, the child floats at that offset and uses its natural size.

// Floats at top-left corner
{ child: logo, anchorTop: 0, anchorLeft: 0 }

// Floats at bottom-right with 16px inset
{ child: fab, anchorBottom: 16, anchorRight: 16 }

Center Anchoring

Use anchorCenterH and anchorCenterV to center a child along one or both axes. The value is an optional offset from center.

// Centered both horizontally and vertically
{ child: spinner, anchorCenterH: 0, anchorCenterV: 0 }

// Centered horizontally, 30px from top
{ child: title, anchorCenterH: 0, anchorTop: 30 }

// Centered vertically with 20px rightward offset
{ child: sidebar, anchorCenterV: 0, anchorCenterH: 20 }

Note: Center anchoring uses CSS transform: translate(). When both anchorCenterH and anchorCenterV are set, they combine into a single translate(-50%, -50%). Non-zero offsets are applied via margin-left or margin-top.

Public API

MethodReturnsDescription
show(container?)voidAppend to container and display
hide()voidRemove from DOM (preserves state)
destroy()voidFull cleanup, destroy all children
getRootElement()HTMLElement | nullThe root container
isVisible()booleanWhether the layout is displayed
setContained(value)voidSet contained mode
addChild(config)voidAdd a child with anchor constraints
removeChild(index)voidRemove child by index
updateAnchors(index, anchors)voidUpdate anchor constraints for a child
getChildCount()numberNumber of children
getState()AnchorLayoutStateSerialisable state snapshot
setState(state)voidNo-op (anchors are config-driven)

AnchorLayoutState

interface AnchorLayoutState {
    childCount: number;
}

Composability

AnchorLayout implements the standard layout container contract. Any component with show(container) / hide() / destroy() can be used as a child. Plain HTMLElements are also supported.

// Pin a toolbar at the top, a status bar at the bottom,
// and fill the center with a content panel
var layout = new AnchorLayout({
    height: "100vh",
    width: "100%",
    children: [
        { child: toolbar, anchorTop: 0, anchorLeft: 0, anchorRight: 0 },
        { child: content, anchorTop: 48, anchorBottom: 32, anchorLeft: 0, anchorRight: 0 },
        { child: statusBar, anchorBottom: 0, anchorLeft: 0, anchorRight: 0 }
    ]
});

Global Exports

When loaded via <script> tag:

CSS Classes

ClassElementDescription
.anchorlayoutRootRelative-positioned container
.anchorlayout-childWrapperAbsolute-positioned child wrapper

AnglePicker

A circular dial input for selecting angles from 0 to 360 degrees. Supports both inline (always-visible dial for property panels) and dropdown (compact trigger button for toolbars) modes. Includes optional live shadow preview, tick mark labels, and snap-to-increment dragging.

Usage

Inline Mode

<link rel="stylesheet" href="components/anglepicker/anglepicker.css">
<script src="components/anglepicker/anglepicker.js"></script>

<div id="my-angle-picker"></div>

<script>
var picker = createAnglePicker("my-angle-picker", {
    value: 225,
    mode: "inline",
    showPreview: true,
    onChange: function(angle) {
        console.log("Angle:", angle + "°");
    }
});
</script>

Dropdown Mode

<div id="toolbar-angle"></div>

<script>
var picker = createAnglePicker("toolbar-angle", {
    value: 45,
    mode: "dropdown",
    size: "sm",
    onChange: function(angle) {
        console.log("Shadow angle:", angle);
    }
});
</script>

Options

OptionTypeDefaultDescription
valuenumber0Initial angle in degrees (0–359)
mode"inline" | "dropdown""inline"Display mode
size"sm" | "md" | "lg""md"Size variant
stepnumber1Arrow key increment in degrees
snapStepnumber15Shift+drag / Shift+arrow snap increment
showTicksbooleantrueShow tick marks on dial
tickLabels"none" | "degrees" | "compass""none"Tick label display mode
showInputbooleantrueShow editable center input
showPreviewbooleanfalseShow live shadow preview square
previewDistancenumber6Shadow offset distance in px
previewBlurnumber8Shadow blur radius in px
previewColorstring"rgba(0,0,0,0.4)"Shadow color
disabledbooleanfalseDisable the picker
onChange(angle: number) => voidFires on angle change
onOpen() => voidFires when dropdown opens
onClose() => voidFires when dropdown closes

API

MethodReturnsDescription
getValue()numberCurrent angle (0–359)
setValue(angle)voidSet angle programmatically
open()voidOpen dropdown (dropdown mode)
close()voidClose dropdown (dropdown mode)
enable()voidEnable the picker
disable()voidDisable the picker
getElement()HTMLElement | nullRoot DOM element
destroy()voidTear down and remove from DOM

Keyboard

Dial Focused

KeyAction
Right / UpIncrease by step (default 1°)
Left / DownDecrease by step (default 1°)
Shift + arrowChange by snapStep (default 15°)
HomeJump to 0°
EndJump to 359°

Dropdown Mode

KeyAction
ArrowDown / EnterOpen dropdown
EscapeClose dropdown

Tick Labels

Shadow Preview

When showPreview: true, a small square appears beside the dial showing a live CSS box-shadow at the currently selected angle. The shadow distance, blur, and color are configurable.

Sizes

SizeDial diameter
sm80px
md120px
lg160px

AppLauncher

Grid-based application launcher with three view modes: dropdown (waffle icon trigger), modal (centered overlay), and fullpage (inline with sidebar). Supports search, favourites, recent apps, categories, badges, and full 2D grid keyboard navigation.

Quick Start

<link rel="stylesheet" href="css/custom.css">
<link rel="stylesheet" href="components/applauncher/applauncher.css">
<script src="components/applauncher/applauncher.js"></script>
// Dropdown mode (default)
const launcher = createAppLauncher({
    apps: [
        { id: "crm", name: "CRM", icon: "bi bi-people" },
        { id: "mail", name: "Mail", icon: "bi bi-envelope" },
        { id: "files", name: "Files", icon: "bi bi-folder" },
    ],
    activeAppId: "crm",
    onSelect: (app) => console.log("Selected:", app.name),
}, "launcher-container");

// Modal mode
const modal = createAppLauncher({
    apps: myApps,
    mode: "modal",
    categories: [
        { id: "prod", label: "Productivity", icon: "bi bi-lightning" },
        { id: "admin", label: "Admin", icon: "bi bi-gear" },
    ],
    onSelect: (app) => window.location.href = app.url,
}, "modal-trigger-container");

// Fullpage mode
const fullpage = createAppLauncher({
    apps: myApps,
    mode: "fullpage",
    categories: myCategories,
}, "fullpage-container");

Options

OptionTypeDefaultDescription
appsAppItem[]requiredList of applications
categoriesAppCategory[]undefinedCategory groupings
activeAppIdstringundefinedCurrently active app ID
mode"dropdown" | "modal" | "fullpage""dropdown"View mode
columnsnumber3/4/4Grid columns (mode-dependent default)
showSearchbooleantrueShow search input
showFavoritesbooleantrueShow favourites section
showRecentbooleantrueShow recent apps section
maxRecentnumber6Max recent apps
showCategoriesbooleantrueShow category tabs/sidebar
placeholderstring"Search apps..."Search placeholder
triggerIconstring"bi bi-grid-3x3-gap"Trigger icon class
triggerLabelstring"Apps"Trigger button text
showTriggerLabelbooleantrueShow trigger label text
size"sm" | "default" | "lg""default"Size variant
favoritesKeystring"applauncher-favorites"localStorage key
recentKeystring"applauncher-recent"localStorage key
cssClassstringundefinedAdditional root CSS classes
keyBindingsRecord<string, string>See belowKey binding overrides
onSelect(app: AppItem) => voidundefinedApp selection callback
onSearch(query: string) => Promise<AppItem[]>undefinedAsync search
onFavoriteToggle(id: string, isFav: boolean) => voidundefinedFavourite callback
onOpen() => voidundefinedOpen callback
onClose() => voidundefinedClose callback

AppItem

PropertyTypeDescription
idstringUnique ID
namestringDisplay name
descriptionstring?Optional description
iconstring?Bootstrap icon class
iconUrlstring?Image URL (overrides icon)
urlstring?Navigation URL
categorystring?Category ID
badgestring?Badge text ("NEW", "3")
badgeVariant"info" | "success" | "warning" | "danger"Badge colour
disabledboolean?Disabled state
dataRecord<string, unknown>?Custom data

Methods

MethodReturnsDescription
show(containerId)voidAppend to container
hide()voidRemove from DOM
destroy()voidFull teardown
open()voidOpen dropdown/modal
close()voidClose dropdown/modal
isOpen()booleanOpen state
getApps()AppItem[]Current app list
setApps(apps)voidReplace app list
addApp(app)voidAdd single app
removeApp(id)voidRemove by ID
updateApp(id, updates)voidPartial update
getActiveAppId()stringActive app ID
setActiveAppId(id)voidSet active app
getFavorites()string[]Favourite IDs
setFavorites(ids)voidSet favourites
toggleFavorite(id)voidToggle favourite
clearFavorites()voidClear all favourites
getRecent()string[]Recent IDs
clearRecent()voidClear recent list
setCategories(cats)voidUpdate categories
setSearchQuery(q)voidProgrammatic search
getMode()stringCurrent mode

Keyboard

KeyContextAction
Enter / SpaceTriggerOpen launcher
EscapeDropdown / ModalClose
Arrow Right/LeftGridHorizontal navigation
Arrow Down/UpGridVertical navigation
Home / EndGridFirst / last tile
EnterFocused tileSelect app
Shift+FFocused tileToggle favourite
/AnyFocus search input

Default Key Bindings

{
    close: "Escape",
    focusDown: "ArrowDown",
    focusUp: "ArrowUp",
    focusLeft: "ArrowLeft",
    focusRight: "ArrowRight",
    focusFirst: "Home",
    focusLast: "End",
    select: "Enter",
    toggleFav: "Shift+F",
    focusSearch: "/",
}

CSS Classes

All classes use the applauncher- prefix. Key classes:

Asset Paths

CSS: components/applauncher/applauncher.css
JS:  components/applauncher/applauncher.js

AuditLogViewer

Read-only filterable audit log viewer with severity badges, expandable detail rows, filter chips, pagination, and CSV/JSON export.

Usage

<link rel="stylesheet" href="components/auditlogviewer/auditlogviewer.css">
<script src="components/auditlogviewer/auditlogviewer.js"></script>
const viewer = createAuditLogViewer({
    entries: [
        {
            id: "1",
            timestamp: new Date(),
            actor: "admin",
            action: "user.login",
            resource: "Session #42",
            ipAddress: "10.0.1.5",
            severity: "info"
        }
    ],
    height: "500px",
    onRowClick: (entry) => console.log("Row clicked:", entry.id),
}, "my-container");

Options

OptionTypeDefaultDescription
entriesAuditLogEntry[][]Initial entries
pageSizenumber50Entries per page
serverSidebooleanfalseServer-side pagination mode
showFiltersbooleantrueShow filter bar
showExportbooleantrueShow export buttons
showDetailbooleantrueEnable expandable detail rows
showSeveritybooleantrueShow severity column
showIPAddressbooleantrueShow IP address column
autoRefreshnumber0Auto-refresh interval (ms)
heightstring"500px"Container height

API

MethodDescription
show(containerId)Mount to container
hide()Remove from DOM
destroy()Full cleanup
setEntries(entries)Replace all entries
addEntry(entry)Prepend a new entry
setFilters(filters)Apply filters
clearFilters()Clear all filters
setPage(page)Navigate to page
exportCSV()Export as CSV
exportJSON()Export as JSON
refresh()Re-render

Severity Levels

LevelColourIcon
infoGraybi-info-circle
warningYellowbi-exclamation-triangle
criticalRedbi-exclamation-octagon

BannerBar

A fixed-to-top viewport banner for announcing significant events such as service status updates, critical issues, maintenance windows, and success confirmations.

Features

Assets

AssetPath
CSScomponents/bannerbar/bannerbar.css
JScomponents/bannerbar/bannerbar.js
Typescomponents/bannerbar/bannerbar.d.ts

Requires: Bootstrap CSS (for SCSS variables), Bootstrap Icons CSS. Does not require Bootstrap JS.

Quick Start

<link rel="stylesheet" href="components/bannerbar/bannerbar.css">
<script src="components/bannerbar/bannerbar.js"></script>
<script>
    var banner = createBannerBar({
        message: "Scheduled maintenance tonight at 02:00 UTC.",
        variant: "warning"
    });
</script>

API

createBannerBar(options) / showBanner(options)

Creates, shows, and returns a BannerBar instance. showBanner is an ergonomic alias.

new BannerBar(options)

Creates a BannerBar instance without showing it. Call .show() to display.

Options

PropertyTypeDefaultDescription
idstringautoUnique identifier
titlestringBold title text before the message
messagestring(required)Main message text
variant"info" | "warning" | "critical" | "success""info"Severity preset
iconstringvariant defaultBootstrap Icons class
actionLabelstringText for action link/button
actionHrefstringIf set, action renders as <a>
onActionfunctionClick handler for action
closablebooleantrueShow the close X button
autoDismissMsnumber0Auto-close after N ms (0 = disabled)
maxHeightnumber200Max height in px before scrolling
backgroundColorstringCSS colour override
textColorstringCSS colour override
borderColorstringCSS border-bottom colour override
zIndexnumber1045CSS z-index
cssClassstringAdditional CSS classes
onClosefunctionCalled after close/destroy

Instance Methods

MethodDescription
show()Show the banner (replaces any active banner)
hide()Hide the banner with slide-out animation
destroy()Remove the banner and release all resources
setMessage(msg)Update the message text
setTitle(title)Update the title text (empty string hides it)
setVariant(variant)Switch severity variant
isVisible()Returns true if the banner is currently shown

Variants

VariantUse CaseDefault Icon
infoAnnouncements, feature noticesbi-info-circle-fill
warningMaintenance windows, degradationbi-exclamation-triangle-fill
criticalOutages, breaking issuesbi-exclamation-octagon-fill
successCompleted operations, confirmationsbi-check-circle-fill

Examples

Basic Info Banner

createBannerBar({
    message: "New dashboard analytics are now available."
});

Critical Banner with Title

createBannerBar({
    title: "Service Disruption",
    message: "Payment processing is currently unavailable. We are investigating.",
    variant: "critical"
});

Banner with Action Link

createBannerBar({
    message: "Your subscription expires in 3 days.",
    variant: "warning",
    actionLabel: "Renew Now",
    actionHref: "/billing/renew"
});

Auto-Dismissing Success Banner

createBannerBar({
    message: "All records imported successfully.",
    variant: "success",
    autoDismissMs: 5000
});

Custom Colours

createBannerBar({
    message: "Beta feature enabled for your account.",
    backgroundColor: "#f0e6ff",
    textColor: "#4a1d8e",
    borderColor: "#7c3aed"
});

CSS Custom Property

When visible, the banner sets --bannerbar-height on <html> to its measured pixel height. Other components (e.g., Sidebar) use this to offset their top position:

.my-fixed-element {
    top: var(--bannerbar-height, 0px);
}

The property is removed when the banner is hidden or destroyed.

Accessibility


BorderLayout

A five-region CSS Grid layout container that divides its area into North, South, East, West, and Center regions. North and South span the full width; East and West fill the remaining height; Center takes all remaining space. Supports region collapsing and dynamic slot assignment.

Assets

AssetPath
CSScomponents/borderlayout/borderlayout.css
JScomponents/borderlayout/borderlayout.js
Typescomponents/borderlayout/borderlayout.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/borderlayout/borderlayout.css">
<script src="components/borderlayout/borderlayout.js"></script>

<script>
    var header = document.createElement("header");
    header.textContent = "Application Header";
    header.style.padding = "12px";
    header.style.background = "#f0f0f0";

    var nav = document.createElement("nav");
    nav.textContent = "Navigation";
    nav.style.padding = "12px";

    var content = document.createElement("main");
    content.textContent = "Main Content Area";

    var layout = createBorderLayout({
        north: header,
        west: nav,
        center: content,
        westWidth: "200px",
        northHeight: "auto",
        gap: 1,
        height: "100vh",
        width: "100%",
        collapsible: ["west", "east"]
    });
</script>

How It Works

BorderLayout creates a CSS Grid with 5 named areas:

┌─────────────────────────────────────┐
│              north                  │  auto / fixed height
├──────┬──────────────────┬───────────┤
│      │                  │           │
│ west │     center       │   east    │  1fr (fills remaining)
│      │                  │           │
├──────┴──────────────────┴───────────┤
│              south                  │  auto / fixed height
└─────────────────────────────────────┘

When a component is passed to a region:

  1. Calls component.setContained(true) if available
  2. Calls component.show(cell) — mounts inside the grid cell
  3. Grid template updates dynamically when regions change

Options

OptionTypeDefaultDescription
idstringautoCustom element ID
northHTMLElement | ComponentNorth (top) region child
southHTMLElement | ComponentSouth (bottom) region child
eastHTMLElement | ComponentEast (right) region child
westHTMLElement | ComponentWest (left) region child
centerHTMLElement | ComponentCenter region child
gapnumber | string"0"Gap between regions
northHeightstring"auto"North region height
southHeightstring"auto"South region height
eastWidthstring"auto"East region width
westWidthstring"auto"West region width
collapsibleBorderRegion[]Regions that can be collapsed
paddingstringContainer padding
cssClassstringAdditional CSS classes
heightstringHeight CSS value
widthstringWidth CSS value
onLayoutChange(state) => voidFired on resize/collapse events

Public API

MethodReturnsDescription
show(container?)voidAppend to container and display
hide()voidRemove from DOM (preserves state)
destroy()voidFull cleanup, destroy all children
getRootElement()HTMLElement | nullThe root grid container
isVisible()booleanWhether the layout is displayed
setNorth(child | null)voidSet or clear north region
setSouth(child | null)voidSet or clear south region
setEast(child | null)voidSet or clear east region
setWest(child | null)voidSet or clear west region
setCenter(child | null)voidSet or clear center region
collapseRegion(region)voidCollapse a collapsible region
expandRegion(region)voidExpand a collapsed region
getRegionElement(region)HTMLElement | nullGrid cell for a region
getState()BorderLayoutStateSerialisable state snapshot
setState(state)voidRestore collapsed regions
setContained(value)voidSet contained mode

BorderLayoutState

interface BorderLayoutState {
    regions: Record<BorderRegion, boolean>;
    collapsed: BorderRegion[];
}

Nesting Example

// BorderLayout as an application shell
var appShell = createBorderLayout({
    north: toolbar,
    west: new BorderLayout({
        north: searchBox,
        center: treeView,
        south: userProfile,
        height: "100%"
    }),
    center: tabPanel,
    east: propertyInspector,
    south: statusBar,
    collapsible: ["west", "east"],
    height: "100vh"
});

Global Exports

When loaded via <script> tag:

CSS Classes

ClassElementDescription
.borderlayoutRootCSS Grid container
.borderlayout-northCellNorth (top) region
.borderlayout-southCellSouth (bottom) region
.borderlayout-eastCellEast (right) region
.borderlayout-westCellWest (left) region
.borderlayout-centerCellCenter region
.borderlayout-collapsedCellCollapsed region modifier

BoxLayout

A single-axis flex layout container that arranges children sequentially along one axis (horizontal or vertical) with configurable flex factors, alignment, and gap. Inspired by Java Swing BoxLayout, WPF StackPanel, and CSS Flexbox.

Assets

AssetPath
CSScomponents/boxlayout/boxlayout.css
JScomponents/boxlayout/boxlayout.js
Typescomponents/boxlayout/boxlayout.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/boxlayout/boxlayout.css">
<script src="components/boxlayout/boxlayout.js"></script>

<script>
    var layout = createBoxLayout({
        direction: "horizontal",
        gap: 8,
        align: "stretch",
        height: "100%",
        children: [
            { child: document.getElementById("sidebar"), flex: 0, minSize: 200 },
            { child: document.getElementById("content"), flex: 1 }
        ]
    });
</script>

How It Works

BoxLayout creates a CSS Flexbox container. Children are arranged sequentially along the main axis (row or column). Each child can be assigned a flex factor to consume proportional remaining space, or it can use its natural size.

direction: "horizontal"
┌──────────┬────────────────────────────────┬──────────┐
│ flex: 0  │          flex: 1               │ flex: 0  │
│ (200px)  │    (fills remaining space)     │ (auto)   │
└──────────┴────────────────────────────────┴──────────┘

direction: "vertical"
┌──────────────────────────────────────────────────────┐
│ flex: 0 (auto height)                                │
├──────────────────────────────────────────────────────┤
│                                                      │
│ flex: 1 (fills remaining space)                      │
│                                                      │
├──────────────────────────────────────────────────────┤
│ flex: 0 (auto height)                                │
└──────────────────────────────────────────────────────┘

Options

OptionTypeDefaultDescription
idstringautoCustom element ID
direction"horizontal" | "vertical"Main axis direction (required)
childrenBoxLayoutChildConfig[][]Initial children
gapnumber | string"0"Gap between children (px or CSS value)
align"start" | "center" | "end" | "stretch" | "baseline""stretch"Cross-axis alignment
justify"start" | "center" | "end" | "space-between" | "space-around" | "space-evenly""start"Main-axis distribution
paddingstringContainer padding (CSS value)
cssClassstringAdditional CSS classes
heightstringHeight CSS value
widthstringWidth CSS value
onLayoutChange(state) => voidFired on resize events

BoxLayoutChildConfig

PropertyTypeDefaultDescription
childHTMLElement | ComponentChild element or component
flexnumber0Flex grow factor. 0 = natural size
minSizenumberMinimum size in px along main axis
maxSizenumberMaximum size in px along main axis
alignSelf"start" | "center" | "end" | "stretch" | "baseline"Cross-axis alignment override

Public API

MethodReturnsDescription
show(container?)voidAppend to container and display
hide()voidRemove from DOM (preserves state)
destroy()voidFull cleanup, destroy all children
getRootElement()HTMLElement | nullThe root flex container
isVisible()booleanWhether the layout is displayed
addChild(config, index?)voidAdd a child at optional index
removeChild(index)voidRemove child by index
getChildCount()numberNumber of children
getChildElement(index)HTMLElement | nullWrapper element at index
getState()BoxLayoutStateSerialisable state snapshot
setState(state)voidRestore state (direction)
setContained(value)voidSet contained mode

BoxLayoutState

interface BoxLayoutState {
    direction: "horizontal" | "vertical";
    childCount: number;
}

Composability

BoxLayout implements the standard layout container contract. Any component with show(container) / hide() / destroy() can be used as a child. Plain HTMLElements are also supported.

// Nest a BoxLayout inside a BorderLayout
var innerBox = new BoxLayout({
    direction: "horizontal",
    gap: 8,
    children: [
        { child: buttonA, flex: 0 },
        { child: spacer, flex: 1 },
        { child: buttonB, flex: 0 }
    ]
});

borderLayout.setNorth(innerBox);

Global Exports

When loaded via <script> tag:

CSS Classes

ClassElementDescription
.boxlayoutRootFlex container
.boxlayout-childWrapperPer-child flex wrapper

Breadcrumb Navigation

Hierarchical path display with clickable segments, optional terminal dropdown actions, and overflow truncation for deep hierarchies. Extends Bootstrap 5 breadcrumb base styles.

Assets

AssetPath
CSScomponents/breadcrumb/breadcrumb.css
JScomponents/breadcrumb/breadcrumb.js
Typescomponents/breadcrumb/breadcrumb.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/breadcrumb/breadcrumb.css">
<script src="components/breadcrumb/breadcrumb.js"></script>
<script>
    var bc = createBreadcrumb({
        container: document.getElementById("my-breadcrumb"),
        items: [
            { label: "Home", icon: "bi-house-fill" },
            { label: "Projects" },
            { label: "Acme Corp" }
        ],
        onItemClick: function(item, index) {
            console.log("Navigate to:", item.label);
        }
    });
</script>

API

createBreadcrumb(options): BreadcrumbHandle

Factory function — creates and mounts a breadcrumb instance.

BreadcrumbOptions

OptionTypeDefaultDescription
containerHTMLElementrequiredMount target element
itemsBreadcrumbItem[][]Initial path segments
actionsBreadcrumbAction[][]Terminal segment dropdown actions
maxVisiblenumber5Max visible items; 0 = no truncation
separatorstring"/"Divider character between segments
size"sm" | "md" | "lg""md"Size variant
cssClassstringAdditional CSS class(es) on root
onItemClick(item, index) => voidClick callback for segments
onActionClick(actionId, item) => voidClick callback for actions

BreadcrumbItem

PropertyTypeDescription
labelstringDisplay text (required)
hrefstringNavigation URL (renders <a> instead of <button>)
iconstringBootstrap Icons class (e.g. "bi-house-fill")
dataunknownArbitrary payload for callbacks

BreadcrumbAction

PropertyTypeDescription
idstringUnique identifier (required)
labelstringDisplay text (required)
iconstringBootstrap Icons class
separatorbooleanShow divider line above this action
disabledbooleanGrey out and prevent click

BreadcrumbHandle

MethodReturnsDescription
setItems(items)voidReplace all segments
addItem(item)voidAppend a new segment
removeItem(index)voidRemove segment by index
getItems()BreadcrumbItem[]Return copy of current items
setActions(actions)voidReplace terminal dropdown actions
getElement()HTMLElementReturn root <nav> element
destroy()voidRemove DOM and event listeners

Overflow Truncation

When the number of items exceeds maxVisible, middle segments collapse into an ellipsis () that opens a dropdown listing the hidden items:

var bc = createBreadcrumb({
    container: el,
    maxVisible: 5,
    items: [
        { label: "Root" },
        { label: "Level 1" },
        { label: "Level 2" },
        { label: "Level 3" },
        { label: "Level 4" },
        { label: "Level 5" },
        { label: "Level 6" },
        { label: "Current" }
    ]
});
// Renders: Root > … > Level 4 > Level 5 > Level 6 > Current

Terminal Actions

Add a dropdown menu to the last segment for contextual actions:

var bc = createBreadcrumb({
    container: el,
    items: [
        { label: "Home" },
        { label: "Settings" }
    ],
    actions: [
        { id: "edit", label: "Edit", icon: "bi-pencil" },
        { id: "delete", label: "Delete", icon: "bi-trash", separator: true }
    ],
    onActionClick: function(actionId, item) {
        console.log("Action:", actionId, "on", item.label);
    }
});

Size Variants

VariantClassFont Size
Smallbreadcrumb-nav-sm0.75rem
Medium(default)$breadcrumb-font-size (0.8125rem)
Largebreadcrumb-nav-lg1rem

Keyboard

KeyAction
TabFocus breadcrumb items sequentially
Enter / SpaceActivate focused item or action
EscapeClose any open dropdown
Arrow Down / Arrow UpNavigate within open dropdown
Alt + DFocus breadcrumb (global, per KEYBOARD.md)

Accessibility

CSS Custom Properties

PropertyDefaultDescription
--breadcrumb-nav-divider"/"Separator character

CardLayout

An indexed-stack layout container that stacks all children in the same space but displays only one at a time. Supports animated transitions (fade, slide) between cards, lazy loading, and keyboard navigation via next()/previous().

Assets

AssetPath
CSScomponents/cardlayout/cardlayout.css
JScomponents/cardlayout/cardlayout.js
Typescomponents/cardlayout/cardlayout.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/cardlayout/cardlayout.css">
<script src="components/cardlayout/cardlayout.js"></script>

<script>
    var step1 = document.createElement("div");
    step1.innerHTML = "<h2>Step 1</h2><p>Enter your details</p>";

    var step2 = document.createElement("div");
    step2.innerHTML = "<h2>Step 2</h2><p>Choose your plan</p>";

    var step3 = document.createElement("div");
    step3.innerHTML = "<h2>Step 3</h2><p>Confirm and submit</p>";

    var wizard = createCardLayout({
        activeKey: "step1",
        transition: "slide-left",
        transitionDuration: 200,
        height: "400px",
        cards: [
            { key: "step1", child: step1 },
            { key: "step2", child: step2 },
            { key: "step3", child: step3 }
        ]
    });

    // Navigate programmatically
    document.getElementById("nextBtn").onclick = function() {
        wizard.next();
    };
</script>

How It Works

CardLayout creates a container where all card wrappers are stacked. Only the active card has display: block; all others have display: none. When switching cards, CSS animation classes are applied for the transition duration, then cleaned up.

┌──────────────────────────────────┐
│                                  │
│         Active Card              │   ← visible (display: block)
│       (e.g. "step2")            │
│                                  │
├──────────────────────────────────┤
│  Card "step1" (display: none)   │   ← hidden
│  Card "step3" (display: none)   │   ← hidden
└──────────────────────────────────┘

Options

OptionTypeDefaultDescription
idstringautoCustom element ID
activeKeystringfirst cardKey of initially active card
cardsCardConfig[][]Initial cards
sizing"largest" | "active" | "fixed""active"Container sizing strategy
transition"none" | "fade" | "slide-left" | "slide-up""none"Transition animation
transitionDurationnumber200Animation duration in ms
preserveStatebooleantrueRetain inactive card state
paddingstringContainer padding
cssClassstringAdditional CSS classes
heightstringHeight CSS value
widthstringWidth CSS value
onLayoutChange(state) => voidFired on card switch/resize

CardConfig

PropertyTypeDefaultDescription
keystringUnique card identifier (required)
childHTMLElement | ComponentCard content (required)
lazyLoadbooleanfalseDefer mounting until first activation

Public API

MethodReturnsDescription
show(container?)voidAppend to container and display
hide()voidRemove from DOM (preserves state)
destroy()voidFull cleanup, destroy all cards
getRootElement()HTMLElement | nullThe root container
isVisible()booleanWhether the layout is displayed
addCard(config)voidAdd a new card
removeCard(key)voidRemove card by key
setActiveCard(key)voidSwitch to card by key
getActiveCard()string | nullKey of the active card
next()voidActivate next card (wraps)
previous()voidActivate previous card (wraps)
getCardCount()numberNumber of cards
getState()CardLayoutStateSerialisable state snapshot
setState(state)voidRestore active card
setContained(value)voidSet contained mode

CardLayoutState

interface CardLayoutState {
    activeKey: string | null;
    cardCount: number;
}

Transitions

All transitions respect prefers-reduced-motion: reduce — when enabled, transitions are disabled and cards switch immediately.

TransitionDescription
"none"Instant switch, no animation
"fade"Outgoing card fades out, incoming fades in
"slide-left"Outgoing slides left, incoming slides in from right
"slide-up"Outgoing slides up, incoming slides in from bottom

Global Exports

When loaded via <script> tag:

CSS Classes

ClassElementDescription
.cardlayoutRootStack container
.cardlayout-cardWrapperPer-card wrapper
.cardlayout-enter-fadeCardFade-in animation
.cardlayout-exit-fadeCardFade-out animation
.cardlayout-enter-slide-leftCardSlide-in from right
.cardlayout-exit-slide-leftCardSlide-out to left
.cardlayout-enter-slide-upCardSlide-in from bottom
.cardlayout-exit-slide-upCardSlide-out upward

CodeEditor

Bootstrap 5-themed code editor wrapping CodeMirror 6 with syntax highlighting, toolbar, and diagnostics.

Prerequisites

CodeMirror 6 is required. The component checks for window.EditorView and window.EditorState at show() time. If these globals are missing, it displays an error message instead of rendering an editor.

Required Globals

GlobalPackageRole
EditorView@codemirror/viewCore view (required)
EditorState@codemirror/stateCore state (required)

Recommended Globals

These are optional but provide the full editing experience:

GlobalPackageRole
keymap, defaultKeymap@codemirror/viewKey binding support
history, historyKeymap@codemirror/commandsUndo/redo
undo, redo, indentSelection@codemirror/commandsEditing commands
syntaxHighlighting, defaultHighlightStyle@codemirror/languageSyntax colours
lineNumbers, drawSelection, dropCursor@codemirror/viewUI features
bracketMatching, closeBrackets, closeBracketsKeymap@codemirror/autocompleteBracket handling
highlightActiveLine, highlightSelectionMatches@codemirror/view / @codemirror/searchHighlighting
search, searchKeymap@codemirror/searchFind/replace
indentOnInput@codemirror/languageAuto-indent
setDiagnostics@codemirror/lintGutter diagnostics

Language Globals

Add language support by exposing factory functions as globals:

GlobalPackage
javascript@codemirror/lang-javascript
json@codemirror/lang-json
yaml@codemirror/lang-yaml (community)
html@codemirror/lang-html
css@codemirror/lang-css
sql@codemirror/lang-sql
python@codemirror/lang-python
markdown@codemirror/lang-markdown

Loading via ESM CDN

CodeMirror 6 is ESM-native. Use a <script type="module"> with an ESM CDN to expose globals:

<script type="module">
    import { EditorView, keymap, lineNumbers, drawSelection, dropCursor,
             highlightActiveLine, highlightSelectionMatches }
        from "https://esm.sh/@codemirror/view@6";
    import { EditorState } from "https://esm.sh/@codemirror/state@6";
    import { history, historyKeymap, defaultKeymap, undo, redo, indentSelection }
        from "https://esm.sh/@codemirror/commands@6";
    import { syntaxHighlighting, defaultHighlightStyle, indentOnInput }
        from "https://esm.sh/@codemirror/language@6";
    import { search, searchKeymap } from "https://esm.sh/@codemirror/search@6";
    import { closeBrackets, closeBracketsKeymap } from "https://esm.sh/@codemirror/autocomplete@6";
    import { bracketMatching } from "https://esm.sh/@codemirror/language@6";
    import { javascript } from "https://esm.sh/@codemirror/lang-javascript@6";
    import { json } from "https://esm.sh/@codemirror/lang-json@6";
    import { html } from "https://esm.sh/@codemirror/lang-html@6";
    import { css } from "https://esm.sh/@codemirror/lang-css@6";
    import { sql } from "https://esm.sh/@codemirror/lang-sql@6";
    import { python } from "https://esm.sh/@codemirror/lang-python@6";
    import { markdown } from "https://esm.sh/@codemirror/lang-markdown@6";

    // Expose as globals for CodeEditor
    Object.assign(window, {
        EditorView, EditorState, keymap, lineNumbers, drawSelection, dropCursor,
        highlightActiveLine, highlightSelectionMatches, history, historyKeymap,
        defaultKeymap, undo, redo, indentSelection, syntaxHighlighting,
        defaultHighlightStyle, indentOnInput, search, searchKeymap,
        closeBrackets, closeBracketsKeymap, bracketMatching,
        javascript, json, html, css, sql, python, markdown
    });
    window.dispatchEvent(new Event("codemirror-ready"));
</script>

Alternatively, provide a pre-bundled UMD build that sets these globals.

Quick Start

<link rel="stylesheet" href="components/codeeditor/codeeditor.css">
<script src="components/codeeditor/codeeditor.js"></script>
<script>
    var editor = createCodeEditor("my-container", {
        value: "function greet() {\n    console.log('Hello!');\n}",
        language: "javascript",
        onSave: function(value) { console.log("Saved:", value); }
    });
</script>

API

CodeEditorOptions

PropertyTypeDefaultDescription
valuestring""Initial text content
languageCodeEditorLanguage"javascript"Syntax highlighting language
readOnlybooleanfalseRead-only mode
lineNumbersbooleantrueShow line numbers (CM mode only)
wordWrapbooleanfalseEnable word wrap
tabSizenumber4Spaces per tab
placeholderstringPlaceholder text
heightstring"300px"Editor height
maxHeightstringMax height for auto-grow
autoGrowbooleanfalseGrow height with content
theme"light" | "dark""light"Theme mode
showToolbarbooleantrueShow toolbar
toolbarActionsCodeEditorToolbarAction[]All actionsWhich toolbar actions to show
diagnosticsCodeEditorDiagnostic[][]Initial diagnostics (CM mode only)
disabledbooleanfalseDisabled state
cssClassstringAdditional CSS class

Callbacks

CallbackSignatureDescription
onChange(value: string) => voidContent changed (100ms debounce)
onSave(value: string) => voidCtrl+S pressed
onLanguageChange(lang) => voidLanguage changed via toolbar
onFocus() => voidEditor focused
onBlur() => voidEditor blurred

Methods

MethodDescription
show(containerId)Render into container, detect CodeMirror
hide()Remove from DOM, preserve state
destroy()Clean up all resources
getElement()Get root DOM element
getValue()Get editor content
setValue(value)Set editor content
getLanguage()Get current language
setLanguage(lang)Change language
setReadOnly(readOnly)Toggle read-only
setTheme(theme)Switch light/dark
setDiagnostics(diags)Set gutter diagnostics (CM mode only)
clearDiagnostics()Clear all diagnostics
focus() / blur()Focus management
getSelection()Get selected text
replaceSelection(text)Replace selection
undo() / redo()Undo/redo
format()Auto-indent (CM mode only)
toggleWordWrap()Toggle word wrap
toggleLineNumbers()Toggle line numbers
getEditorInstance()Get raw CodeMirror EditorView

Features

Keyboard

KeyAction
Ctrl/Cmd + SSave
Ctrl/Cmd + ZUndo
Ctrl/Cmd + Shift + ZRedo
Ctrl/Cmd + FFind (CM mode)
TabIndent / insert spaces
Shift + TabOutdent

Accessibility


ColorPicker

A canvas-based colour selection control with saturation/brightness gradient, vertical hue strip, optional opacity slider, hex/RGB/HSL format tabs, text inputs, and configurable preset swatches. Operates in popup or inline mode.

Assets

AssetPath
CSScomponents/colorpicker/colorpicker.css
JScomponents/colorpicker/colorpicker.js
Typescomponents/colorpicker/colorpicker.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/colorpicker/colorpicker.css">
<script src="components/colorpicker/colorpicker.js"></script>
<script>
    var picker = createColorPicker("my-container", {
        value: "#FF5733",
        showOpacity: true,
        swatches: ["#EF4444", "#F59E0B", "#10B981", "#3B82F6", "#8B5CF6"],
        onChange: function(color, alpha) {
            console.log("Selected:", color, "Alpha:", alpha);
        }
    });
</script>

Options (ColorPickerOptions)

OptionTypeDefaultDescription
valuestring"#3B82F6"Initial colour (hex string)
format"hex" | "rgb" | "hsl""hex"Output format for getValue()
showOpacitybooleanfalseShow alpha/opacity slider
showFormatTabsbooleantrueShow hex/RGB/HSL format tabs
showInputsbooleantrueShow text input fields
swatchesstring[]Preset swatch colours (hex strings)
inlinebooleanfalseRender inline (true) or popup (false)
popupPositionstring"bottom-start"Popup position: bottom-start, bottom-end, top-start, top-end
triggerElementHTMLElementCustom trigger element (popup only)
labelstringOptional label displayed above the picker
disabledbooleanfalseDisable the component
size"mini" | "sm" | "default" | "lg""default"Size variant
onChangefunctionCalled on commit (swatch click, Enter, popup close, drag end)
onInputfunctionCalled continuously during drag and on every colour adjustment
onOpenfunctionCalled when popup opens
onClosefunctionCalled when popup closes

API

MethodReturnsDescription
show(containerId?)voidAppend to container (or body)
hide()voidRemove from DOM, keep state
destroy()voidHide, clean up, null references
getValue()stringColour in configured format
getValueWithAlpha()stringColour as rgba() string
setValue(color)voidSet colour (hex, rgb(), or hsl())
getAlpha()numberCurrent alpha (0-1)
setAlpha(a)voidSet alpha (clamped 0-1)
open()voidOpen popup (no-op inline)
close()voidClose popup (no-op inline)
getElement()HTMLElementRoot DOM element
getPopupElement()HTMLElement | nullPopup panel element (null if inline or closed)
setLabel(label)voidUpdate or create the label text
enable()voidEnable the component
disable()voidDisable; closes popup if open

Convenience Function

createColorPicker(containerId, options?)  // Create, show, and return

Global Exports

window.ColorPicker
window.createColorPicker

Keyboard Accessibility

ContextKeyAction
PaletteArrow keysMove saturation/brightness by 1%
PaletteShift+ArrowMove by 10%
Hue stripArrowUp/DownChange hue by 1 degree
Hue stripShift+ArrowChange hue by 10 degrees
OpacityArrowLeft/RightChange alpha by 1%
OpacityShift+ArrowChange alpha by 10%
SwatchEnter/SpaceSelect swatch colour
Text inputEnterApply typed value
PopupEscapeClose popup, return focus to trigger

Accessibility

onInput vs onChange

CallbackWhen it firesUse case
onInputEvery pointer move during drag, every keyboard step, every commitLive preview (e.g. update canvas fill in real time)
onChangeSwatch click, Enter in hex input, drag end (pointer up), keyboard stepsPersist to backend, commit undo history

Both callbacks receive (color: string, alpha?: number). During a drag, onInput fires continuously and onChange fires once on pointer release. On discrete actions (keyboard, swatch, Enter), both fire together.

Disabled State

When disabled (disabled: true or disable() method):

Examples

Live preview with onInput

var picker = createColorPicker("canvas-tools", {
    value: "#3B82F6",
    label: "Fill",
    onInput: function(color) {
        // Real-time preview while dragging
        selectedShape.style.fill = color;
    },
    onChange: function(color) {
        // Commit: save to backend
        api.updateShapeFill(shapeId, color);
    }
});

Accessing the popup element

var picker = createColorPicker("dock-panel", {
    value: "#10B981",
    onOpen: function() {
        var popup = picker.getPopupElement();
        if (popup) { popup.style.zIndex = "2000"; }
    }
});

Label option

var fill = createColorPicker("fill-container", {
    label: "Fill",
    inline: true,
    value: "#3B82F6"
});

var stroke = createColorPicker("stroke-container", {
    label: "Stroke",
    inline: true,
    value: "#1F2937"
});

Inline picker with opacity

var picker = createColorPicker("theme-editor", {
    value: "#6366F1",
    inline: true,
    showOpacity: true,
    format: "rgb",
    onChange: function(color, alpha) {
        document.getElementById("preview").style.backgroundColor = color;
    }
});

Popup with preset swatches

var picker = createColorPicker("brand-color", {
    value: "#10B981",
    swatches: [
        "#EF4444", "#F59E0B", "#10B981", "#3B82F6",
        "#8B5CF6", "#EC4899", "#6B7280", "#1F2937"
    ],
    onChange: function(color) {
        console.log("Brand colour:", color);
    }
});

See specs/colorpicker.prd.md for the complete specification.


ColumnsPicker

A dropdown showing column layout presets with visual SVG page thumbnails. Each option displays a small page icon with vertical column dividers and horizontal placeholder lines illustrating the layout. Designed for use in Ribbon toolbars, standalone toolbars, and property panels.

Usage

<link rel="stylesheet" href="components/columnspicker/columnspicker.css">
<script src="components/columnspicker/columnspicker.js"></script>

<div id="my-columns"></div>

<script>
var picker = createColumnsPicker({
    container: "my-columns",
    value: "Two",
    onChange: function(preset) {
        console.log("Columns:", preset.columns, "Widths:", preset.widths);
    }
});
</script>

Options

OptionTypeDefaultDescription
containerHTMLElement | stringrequiredContainer element or ID
valuestring"One"Initial selected preset name
presetsColumnPreset[]built-in 5Custom preset definitions
showCustombooleantrueShow "Custom Columns..." link
onChange(preset) => voidCallback on selection
onCustom() => voidCallback for custom link
ribbonModebooleantrueRibbon-compatible rendering

Default Presets

NameColumnsWidths
One1[1]
Two2[1, 1]
Three3[1, 1, 1]
Left2[1, 2] (narrow-wide)
Right2[2, 1] (wide-narrow)

API

MethodDescription
getValue()Returns the currently selected ColumnPreset
setValue(name)Select a preset by name
setPresets(presets)Replace all presets
show()Open the dropdown
hide()Close the dropdown
destroy()Remove from DOM and clean up
getElement()Get the root DOM element

Keyboard

KeyAction
Arrow Down / Arrow UpNavigate items
Enter / SpaceSelect focused item
EscapeClose dropdown

CSS Classes

ClassPurpose
.columnspickerRoot container
.columnspicker-triggerDropdown button
.columnspicker-panelDropdown panel
.columnspicker-itemPreset item
.columnspicker-item--selectedSelected state
.columnspicker-thumbSVG thumbnail
.columnspicker-customCustom columns link

CommandPalette

Keyboard-first command palette (Ctrl+K omnibar) for searching and executing registered commands with fuzzy matching, category grouping, recent history, and match highlighting.

Assets

FilePurpose
commandpalette.tsTypeScript source
commandpalette.scssComponent styles
commandpalette.jsCompiled JS (IIFE-wrapped)
commandpalette.cssCompiled CSS

Usage

<link rel="stylesheet" href="components/commandpalette/commandpalette.css">
<script src="components/commandpalette/commandpalette.js"></script>

Basic — Register Commands

CommandPalette.configure({
    commands: [
        { id: "save", label: "Save Document", icon: "bi-save", category: "Actions", shortcut: "Ctrl+S", action: function() { console.log("Save!"); } },
        { id: "settings", label: "Open Settings", icon: "bi-gear", category: "Navigation", shortcut: "Ctrl+,", action: function() { console.log("Settings!"); } }
    ]
});
// Now press Ctrl+K (or Cmd+K on macOS) to open

Programmatic Open

openCommandPalette();

Register Commands Individually

registerCommand({
    id: "toggle-sidebar",
    label: "Toggle Sidebar",
    icon: "bi-layout-sidebar",
    category: "View",
    shortcut: "Ctrl+B",
    action: function() { sidebar.toggleCollapse(); }
});

Interfaces

PaletteCommand

interface PaletteCommand {
    id: string;
    label: string;
    icon?: string;                   // Bootstrap Icons class
    category?: string;               // Grouping category
    shortcut?: string;               // Display-only shortcut badge
    keywords?: string[];             // Additional search terms
    description?: string;            // Secondary text
    disabled?: boolean;              // Default: false
    hidden?: boolean;                // Excluded from search. Default: false
    action: () => void | Promise<void>;
}

CommandPaletteOptions

interface CommandPaletteOptions {
    commands?: PaletteCommand[];
    placeholder?: string;            // Default: "Type a command..."
    hotkey?: string;                 // Default: "ctrl+k" ("meta+k" on macOS)
    maxResults?: number;             // Default: 20
    showRecent?: boolean;            // Default: true
    maxRecent?: number;              // Default: 5
    showShortcuts?: boolean;         // Default: true
    showCategories?: boolean;        // Default: true
    width?: string;                  // Default: "600px"
    zIndex?: number;                 // Default: 1080
    backdropOpacity?: number;        // Default: 0.5
    cssClass?: string;
    onOpen?: () => void;
    onClose?: () => void;
    onSelect?: (command: PaletteCommand) => void;
    onSearch?: (query: string) => void;
}

API

MethodDescription
CommandPalette.getInstance()Returns singleton instance
CommandPalette.configure(options)Configure the singleton
registerCommand(cmd)Register a command
registerCommands(cmds)Register multiple commands
unregisterCommand(id)Remove a command
setCommands(cmds)Replace all commands
getCommand(id)Get a command by ID
getCommands()Get all commands
open()Open the palette
close()Close the palette
isOpen()Check if open
clearRecent()Clear recent history
destroy()Full cleanup
openCommandPalette()Global: open singleton

Keyboard

KeyAction
Ctrl+K (Cmd+K on macOS)Open/close palette
Arrow DownHighlight next result
Arrow UpHighlight previous result
EnterExecute highlighted command
EscapeClose palette
HomeHighlight first result
EndHighlight last result
TabTrapped (focus stays in input)

Fuzzy Search

The palette implements a lightweight fuzzy search:

  1. Exact prefix match — Highest score
  2. Substring match — High score
  3. Character-skip match — Score based on contiguity
  4. Keyword match — 70% of label match score
  5. Description match — 50% of label match score

Matched characters are highlighted with <mark> elements.

Singleton Pattern

Only one CommandPalette exists per page. Use CommandPalette.configure() or the global registerCommand() functions.

Dependencies


CommentOverlay

Transparent overlay system for anchoring comment pins to DOM elements, enabling inline annotation with threaded discussions, @mentions, resolve/unresolve, drag-to-reposition, and visual connector lines.

Quick Start

<link rel="stylesheet" href="components/commentoverlay/commentoverlay.css">
<script src="components/commentoverlay/commentoverlay.js"></script>
<script>
    var overlay = createCommentOverlay("my-container", {
        currentUser: { id: "u1", name: "Alice Chen" },
        mentionUsers: [
            { id: "u1", name: "Alice Chen" },
            { id: "u2", name: "Bob Smith", email: "bob@example.com" }
        ],
        pins: [
            {
                id: "pin-1",
                anchorElement: document.getElementById("my-paragraph"),
                offsetX: 12, offsetY: -8,
                thread: {
                    id: "pin-1",
                    rootComment: {
                        id: "c1", author: { id: "u2", name: "Bob Smith" },
                        text: "Check this value @[u1]",
                        createdAt: "2025-12-01T10:00:00Z",
                        edited: false, mentions: ["u1"]
                    },
                    replies: [],
                    resolved: false
                }
            }
        ],
        onCommentCreate: function(threadId, comment) {
            console.log("New comment:", threadId, comment.text);
        }
    });
</script>

Assets

AssetPath
CSScomponents/commentoverlay/commentoverlay.css
JScomponents/commentoverlay/commentoverlay.js
Typescomponents/commentoverlay/commentoverlay.d.ts

Requires: Bootstrap CSS, Bootstrap Icons CSS.

Interfaces

MentionUser

PropertyTypeDescription
idstringUnique user identifier
namestringDisplay name
avatarUrlstringOptional avatar image URL
emailstringOptional email for autocomplete

CommentData

PropertyTypeDescription
idstringUnique comment identifier
authorMentionUserComment author
textstringComment text (@[userId] for mentions)
createdAtstringISO 8601 timestamp
editedAtstringLast edit timestamp
editedbooleanWhether edited
mentionsstring[]Mentioned user IDs

CommentThread

PropertyTypeDescription
idstringThread identifier
rootCommentCommentDataFirst comment
repliesCommentData[]Reply comments
resolvedbooleanWhether resolved
resolvedByMentionUserWho resolved
resolvedAtstringWhen resolved
metadataRecordCustom data

CommentPinData

PropertyTypeDescription
idstringPin identifier
anchorElementHTMLElementDOM element to anchor to
anchorSelectorstringCSS selector fallback
offsetXnumberX offset from anchor
offsetYnumberY offset from anchor
threadCommentThreadAssociated thread
iconstringBootstrap Icons class
colourstringCSS colour value
showConnectorbooleanShow connector line
visiblebooleanWhether visible

Options (CommentOverlayOptions)

PropertyTypeDefaultDescription
containerHTMLElementdocument.bodyElement to overlay
pinsCommentPinData[][]Initial pins
mentionUsersMentionUser[][]Users for @mention
currentUserMentionUser--Current user (required)
showConnectorsbooleantrueShow SVG connector lines
draggablePinsbooleantrueAllow drag-to-reposition
allowResolvebooleantrueShow resolve button
resolvedDisplaystring"muted""muted", "hidden", or "normal"
enableMentionsbooleantrueEnable @mention autocomplete
defaultPinIconstring"bi-chat-dots-fill"Default pin icon
defaultPinColourstring$blue-600Default pin colour
zIndexnumber1075Overlay z-index
timestampFormatstring"relative""relative" or "absolute"
panelWidthnumber320Thread popover width
panelMaxHeightnumber400Thread popover max height
pinSizenumber24Pin diameter in pixels
onCommentCreate(threadId, comment) => void--New comment callback
onCommentEdit(threadId, commentId, text) => void--Edit callback
onCommentDelete(threadId, commentId) => void--Delete callback
onThreadResolve(threadId, resolvedBy) => void--Resolve callback
onThreadUnresolve(threadId) => void--Unresolve callback
onPinCreate(pin) => void--New pin callback
onPinMove(pinId, x, y) => void--Pin repositioned callback
onPinDelete(pinId) => void--Pin deleted callback
onMention(userId, threadId, commentId) => void--@mention used
onMentionSearch(query) => Promise<MentionUser[]>--Async mention search
onPinClick(pinId) => void--Pin clicked

API Methods

MethodDescription
addPin(pin)Add a pin with its thread
removePin(pinId)Remove a pin and thread
updatePin(pinId, updates)Update pin properties
getPin(pinId)Get pin by ID
getAllPins()Get all pins
addComment(threadId, comment)Add a comment to a thread
updateComment(threadId, commentId, text)Update comment text
removeComment(threadId, commentId)Remove a comment
resolveThread(threadId, resolvedBy)Resolve a thread
unresolveThread(threadId)Reopen a thread
setMentionUsers(users)Update mention user list
openThread(pinId)Open thread popover
closeAllThreads()Close all popovers
enterPlacementMode()Enter pin placement mode
exitPlacementMode()Exit placement mode
reposition()Recalculate pin positions
setVisible(visible)Show/hide overlay
setResolvedFilter(show)Filter by resolved state
getElement()Get overlay DOM element
exportComments()Export as JSON string
destroy()Full teardown

Keyboard

KeyContextAction
Enter / SpacePin focusedOpen thread popover
EscapePopover openClose popover
EscapePlacement modeCancel placement
EnterReply inputSend reply
Shift+EnterReply inputNewline
Ctrl+EnterEdit textareaSave edit
EscapeEdit textareaCancel edit
ArrowDown/Up@mention dropdownNavigate suggestions
Enter@mention highlightedSelect user
Escape@mention dropdownClose dropdown

@Mention Format

Mentions are stored as @[userId] in comment text. When rendered, they are resolved against the mentionUsers list and displayed as styled chips. Use onMentionSearch for async user lookups.

See specs/commentoverlay.prd.md for the full specification.


ConfirmDialog

A general-purpose confirmation modal with customizable title, message, icon, buttons, and a promise-based API. Use this for "Are you sure?" / "Do you want to proceed?" flows. Distinct from ErrorDialog, which is designed for literate error messages with technical details and correlation IDs.

Assets

AssetPath
CSScomponents/confirmdialog/confirmdialog.css
JScomponents/confirmdialog/confirmdialog.js
Typescomponents/confirmdialog/confirmdialog.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="css/custom.css">
<link rel="stylesheet" href="icons/bootstrap-icons.css">
<link rel="stylesheet" href="components/confirmdialog/confirmdialog.css">

<script src="components/confirmdialog/confirmdialog.js"></script>
<script>
    async function handleDelete() {
        const confirmed = await showConfirmDialog({
            title: "Delete Item",
            message: "Are you sure you want to delete this item? This action cannot be undone.",
            variant: "danger",
            confirmLabel: "Delete",
        });

        if (confirmed) {
            deleteItem();
        }
    }

    // Danger shortcut
    async function handleQuickDelete() {
        const confirmed = await showDangerConfirmDialog(
            "This will permanently delete the project and all its data."
        );
        if (confirmed) { deleteProject(); }
    }
</script>

API

Global Functions

FunctionReturnsDescription
showConfirmDialog(options)Promise<boolean>Show confirmation dialog; resolves true on confirm, false on cancel
showDangerConfirmDialog(message, title?)Promise<boolean>Danger variant shortcut with "Delete" label

Class: ConfirmDialog

const dialog = new ConfirmDialog({ message: "Proceed with this action?" });
const confirmed = await dialog.show();

ConfirmDialogOptions

OptionTypeDefaultDescription
titlestring"Confirm"Dialog title text
messagestringrequiredMain message body
iconstringPer variantBootstrap Icons class (e.g. "bi-exclamation-triangle")
variantstring"default""default", "danger", "warning", "info"
confirmLabelstring"Confirm"Confirm button text
cancelLabelstring"Cancel"Cancel button text
showCancelbooleantrueWhether to show the cancel button
onConfirm() => void--Callback fired on confirm (in addition to promise)
onCancel() => void--Callback fired on cancel (in addition to promise)
closeOnBackdropbooleantrueClose the dialog when the backdrop is clicked
cssClassstring--Additional CSS class on the dialog root
keyBindingsPartial<Record<string, string>>--Override default key combos

Variants

VariantIconButton ClassUse Case
defaultbi-question-circlebtn-primaryGeneral confirmation
dangerbi-exclamation-trianglebtn-dangerDestructive actions (delete, remove)
warningbi-exclamation-circlebtn-warningPotentially risky actions
infobi-info-circlebtn-infoInformational confirmation

Keyboard

KeyAction
EscapeCancel (resolve false)
EnterConfirm -- only when the confirm button is focused
TabCycle focus between cancel and confirm buttons (focus trap)

Key bindings can be overridden via the keyBindings option:

showConfirmDialog({
    message: "Proceed?",
    keyBindings: { confirm: "Ctrl+Enter" },
});

Accessibility

DOM Structure

div.confirmdialog-backdrop
  div.confirmdialog.confirmdialog-{variant} [role="alertdialog" aria-modal="true"]
    div.confirmdialog-header
      i.bi.{icon}.confirmdialog-icon.confirmdialog-icon-{variant}
      h5.confirmdialog-title "Confirm Action"
      button.confirmdialog-close [aria-label="Close"]
    div.confirmdialog-body
      p.confirmdialog-message "Are you sure?"
    div.confirmdialog-footer
      button.confirmdialog-cancel.btn.btn-secondary "Cancel"
      button.confirmdialog-confirm.btn.btn-{variant} "Confirm"

Features


ContextMenu

A theme-aware, accessible context menu component with icons, keyboard shortcuts, separators, sub-menus, checked/radio states, and viewport edge detection. Designed for right-click (desktop) or programmatic invocation across all applications.

Assets

AssetPath
CSScomponents/contextmenu/contextmenu.css
JScomponents/contextmenu/contextmenu.js
Typescomponents/contextmenu/contextmenu.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/contextmenu/contextmenu.css">
<script src="components/contextmenu/contextmenu.js"></script>
<script>
    document.addEventListener("contextmenu", function(e) {
        e.preventDefault();
        createContextMenu({
            x: e.clientX,
            y: e.clientY,
            items: [
                { id: "cut", label: "Cut", icon: "scissors", shortcut: "Ctrl+X" },
                { id: "copy", label: "Copy", icon: "files", shortcut: "Ctrl+C" },
                { id: "paste", label: "Paste", icon: "clipboard", shortcut: "Ctrl+V" },
                { id: "sep1", label: "", type: "separator" },
                { id: "delete", label: "Delete", icon: "trash", shortcut: "Del", danger: true }
            ],
            onClose: function() { console.log("Menu closed"); }
        });
    });
</script>

API

Global Functions

FunctionReturnsDescription
createContextMenu(options)ContextMenuCreate and display a context menu

ContextMenuOptions

OptionTypeDefaultDescription
itemsContextMenuItem[]requiredMenu items
xnumberrequiredHorizontal position (px)
ynumberrequiredVertical position (px)
themestring"auto"light, dark, or auto
minWidthnumber200Minimum width (px)
maxWidthnumber320Maximum width (px)
onClosefunctionCalled when menu closes
autoClosebooleantrueClose on outside click
closeOnEscapebooleantrueClose on Escape key
animationstring"fade"fade, scale, or none

ContextMenuItem

OptionTypeDefaultDescription
idstringrequiredUnique identifier
labelstringrequiredDisplay text
iconstringBootstrap icon name (without bi- prefix)
shortcutstringKeyboard shortcut hint
disabledbooleanfalseDisables the item
checkedbooleanCheckmark state
activebooleanRadio-style active state
typestring"item"item, separator, header, submenu
dangerbooleanfalseRed styling for destructive actions
childrenContextMenuItem[]Sub-menu items (for type: "submenu")
onClickfunctionClick handler receiving the item

ContextMenu (Handle)

MethodReturnsDescription
close()voidClose the menu
isOpen()booleanWhether the menu is visible
updateItems(items)voidReplace menu items
destroy()voidTear down permanently

Features

Accessibility

Visual Design

See specs/explorer-context-menu.req.md for the full specification.


Conversation

A programmable turn-by-turn conversation UI component for AI agent interactions in enterprise SaaS applications. Supports rich text rendering via Vditor, streaming responses, session management, user feedback, copy in multiple formats, and inline error display.

Features

Assets

AssetPath
CSScomponents/conversation/conversation.css
JScomponents/conversation/conversation.js
Typescomponents/conversation/conversation.d.ts

Dependencies:

DependencyPurposeRequired
Bootstrap CSSLayout and base stylesYes
Bootstrap IconsAvatar and action button iconsYes
Vditor (CDN)Markdown rendering for assistant messagesYes
DOMPurify (CDN)HTML sanitisation on copy export pathsRecommended

Quick Start

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css">
<link rel="stylesheet" href="components/conversation/conversation.css">

<script src="https://cdn.jsdelivr.net/npm/vditor/dist/index.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
<script src="components/conversation/conversation.js"></script>

<div id="chat-container" style="height: 600px;"></div>

<script>
    var chat = createConversation(
    {
        title: "Support Agent",
        onSendMessage: function(message, session)
        {
            // Call your backend API here
            chat.showTypingIndicator();
            fetch("/api/chat", { method: "POST", body: JSON.stringify({ message: message }) })
                .then(function(res) { return res.json(); })
                .then(function(data)
                {
                    chat.hideTypingIndicator();
                    chat.addAssistantMessage(data.reply);
                });
        }
    }, "chat-container");
</script>

API

Constructor

const chat = new Conversation(options?: ConversationOptions);

Creates the conversation DOM but does not attach it to the page. Call show() to mount.

ConversationOptions

OptionTypeDefaultDescription
maxMessagesnumber0 (unlimited)Maximum messages to retain in buffer. Oldest messages are evicted when exceeded.
sessionConversationSessionInitial session to load. A default empty session is created if omitted.
placeholderstring"Type a message..."Placeholder text for the input textarea.
showHeaderbooleantrueShow the session header with title and action buttons.
titlestring"Conversation"Title displayed in the header.
showFeedbackbooleantrueShow thumbs-up/down feedback buttons on assistant messages.
showMessageCopybooleantrueShow copy button on individual messages.
showConversationCopybooleantrueShow conversation-level copy dropdown in header.
showNewConversationbooleantrueShow the "New conversation" button in the header.
showClearContextbooleantrueShow the "Clear context" button in the header.
showTimestampsbooleanfalseShow timestamp (HH:MM) on each message.
userDisplayNamestring"You"Display name for user messages.
assistantDisplayNamestring"Assistant"Display name for assistant messages.
userAvatarstring"bi-person-circle"User avatar: a Bootstrap Icons class (e.g., "bi-person-circle") or an image URL.
assistantAvatarstring"bi-robot"Assistant avatar: a Bootstrap Icons class or an image URL.
typingIndicatorTextstring"Thinking..."Text shown beside the typing indicator dots.
autoFocusbooleantrueAuto-focus the input textarea on show().
disabledbooleanfalseDisable the input area (prevents sending).
heightstring"100%"CSS height of the component.
widthstring"100%"CSS width of the component.
size"sm" | "md" | "lg""md"Bootstrap size variant controlling density.
cssClassstringAdditional CSS class(es) on the root element.
onNewSession() => Promise<ConversationSession> | ConversationSessionCalled when a new session is requested.
onLoadSession(sessionId: string) => Promise<ConversationSession> | ConversationSessionCalled when loading an existing session.
onSaveSession(session: ConversationSession) => Promise<void> | voidCalled to persist the session after each message.
onClearSession(sessionId: string) => Promise<void> | voidCalled when the user clicks "Clear context".
onSendMessage(message: string, session: ConversationSession) => voidCalled when the user sends a message.
onFeedback(messageId: string, feedback: FeedbackData, session: ConversationSession) => voidCalled when feedback is given on an assistant message.
onCopy(content: string, format: CopyFormat) => voidCalled when content is copied.
onError(error: Error) => voidCalled when an error occurs.
enableMcpAppsbooleanfalseEnable MCP App rendering in messages. When true, messages with metadata.mcpApp are rendered as sandboxed iframes.
showCanvasbooleanfalseShow the canvas side panel for full-size MCP apps. Adds a .conversation-with-canvas wrapper around the conversation.
canvasWidthnumber480Default canvas panel width in pixels.
canvasMinWidthnumber280Minimum canvas panel width in pixels.
canvasMaxWidthFractionnumber0.6Maximum canvas width as a fraction of the container width.
onMcpAppMessage(appId: string, method: string, params: unknown) => voidCalled when an MCP app sends a JSON-RPC message to the host.
onCanvasToggle(open: boolean) => voidCalled when the canvas panel is opened or closed.

McpAppConfig

Configuration for an MCP App resource to render within a message. The app HTML runs in a sandboxed iframe with no access to the host page.

PropertyTypeDefaultDescription
htmlstringThe HTML content of the MCP app (text/html;profile=mcp-app). Required.
titlestring"App"Title for the app panel header.
preferredWidthnumber480Preferred width in pixels for canvas mode.
preferredHeightnumber300Preferred height in pixels for inline mode.
connectDomainsstring[]Allowed connect-src domains for the iframe CSP.
displayMode"inline" | "canvas""inline"Render inline within the message or in the canvas panel.
sandboxFlagsstring"allow-scripts allow-forms"Override iframe sandbox flags. allow-same-origin is never permitted.

ConversationMessage

PropertyTypeDescription
idstringUnique message identifier. Auto-generated if not provided.
role"user" | "assistant" | "system" | "error"Who sent the message.
contentstringMessage content (Markdown for assistant, plain text for user).
timestampDateUTC timestamp.
feedbackFeedbackDataFeedback on this message (assistant messages only). Optional.
metadataRecord<string, unknown>Arbitrary metadata the consumer can attach. Optional.

ConversationSession

PropertyTypeDescription
idstringUnique session identifier.
titlestringHuman-readable session title. Optional.
messagesConversationMessage[]Messages in this session.
createdAtDateWhen the session was created.
updatedAtDateWhen the session was last updated.
metadataRecord<string, unknown>Arbitrary session metadata. Optional.

StreamHandle

Property / MethodTypeDescription
messageIdstring (readonly)The message ID being streamed.
appendChunk(text)(text: string) => voidAppend a text chunk to the message.
complete(metadata?)(metadata?: Record<string, unknown>) => voidMark streaming as complete. Triggers final Vditor render. Pass { mcpApp: config } to transition into MCP app display.
error(message?)(message?: string) => voidMark streaming as errored. Shows inline error state.
getContent()() => stringReturns current accumulated content.
getState()() => StreamStateReturns current stream state: "streaming", "complete", or "error".

ConversationError

PropertyTypeDescription
titlestringShort error title. Required.
messagestringExplanation text. Required.
suggestionstringActionable suggestion. Optional.
technicalDetailstringTechnical detail shown in an expandable section. Optional.
errorCodestringError code for searchability. Optional.
correlationIdstringCorrelation ID for backend log tracing. Optional.

FeedbackData

PropertyTypeDescription
sentiment"positive" | "negative"Positive or negative sentiment.
commentstringOptional written comment from the user.
timestampDateUTC timestamp of when feedback was given.

Methods

MethodDescription
show(container?)Appends to container (ID string or HTMLElement) and makes visible.
hide()Removes from DOM without destroying state.
destroy()Hides and releases all references and timers.
addUserMessage(text)Adds a user message to the conversation. Returns ConversationMessage.
addAssistantMessage(text)Adds a complete assistant message rendered via Vditor. Returns ConversationMessage.
addSystemMessage(text)Adds an informational system message. Returns ConversationMessage.
addError(error)Adds an inline error with expandable details. Returns ConversationMessage.
startAssistantMessage()Begins a streaming assistant message. Returns a StreamHandle.
showTypingIndicator()Shows the animated typing indicator.
hideTypingIndicator()Hides the typing indicator.
loadSession(session)Replaces the current session and re-renders all messages.
newSession()Creates a new session via callback and reinitialises. Returns Promise<void>.
clearSession()Clears messages in the current session. Returns Promise<void>.
getSession()Returns a deep copy of the current session.
getMessages()Returns a copy of the messages array.
copyMessage(messageId, format, btn?)Copies a single message to the clipboard.
copyConversation(format, btn?)Copies the entire conversation to the clipboard.
setDisabled(disabled)Enables or disables the input area.
setTitle(title)Updates the header title.
scrollToBottom()Forces scroll to the bottom of the message list.
focus()Focuses the input textarea.
getMessageCount()Returns current message count.
isStreaming()Returns true if a stream is active.
isVisible()Returns visibility state.
getElement()Returns the root DOM element (or null).
addAppMessage(text, appConfig)Adds an assistant message with an inline MCP app. Returns ConversationMessage.
openCanvas(config)Opens the canvas side panel with the given MCP app config. Requires showCanvas: true.
closeCanvas()Closes the canvas side panel. The previously expanded inline frame is restored.
isCanvasOpen()Returns true if the canvas panel is currently open.

Convenience Functions

createConversation(options?, container?)    // Create, show, and return

Global Exports

window.Conversation
window.createConversation

Streaming Example

Use startAssistantMessage() to stream tokens from your backend into the conversation. During streaming, chunks are displayed as plain text. On complete(), the full content is rendered through Vditor.

chat.showTypingIndicator();

fetch("/api/stream", { method: "POST", body: JSON.stringify({ prompt: userText }) })
    .then(function(response)
    {
        chat.hideTypingIndicator();
        var stream = chat.startAssistantMessage();
        var reader = response.body.getReader();
        var decoder = new TextDecoder();

        function pump()
        {
            reader.read().then(function(result)
            {
                if (result.done)
                {
                    stream.complete();
                    return;
                }
                var chunk = decoder.decode(result.value, { stream: true });
                stream.appendChunk(chunk);
                pump();
            }).catch(function(err)
            {
                stream.error("Connection lost: " + err.message);
            });
        }

        pump();
    });

Session Management Example

Provide session lifecycle callbacks to persist conversations to your backend.

var chat = createConversation(
{
    title: "Project Assistant",

    onNewSession: function()
    {
        return fetch("/api/sessions", { method: "POST" })
            .then(function(res) { return res.json(); });
    },

    onSaveSession: function(session)
    {
        return fetch("/api/sessions/" + session.id,
        {
            method: "PUT",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(session)
        });
    },

    onClearSession: function(sessionId)
    {
        return fetch("/api/sessions/" + sessionId + "/clear",
        {
            method: "POST"
        });
    },

    onLoadSession: function(sessionId)
    {
        return fetch("/api/sessions/" + sessionId)
            .then(function(res) { return res.json(); });
    },

    onSendMessage: function(message, session)
    {
        // Handle message and respond
    }
}, "chat-container");

You can also load a session programmatically:

var savedSession = {
    id: "session-abc123",
    title: "Previous chat",
    messages: [],
    createdAt: new Date("2026-01-15"),
    updatedAt: new Date("2026-01-15")
};

chat.loadSession(savedSession);

Feedback Example

When feedback buttons are enabled (showFeedback: true), users can rate assistant messages as helpful or unhelpful. Clicking a feedback button opens a modal for an optional written comment.

var chat = createConversation(
{
    showFeedback: true,

    onFeedback: function(messageId, feedback, session)
    {
        console.log("Feedback on", messageId, ":", feedback.sentiment);
        if (feedback.comment)
        {
            console.log("Comment:", feedback.comment);
        }

        fetch("/api/feedback",
        {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(
            {
                messageId: messageId,
                sentiment: feedback.sentiment,
                comment: feedback.comment,
                sessionId: session.id
            })
        });
    }
}, "chat-container");

Copy Functionality

The component supports copying content in three formats:

FormatDescription
"markdown"Raw Markdown source. For assistant messages, this is the original Markdown content.
"html"Rendered HTML from the DOM, sanitised through DOMPurify.
"plaintext"Plain text extracted from the rendered DOM via textContent.

Conversation-level copy is available through a dropdown in the header (when showConversationCopy is true). Each message is prefixed with the sender's display name.

Message-level copy is available through a clipboard button on individual assistant messages (when showMessageCopy is true). Message-level copy defaults to Markdown format.

The onCopy callback is fired after each copy operation:

var chat = createConversation(
{
    onCopy: function(content, format)
    {
        console.log("Copied as", format, ":", content.length, "characters");
    }
}, "chat-container");

Error Display

Use addError() to display structured, user-friendly error messages inline in the conversation. Errors follow the literate error pattern with a title, explanation, suggestion, and expandable technical details.

chat.addError(
{
    title: "Request failed",
    message: "The assistant could not process your request because the service is temporarily unavailable.",
    suggestion: "Wait a moment and try again, or contact support if the problem persists.",
    technicalDetail: "HTTP 503 Service Unavailable\nEndpoint: POST /api/v2/chat/completions",
    errorCode: "CHAT-503",
    correlationId: "req-7f3a-4b2c-9d1e"
});

The error code and correlation ID appear in the expandable "Technical Details" section, making it straightforward for support teams to trace issues in backend logs.

MCP App Rendering

The component supports the MCP Apps specification (stable 2026-01-26) for embedding interactive content within chat messages. MCP apps run in sandboxed iframes with JSON-RPC 2.0 communication via postMessage.

Inline MCP App

Add a message with an inline MCP app using addAppMessage():

var chat = createConversation(
{
    enableMcpApps: true,

    onMcpAppMessage: function(appId, method, params)
    {
        console.log("MCP message from", appId, ":", method, params);

        // Example: respond to a data request from the app
        if (method === "getData")
        {
            // Send response back to the iframe
            // (handled internally via the McpAppFrame postMessage bridge)
        }
    },

    onSendMessage: function(message, session)
    {
        // Simulate an API response that returns MCP app content
        chat.showTypingIndicator();
        setTimeout(function()
        {
            chat.hideTypingIndicator();
            chat.addAppMessage("Here are the server metrics:", {
                html: "<html><body><h2>CPU Usage</h2><p>42%</p></body></html>",
                title: "Server Metrics",
                preferredHeight: 200
            });
        }, 1000);
    }
}, "chat-container");

Streaming into MCP App

Stream text first, then transition to an MCP app on completion:

var stream = chat.startAssistantMessage();
stream.appendChunk("Analysing your data");
stream.appendChunk("...");

// On completion, attach MCP app content
stream.complete({
    mcpApp: {
        html: "<html><body><div id='chart'>...</div></body></html>",
        title: "Analysis Results",
        preferredHeight: 400
    }
});

Canvas Side Panel

Enable the canvas panel for full-size MCP app display:

var chat = createConversation(
{
    enableMcpApps: true,
    showCanvas: true,
    canvasWidth: 500,

    onCanvasToggle: function(open)
    {
        console.log("Canvas", open ? "opened" : "closed");
    }
}, "chat-container");

// Programmatically open an app in the canvas
chat.openCanvas({
    html: "<html><body><div id='editor'>Full-size editor...</div></body></html>",
    title: "Code Editor",
    preferredWidth: 600
});

// Close the canvas
chat.closeCanvas();

When showCanvas is true, inline MCP apps display an "expand" button. Clicking it moves the app to the canvas panel. The canvas panel supports pointer-capture resize with min/max width constraints and keyboard navigation (Esc to close, Arrow keys to resize).

MCP App HTML Structure

MCP app HTML is injected into a sandboxed iframe via srcdoc. The component automatically prepends:

  1. A Content Security Policy meta tag restricting resource access
  2. A theme style block injecting --mcp-* CSS custom properties (colours, fonts) computed from the host page's Bootstrap theme at runtime
  3. A bridge script providing window.mcpBridge.send(method, params) and window.mcpBridge.onMessage for JSON-RPC 2.0 communication with the host

Example MCP app HTML that communicates with the host:

<html>
<body>
    <button id="btn">Request Data</button>
    <pre id="output"></pre>
    <script>
        document.getElementById("btn").addEventListener("click", function()
        {
            window.mcpBridge.send("getData", { query: "metrics" });
        });

        window.mcpBridge.onMessage = function(method, params)
        {
            document.getElementById("output").textContent = JSON.stringify(params, null, 2);
        };
    </script>
</body>
</html>

Security

The Conversation component applies defence-in-depth to prevent cross-site scripting (XSS):

Loading DOMPurify via CDN is recommended for production deployments to ensure HTML export paths are sanitised.

Accessibility

The component implements the following accessibility features:

FeatureImplementation
Region landmarkRoot element uses role="region" with aria-label set to the conversation title.
Live regionMessage list uses role="log" with aria-live="polite" and aria-relevant="additions" so screen readers announce new messages.
Message articlesEach message bubble uses role="article" with a descriptive aria-label (sender name and time).
Typing indicatorUses role="status" with aria-label matching the typing indicator text.
Input labelThe textarea has aria-label="Message input".
Button labelsAll action buttons have aria-label attributes (e.g., "Send message", "Rate as helpful", "Copy message").
Keyboard sendPress Enter to send a message. Press Shift+Enter for a new line.
Copy menuThe copy dropdown menu uses role="menu" with role="menuitem" on each option.
Focus managementautoFocus places focus on the input textarea when the component is shown.
KeyAction
EnterSend message
Shift+EnterInsert new line
TabNavigate between focusable elements
EscapeClose canvas panel (when canvas has focus)
Arrow Left/RightResize canvas panel (when resize handle has focus)
FeatureImplementation
Canvas landmarkCanvas panel uses role="complementary" with aria-label="MCP App Canvas".
Resize handleResize divider uses role="separator" with aria-orientation="vertical" and keyboard resize.

See specs/conversation.prd.md and specs/conversation-mcpui.prd.md for the complete specifications.


CronPicker

A visual builder for extended 6-field CRON expressions (second, minute, hour, day-of-month, month, day-of-week) with presets, mode-based field editors, human-readable descriptions, and bidirectional raw expression editing.

Quick Start

<link rel="stylesheet" href="components/cronpicker/cronpicker.css">
<script src="components/cronpicker/cronpicker.js"></script>

<div id="my-cron"></div>
<script>
    var picker = createCronPicker("my-cron", {
        value: "0 0 9 * * 1-5",
        onChange: function(expr) { console.log("Expression:", expr); }
    });
</script>

Configuration Options

OptionTypeDefaultDescription
valuestring"0 * * * * *"Initial CRON expression
showPresetsbooleantrueShow preset dropdown
showDescriptionbooleantrueShow human-readable description
showRawExpressionbooleantrueShow raw expression input
allowRawEditbooleantrueAllow editing raw expression
showFormatHintbooleantrueShow field order hint
presetsCronPreset[]Default listCustom presets
disabledbooleanfalseDisable the component
readonlybooleanfalseRead-only mode
size"sm" | "default" | "lg""default"Size variant
onChange(value: string) => voidExpression change callback
onPresetSelect(preset: CronPreset) => voidPreset selection callback

CronPreset Interface

interface CronPreset {
    label: string;  // Display label
    value: string;  // CRON expression
}

Public API

MethodReturnsDescription
getValue()stringCurrent CRON expression
getDescription()stringHuman-readable description
setValue(cron)voidSet expression programmatically
clear()voidReset to default expression
enable()voidEnable component
disable()voidDisable component
destroy()voidRemove from DOM

CRON Expression Format

Six-field extended format: second minute hour day-of-month month day-of-week

FieldRangeLabels
Second0–59Numeric
Minute0–59Numeric
Hour0–23Numeric
Day of Month1–31Numeric
Month1–12Jan–Dec
Day of Week0–6Sun–Sat

Supported Syntax

SyntaxMeaningExample
*Every valueEvery minute
NSpecific value30
N,MList1,15
N-MRange1-5 (Mon–Fri)
*/NStep from 0*/15 (every 15)
N/MStep from N5/10

Default Presets

PresetExpression
Every second* * * * * *
Every minute0 * * * * *
Every hour0 0 * * * *
Daily at midnight0 0 0 * * *
Daily at noon0 0 12 * * *
Weekdays at 9am0 0 9 * * 1-5
Weekly on Monday0 0 0 * * 1
Monthly on the 1st0 0 0 1 * *
Yearly on Jan 10 0 0 1 1 *

Features

Dependencies


DataGrid

High-performance flat data table with sorting, filtering, pagination, column resize, row selection, inline editing, virtual scrolling, footer aggregation, and CSV export.

Quick Start

<link rel="stylesheet" href="components/datagrid/datagrid.css">
<script src="components/datagrid/datagrid.js"></script>
<script>
    var grid = createDataGrid({
        columns: [
            { id: "name", label: "Name", sortable: true, filterable: true },
            { id: "status", label: "Status", filterType: "select",
              filterOptions: [
                  { value: "active", label: "Active" },
                  { value: "inactive", label: "Inactive" }
              ]},
            { id: "amount", label: "Amount", align: "right", aggregate: "sum" }
        ],
        rows: [
            { id: "1", data: { name: "Acme Corp", status: "active", amount: 1234.56 } },
            { id: "2", data: { name: "Beta Inc", status: "inactive", amount: 567.89 } }
        ],
        showFooter: true,
        selectable: "checkbox"
    }, "my-container");
</script>

API

DataGridOptions

PropertyTypeDefaultDescription
columnsDataGridColumn[]requiredColumn definitions
rowsDataGridRow[][]Initial row data
pageSizenumber50Rows per page (0 = no pagination)
pageSizeOptionsnumber[][25, 50, 100, 250]Page size dropdown options
serverSidebooleanfalseServer-side pagination mode
virtualScrolling"auto" | "enabled" | "disabled""auto"Virtual scrolling mode. "auto" activates above 5,000 rows
selectableboolean | "single" | "multi" | "checkbox"falseRow selection mode
showRowNumbersbooleanfalseShow row number column
showFooterbooleanfalseShow footer aggregation row
stripedbooleantrueAlternating row backgrounds
densebooleanfalseCompact row height (24px vs 32px)
enableColumnReorderbooleanfalseAllow drag-and-drop column reorder
externalSortbooleanfalseDisable built-in sorting (emit events only)
externalFilterbooleanfalseDisable built-in filtering (emit events only)
heightstring"400px"Scroll container height
emptyStateOptionsobjectOptions for empty state display
cssClassstringAdditional CSS class on root

DataGridColumn

PropertyTypeDefaultDescription
idstringrequiredUnique column identifier
labelstringrequiredColumn header text
widthnumber120Column width in pixels
minWidthnumber60Minimum column width
maxWidthnumberMaximum column width
resizablebooleantrueAllow column resize
sortablebooleantrueAllow sorting
filterablebooleanfalseShow filter in filter row
filterType"text" | "select" | "number-range" | "date-range""text"Filter input type
filterOptionsArray<{ value, label }>Options for select filter
editablebooleanfalseAllow inline cell editing
editorType"text" | "number" | "select" | "date""text"Editor input type
editorOptionsArray<{ value, label }>Options for select editor
align"left" | "center" | "right""left"Cell text alignment
renderer(cell, row, value) => voidCustom cell renderer
comparator(a, b) => numberCustom sort comparator
aggregate"sum" | "avg" | "count" | "min" | "max" | FunctionFooter aggregation
cssClassstringAdditional CSS class on cells
pinned"left" | "right"Pin column to side
hiddenbooleanfalseHide column
sizeHint"xs" | "s" | "m" | "l" | "xl"Initial size preset (xs=60, s=100, m=160, l=240, xl=360). Sets width and minWidth if not already specified

DataGridRow

PropertyTypeDescription
idstringUnique row identifier
dataRecord<string, unknown>Row data keyed by column ID
cssClassstringAdditional CSS class on row
disabledbooleanDisable selection and editing

Callbacks

CallbackSignatureDescription
onSort(sorts: SortEntry[]) => voidSort state changed
onFilter(filters: Record<string, unknown>) => voidFilter state changed
onPageChange(page: number, pageSize: number) => voidPage or page size changed
onCellEdit(rowId, colId, oldValue, newValue) => boolean | voidCell edit committed. Return false to reject
onRowSelect(selectedIds: string[]) => voidSelection changed
onRowClick(row: DataGridRow) => voidRow clicked
onRowDoubleClick(row: DataGridRow) => voidRow double-clicked
onColumnResize(columnId, width) => voidColumn resized
onColumnReorder(columnId, newIndex) => voidColumn reordered
onExport(format: "csv") => voidExport completed

Methods

MethodDescription
show(containerId)Render into a container element
hide()Remove from DOM (preserves state)
destroy()Clean up all resources
getElement()Returns the root DOM element
setRows(rows)Replace all row data
addRow(row, index?)Add a row at optional index
removeRow(rowId)Remove a row by ID
updateRow(rowId, data)Merge data into a row
getRow(rowId)Get a row by ID
setTotalRowCount(count)Set total count for server-side mode
getSelectedRows()Get selected row objects
selectRow(rowId)Select a row
deselectRow(rowId)Deselect a row
selectAll()Select all rows on current page
deselectAll()Deselect all rows
setPage(page)Navigate to a page
getPage()Get current page number
getPageCount()Get total page count
setPageSize(size)Change rows per page
sort(columnId, direction?)Sort by column
clearSort()Clear all sorts
setFilter(columnId, value)Set a column filter
clearFilters()Clear all filters
refresh()Re-process data and re-render
exportCSV(filename?)Export filtered/sorted data as CSV
getColumns()Get column definitions (copy)
showColumn(columnId)Un-hide a column
hideColumn(columnId)Hide a column
setColumnWidth(columnId, width)Set column width
scrollToRow(rowId)Scroll a row into view

Features

Keyboard

KeyAction
Arrow keysNavigate cells (2D grid pattern)
Home / EndFirst / last cell in row
Ctrl+Home / Ctrl+EndFirst / last cell in grid
SpaceToggle row selection
Enter / F2Start inline editing
EscapeCancel editing
Tab / Shift+TabNext / previous editable cell (while editing)
Ctrl+ASelect all rows on page

Accessibility


DatePicker

A calendar date picker with day, month, and year navigation views.

Quick Start

Script Tag

<link rel="stylesheet" href="https://theme.priyavijai-kalyan2007.workers.dev/components/datepicker/datepicker.css">
<script src="https://theme.priyavijai-kalyan2007.workers.dev/components/datepicker/datepicker.js"></script>

<div id="my-date"></div>
<script>
    var picker = createDatePicker("my-date", {
        format: "yyyy-MM-dd",
        firstDayOfWeek: 0,
        onSelect: function(date) { console.log("Selected:", date); }
    });
</script>

Configuration Options

OptionTypeDefaultDescription
valueDateTodayInitial selected date
formatstring"yyyy-MM-dd"Display format
firstDayOfWeeknumber00=Sunday, 1=Monday, …, 6=Saturday
showWeekNumbersbooleanfalseShow ISO week numbers
showTodayButtonbooleantrueShow Today button
minDateDateEarliest selectable date
maxDateDateLatest selectable date
disabledDatesDate[]Specific non-selectable dates
isDateDisabled(date: Date) => booleanCustom disable function
disabledbooleanfalseDisable the component
readonlybooleanfalseRead-only input, calendar works
placeholderstringFormat stringPlaceholder text
size"mini" | "sm" | "default" | "lg""default"Size variant
localestring"en-US"Locale for month/day names
showFormatHintbooleantrueShow format hint below input
formatHintstringFormat stringCustom hint text
showFormatHelpbooleantrueShow help icon
formatHelpTextstringAuto-generatedCustom help tooltip
onSelect(date: Date) => voidSelection callback
onChange(date: Date | null) => voidValue change callback
onOpen() => voidCalendar open callback
onClose() => voidCalendar close callback

Instance Methods

MethodReturnsDescription
getValue()Date | nullCurrent selected date
getFormattedValue()stringFormatted date string
setValue(date)voidSet date programmatically
open()voidOpen the calendar
close()voidClose the calendar
navigateTo(year, month)voidNavigate to specific month
enable()voidEnable the component
disable()voidDisable the component
setMinDate(date)voidSet minimum date
setMaxDate(date)voidSet maximum date
destroy()voidRemove from DOM

Keyboard Interactions

KeyDay View
Arrow keysNavigate by day/week
Home / EndFirst/last day of week
PageUp / PageDownPrevious/next month
Shift+PageUp/DownPrevious/next year
Enter / SpaceSelect focused date
EscapeClose calendar
tJump to today

Dependencies


DiagramEngine

Universal vector canvas engine for diagramming, graph visualization, technical drawing, poster creation, and embedded document surfaces. 22,000+ lines across 26 source modules with full gradient, image, text-along-path, and raster painting support.

Quick Start

<link rel="stylesheet" href="components/diagramengine/diagramengine.css">
<script src="components/diagramengine/diagramengine.js"></script>

<div id="canvas" style="width: 100%; height: 600px;"></div>

<script>
var engine = createDiagramEngine("canvas", {
    grid: { visible: true, size: 20, style: "dots" },
    editable: true,
    textEditable: true,
});

engine.loadStencilPack("flowchart");

engine.addObject({
    semantic: { type: "process", data: { label: "Start" } },
    presentation: {
        shape: "rectangle",
        bounds: { x: 100, y: 100, width: 160, height: 80 },
        textContent: {
            runs: [{ text: "Start", bold: true }],
            overflow: "visible",
            verticalAlign: "middle",
            horizontalAlign: "center",
            padding: 8,
        },
    },
});
</script>

Features

Core

Drawing & Painting

Gradients

Text

Connectors

Images

Viewport

Organisation

Persistence & Export

Analysis & Collaboration

Stencil Packs

PackShapesLoad
basicrectangle, ellipse, diamond, triangle, textAuto-loaded
extendedhexagon, star, cross, parallelogram, arrow-right, chevron, callout, donut, image, icon, path, paintableAuto-loaded
flowchartprocess, decision, terminator, data, document, preparation, databaseloadStencilPack("flowchart")
umluml-class, uml-actor, uml-note, uml-component, uml-packageloadStencilPack("uml")
bpmnbpmn-task, bpmn-start-event, bpmn-end-event, bpmn-gatewayloadStencilPack("bpmn")
erer-entity, er-relationshiploadStencilPack("er")
networkserver, cloud, firewallloadStencilPack("network")

Custom shapes can be registered via registerStencilPack(name, shapes).

Tools

ToolNameCursorDescription
SelectselectdefaultClick, multi-select, move, resize, rotate, rubber band, connector selection
DrawdrawcrosshairClick/drag to place shapes from stencil palette
TexttexttextClick to create text objects
ConnectconnectcrosshairClick source → drag → release on target; port indicators shown on nearby shapes
PenpencrosshairClick to add anchor points; click near first point to close shape; Enter to finalise
BrushbrushcrosshairFreehand vector drawing with point simplification
HighlighterhighlightercrosshairSemi-transparent marker strokes; 6 preset colours
PaintbrushpaintbrushcrosshairRaster painting inside paintable shapes; configurable size, hardness, alpha
MeasuremeasurecrosshairClick+drag to measure distances
PanpangrabDrag to pan canvas; also via middle-mouse or Space+drag
ZoomScroll wheel, +/-, 0 for zoom-to-fit

Tool Configuration

// Access any tool instance for property configuration
var brush = engine.getToolInstance("paintbrush");
brush.brushSize = 12;
brush.brushHardness = 0.3;  // 0 = airbrush, 1 = hard edge
brush.brushColor = "#ff0000";
brush.brushAlpha = 0.6;
brush.brushShape = "circle";  // or "square"

var highlighter = engine.getToolInstance("highlighter");
highlighter.highlightColor = "rgba(255, 182, 193, 0.4)";  // pink

Style System

Fill Types

// Solid fill
{ type: "solid", color: "#1c7ed6" }

// Linear gradient fill
{ type: "gradient", gradient: {
    type: "linear", angle: 90,
    stops: [
        { offset: 0, color: "rgba(255, 0, 0, 0.8)" },
        { offset: 1, color: "rgba(0, 0, 255, 0.8)" }
    ]
}}

// Radial gradient fill
{ type: "gradient", gradient: {
    type: "radial", center: { x: 0.3, y: 0.3 }, radius: 0.8,
    stops: [
        { offset: 0, color: "#ffffff" },
        { offset: 1, color: "#000000" }
    ]
}}

// No fill
{ type: "none" }

Stroke Types

// Solid stroke
{ color: "#000000", width: 2 }

// Gradient stroke
{ color: { type: "linear", angle: 90, stops: [...] }, width: 3 }

// Dashed stroke
{ color: "#333", width: 1.5, dashPattern: [6, 3] }

Per-Edge Borders

perEdgeStroke: {
    top: { visible: true, color: "#1c7ed6", width: 3 },
    right: { visible: true, color: { type: "linear", stops: [...] }, width: 4 },
    bottom: { visible: true, color: "#dc2626", width: 3 },
    left: { visible: false }  // hidden
}

Text Styling

textContent: {
    runs: [
        { text: "Bold ", bold: true, color: "#1c7ed6" },
        { text: "Gradient ", color: { type: "linear", angle: 0,
            stops: [{ offset: 0, color: "#FF0000" }, { offset: 1, color: "#0000FF" }]
        }},
        { text: "Alpha", color: "rgba(111, 66, 193, 0.5)" }
    ],
    overflow: "visible",
    verticalAlign: "middle",
    horizontalAlign: "center",
    padding: 8
}

Text Along Path (WordArt)

textContent: {
    runs: [{ text: "Curved Text!", fontSize: 24, bold: true }],
    textPath: {
        path: "M 0,100 Q 200,0 400,100",  // SVG path in local coordinates
        startOffset: 0.5,                   // 50% along path
        textAnchor: "middle"                // centre text at offset
    },
    overflow: "visible",
    verticalAlign: "middle",
    horizontalAlign: "center",
    padding: 0
}

Image Objects

presentation: {
    shape: "image",
    image: {
        src: "https://example.com/photo.png",
        fit: "contain",  // cover, contain, stretch, original
        headers: { "Authorization": "Bearer token123" }  // optional auth
    }
}

Paintable Shapes

presentation: {
    shape: "paintable",
    paintable: {
        clipShape: "rectangle",  // rectangle, circle, ellipse, triangle
        clipToBounds: true,      // clip paint to shape boundary
        canvasData: "data:image/png;base64,..."  // persisted canvas content
    }
}

API Reference

Document

MethodDescription
getDocument()Returns the full document object
setDocument(doc)Replaces the document
clear()Clears all objects, connectors, comments
toJSON(indent?)Serialises to JSON string
fromJSON(json)Loads from JSON string
isDirty()Whether document has unsaved changes
markClean()Resets dirty flag

Objects

MethodDescription
addObject(partial)Add object with defaults filled in
removeObject(id)Remove by ID (also removes attached connectors)
updateObject(id, changes)Deep-merge changes (preserves sibling style properties)
getObject(id)Get by ID or null
getObjects()All objects
getObjectsBySemanticType(type)Filter by semantic type

Connectors

MethodDescription
addConnector(partial)Create connector between objects
removeConnector(id)Remove by ID
updateConnector(id, changes)Update properties
getConnector(id)Get by ID or null
getConnectors()All connectors
getConnectorsBetween(a, b)Connectors between two objects

Selection

MethodDescription
select(ids)Select objects by ID
clearSelection()Clear all selection
getSelectedObjects()Selected objects
getSelectedConnectors()Selected connectors

Viewport

MethodDescription
zoomIn() / zoomOut()Step zoom (0.15 increments)
zoomToFit()Fit all objects in view
setZoomLevel(n)Set exact zoom (0.1 – 32.0)
getZoomLevel()Current zoom level
getViewport()Viewport state (x, y, zoom)

Tools

MethodDescription
setActiveTool(name)Switch active tool (cursor updates automatically)
getActiveTool()Current tool name
getToolInstance(name)Get tool instance for property configuration
setDrawShape(type)Set shape type for draw tool

Transform

MethodDescription
rotateObjects(ids, degrees)Rotate objects
flipHorizontal(ids) / flipVertical(ids)Mirror objects
alignObjects(ids, alignment)Align (left, center, right, top, middle, bottom)
distributeObjects(ids, axis)Distribute evenly (horizontal, vertical)
bringToFront(ids) / sendToBack(ids)Z-ordering

Groups & Layers

MethodDescription
group(ids) / ungroup(groupId)Group/ungroup objects
collapseGroup(id) / expandGroup(id)Collapse/expand groups
addLayer(partial) / removeLayer(id) / getLayers()Layer management

Clipboard & History

MethodDescription
copy() / cut() / paste() / duplicate()Clipboard operations
undo() / redo()History navigation
canUndo() / canRedo()History state

Page Frames

MethodDescription
addPageFrame(sizeName, position?)Add frame (40+ predefined sizes)
removePageFrame(id)Remove frame
lockPageFrame(id) / unlockPageFrame(id)Lock/unlock position
setPageFrameMargins(id, margins)Set margins (normal, narrow, wide, none, custom)
setPageFrameBorder(id, color, width)Customise border
setPageFrameBackground(id, color)Set background
getPageFrames() / getPageFrameSizes()Query frames and available sizes

Export

MethodDescription
exportSVG()Vector SVG export
exportJSON()Full document JSON
exportPNG(options?)Deprecated — use exportSVG() + server-side rendering

Format & Search

MethodDescription
pickFormat(id) / applyFormat(ids)Format painter
clearFormat() / hasFormat()Format state
findText(query, options?)Search text content
replaceText(query, replacement, options?)Replace text

Graph Analysis

MethodDescription
getShortestPath(from, to)BFS shortest path
getConnectedComponents()DFS connected components
getIncomingConnectors(id) / getOutgoingConnectors(id)Connector queries

Spatial Queries

MethodDescription
findObjectsInRect(rect)Objects within rectangle
findObjectsAtPoint(point)Objects at point

Comments & Navigation

MethodDescription
addComment(anchor, content, metadata?)Add comment
getComments() / getCommentsForObject(id)Query comments
resolveComment(id)Mark resolved
navigateToURI(uri)Deep link navigation

Events

EventPayload
object:add / object:remove / object:changeDiagramObject
connector:add / connector:removeDiagramConnector
selection:change{ objects, connectors }
viewport:changeViewportState
tool:changetool name string
history:undo / history:redo
dirty:changeboolean
text:edit:start / text:edit:endDiagramObject
comment:addDiagramComment

Lifecycle

MethodDescription
resize()Recalculate SVG dimensions
getElement()Root container element
destroy()Full cleanup

Keyboard Shortcuts

KeyAction
Ctrl+ZUndo
Ctrl+Y / Ctrl+Shift+ZRedo
Delete / BackspaceDelete selected objects or connectors
Ctrl+ASelect all
ArrowsNudge 1px (10px with Shift)
Ctrl+C / Ctrl+X / Ctrl+V / Ctrl+DCopy / Cut / Paste / Duplicate
Ctrl+G / Ctrl+Shift+GGroup / Ungroup
+/=Zoom in
-Zoom out
0Zoom to fit
Middle-click dragPan
Scroll wheelZoom at cursor position
Space+dragPan
EnterFinalise pen path
EscapeCancel current tool operation

Architecture

components/diagramengine/
├── diagramengine.ts        # Bundled output (22,000+ lines)
├── diagramengine.scss      # Component styles
├── diagramengine.test.ts   # Per-edge stroke + gradient tests
├── diagramengine-core.test.ts      # Factory, CRUD, selection, zoom, undo, serialisation
├── diagramengine-features.test.ts  # Connectors, layers, groups, transforms, clipboard
├── diagramengine-advanced.test.ts  # Find/replace, format painter, graph analysis, events
├── README.md               # This file
└── src/                    # 26 source modules (concatenated by bundle script)
    ├── types.ts            # All interfaces and type definitions
    ├── event-bus.ts        # Pub/sub event system
    ├── undo-stack.ts       # Command pattern undo/redo
    ├── shape-registry.ts   # Shape registration + SVG rendering utilities
    ├── shapes-basic.ts     # 5 basic shapes (rectangle, ellipse, diamond, triangle, text)
    ├── shapes-extended.ts  # 12 extended shapes (hexagon, star, path, paintable, etc.)
    ├── stencils-*.ts       # Flowchart, UML, BPMN, ER, Network stencil packs
    ├── connectors.ts       # Connector routing, rendering, hit testing
    ├── guides.ts           # Alignment and spacing guides
    ├── render-engine.ts    # SVG DOM rendering, selection, inline editing, images, text
    ├── tool-select.ts      # Selection, move, resize, rotate, connector selection
    ├── tool-draw.ts        # Shape placement
    ├── tool-text.ts        # Text object creation
    ├── tool-connect.ts     # Connector creation with port indicators
    ├── tool-pen.ts         # Vector path drawing with close-shape support
    ├── tool-brush.ts       # Freehand vector drawing
    ├── tool-highlighter.ts # Semi-transparent marker strokes
    ├── tool-paintbrush.ts  # Raster painting with hardness control
    ├── tool-measure.ts     # Distance measurement
    ├── tool-pan.ts         # Canvas panning
    ├── tool-manager.ts     # Tool lifecycle, cursor management
    ├── templates.ts        # Template variable resolution
    ├── page-frames.ts      # Page frame rendering (40+ sizes)
    └── engine.ts           # Main engine class (public API)

Spec

Full PRD: specs/diagramengine.prd.md (3,685 lines, 21 sections)


DockLayout

A CSS Grid-based layout coordinator that arranges Toolbar, Sidebar, TabbedPanel, StatusBar, and content into a 5-zone application shell. Inspired by Java Swing's BorderLayout — child components are automatically positioned and resized without manual pixel-positioning.

Assets

AssetPath
CSScomponents/docklayout/docklayout.css
JScomponents/docklayout/docklayout.js
Typescomponents/docklayout/docklayout.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/docklayout/docklayout.css">
<link rel="stylesheet" href="components/toolbar/toolbar.css">
<link rel="stylesheet" href="components/sidebar/sidebar.css">
<link rel="stylesheet" href="components/statusbar/statusbar.css">

<script src="components/toolbar/toolbar.js"></script>
<script src="components/sidebar/sidebar.js"></script>
<script src="components/statusbar/statusbar.js"></script>
<script src="components/docklayout/docklayout.js"></script>

<div id="my-content">
    <h1>Hello World</h1>
</div>

<script>
    var toolbar = new Toolbar({
        label: "My App",
        mode: "docked",
        dockPosition: "top",
        regions: [{ id: "main", tools: [{ id: "save", label: "Save", icon: "bi-floppy" }] }]
    });

    var sidebar = new Sidebar({
        title: "Explorer",
        dockPosition: "left",
        width: 260
    });

    var statusBar = new StatusBar({
        size: "sm",
        regions: [{ id: "status", label: "Ready" }]
    });

    var layout = createDockLayout({
        toolbar: toolbar,
        leftSidebar: sidebar,
        statusBar: statusBar,
        content: document.getElementById("my-content")
    });
</script>

How It Works

DockLayout creates a CSS Grid with 6 named areas:

┌─────────────────────────────────────┐
│              toolbar                │  auto height
├──────┬──────────────────┬───────────┤
│      │                  │           │
│ left │     center       │   right   │  1fr (fills remaining)
│      │                  │           │
├──────┴──────────────────┴───────────┤
│              bottom                 │  auto height
├─────────────────────────────────────┤
│              status                 │  auto height
└─────────────────────────────────────┘

When a component is passed to DockLayout, it automatically:

  1. Calls component.setContained(true) — switches from position: fixed to position: relative
  2. Calls component.show(gridCell) — mounts inside the grid cell
  3. Hooks resize/collapse listeners — updates grid template dynamically

Options

OptionTypeDefaultDescription
idstringautoCustom element ID
containerHTMLElement | stringdocument.bodyMount target
toolbarToolbarTop row, spans full width
leftSidebarSidebarLeft column
rightSidebarSidebarRight column
bottomPanelTabbedPanelBottom row, above status bar
statusBarStatusBarBottom-most row
contentHTMLElement | stringCenter cell element or CSS selector
cssClassstringAdditional CSS classes on root
heightstring"100vh"Height CSS value
widthstring"100vw"Width CSS value
onLayoutChange(layout: LayoutState) => voidFired on any resize/collapse/slot change

Public API

MethodReturnsDescription
show()voidAppend to container, display layout
hide()voidRemove from DOM (preserves state)
destroy()voidFull cleanup, destroy all child components
setToolbar(toolbar)voidSet or remove (null) the toolbar
setLeftSidebar(sidebar)voidSet or remove the left sidebar
setRightSidebar(sidebar)voidSet or remove the right sidebar
setBottomPanel(panel)voidSet or remove the bottom panel
setStatusBar(statusBar)voidSet or remove the status bar
setContent(element)voidSet or replace center content
getLayoutState()LayoutStateCurrent dimensions of all slots
getContentElement()HTMLElementThe center grid cell element
getRootElement()HTMLElementThe root grid container
isVisible()booleanWhether the layout is displayed

LayoutState

interface LayoutState {
    toolbar: { height: number } | null;
    leftSidebar: { width: number; collapsed: boolean } | null;
    rightSidebar: { width: number; collapsed: boolean } | null;
    bottomPanel: { height: number; collapsed: boolean } | null;
    statusBar: { height: number } | null;
    content: { width: number; height: number };
}

Contained Mode

DockLayout automatically sets contained: true on child components. This switches them from viewport-fixed positioning to parent-relative positioning. All existing component features (resize, collapse, callbacks) continue to work.

Components that support contained mode:

Dynamic Slot Management

// Add a bottom panel later
var chatPanel = new TabbedPanel({
    dockPosition: "bottom",
    height: 250,
    tabs: [{ id: "chat", title: "Chat", icon: "bi-chat" }]
});
layout.setBottomPanel(chatPanel);

// Remove it later
layout.setBottomPanel(null);

Integration with Content Components

Content placed in the center cell automatically fills it via CSS:

.dock-layout-center > * {
    width: 100%;
    height: 100%;
}

Components like TreeGrid, TreeView, Conversation, and Timeline work naturally in the center cell. For MarkdownEditor, pass contained: true to use height: "100%" instead of the default "70vh".

Global Exports

When loaded via <script> tag:

CSS Classes

ClassElementDescription
.dock-layoutRootCSS Grid container
.dock-layout-toolbarCellToolbar slot
.dock-layout-leftCellLeft sidebar slot
.dock-layout-centerCellCenter content area
.dock-layout-rightCellRight sidebar slot
.dock-layout-bottomCellBottom panel slot
.dock-layout-statusCellStatus bar slot

DocViewer

Full-page three-column documentation layout.

Overview

DocViewer renders documentation in a three-column CSS Grid layout: a hierarchical TOC tree on the left, Vditor-rendered content in the center, and an "On This Page" outline on the right with IntersectionObserver-based scroll tracking.

Usage

<!-- Vditor CDN (required) -->
<link rel="stylesheet" href="https://unpkg.com/vditor@3.11.2/dist/index.css" />
<script src="https://unpkg.com/vditor@3.11.2/dist/index.min.js"></script>

<!-- DocViewer -->
<link rel="stylesheet" href="components/docviewer/docviewer.css" />
<script src="components/docviewer/docviewer.js"></script>
var viewer = createDocViewer({
    container: document.getElementById("docs-container"),
    pages: [
        {
            id: "intro",
            title: "Introduction",
            markdown: "# Introduction\n\nWelcome to the docs.",
            children: [
                { id: "getting-started", title: "Getting Started", url: "/docs/getting-started.md" },
                { id: "installation", title: "Installation", url: "/docs/installation.md" }
            ]
        },
        {
            id: "api",
            title: "API Reference",
            icon: "bi-code-slash",
            url: "/docs/api.md"
        }
    ],
    activePage: "intro",
    onPageChange: function(pageId) { console.log("Page:", pageId); }
});

// Navigate programmatically
viewer.navigateTo("api");

Factory

FunctionReturnsDescription
createDocViewer(options)DocViewerHandleCreates documentation viewer

Options

PropertyTypeDefaultDescription
containerHTMLElementRequiredParent element
pagesDocPage[]RequiredPage tree
activePagestringFirst pageInitial page ID
showTocbooleantrueShow TOC panel
showOutlinebooleantrueShow outline panel
tocWidthnumber260TOC width in px
outlineWidthnumber220Outline width in px
onPageChange(pageId) => voidNavigation callback
onReady() => voidRender complete callback

DocPage

PropertyTypeRequiredDescription
idstringYesUnique identifier
titlestringYesTOC display title
markdownstringNoInline markdown
urlstringNoURL to fetch
childrenDocPage[]NoChild pages
iconstringNoBootstrap Icons class

API

MethodDescription
navigateTo(pageId)Navigate to page
getActivePage()Current page ID
expandTocNode(pageId)Expand TOC node
collapseTocNode(pageId)Collapse TOC node
searchToc(query)Filter TOC
getElement()Root element
destroy()Clean up

Layout

Three-column CSS Grid: 260px 1fr 220px (configurable).

Content Enhancements

Responsive

Keyboard

KeyAction
Arrow keysNavigate TOC tree
EnterSelect TOC item
ArrowRightExpand TOC node
ArrowLeftCollapse TOC node

Dependencies

Asset Paths

CSS: components/docviewer/docviewer.css
JS:  components/docviewer/docviewer.js

DurationPicker

A duration/interval picker with configurable unit patterns and ISO 8601 support.

Quick Start

<link rel="stylesheet" href="https://theme.priyavijai-kalyan2007.workers.dev/components/durationpicker/durationpicker.css">
<script src="https://theme.priyavijai-kalyan2007.workers.dev/components/durationpicker/durationpicker.js"></script>

<div id="my-duration"></div>
<script>
    var picker = createDurationPicker("my-duration", {
        pattern: "h-m",
        onChange: function(val) { console.log("Duration:", val); }
    });
</script>

Supported Patterns

d-h-m, h-m, h-m-s, h, m, s, m-s, w, fn, mo, q, y, y-mo, y-mo-d, w-d, w-d-h, d-h-m-s

Configuration Options

OptionTypeDefaultDescription
patternstring"h-m"Unit pattern
valueDurationValueAll zerosInitial value
unitStepsRecord<DurationUnit, number>All 1Step per unit
unitMaxRecord<DurationUnit, number>Natural rangesMax per unit
carrybooleanfalseOverflow carries to next unit
hideZeroLeadingbooleantrueHide leading zeros in display
displayFormat(val) => stringCustom formatter
showClearButtonbooleantrueShow Clear button
showFormatHintbooleantrueShow ISO 8601 hint
showFormatHelpbooleantrueShow help icon
disabledbooleanfalseDisable component
readonlybooleanfalseRead-only input
size"mini" | "sm" | "default" | "lg""default"Size variant
onChange(val: DurationValue) => voidChange callback

Instance Methods

MethodReturnsDescription
getValue()DurationValueCurrent duration
getFormattedValue()stringDisplay string
toISO()stringISO 8601 string
toTotalSeconds()numberTotal in seconds
setValue(val)voidSet duration
setFromISO(iso)voidSet from ISO string
clear()voidReset to zero
open() / close()voidToggle dropdown
enable() / disable()voidToggle state
destroy()voidRemove from DOM

Manual Input

Accepts ISO 8601 (PT4H30M) or shorthand (4h 30m).

Dependencies


EditableComboBox

A combined text input and dropdown list component built on Bootstrap 5. Users can type free text or select a value from a filterable dropdown.

Purpose and Use Cases

Quick Start

Script Tag

<!-- Dependencies -->
<link rel="stylesheet" href="css/custom.css">
<link rel="stylesheet" href="icons/bootstrap-icons.css">
<link rel="stylesheet" href="components/editablecombobox/editablecombobox.css">

<!-- Component -->
<div id="my-combo"></div>
<script src="components/editablecombobox/editablecombobox.js"></script>
<script>
    createEditableComboBox("my-combo", {
        items: [
            { label: "Apple" },
            { label: "Banana" },
            { label: "Cherry" }
        ],
        placeholder: "Pick a fruit..."
    });
</script>

ES Module

import { createEditableComboBox } from "./components/editablecombobox/editablecombobox.js";

const combo = createEditableComboBox("my-combo", {
    items: [{ label: "Red" }, { label: "Green" }, { label: "Blue" }],
    onSelect: (item) => console.log("Selected:", item.label)
});

Configuration Options

OptionTypeDefaultDescription
itemsComboBoxItem[]RequiredThe items to display in the dropdown
placeholderstringundefinedPlaceholder text for the input
valuestringundefinedInitial input value
restrictToItemsbooleanfalseWhen true, only list values are accepted
maxVisibleItemsnumber8Max visible items before scrolling
minFilterLengthnumber0Min characters before filtering starts
disabledbooleanfalseDisables the component
readonlybooleanfalseMakes input non-editable; dropdown still works
size"mini" | "sm" | "default" | "lg""default"Size variant
filterFnfunctionSubstring matchCustom filter function
onSelectfunctionundefinedCalled when an item is selected
onChangefunctionundefinedCalled when input value changes
onOpenfunctionundefinedCalled when dropdown opens
onClosefunctionundefinedCalled when dropdown closes

ComboBoxItem

PropertyTypeDefaultDescription
labelstringRequiredDisplay text
valuestringundefinedProgrammatic value (distinct from label)
disabledbooleanfalseItem is visible but not selectable
groupstringundefinedGroup header under which the item appears

Instance Methods

MethodReturnsDescription
getValue()stringCurrent input text
getSelectedItem()ComboBoxItem | nullSelected item, or null for free text
setValue(value)voidSet input value programmatically
setItems(items)voidReplace the dropdown items
open()voidOpen the dropdown
close()voidClose the dropdown
enable()voidEnable the component
disable()voidDisable the component
destroy()voidRemove from DOM and clean up

Keyboard Interactions

KeyClosedOpen
ArrowDownOpens, highlights firstHighlights next
ArrowUpOpens, highlights lastHighlights previous
EnterNo effectSelects highlighted item
EscapeNo effectCloses dropdown
TabNormal tabCommits highlight, moves focus
Home/EndCursor in inputFirst/last item
PageUp/DownNo effectScroll by 10 items

Accessibility

Dependencies

DependencyRequiredNotes
Bootstrap 5 CSSYesFor input-group, form-control, btn
Bootstrap 5 JSNoNot used by this component
Bootstrap IconsYesFor bi-chevron-down
Enterprise Theme CSSYesFor theme variable overrides

EmptyState

A centered placeholder component shown when a view, list, table, or container has no data. Presents a large icon (or custom illustration), heading, optional description, primary CTA button, and secondary link. Supports three size variants and compact mode.

Assets

AssetPath
CSScomponents/emptystate/emptystate.css
JScomponents/emptystate/emptystate.js
Typescomponents/emptystate/emptystate.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/emptystate/emptystate.css">
<script src="components/emptystate/emptystate.js"></script>
<script>
    var empty = createEmptyState("my-container", {
        heading: "No projects found",
        description: "Create your first project to get started.",
        actionLabel: "Create Project",
        actionIcon: "bi-plus-lg",
        onAction: function() { console.log("Create clicked"); }
    });
</script>

Options (EmptyStateOptions)

OptionTypeDefaultDescription
iconstring"bi-inbox"Bootstrap Icons class
iconColorstringCSS colour for icon
headingstringrequiredPrimary heading text
descriptionstringDescriptive text below heading
actionLabelstringPrimary CTA button label
actionIconstringCTA button icon class
actionVariantstring"primary"Bootstrap button variant
onActionfunctionCTA click handler
secondaryLabelstringSecondary link text
onSecondaryfunctionSecondary link handler
illustrationHTMLElementCustom element replacing icon
size"sm" | "md" | "lg""md"Size variant
compactbooleanfalseReduced padding for panels
cssClassstringAdditional CSS class(es)

API

MethodReturnsDescription
show(containerId?)voidAppend to container (or body)
hide()voidRemove from DOM, keep state
destroy()voidHide, clean up, null references
getElement()HTMLElementRoot DOM element
setHeading(text)voidUpdate heading text
setDescription(text)voidUpdate description text
setIcon(iconClass)voidReplace icon class
showAction(label, callback)voidShow/update CTA button
hideAction()voidHide CTA button

Global Exports

window.EmptyState
window.createEmptyState

Accessibility

See specs/emptystate.prd.md for the complete specification.


ErrorDialog Component

A Bootstrap 5 modal that displays literate error messages with user-friendly narrative and collapsible technical details.

Dependencies

Quick Start

<!-- In your HTML head -->
<link rel="stylesheet" href="css/custom.css">
<link rel="stylesheet" href="icons/bootstrap-icons.css">
<link rel="stylesheet" href="components/errordialog/errordialog.css">

<!-- Container where modals will be injected -->
<div id="error-dialog-container"></div>

<!-- Scripts -->
<script src="js/bootstrap.bundle.min.js"></script>
<script src="components/errordialog/errordialog.js"></script>
<script>
    showErrorDialog("error-dialog-container", {
        title: "Document Could Not Be Saved",
        message: "The server rejected the save request.",
        suggestion: "Please try again in a moment.",
        errorCode: "DOC_SAVE_FAILED",
        correlationId: "a1b2c3d4-e5f6-7890"
    });
</script>

LiterateError Interface

FieldTypeRequiredDescription
titlestringYesShort, non-alarming summary
messagestringYesFull sentence in plain language
suggestionstringNoActionable advice for the user
errorCodestringNoUnique, searchable error code
correlationIdstringNoUUID linking to backend logs
timestampstringNoUTC timestamp of the error
technicalDetailstringNoStack trace, API response, etc.
contextRecord<string, string>NoKey-value pairs of system state
onRetry() => voidNoCallback for the Retry button

API

Class: ErrorDialog

const dialog = new ErrorDialog("container-id");
dialog.show({ title: "Error", message: "Something went wrong." });
dialog.hide();
dialog.destroy();

Function: showErrorDialog(containerId, error)

One-liner convenience function. Creates an ErrorDialog instance and immediately shows it.

Features

Accessibility


ExplorerPicker

A reusable resource-selection widget that renders an Explorer tree in "picker mode." Users browse the organisational hierarchy or search by name to select one or more resources, then confirm their selection. Self-contained — a single CSS + JS file pair deployable via CDN with no runtime dependency on other library components.

Usage

CDN

<link rel="stylesheet" href="components/explorerpicker/explorerpicker.css">
<script src="components/explorerpicker/explorerpicker.js"></script>

Basic Example

const picker = createExplorerPicker({
    container: "my-picker-container",
    mode: "single",
    selectionTarget: "resource",
    onConfirm: (selections) =>
    {
        console.log("Selected:", selections);
    },
    onCancel: () =>
    {
        console.log("Cancelled");
    },
});

Modal via FormDialog

const dialog = createFormDialog({
    title: "Link to Resource",
    size: "large",
    fields: [],
    customContent: (container) =>
    {
        createExplorerPicker({
            container,
            mode: "multi",
            height: "100%",
            onConfirm: (selections) =>
            {
                dialog.close();
                createRelationships(selections);
            },
            onCancel: () => dialog.close(),
        });
    },
    hideDefaultButtons: true,
});
dialog.open();

Options

OptionTypeDefaultDescription
containerHTMLElement | stringContainer element or element ID (required)
mode"single" | "multi""single"Selection mode
selectionTarget"resource" | "container" | "any""resource"What types of nodes are selectable
resourceTypeFilterstring[]Only show matching resource types
excludeNodeIdsstring[]IDs of nodes that cannot be selected (dimmed)
initialExpandedNodeIdstringAuto-expand tree to this node on load
preSelectedNodeIdsstring[]Pre-selected nodes on render
apiBasestring"/api/v1/explorer"Base URL for Explorer API
fetchFntypeof fetchwindow.fetchCustom fetch function
showRecentItemsbooleantrueShow recent items section
showStarredItemsbooleantrueShow starred items section
quickAccessLimitnumber5Max items per quick-access section
searchPlaceholderstring"Search resources..."Search input placeholder
heightstring"400px"Picker height
cssClassstringAdditional CSS class(es)
iconResolver(node) => string | undefinedCustom icon resolver callback
ontologyApiBasestring"/api/v1/ontology"Base URL for ontology API
ontologyTypesOntologyTypeEntry[]Pre-loaded ontology types (skips API)
confirmButtonTextstring | (count) => string"Select"Confirm button text
cancelButtonTextstring"Cancel"Cancel button text
onConfirm(selections) => voidCalled on confirm (required)
onCancel() => voidCalled on cancel
onSelectionChange(selections) => voidCalled on every selection change
onError(error, context) => voidCalled on API errors

Instance API

MethodReturnsDescription
getSelection()ExplorerPickerSelection[]Get current selection
clearSelection()voidClear all selections
setSearchQuery(query)voidProgrammatically search
expandNode(nodeId)Promise<void>Expand a tree node
collapseNode(nodeId)voidCollapse a tree node
scrollToNode(nodeId)voidScroll node into view
refresh()Promise<void>Re-fetch all data
exportState()ExplorerPickerStateExport UI state for persistence
restoreState(state)Promise<void>Restore exported state
show(container?)voidMount into container
hide()voidRemove from DOM (preserves state)
destroy()voidFull cleanup
isLoading()booleanWhether data is loading
getElement()HTMLElement | nullRoot DOM element

Data Types

ExplorerNode

interface ExplorerNode
{
    id: string;
    name: string;
    nodeType: "ORG_UNIT" | "FOLDER" | "ASSET_REF" | "LINK";
    resourceId: string | null;
    resourceType: string | null;
    sourceId: string | null;
    url: string | null;
    linkType: string | null;
    hasChildren: boolean;
    parentId: string | null;
}

ExplorerPickerSelection

interface ExplorerPickerSelection
{
    nodeId: string;
    resourceId: string | null;
    name: string;
    nodeType: ExplorerNodeType;
    resourceType: string | null;
    sourceId: string | null;
    breadcrumb: string;
    url: string | null;
}

Features

Keyboard Shortcuts

KeyAction
ArrowDown/UpNavigate nodes
ArrowRightExpand node / move to first child
ArrowLeftCollapse node / move to parent
Home/EndJump to first/last node
EnterSelect (single) or toggle (multi)
SpaceToggle checkbox
EscapeClear search or cancel
/Focus search input

Chrome Effects

The component uses the Enterprise Theme chrome system:

Tests

82 unit tests covering:

npx vitest run components/explorerpicker/explorerpicker.test.ts

FacetSearch

Facet-aware search bar that combines free-text search with structured key:value query facets. Parsed facets appear as removable chips inline, while an autocomplete dropdown assists with facet key discovery and value selection.

Quick Start

<link rel="stylesheet" href="components/facetsearch/facetsearch.css">
<script src="components/facetsearch/facetsearch.js"></script>
<script>
    var search = createFacetSearch("my-container", {
        facets: [
            { key: "status", label: "Status", valueType: "enum",
              values: ["open", "closed", "pending"], icon: "bi bi-circle" },
            { key: "priority", label: "Priority", valueType: "enum",
              values: ["critical", "high", "medium", "low"] },
            { key: "author", label: "Author", valueType: "text" }
        ],
        onSearch: function(query) {
            console.log("Facets:", query.facets, "Text:", query.text);
        }
    });
</script>

Assets

AssetPath
CSScomponents/facetsearch/facetsearch.css
JScomponents/facetsearch/facetsearch.js
Typescomponents/facetsearch/facetsearch.d.ts

Requires: Bootstrap CSS, Bootstrap Icons CSS.

Facet Definition (FacetDefinition)

PropertyTypeDefaultDescription
keystring--Internal key for query syntax (e.g., "status")
labelstring--Human-readable label
valueTypeFacetValueType--"text", "enum", "date", "number", "boolean"
valuesstring[]undefinedStatic list of valid values (for enum type)
loadValues(query: string) => Promise<string[]>undefinedAsync value loader
iconstringundefinedBootstrap Icons class
multiplebooleanfalseAllow multiple values for the same key
defaultOperatorstring":"Default operator
operatorsstring[][":", "!:"]Allowed operators
colorstringundefinedChip background colour
valuePlaceholderstringundefinedHint for value input

Options (FacetSearchOptions)

PropertyTypeDefaultDescription
facetsFacetDefinition[]--Available facet definitions (required)
valuestring""Initial search string
placeholderstring"Search..."Input placeholder
showFacetChipsbooleantrueRender facets as inline chips
showHistorybooleanfalseShow recent searches in dropdown
maxHistorynumber10Maximum recent searches stored
submitOnEnterbooleantrueSubmit on Enter key
clearOnSubmitbooleanfalseClear after submit
size"sm" | "default" | "lg""default"Size variant
disabledbooleanfalseDisable all interaction
cssClassstringundefinedAdditional CSS class
onSearch(query) => voidundefinedSubmit callback
onChange(value) => voidundefinedInput change callback
onFacetAdd(key, value) => voidundefinedFacet added callback
onFacetRemove(key) => voidundefinedFacet removed callback
onClear() => voidundefinedClear callback

API Methods

MethodSignatureDescription
show(containerId: string) => voidRender into container
hide() => voidRemove from DOM, keep state
destroy() => voidFull teardown
getValue() => stringGet raw query string
setValue(value: string) => voidSet and parse query
getQuery() => FacetSearchQueryGet parsed query object
clear() => voidClear all chips and input
focus() => voidFocus the input
addFacet(key, value) => voidAdd a facet chip programmatically
removeFacet(key) => voidRemove a facet chip by key
getFacets() => ParsedFacet[]Get active facets
enable() => voidEnable the component
disable() => voidDisable the component

Query Syntax

SyntaxMeaning
status:openFacet equals
status!:openFacet not equals
-status:openNegated facet
count>10Greater than
count<=100Less than or equal
author:"Jane Doe"Quoted value with spaces
bug fixFree text

Keyboard Interaction

KeyAction
ArrowDown/UpNavigate dropdown suggestions
EnterSelect suggestion or submit search
EscapeClose dropdown
Backspace (at start)Remove last chip
TabAccept suggestion, close dropdown
Home/EndJump to first/last suggestion

Standalone Parser

The query parser is exported as a standalone pure function:

var result = parseFacetQuery("status:open priority:high bug fix", facetDefs);
// result.facets = [{key:"status", op:":", value:"open", negated:false}, ...]
// result.text = "bug fix"

See specs/facetsearch.prd.md for the full specification.


FileExplorer

Two-pane file navigation component with a folder tree sidebar, breadcrumb navigation, three view modes (grid, list, detail), multi-selection, context menu, inline rename, sorting, drag-and-drop, and callback-driven file operations. Supports both tree mode (sidebar + hierarchy) and flat mode (host-driven items + breadcrumbs).

Quick Start

Tree Mode (Default)

<link rel="stylesheet" href="components/fileexplorer/fileexplorer.css">
<script src="components/fileexplorer/fileexplorer.js"></script>
<script>
    var explorer = createFileExplorer("my-container", {
        roots: [
            {
                id: "root", name: "Documents", type: "folder",
                children: [
                    { id: "f1", name: "Report.pdf", type: "file", size: 245000 },
                    { id: "f2", name: "Budget.xlsx", type: "file", size: 89000 },
                    {
                        id: "sub", name: "Images", type: "folder",
                        children: [
                            { id: "f3", name: "logo.png", type: "file", size: 34000 }
                        ]
                    }
                ]
            }
        ],
        onOpen: function(file) { console.log("Open:", file.name); },
        onNavigate: function(folder) { console.log("Navigate:", folder.name); }
    });
</script>

Flat Mode (Host-Driven)

<script>
    var explorer = createFileExplorer(document.getElementById("panel"), {
        showTreePane: false,
        items: [
            { id: "d1", name: "Projects", type: "folder" },
            { id: "d2", name: "Design.fig", type: "file", typeLabel: "Diagram", iconColor: "#e74c3c" },
        ],
        breadcrumb: [
            { id: null, label: "Home" },
            { id: "org1", label: "My Org" },
        ],
        onBreadcrumbNavigate: function(segmentId) { loadFolder(segmentId); },
        onOpen: function(node) { openItem(node); },
    });
</script>

See the FileExplorer Flat Mode Guide for the full Apps Team Integration Guide.

Assets

AssetPath
CSScomponents/fileexplorer/fileexplorer.css
JScomponents/fileexplorer/fileexplorer.js
Typescomponents/fileexplorer/fileexplorer.d.ts

Requires: Bootstrap CSS, Bootstrap Icons CSS.

FileNode Interface

PropertyTypeDescription
idstringUnique identifier
namestringDisplay name
type"file" | "folder"Node type
iconstringBootstrap Icons class override
iconColorstringIcon color (CSS color value)
sizenumberFile size in bytes
modifiedDateLast modified date
mimeTypestringMIME type string
typeLabelstringType label for display (e.g. "Diagram")
ownerstringDisplay name of the item owner
readOnlybooleanDisables rename/delete
isSystembooleanDisables most actions
parentIdstringParent folder ID
childrenFileNode[]Child nodes (folders)
dataRecord<string, unknown>Custom data

BreadcrumbSegment Interface

PropertyTypeDescription
idstring | nullSegment ID. null = root.
labelstringDisplay label

FileExplorerColumn Interface

PropertyTypeDescription
idstringColumn identifier used in sort callbacks
labelstringColumn header label
widthstringFixed width (CSS value). Omit for flex.
sortablebooleanWhether clicking sorts. Default: true
render(node: FileNode) => string | HTMLElementCustom cell renderer

Options (FileExplorerOptions)

PropertyTypeDefaultDescription
rootsFileNode[][]Root-level nodes for tree mode
itemsFileNode[]--Initial flat items (enables flat mode)
breadcrumbBreadcrumbSegment[]--Initial breadcrumb path
viewMode"grid" | "list" | "detail""detail"Initial view mode
columnsFileExplorerColumn[]defaultsDetail view column definitions
showBreadcrumbsbooleantrueShow breadcrumb bar
showToolbarbooleantrueShow toolbar
showTreePanebooleantrueShow tree sidebar
treePaneWidthnumber250Initial tree pane width (px)
selectable"single" | "multi""multi"Selection mode
multiSelectboolean--Sugar for selectable: "multi"
contextMenuItemsFileContextMenuItem[] | ((node) => FileContextMenuItem[])[]Right-click menu items (static or dynamic)
heightstring"500px"Explorer height
showStatusLinebooleantrueShow status line
sortFieldstring"name"Initial sort field
sortDirectionstring"asc"Initial sort direction
groupBy"type-first" | "none" | function--Grouping strategy
emptyState{ icon?, title?, description? }--Empty state configuration
onNavigate(folder) => void--Folder navigation callback
onSelect(files) => void--Selection change callback
onSelectionChange(nodes) => void--Selection change (multi)
onOpen(file) => void--File open callback
onRename(file, name) => Promise<boolean>--Rename callback
onDelete(files) => Promise<boolean>--Delete callback
onMove(files, target) => Promise<boolean>--Move callback
onCreateFolder(parent, name) => Promise<FileNode>--New folder callback
onLoadChildren(folder) => Promise<FileNode[]>--Lazy load callback
onUpload(folder, files) => Promise<void>--Upload callback
onSort(field, direction) => void--Sort change callback
onViewModeChange(mode) => void--View mode change callback
onBreadcrumbNavigate(segmentId) => void--Breadcrumb click callback (flat mode)
onContextMenuAction(actionId, node) => void--Context menu action callback
onDragStart(nodes) => void--Drag start callback
onDrop(target, ids) => void--Drop on folder callback
onExternalDrop(target, dataTransfer) => void--External drop callback

API Methods

MethodDescription
show(containerOrId)Render into container (HTMLElement or string ID)
hide()Remove from DOM, keep state
destroy()Full teardown
navigate(folderId)Navigate to a folder (tree mode)
setItems(items)Replace displayed items (flat mode)
getItems()Get current items
setBreadcrumb(segments)Update breadcrumb path (flat mode)
getSelectedFiles()Get selected nodes
getSelectedIds()Get selected IDs
selectItem(id)Select a single item
selectItems(ids)Select multiple items
deselectAll()Clear selection
refresh()Re-render current view
setViewMode(mode)Switch view mode
getViewMode()Get current view mode
getCurrentFolder()Get current folder node
getPath()Get breadcrumb path array
addFile(file, parentId)Add a file node
removeFile(fileId)Remove a file node
updateFile(fileId, changes)Update a file node
setSort(field, direction)Change sort
showLoading()Show skeleton loading state
showEmpty(config?)Show configurable empty state
startRenamePublic(nodeId?)Start inline rename
focus()Focus listing for keyboard nav

View Modes

Grouping

Keyboard

KeyAction
ArrowDown/UpNavigate items
EnterOpen file or navigate folder
F2Start inline rename (guarded by readOnly/isSystem)
DeleteDelete selected items (filters out readOnly/isSystem)
EscapeCancel rename, close menu, deselect
BackspaceNavigate to parent (tree) or fire onBreadcrumbNavigate (flat)
Ctrl+ASelect all (multi mode)

CSS Custom Properties

PropertyDefaultDescription
--file-explorer-bgvar(--theme-surface-bg)Background color
--file-explorer-bordervar(--theme-border-subtle)Border color
--file-explorer-font-size$font-size-smBase font size
--file-explorer-toolbar-bgvar(--theme-surface-raised-bg)Toolbar background
--file-explorer-row-hover-bgvar(--theme-hover-bg)Row hover background
--file-explorer-row-selected-bgrgba($blue-100, 0.5)Selected row background
--file-explorer-group-colorvar(--theme-text-muted)Group header text color
--file-explorer-group-font-size$font-size-2xsGroup header font size

Override on .fileexplorer:

.fileexplorer {
    --file-explorer-bg: #1a1a2e;
    --file-explorer-border: #2d2d44;
    --file-explorer-row-selected-bg: rgba(59, 130, 246, 0.2);
}

File Type Icons

Extensions are auto-mapped to Bootstrap Icons (PDF, Word, Excel, images, video, audio, code, text, markdown, archives). Set FileNode.icon to override. Set FileNode.iconColor for custom icon colors.

See specs/fileexplorer.component.prd.md for the full specification.


FileUpload

A drag-and-drop file upload zone with progress bars, file type validation, size limits, batch upload, and an optional download queue. Modelled after Dropbox, Google Drive, and AWS S3 Console upload patterns.

Features

Assets

AssetPath
CSScomponents/fileupload/fileupload.css
JScomponents/fileupload/fileupload.js
Typescomponents/fileupload/fileupload.d.ts

Requires: Bootstrap CSS (for SCSS variables), Bootstrap Icons (for file type and action icons). Does not require Bootstrap JS.

Quick Start

Basic Upload

<link rel="stylesheet" href="components/fileupload/fileupload.css">
<script src="components/fileupload/fileupload.js"></script>

<div id="upload-container"></div>

<script>
    var uploader = createFileUpload("upload-container", {
        accept: "image/*,.pdf",
        maxFileSize: 5 * 1024 * 1024,
        maxFiles: 5,
        onUpload: function(file, onProgress) {
            return new Promise(function(resolve, reject) {
                // Simulate upload with progress
                var progress = 0;
                var interval = setInterval(function() {
                    progress += 0.1;
                    onProgress(progress);
                    if (progress >= 1) {
                        clearInterval(interval);
                        resolve();
                    }
                }, 200);
            });
        }
    });
</script>

With Downloads

<script>
    var uploader = createFileUpload("upload-container", {
        showDownloads: true,
        downloads: [
            { name: "report.pdf", size: 1258291, url: "/files/report.pdf" },
            { name: "data.csv", size: 45032, url: "/files/data.csv", icon: "bi-file-text" }
        ],
        onUpload: function(file, onProgress) {
            // Your upload logic here
            return fetch("/api/upload", { method: "POST", body: file });
        }
    });
</script>

ES Module

import { FileUpload, createFileUpload } from "./components/fileupload/fileupload";

const uploader = createFileUpload("my-container", {
    accept: "image/*",
    maxFileSize: 10 * 1024 * 1024,
    autoUpload: false,
    onUpload: async (file, onProgress) => {
        const formData = new FormData();
        formData.append("file", file);
        await uploadWithProgress("/api/upload", formData, onProgress);
    },
});

// Later, start all uploads manually
uploader.uploadAll();

API

Factory Function

createFileUpload(containerId: string, options: FileUploadOptions): FileUpload

Creates a FileUpload instance, appends it to the container, and returns the instance.

Constructor

const uploader = new FileUpload(options: FileUploadOptions);

Creates the FileUpload DOM but does not attach to the page. Call show() to attach.

FileUploadOptions

OptionTypeDefaultDescription
acceptstring--Accepted MIME types/extensions (e.g., "image/*,.pdf")
maxFileSizenumber10485760 (10 MB)Max individual file size in bytes
maxTotalSizenumber104857600 (100 MB)Max total upload size in bytes
maxFilesnumber10Max number of files
multiplebooleantrueAllow multiple file selection
onUploadfunction--Upload handler called per file with progress callback
onRemovefunction--Called when a file is removed from the queue
autoUploadbooleantrueStart uploads immediately on file add
showDownloadsbooleanfalseShow the download section
downloadsFileDownloadItem[]--Pre-populated download items
disabledbooleanfalseDisabled state
size"sm" | "md" | "lg""md"Size variant
cssClassstring--Additional CSS class(es)
keyBindingsPartial<Record<string, string>>--Override default key combos

FileDownloadItem

PropertyTypeRequiredDescription
namestringYesDisplay name of the file
sizenumberNoFile size in bytes (for display)
urlstringYesDownload URL
iconstringNoBootstrap icon class (e.g., "bi-file-pdf")

Methods

MethodDescription
show(containerId?)Appends to container (ID string) and activates listeners
hide()Removes from DOM without destroying state
destroy()Hides, releases all references
getElement()Returns the root DOM element
addFiles(files)Programmatically adds files to the queue
removeFile(fileId)Removes a file from the queue by internal ID
retryFile(fileId)Retries a failed file upload
clearAll()Removes all files from the queue
getFiles()Returns array of { name, status, progress }
setDisabled(disabled)Toggles the disabled state
setDownloads(downloads)Replaces the download section items
uploadAll()Starts uploading all queued files

Global Exports

window.FileUpload
window.createFileUpload

File States

StateDescriptionVisual
queuedFile added, upload not startedDefault styling, "Queued" text
uploadingUpload in progressBlue progress bar animating, percentage text
completedUpload succeededGreen progress bar at 100%, "Completed" text
failedUpload failedRed progress bar, error text, retry action button

Keyboard Shortcuts

KeyAction
EnterOpen file browser (when dropzone focused)
SpaceOpen file browser (when dropzone focused)
DeleteRemove file (future: when file row focused)
TabNavigate between dropzone, file rows, and action buttons

Override key bindings via the keyBindings option:

createFileUpload("container", {
    keyBindings: {
        browse: "Ctrl+O",
    },
});

Available Action Names

ActionDefaultDescription
browseEnterOpen file browser
browseAltSpaceOpen file browser (alternate)
removeFileDeleteRemove focused file

Accessibility


FlexGridLayout

An advanced CSS Grid layout container with mixed track sizes and cell spanning. Supports variable column and row definitions (px, fr, auto), named grid areas, per-cell alignment overrides, and multi-column/multi-row spanning. Designed for complex enterprise form layouts, dashboard grids, and any arrangement requiring non-uniform track sizing.

Assets

AssetPath
CSScomponents/flexgridlayout/flexgridlayout.css
JScomponents/flexgridlayout/flexgridlayout.js
Typescomponents/flexgridlayout/flexgridlayout.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/flexgridlayout/flexgridlayout.css">
<script src="components/flexgridlayout/flexgridlayout.js"></script>

<script>
    // A two-column form layout: fixed label column, flexible field column
    var layout = createFlexGridLayout({
        columns: ["160px", "1fr"],
        rows: ["auto", "auto", "auto"],
        gap: 8,
        padding: "16px",
        cells: [
            { child: document.getElementById("label-name"),  column: 0, row: 0 },
            { child: document.getElementById("field-name"),  column: 1, row: 0 },
            { child: document.getElementById("label-email"), column: 0, row: 1 },
            { child: document.getElementById("field-email"), column: 1, row: 1 },
            { child: document.getElementById("label-notes"), column: 0, row: 2, alignSelf: "start" },
            { child: document.getElementById("field-notes"), column: 1, row: 2 }
        ]
    });
</script>

How It Works

FlexGridLayout creates a CSS Grid container. Columns and rows are defined as arrays of track size strings. Each cell is placed at a specific column/row position and can optionally span multiple tracks.

columns: ["160px", "1fr", "1fr"]
rows:    ["auto", "1fr", "auto"]

     160px        1fr           1fr
   +----------+-------------+-------------+
   |  Header (columnSpan: 3)              |  auto
   +----------+-------------+-------------+
   |  Nav     |  Main content             |  1fr
   |  (fixed) |  (columnSpan: 2)          |
   +----------+-------------+-------------+
   |  Footer (columnSpan: 3)              |  auto
   +----------+-------------+-------------+

Track sizes can be mixed freely:

Options

FlexGridLayoutOptions

OptionTypeDefaultDescription
idstringautoCustom element ID
columnsstring[]--Column track definitions (required)
rowsstring[]--Row track definitions
gapnumber | string"0"Uniform gap between cells
rowGapnumber | string--Override gap for rows
columnGapnumber | string--Override gap for columns
areasstring[]--CSS grid-template-areas row strings
cellsFlexGridCellConfig[][]Initial cells to place
paddingstring--Container padding (CSS value)
cssClassstring--Additional CSS classes
heightstring--Height CSS value
widthstring--Width CSS value
onLayoutChange(state) => void--Fired on resize or structural changes

FlexGridCellConfig

PropertyTypeDefaultDescription
childHTMLElement | Component--Child element or component
columnnumber--0-based column index (required)
rownumber--0-based row index (required)
columnSpannumber1Number of columns to span
rowSpannumber1Number of rows to span
alignSelf"start" | "center" | "end" | "stretch"--Block-axis alignment override
justifySelf"start" | "center" | "end" | "stretch"--Inline-axis alignment override

Public API

MethodReturnsDescription
show(container?)voidAppend to container and display
hide()voidRemove from DOM (preserves state)
destroy()voidFull cleanup, destroy all cells
getRootElement()HTMLElement | nullThe root grid container
isVisible()booleanWhether the layout is displayed
addCell(config)voidPlace a child at a grid position
removeCell(column, row)voidRemove child at position
getCellElement(column, row)HTMLElement | nullWrapper element at position
setColumns(columns)voidUpdate column track definitions
setRows(rows)voidUpdate row track definitions
getState()FlexGridLayoutStateSerialisable state snapshot
setState(state)voidRestore column/row definitions
setContained(value)voidSet contained mode

FlexGridLayoutState

interface FlexGridLayoutState {
    columns: string[];
    rows: string[];
    cellCount: number;
}

Composability

FlexGridLayout implements the standard layout container contract. Any component with show(container) / hide() / destroy() can be used as a child. Plain HTMLElements are also supported. Components with a setContained(true) method are automatically put into contained mode.

// A dashboard layout with mixed track sizes
var dashboard = new FlexGridLayout({
    columns: ["240px", "1fr", "1fr"],
    rows: ["48px", "1fr", "32px"],
    gap: 4,
    height: "100vh",
    cells: [
        { child: toolbar,    column: 0, row: 0, columnSpan: 3 },
        { child: sidebar,    column: 0, row: 1 },
        { child: mainPanel,  column: 1, row: 1, columnSpan: 2 },
        { child: statusBar,  column: 0, row: 2, columnSpan: 3 }
    ]
});

Global Exports

When loaded via <script> tag:

CSS Classes

ClassElementDescription
.flexgridlayoutRootCSS Grid container
.flexgridlayout-cellWrapperPer-cell grid wrapper with placement

FlowLayout

A wrapping flex layout container that arranges children sequentially and wraps to the next line when the boundary is reached. Supports configurable gap, alignment, content distribution, and separate row/column gaps.

Assets

AssetPath
CSScomponents/flowlayout/flowlayout.css
JScomponents/flowlayout/flowlayout.js
Typescomponents/flowlayout/flowlayout.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/flowlayout/flowlayout.css">
<script src="components/flowlayout/flowlayout.js"></script>

<script>
    var tags = ["JavaScript", "TypeScript", "CSS", "HTML", "React", "Vue"];
    var children = tags.map(function(tag) {
        var el = document.createElement("span");
        el.className = "badge bg-primary";
        el.textContent = tag;
        return el;
    });

    var layout = createFlowLayout({
        direction: "horizontal",
        gap: 8,
        align: "center",
        children: children
    });
</script>

How It Works

FlowLayout creates a CSS Flexbox container with flex-wrap: wrap. Children are placed sequentially along the primary axis and wrap to new lines when they exceed the container boundary.

direction: "horizontal", gap: 8
┌─────────────────────────────────────────────┐
│ [Tag 1] [Tag 2] [Tag 3] [Tag 4] [Tag 5]   │
│ [Tag 6] [Tag 7] [Tag 8]                    │
└─────────────────────────────────────────────┘

Options

OptionTypeDefaultDescription
idstringautoCustom element ID
direction"horizontal" | "vertical""horizontal"Primary flow direction
childrenArray<HTMLElement | Component>[]Initial children
gapnumber | string"0"Gap between children
rowGapnumber | stringOverride gap for rows
columnGapnumber | stringOverride gap for columns
align"start" | "center" | "end" | "stretch" | "baseline""stretch"Per-line cross-axis alignment
justify"start" | "center" | "end" | "space-between" | "space-around" | "space-evenly""start"Main-axis distribution
alignContent"start" | "center" | "end" | "stretch" | "space-between" | "space-around""start"Distribution of lines
paddingstringContainer padding
cssClassstringAdditional CSS classes
heightstringHeight CSS value
widthstringWidth CSS value
onLayoutChange(state) => voidFired on resize events

Public API

MethodReturnsDescription
show(container?)voidAppend to container and display
hide()voidRemove from DOM (preserves state)
destroy()voidFull cleanup, destroy all children
getRootElement()HTMLElement | nullThe root flex container
isVisible()booleanWhether the layout is displayed
addChild(child, index?)voidAdd a child at optional index
removeChild(index)voidRemove child by index
clear()voidRemove all children
getChildCount()numberNumber of children
getChildElement(index)HTMLElement | nullWrapper element at index
getState()FlowLayoutStateSerialisable state snapshot
setState(state)voidRestore state (direction)
setContained(value)voidSet contained mode

Global Exports

When loaded via <script> tag:

CSS Classes

ClassElementDescription
.flowlayoutRootFlex-wrap container
.flowlayout-childWrapperPer-child wrapper

FontDropdown

A dropdown where each font name renders in its own typeface, similar to the font picker in Google Docs. Includes a search input for filtering, recently-used tracking via localStorage, and full keyboard navigation.

Purpose and Use Cases

Quick Start

Script Tag

<!-- Dependencies -->
<link rel="stylesheet" href="css/custom.css">
<link rel="stylesheet" href="icons/bootstrap-icons.css">
<link rel="stylesheet" href="components/fontdropdown/fontdropdown.css">

<!-- Component -->
<div id="my-fonts"></div>
<script src="components/fontdropdown/fontdropdown.js"></script>
<script>
    createFontDropdown("my-fonts", {
        value: "Georgia, serif",
        showRecent: true,
        onChange: (font) => console.log("Selected:", font.label)
    });
</script>

ES Module

import { createFontDropdown } from "./components/fontdropdown/fontdropdown.js";

const picker = createFontDropdown("my-fonts", {
    placeholder: "Choose a font...",
    previewText: "The quick brown fox",
    onChange: (font) => applyFont(font.value)
});

Configuration Options

OptionTypeDefaultDescription
fontsFontItem[]17 web-safe fontsCustom font list; see Default Fonts below
valuestringundefinedInitially selected font value
placeholderstring"Select font..."Placeholder text when no font is selected
showRecentbooleanfalseShow recently-used fonts section
maxRecentnumber5Maximum number of recent fonts to remember
previewTextstringundefinedOptional preview text shown beside each font name
size"mini" | "sm" | "default" | "lg""default"Size variant
disabledbooleanfalseDisables the dropdown
maxVisibleItemsnumber8Max visible items before scrolling
onChangefunctionundefinedCalled when the selected font changes
onOpenfunctionundefinedCalled when the dropdown opens
onClosefunctionundefinedCalled when the dropdown closes
keyBindingsPartial<Record<string, string>>undefinedOverride default keyboard bindings

FontItem

PropertyTypeDescription
labelstringDisplay name shown in the dropdown (e.g. "Arial")
valuestringCSS font-family value (e.g. "Arial, sans-serif")

Instance Methods

MethodReturnsDescription
getValue()stringCurrent font-family value, or "" if none
getSelectedFont()FontItem | nullSelected font object, or null
setValue(value)voidSelect a font by value or label
setFonts(fonts)voidReplace the entire font list
open()voidOpen the dropdown
close()voidClose the dropdown
enable()voidEnable the component
disable()voidDisable the component
getElement()HTMLElement | nullRoot DOM element
destroy()voidRemove from DOM and clean up

Default Fonts

The component ships with 17 web-safe fonts when no custom fonts array is provided:

Arial, Calibri, Cambria, Comic Sans MS, Consolas, Courier New, Georgia, Helvetica, Impact, JetBrains Mono, Lucida Console, Open Sans, Palatino, Tahoma, Times New Roman, Trebuchet MS, Verdana.

Keyboard Interactions

KeyClosedOpen
ArrowDownOpens dropdownHighlights next item
ArrowUpOpens dropdownHighlights previous item
SpaceOpens dropdownTypes in search
EnterNo effectSelects highlighted item
EscapeNo effectCloses dropdown
HomeNo effectFirst item
EndNo effectLast item
PageUp/DownNo effectScroll by page

Key bindings can be overridden via the keyBindings option using action names: openOrMoveDown, openOrMoveUp, confirmSelection, closeDropdown, jumpToFirst, jumpToLast, pageDown, pageUp.

Recently Used Fonts

When showRecent: true, the dropdown displays a "Recently Used" section above the full font list. Selections are persisted to localStorage under the key fontdropdown-recent. Configure the maximum count with maxRecent (default 5).

Size Variants

<script>createFontDropdown("sm-picker", { size: "sm" });</script>
<script>createFontDropdown("default-picker", {});</script>
<script>createFontDropdown("lg-picker", { size: "lg" });</script>

Size classes applied: fontdropdown-sm, fontdropdown-lg.

Ribbon Integration

Register FontDropdown as a custom control in the Ribbon component:

const ribbon = createRibbon("ribbon-host", {
    tabs: [{
        label: "Home",
        groups: [{
            label: "Font",
            controls: [{
                type: "custom",
                render: (container) => {
                    createFontDropdown(container.id, {
                        size: "sm",
                        showRecent: true,
                        onChange: (font) => applyFont(font.value)
                    });
                }
            }]
        }]
    }]
});

Accessibility

Dependencies

DependencyRequiredNotes
Bootstrap 5 CSSYesFor base styling variables
Bootstrap 5 JSNoNot used by this component
Bootstrap IconsYesFor bi-chevron-down caret icon
Enterprise Theme CSSYesFor theme variable overrides

FormDialog

A modal dialog optimized for form-based workflows (create, edit, invite, assign). Supports two mutually exclusive modes: single-page (scrollable form body with optional collapsible sections) and wizard (multi-step form with step indicator, Back/Next navigation, and per-step validation). Optionally includes a resizable sidebar panel for help text, previews, or contextual information.

Assets

AssetPath
CSScomponents/formdialog/formdialog.css
JScomponents/formdialog/formdialog.js
Typescomponents/formdialog/formdialog.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="css/custom.css">
<link rel="stylesheet" href="icons/bootstrap-icons.css">
<link rel="stylesheet" href="components/formdialog/formdialog.css">

<script src="components/formdialog/formdialog.js"></script>
<script>
    const dialog = createFormDialog({
        title: "Invite New User",
        size: "sm",
        description: "Send an invitation to join your workspace.",
        submitLabel: "Send Invitation",
        fields: [
            { name: "email", label: "Email Address", type: "email", required: true, placeholder: "user@example.com" },
            { name: "role", label: "Role", type: "select", required: true, value: "MEMBER",
              options: [{ value: "MEMBER", label: "Member" }, { value: "ADMIN", label: "Admin" }] }
        ],
        onSubmit: async (values) => {
            await fetch("/api/invitations", { method: "POST", body: JSON.stringify(values) });
        }
    });

    dialog.show();
</script>

API

Factory Function

FunctionReturnsDescription
createFormDialog(options)FormDialogCreate a form dialog instance

FormDialog Instance

MethodReturnsDescription
show()voidOpen dialog, focus first field
close()voidClose dialog, call onCancel
destroy()voidRemove from DOM, clean up listeners
getValue(name)stringGet field value by name
setValue(name, value)voidSet field value by name
getValues()Record<string, string>Get all field values as flat object
setFieldError(name, msg)voidShow inline error below a field
clearFieldErrors()voidRemove all field error messages
setLoading(loading)voidToggle loading state (spinner, disable fields)
setTitle(title)voidUpdate dialog title text
isDirty()booleanCheck if any field changed from initial value
goToStep(index)voidNavigate to wizard step by index
nextStep()voidAdvance to next wizard step (validates current)
prevStep()voidGo back to previous wizard step
getCurrentStep()numberGet current wizard step index
toggleSection(id)voidToggle a collapsible section
setSectionCollapsed(id, collapsed)voidSet section collapsed state
updatePanel(content)voidReplace panel content with new element
getElement()HTMLElementGet the overlay root element
getContentElement()HTMLElementGet the form body element

FormDialogOptions

OptionTypeDefaultDescription
titlestringrequiredDialog title
descriptionstring--Description text below the header
sizestring"md""sm" (400px), "md" (550px), "lg" (750px), "xl" (960px)
fieldsFormFieldDef[]--Single-page mode field definitions
sectionsFormSection[]--Collapsible section definitions
stepsFormStep[]--Wizard mode step definitions (mutually exclusive with fields)
stepTransitionstring"slide"Wizard transition: "slide", "fade", "none"
panelFormDialogPanel--Resizable sidebar panel configuration
submitLabelstring"Submit"Submit button text
cancelLabelstring"Cancel"Cancel button text
nextLabelstring"Next"Wizard next button text
backLabelstring"Back"Wizard back button text
onSubmitfunctionrequiredCalled with all field values; may return a Promise
onCancelfunction--Called when dialog is cancelled
onStepChangefunction--Called with (stepIndex, stepId) on wizard navigation
onFieldChangefunction--Called with (name, value) on any field input
customContentHTMLElement--Custom DOM appended after fields
showFooterbooleantrueShow footer with Submit/Cancel buttons; set false when customContent manages its own actions
autoClosebooleantrueAuto-close after successful submit
closeOnBackdropbooleantrueClose on backdrop click
closeOnEscapebooleantrueClose on Escape key
warnOnDirtybooleanfalseShow confirm dialog before discarding changes
cssClassstring--Additional CSS class on dialog
keyBindingsobject--Override default key combos

Field Types

TypeElementNotes
text<input type="text">Standard text input
email<input type="email">Basic email validation
password<input type="password">Password input
number<input type="number">Numeric input
select<select>Dropdown from options array
textarea<textarea>Multi-line, rows configurable (default 3)
readonly<input readonly>Grey background, value included in submit
hidden<input type="hidden">No visible UI, value included in submit
checkbox<input type="checkbox">Boolean field, uses checked
toggleToggle switchStyled checkbox as toggle, uses checked
date<input type="date">Native date picker
customConsumer-providedcustomElement inserted; label/help/error managed by dialog

FormFieldDef

OptionTypeDefaultDescription
namestringrequiredUnique field name (key in values object)
labelstringrequiredDisplay label above the field
typeFormFieldTyperequiredField type (see table above)
placeholderstring--Placeholder text
requiredbooleanfalseWhether field is required
valuestring--Initial value
checkedboolean--Initial checked state (checkbox/toggle)
optionsarray--Options for select: { value, label }[]
helpTextstring--Help text shown below field
rowsnumber3Textarea visible rows
disabledbooleanfalseDisable the field
autocompletestring--HTML autocomplete attribute
widthstring"full""full", "half", "third" for multi-column layout
sectionstring--Section ID for grouping into collapsible sections
customElementHTMLElement--Custom DOM for type: "custom"
validatefunction--Custom validator: returns error string or null

FormSection

OptionTypeDefaultDescription
idstringrequiredUnique section identifier
labelstringrequiredSection heading text
collapsedbooleanfalseInitial collapsed state
iconstring--Bootstrap icon class for section heading

FormStep (Wizard Mode)

OptionTypeDefaultDescription
idstringrequiredUnique step identifier
labelstringrequiredStep label in indicator
descriptionstring--Description text shown at top of step
iconstring--Bootstrap icon class
fieldsFormFieldDef[]requiredFields for this step
sectionsFormSection[]--Collapsible sections within step
validatefunction--Per-step cross-field validator

FormDialogPanel

OptionTypeDefaultDescription
contentHTMLElement or functionrequiredStatic element or (values) => HTMLElement reactive callback
widthnumber300Initial panel width in px
minWidthnumber200Minimum drag width
maxWidthnumber500Maximum drag width
titlestring--Panel header title

Modes

Single-Page Mode

Provide fields (and optionally sections) for a single scrollable form. Fields are rendered in order; adjacent half or third fields are placed in multi-column rows.

createFormDialog({
    title: "Edit User",
    size: "lg",
    sections: [
        { id: "personal", label: "Personal Information" },
        { id: "access", label: "Access & Permissions", collapsed: true }
    ],
    fields: [
        { name: "first", label: "First Name", type: "text", required: true, width: "third", section: "personal" },
        { name: "middle", label: "Middle Name", type: "text", width: "third", section: "personal" },
        { name: "last", label: "Last Name", type: "text", required: true, width: "third", section: "personal" },
        { name: "role", label: "Role", type: "select", width: "half", section: "access",
          options: [{ value: "admin", label: "Admin" }, { value: "member", label: "Member" }] },
        { name: "status", label: "Status", type: "select", width: "half", section: "access",
          options: [{ value: "active", label: "Active" }, { value: "inactive", label: "Inactive" }] }
    ],
    onSubmit: async (values) => { /* save */ }
});

Wizard Mode

Provide steps for a multi-step wizard. Each step has its own fields. The step indicator shows progress, and Back/Next buttons navigate between steps.

createFormDialog({
    title: "Create Workspace",
    size: "xl",
    stepTransition: "slide",
    steps: [
        { id: "basics", label: "Basics", fields: [
            { name: "name", label: "Workspace Name", type: "text", required: true }
        ]},
        { id: "members", label: "Members", description: "Invite team members.", fields: [
            { name: "email", label: "Email", type: "email", required: true },
            { name: "role", label: "Role", type: "select", options: [
                { value: "member", label: "Member" }, { value: "admin", label: "Admin" }
            ]}
        ]},
        { id: "review", label: "Review", fields: [] }
    ],
    onSubmit: async (values) => { /* create workspace */ }
});

With Panel

Add a resizable sidebar panel for help text or previews. If panel.content is a function, it re-renders reactively when fields change.

createFormDialog({
    title: "Create Role",
    size: "xl",
    panel: {
        title: "Help",
        width: 280,
        content: (values) => {
            const el = document.createElement("div");
            el.textContent = values.name
                ? `Role "${values.name}" will be created.`
                : "Fill in the role details.";
            return el;
        }
    },
    fields: [
        { name: "name", label: "Role Name", type: "text", required: true },
        { name: "description", label: "Description", type: "textarea" }
    ],
    onSubmit: async (values) => { /* save */ }
});

Keyboard

KeyContextAction
EscapeDialogClose (with dirty warning if warnOnDirty)
Tab / Shift+TabDialogCycle focus within dialog (focus trap)
EnterSingle-line fieldSubmit form (single-page) or next step (wizard)

Key bindings can be overridden via the keyBindings option.

Validation

Accessibility

DOM Structure

Single-Page (with panel)

div.formdialog-overlay
  div.formdialog-backdrop
  div.formdialog-dialog.formdialog-dialog-{size} [role="dialog" aria-modal="true"]
    div.formdialog-header
      h2.formdialog-title
      button.formdialog-close [aria-label="Close"]
    div.formdialog-description?
    div.formdialog-body.formdialog-body-split?
      div.formdialog-form
        div.formdialog-section
          button.formdialog-section-toggle [aria-expanded]
            i.formdialog-section-chevron
            i.formdialog-section-icon?
            span (label)
          div.formdialog-section-body [id]
            div.formdialog-row.formdialog-row-half?
              div.formdialog-group
                label.formdialog-label
                input.formdialog-input
                span.formdialog-help
                span.formdialog-error
      div.formdialog-divider?
      div.formdialog-panel?
        div.formdialog-panel-header?
        div.formdialog-panel-content
    div.formdialog-footer
      div.formdialog-footer-left
      div.formdialog-actions
        button.btn.btn-secondary (Cancel)
        button.btn.btn-primary (Submit)

Wizard Mode

div.formdialog-overlay
  div.formdialog-backdrop
  div.formdialog-dialog [role="dialog"]
    div.formdialog-header
    div.formdialog-steps
      button.formdialog-step.formdialog-step-complete
        span.formdialog-step-number (check icon)
        span.formdialog-step-label
      span.formdialog-step-connector.formdialog-step-connector-done
      button.formdialog-step.formdialog-step-active [aria-current="step"]
        span.formdialog-step-number (2)
        span.formdialog-step-label
      span.formdialog-step-connector
      button.formdialog-step.formdialog-step-pending
        span.formdialog-step-number (3)
        span.formdialog-step-label
    div.formdialog-body
      div.formdialog-form (current step fields)
    div.formdialog-footer
      div.formdialog-footer-left "Step 2 of 3"
      div.formdialog-actions
        button (Back)
        button (Next / Submit)

Features


Gauge

A visual measure component modeled after the ASN.1 Gauge type. Displays a value on a scale with configurable colour thresholds. Supports three shapes (tile, ring, bar) and two modes (value, time countdown).

Features

Assets

AssetPath
CSScomponents/gauge/gauge.css
JScomponents/gauge/gauge.js
Typescomponents/gauge/gauge.d.ts

Requires: Bootstrap CSS (for SCSS variables). Does not require Bootstrap JS or Bootstrap Icons.

Quick Start

<link rel="stylesheet" href="components/gauge/gauge.css">
<script src="components/gauge/gauge.js"></script>
<script>
    // Value tile
    var storage = createTileGauge({
        mode: "value",
        title: "Storage",
        value: 50,
        max: 100,
        unit: "GiB"
    }, "my-container");

    // Countdown ring
    var deadline = createRingGauge({
        mode: "time",
        title: "Sprint End",
        targetDate: new Date("2026-03-01"),
        max: 30 * 86400000  // 30 days in ms
    }, "timer-container");
</script>

API

Constructor

const gauge = new Gauge(options: GaugeOptions);

Creates the gauge DOM but does not attach to the page.

GaugeOptions

OptionTypeDefaultDescription
shape"tile" | "ring" | "bar"requiredVisual shape
mode"value" | "time""value"Data mode
valuenumber0Current value (value mode)
minnumber0Minimum value
maxnumber100Maximum value
unitstringUnit label (e.g., "GiB")
targetDateDate | stringCountdown target (time mode)
autoTickbooleantrueEnable auto-tick timer (time mode)
titlestringGauge title/name
subtitlestringSubtitle text (auto-generated if omitted)
sizeGaugeSize | numberfluidPredefined or pixel size
orientation"horizontal" | "vertical""horizontal"Bar orientation
thresholdsGaugeThreshold[]70/30/10/0%Colour thresholds
invertThresholdsbooleanfalseLower % = worse
overLimitColorstring"#dc3545"Over-limit colour
overLimitLabelstring"Over Limit"Over-limit text
overdueColorstring"#dc3545"Overdue colour
overdueLabelstring"Overdue"Overdue text
formatValuefunctionCustom value formatter
cssClassstringAdditional CSS classes
ariaLabelstringautoScreen reader label
onChangefunctionFires on value change
onOverLimitfunctionFires when over-limit state entered
onOverduefunctionFires when overdue state entered

Methods

MethodDescription
show(container?)Appends to container (ID string or HTMLElement)
hide()Removes from DOM without destroying
destroy()Hides, releases all references and timers
setValue(value)Updates value and refreshes display
setTargetDate(date)Updates target date and restarts auto-tick
setThresholds(thresholds)Replaces threshold configuration
getValue()Returns current value
getPercentage()Returns current percentage
isOverLimit()Returns true if value exceeds max
isOverdue()Returns true if target date has passed
isVisible()Returns visibility state
getElement()Returns the root DOM element

Convenience Functions

createGauge(options, container?)        // Create, show, and return
createTileGauge(options, container?)    // Shorthand for tile shape
createRingGauge(options, container?)    // Shorthand for ring shape
createBarGauge(options, container?)     // Shorthand for bar shape

Global Exports

window.Gauge
window.createGauge
window.createTileGauge
window.createRingGauge
window.createBarGauge

Shapes

Tile

Square card with large value text on a coloured background. Best for dashboard KPI tiles.

Ring

Circular SVG arc that fills based on percentage. Value displayed in the centre. Best for utilisation metrics.

Bar

Horizontal or vertical bar with percentage fill. Best for inline progress/quota indicators.

Threshold Configuration

Thresholds define colour boundaries based on percentage:

createTileGauge({
    mode: "value",
    value: 75,
    max: 100,
    thresholds: [
        { value: 70, color: "#2b8a3e", label: "Good" },
        { value: 30, color: "#e67700", label: "Warning" },
        { value: 10, color: "#d9480f", label: "Danger" },
        { value: 0,  color: "#c92a2a", label: "Critical" }
    ]
}, "container");

For "remaining" scenarios where lower is worse, use invertThresholds: true.

Size Variants

SizePixels
"xs"80px
"sm"120px
"md"180px
"lg"260px
"xl"360px

Omit size for fluid mode — the gauge fills its parent container with aspect-ratio: 1 (tile/ring) and scales typography via CSS container queries.

Keyboard Accessibility

KeyAction
TabFocus gauge element

Gauge elements use role="meter" (value mode) or role="timer" (time mode) with aria-valuenow, aria-valuemin, aria-valuemax, and aria-label attributes.

See specs/gauge.prd.md for the complete specification.


GradientPicker

A gradient colour picker that enables users to create, edit, and preview linear and radial gradients with full alpha support. Features draggable stop handles, live CSS preview, preset swatches, and configurable min/max stops. Composes the existing ColorPicker and AnglePicker components for stop colour and angle editing. Operates in popup or inline mode.

Assets

AssetPath
CSScomponents/gradientpicker/gradientpicker.css
JScomponents/gradientpicker/gradientpicker.js
Typescomponents/gradientpicker/gradientpicker.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/gradientpicker/gradientpicker.css">
<script src="components/gradientpicker/gradientpicker.js"></script>
<script>
    var gradient = createGradientPicker("my-container", {
        value: {
            type: "linear",
            angle: 90,
            stops: [
                { position: 0, color: "#3B82F6", alpha: 1 },
                { position: 1, color: "#8B5CF6", alpha: 1 }
            ]
        },
        onChange: function(value) {
            console.log("Gradient:", value);
        }
    });
</script>

Options (GradientPickerOptions)

OptionTypeDefaultDescription
valuePartial<GradientValue>Initial gradient value
mode"inline" | "popup""popup"Display mode
size"mini" | "sm" | "default" | "lg""default"Size variant
popupPosition"bottom-start" | "bottom-end" | "top-start" | "top-end""bottom-start"Popup position relative to trigger
minStopsnumber2Minimum number of gradient stops
maxStopsnumber8Maximum number of gradient stops
showTypeTogglebooleantrueShow linear/radial type toggle
showAnglebooleantrueShow angle control (linear mode)
showRadialControlsbooleantrueShow centre/radius controls (radial mode)
showReversebooleantrueShow reverse button
showClearbooleantrueShow clear button
presetsGradientPreset[](built-in)Preset gradient swatches
disabledbooleanfalseDisable the component
labelstringLabel text above the picker
onChangefunctionFires on any gradient change (stops, angle, type, centre, radius)
onInputfunctionFires continuously during drag operations
onClearfunctionFires when gradient is cleared
onOpenfunctionFires when popup opens
onClosefunctionFires when popup closes

API

MethodReturnsDescription
getValue()GradientValueGet the current gradient value
setValue(value)voidSet the gradient programmatically
getStops()GradientStop[]Get stops only
setStops(stops)voidSet stops only (preserves type, angle, etc.)
getAngle()numberGet the angle (linear mode)
setAngle(angle)voidSet the angle (linear mode)
getType()"linear" | "radial"Get the gradient type
setType(type)voidSet the gradient type
reverse()voidReverse all stop positions (1 - position)
clear()voidClear gradient (reset to default two-stop)
toGradientDefinition()GradientDefinitionConvert current value to DiagramEngine format
fromGradientDefinition(def)voidLoad from a DiagramEngine GradientDefinition
open()voidOpen popup (popup mode only)
close()voidClose popup (popup mode only)
enable()voidEnable the component
disable()voidDisable the component
getElement()HTMLElement | nullGet root DOM element
destroy()voidTear down and clean up

Factory Function

createGradientPicker(containerId, options?)  // Create, show, and return

Global Exports

window.GradientPicker
window.createGradientPicker

GradientStop

Each stop defines a colour at a position along the gradient axis.

interface GradientStop
{
    /** Position along the gradient axis. 0.0 = start, 1.0 = end. */
    position: number;

    /** Colour in hex (#RRGGBB) or rgba string. */
    color: string;

    /** Opacity for this stop. 0.0 = transparent, 1.0 = opaque. Default: 1.0. */
    alpha: number;
}

GradientValue

The complete gradient state, including type, stops, angle, and radial parameters.

interface GradientValue
{
    /** Gradient interpolation type. */
    type: "linear" | "radial";

    /** Ordered colour stops (minimum 2). */
    stops: GradientStop[];

    /** Angle in degrees for linear gradients (0 = right, 90 = down). */
    angle: number;

    /** Centre point for radial gradients (0-1 normalised). */
    center: { x: number; y: number };

    /** Radius for radial gradients (0-1 normalised). */
    radius: number;
}

GradientPreset

A named preset with a full gradient definition.

interface GradientPreset
{
    /** Display name for tooltip. */
    name: string;

    /** Gradient definition for this preset. */
    value: GradientValue;
}

Size Variants

SizeTrigger HeightPreview BarHandle SizePanel Width
mini22px16px8x8px260px
sm28px18px10x10px280px
default34px24px12x12px320px
lg42px32px14x14px380px

Panel width applies to both inline and popup modes. In popup mode, the trigger inherits the size variant height; the popup panel always uses the width above regardless of trigger size.

Composition Dependencies

GradientPicker composes two sibling components at runtime via window factory lookup. Both are optional -- the picker degrades gracefully without them.

DependencyLookupAvailableBehaviour
ColorPickerwindow.createColorPickerYesFull colour picker with alpha for each stop
ColorPickerwindow.createColorPickerNoSimple <input type="color"> + alpha slider
AnglePickerwindow.createAnglePickerYesCircular dial for angle selection
AnglePickerwindow.createAnglePickerNo<input type="number" min="0" max="359">

ColorPicker Composition

AnglePicker Composition

DiagramEngine Integration

GradientValue maps to DiagramEngine's GradientDefinition format. Use the conversion helpers:

GradientValueGradientDefinition
typetype
stops[].positionstops[].offset
stops[].color + stops[].alphastops[].color (as rgba string)
angleangle
centercenter
radiusradius
// Export to DiagramEngine
var gradDef = gradient.toGradientDefinition();
diagramEngine.setShapeFill(shapeId, gradDef);

// Import from DiagramEngine
var def = diagramEngine.getShapeFill(shapeId);
gradient.fromGradientDefinition(def);

Keyboard Accessibility

ContextKeyAction
Preview barLeft / RightSelect prev / next stop handle
Selected handleLeft / RightMove stop position by 1%
Selected handleShift+Left / Shift+RightMove stop position by 5%
Selected handleEnter / SpaceOpen ColorPicker for the stop
Selected handleDelete / BackspaceRemove stop (if above minStops)
PopupEscapeClose ColorPicker or popup
All controlsTabMove focus between type toggle, preview bar, stop handles, position input, angle/radial controls, presets, action buttons

Stop Handle Interactions

ActionBehaviour
Click on bar (empty area)Add new stop at click position with interpolated colour
Click on handleSelect the stop for editing
Drag handleReposition stop (clamped 0-1, cannot pass adjacent stops)
Right-click handleRemove stop (if above minStops)
Double-click handleOpen ColorPicker for that stop
Delete/Backspace on selectedRemove selected stop (if above minStops)

Default Presets

When presets is not provided, these built-in presets are available:

NameTypeAngleStops
Sunsetlinear90#FF512F 0% -> #F09819 50% -> #DD2476 100%
Oceanlinear135#2193B0 0% -> #6DD5ED 100%
Grayscalelinear90#000000 0% -> #FFFFFF 100%
Forestlinear135#134E5E 0% -> #71B280 100%
Berrylinear135#8E2DE2 0% -> #4A00E0 100%
Firelinear0#F83600 0% -> #F9D423 100%

onInput vs onChange

CallbackWhen it firesUse case
onInputEvery pointer move during stop drag, every keyboard stepLive preview (e.g. update shape fill in real time)
onChangeStop drag end (pointer up), keyboard steps, type change, preset applied, angle changePersist to backend, commit undo history

Both callbacks receive (value: GradientValue). During a drag, onInput fires continuously and onChange fires once on pointer release.

Examples

Inline gradient editor

var gradient = createGradientPicker("fill-editor", {
    mode: "inline",
    value: {
        type: "linear",
        angle: 135,
        stops: [
            { position: 0, color: "#6366F1", alpha: 1 },
            { position: 0.5, color: "#EC4899", alpha: 0.8 },
            { position: 1, color: "#F59E0B", alpha: 1 }
        ]
    },
    onChange: function(value) {
        document.getElementById("preview").style.background = toCssGradient(value);
    }
});

Popup with custom presets

var gradient = createGradientPicker("toolbar-fill", {
    size: "sm",
    presets: [
        {
            name: "Brand",
            value: {
                type: "linear", angle: 90,
                stops: [
                    { position: 0, color: "#1E40AF", alpha: 1 },
                    { position: 1, color: "#7C3AED", alpha: 1 }
                ],
                center: { x: 0.5, y: 0.5 }, radius: 0.5
            }
        }
    ],
    onChange: function(value) {
        console.log("Gradient:", value);
    }
});

DiagramEngine integration

var gradient = createGradientPicker("shape-fill", {
    mode: "popup",
    size: "mini",
    onOpen: function() {
        // Load current shape gradient when picker opens
        var def = diagramEngine.getSelectedShapeFill();
        if (def) { gradient.fromGradientDefinition(def); }
    },
    onChange: function(value) {
        diagramEngine.setSelectedShapeFill(gradient.toGradientDefinition());
    }
});

CDN Usage

<!-- Bootstrap CSS and Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">

<!-- Optional: ColorPicker (for full colour editing) -->
<link rel="stylesheet" href="components/colorpicker/colorpicker.css">
<script src="components/colorpicker/colorpicker.js"></script>

<!-- Optional: AnglePicker (for angle dial) -->
<link rel="stylesheet" href="components/anglepicker/anglepicker.css">
<script src="components/anglepicker/anglepicker.js"></script>

<!-- GradientPicker -->
<link rel="stylesheet" href="components/gradientpicker/gradientpicker.css">
<script src="components/gradientpicker/gradientpicker.js"></script>

<div id="gradient-container"></div>
<script>
    var gradient = createGradientPicker("gradient-container", {
        mode: "inline",
        showTypeToggle: true,
        onChange: function(value) {
            console.log("Type:", value.type, "Stops:", value.stops.length);
        }
    });
</script>

See specs/gradientpicker.prd.md for the complete specification.


GraphCanvas

Interactive SVG graph visualization with multiple layout algorithms, zoom/pan, selection, edge creation, keyboard shortcuts, and export.

Usage

<link rel="stylesheet" href="components/graphcanvas/graphcanvas.css" />
<script src="components/graphcanvas/graphcanvas.js"></script>
const canvas = createGraphCanvas({
    container: document.getElementById("graph"),
    mode: "schema",
    layout: "force",
    nodes: [
        { id: "1", label: "OKR", type: "strategy.okr", color: "#C0392B" },
        { id: "2", label: "Project", type: "work.project", color: "#2196f3" }
    ],
    edges: [
        { id: "e1", sourceId: "1", targetId: "2", label: "aligns_to", type: "aligns_to" }
    ],
    onNodeClick: (node) => console.log("Clicked:", node.label),
    onSelectionChange: (nodes, edges) => console.log("Selected:", nodes.length)
});

Modes

Layout Algorithms

LayoutDescription
forceSpring-embedder with repulsion + spring attraction (default)
hierarchicalBFS-based level assignment with configurable direction
radialConcentric rings from root nodes
dagreDelegates to window.dagre if loaded; falls back to hierarchical
group-by-namespaceGroups nodes by namespace, grid arrangement, mini-force within

Keyboard Shortcuts

KeyAction
+ / -Zoom in / out
0Zoom to fit
DeleteRemove selected edges
EscapeClear selection
Ctrl+ASelect all visible nodes
FCenter on selected node
Arrow keysNudge selected nodes

Interactions

Public API

Data

Selection

Viewport

Layout

Filtering

Highlighting

Export

Lifecycle

Global

window.createGraphCanvas(options)

GraphLegend

Collapsible legend panel showing color/icon/shape key for graph node types and edge types. Designed for the Ontology Visualizer but works with any graph canvas that needs a visual key.

Usage

<link rel="stylesheet" href="components/graphlegend/graphlegend.css" />
<link rel="stylesheet" href="components/typebadge/typebadge.css" />
<script src="components/typebadge/typebadge.js"></script>
<script src="components/graphlegend/graphlegend.js"></script>
const legend = createGraphLegend({
    container: document.getElementById("graph-container"),
    nodeTypes: [
        {
            typeKey: "strategy.okr",
            displayName: "OKR",
            icon: "crosshair",
            color: "#C0392B",
            count: 5
        },
        {
            typeKey: "org.team",
            displayName: "Team",
            icon: "people",
            color: "#2980B9",
            count: 3
        }
    ],
    edgeTypes: [
        {
            relationshipKey: "owned_by",
            displayName: "owned by",
            color: "#94a3b8",
            style: "solid",
            count: 8
        }
    ],
    showCounts: true,
    position: "bottom-left"
});

Options

OptionTypeDefaultDescription
containerHTMLElementContainer element to mount into. Required.
nodeTypesLegendNodeType[]List of node types to display. Required.
edgeTypesLegendEdgeType[][]List of edge types to display.
titlestring"Legend"Panel header title.
collapsedbooleanfalseStart collapsed.
showEdgeTypesbooleantrueShow the edge types section.
showCountsbooleanfalseShow count badges next to types.
positionstring"bottom-left""bottom-left", "bottom-right", "top-left", "top-right"
maxHeightnumber300Max height in px before scrolling.

LegendNodeType

FieldTypeDescription
typeKeystringOntology type key, e.g. "strategy.okr"
displayNamestringDisplay label, e.g. "OKR"
iconstringBootstrap icon name (without bi bi- prefix)
colorstringHex color
countnumber?Number of this type in the graph
statusstring?"active", "planned", "deprecated", or "external"

LegendEdgeType

FieldTypeDescription
relationshipKeystringRelationship key, e.g. "owned_by"
displayNamestringDisplay label, e.g. "owned by"
colorstring?Edge color hex. Default: "#94a3b8"
stylestring?"solid", "dashed", or "dotted". Default: "solid"
countnumber?Number of this edge type in the graph

Public API

MethodDescription
setNodeTypes(types)Replace the list of node types and re-render.
setEdgeTypes(types)Replace the list of edge types and re-render.
updateCounts(nodeCounts, edgeCounts)Update count badges in-place.
show()Show the legend panel.
hide()Hide the legend panel.
toggle()Toggle visibility.
isVisible()Returns whether the panel is visible.
setCollapsed(collapsed)Programmatically collapse or expand the body.
destroy()Remove from DOM and clean up.

Callbacks

PropertySignatureDescription
onTypeClick(typeKey: string) => voidCalled when a type item is clicked.
onTypeHover(typeKey: string | null) => voidCalled on mouseenter/mouseleave.

Dependencies

Status Indicators

Accessibility

Global

window.createGraphLegend(options)

GraphMinimap

A small, always-visible overview widget that shows a miniaturised view of the entire graph canvas. Displays a viewport rectangle indicating the currently visible region. Users can click or drag the viewport rectangle to pan the main canvas.

Assets

AssetPath
CSScomponents/graphminimap/graphminimap.css
JScomponents/graphminimap/graphminimap.js
Typescomponents/graphminimap/graphminimap.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/graphminimap/graphminimap.css">
<script src="components/graphminimap/graphminimap.js"></script>
<script>
    var minimap = createGraphMinimap({
        container: document.getElementById("minimap-host"),
        graphCanvas: myGraphCanvasInstance
    });

    // Manually refresh after data changes
    minimap.refresh();

    // Toggle visibility
    minimap.toggle();

    // Clean up
    minimap.destroy();
</script>

API

Factory Function

FunctionReturnsDescription
createGraphMinimap(options)GraphMinimapCreate a minimap instance

GraphMinimapOptions

OptionTypeDefaultDescription
containerHTMLElementrequiredHost element for the minimap
graphCanvasGraphCanvasHandlerequiredGraph canvas to synchronise with
widthnumber200Minimap width in px
heightnumber150Minimap height in px
backgroundColorstring'#f8f9fa'SVG background colour
viewportColorstring'rgba(59,130,246,0.3)'Viewport rectangle fill
viewportBorderColorstring'#3b82f6'Viewport rectangle border
nodeColorstring'#94a3b8'Default node dot colour
edgeColorstring'#cbd5e1'Default edge line colour
showEdgesbooleantrueRender edges (auto-disabled for >500 nodes)
collapsedbooleanfalseStart collapsed
positionstring'bottom-right'Corner position hint

GraphCanvasHandle Interface

The graph canvas passed to the minimap must implement this interface:

interface GraphCanvasHandle {
    getNodes(): { id: string; x: number; y: number; color?: string }[];
    getEdges(): { source: string; target: string }[];
    getViewport(): { x: number; y: number; zoom: number; width: number; height: number };
    panTo(x: number, y: number): void;
    on(event: string, callback: Function): void;
    off(event: string, callback: Function): void;
}

GraphMinimap Handle

MethodReturnsDescription
refresh()voidRe-read graph data and redraw
show()voidShow the minimap widget
hide()voidHide the minimap widget
toggle()voidToggle minimap visibility
isVisible()booleanCheck if minimap is visible
destroy()voidRemove from DOM and clean up

Behaviour

  1. Nodes are rendered as small filled circles (3px radius). Nodes with a color property use that colour; otherwise the configured nodeColor is used.
  2. Edges are rendered as 1px lines connecting source and target nodes.
  3. Viewport rectangle shows the currently visible region with a semi-transparent fill and solid border.
  4. Click anywhere on the minimap to pan the main canvas to that position.
  5. Drag the viewport rectangle (or anywhere) to pan the main canvas in real-time.
  6. Auto-refresh occurs when the graph canvas fires a layoutComplete event.
  7. Collapse/expand via the toggle button in the header.
  8. Performance: For graphs with more than 500 nodes, edge rendering is automatically skipped.

Accessibility

Dark Mode

The component uses var(--theme-*) CSS tokens for backgrounds, borders, and text. SVG fill colours are passed by the consumer and do not change automatically on theme toggle.


GraphToolbar

Factory function that creates a preconfigured Toolbar instance for graph visualization applications. Assembles standard regions for graph editing (undo/redo/delete), layout algorithm selection, zoom controls, grid snap/minimap toggles, export, and node search.

GraphToolbar is not a new component class — it wraps the existing Toolbar component (ADR-030).

Usage

<link rel="stylesheet" href="components/toolbar/toolbar.css">
<link rel="stylesheet" href="components/graphtoolbar/graphtoolbar.css">
<script src="components/toolbar/toolbar.js"></script>
<script src="components/graphtoolbar/graphtoolbar.js"></script>
const handle = createGraphToolbar({
    onUndo: () => graph.undo(),
    onRedo: () => graph.redo(),
    onDelete: () => graph.deleteSelected(),
    onApplyLayout: (id) => graph.applyLayout(id),
    onZoomIn: () => { graph.zoomIn(); handle.setZoomLabel(graph.getZoom()); },
    onZoomOut: () => { graph.zoomOut(); handle.setZoomLabel(graph.getZoom()); },
    onZoomToFit: () => { graph.fitToView(); handle.setZoomLabel(graph.getZoom()); },
    onGridSnapToggle: (on) => graph.setGridSnap(on),
    onMinimapToggle: (on) => graph.setMinimapVisible(on),
    onExport: (format) => graph.export(format),
    onSearch: (query) => graph.highlightMatches(query),
    onSearchSubmit: (query) => graph.selectFirstMatch(query)
});

Options

OptionTypeDefaultDescription
layoutsGraphToolbarLayout[]6 defaultsLayout algorithm choices
defaultLayoutstring"hierarchical"Initially selected layout
showUndobooleantrueShow Undo button
showRedobooleantrueShow Redo button
showDeletebooleantrueShow Delete button
showLayoutSelectorbooleantrueShow layout dropdown + Apply
showZoomControlsbooleantrueShow zoom in/out/fit/label
showGridSnapbooleantrueShow grid snap toggle
showMinimapbooleantrueShow minimap toggle
showExportbooleantrueShow export dropdown
showSearchbooleantrueShow search input
exportFormatsstring[]["png","svg","json"]Export format options
initialZoomnumber100Initial zoom percentage
gridSnapEnabledbooleanfalseInitial grid snap state
minimapEnabledbooleanfalseInitial minimap state
enableKeyboardShortcutsbooleantrueRegister document shortcuts
toolbarOptionsobject{}Pass-through Toolbar options

Handle API

MethodDescription
setZoomLabel(zoom)Update zoom percentage display
setGridSnapState(enabled)Set grid snap toggle state
setMinimapState(enabled)Set minimap toggle state
setUndoEnabled(enabled)Enable/disable Undo button
setRedoEnabled(enabled)Enable/disable Redo button
setDeleteEnabled(enabled)Enable/disable Delete button
setLayout(layoutId)Select a layout in the dropdown
destroy()Clean up toolbar and keyboard listeners
toolbarThe underlying Toolbar instance

Keyboard Shortcuts

ShortcutAction
Ctrl+ZUndo
Ctrl+Y / Ctrl+Shift+ZRedo
DeleteDelete selected
Ctrl+=Zoom in
Ctrl+-Zoom out
Ctrl+0Zoom to fit

Shortcuts are suppressed when focus is in an input field or a modal is open.


GridLayout

A uniform CSS Grid layout container where all cells are the same size, arranged via grid-template-columns: repeat(N, 1fr). Supports a fixed column count or responsive auto-fit columns calculated from container width and a minimum cell width. Optional aspect ratio enforcement keeps cells proportional.

Assets

AssetPath
CSScomponents/gridlayout/gridlayout.css
JScomponents/gridlayout/gridlayout.js
Typescomponents/gridlayout/gridlayout.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/gridlayout/gridlayout.css">
<script src="components/gridlayout/gridlayout.js"></script>

<script>
    var cards = [];
    for (var i = 0; i < 6; i++) {
        var card = document.createElement("div");
        card.className = "p-3 border bg-white";
        card.textContent = "Card " + (i + 1);
        cards.push(card);
    }

    var grid = createGridLayout({
        columns: 3,
        gap: 12,
        padding: "16px",
        children: cards
    });
</script>

How It Works

GridLayout creates a CSS Grid container with equal-width columns. When columns is a number, the grid uses a fixed column count. When columns is "auto", a ResizeObserver monitors the container width and recalculates the column count as Math.floor(containerWidth / minCellWidth), clamped to the child count.

columns: 3, gap: 12
+----------+----------+----------+
|  Cell 1  |  Cell 2  |  Cell 3  |
+----------+----------+----------+
|  Cell 4  |  Cell 5  |  Cell 6  |
+----------+----------+----------+

columns: "auto", minCellWidth: 200 (container: 650px -> 3 cols)
+----------+----------+----------+
|  Cell 1  |  Cell 2  |  Cell 3  |
+----------+----------+----------+
|  Cell 4  |  Cell 5  |  Cell 6  |
+----------+----------+----------+

columns: "auto", minCellWidth: 200 (container: 350px -> 1 col)
+------------------------------+
|           Cell 1             |
+------------------------------+
|           Cell 2             |
+------------------------------+
|           Cell 3             |
+------------------------------+

Options

OptionTypeDefaultDescription
idstringautoCustom element ID
columnsnumber | "auto"--Column count or "auto" for responsive (required)
rowsnumber | "auto""auto"Row count or "auto" for natural flow
gapnumber | string"0"Gap between cells (px or CSS value)
aspectRationumber--Width/height ratio for cells (e.g. 1.0 = square)
minCellWidthnumber200Minimum cell width in px when columns is "auto"
childrenArray<HTMLElement | Component>[]Initial children
paddingstring--Container padding (CSS value)
cssClassstring--Additional CSS classes
heightstring--Height CSS value
widthstring--Width CSS value
onLayoutChange(state) => void--Fired on column recalculation or child add/remove

Public API

MethodReturnsDescription
show(container?)voidAppend to container and display
hide()voidRemove from DOM (preserves state)
destroy()voidFull cleanup, destroy all children
getRootElement()HTMLElement | nullThe root grid container
isVisible()booleanWhether the grid is displayed
addChild(child, index?)voidAdd a child at optional index
removeChild(index)voidRemove child by index
clear()voidRemove all children
getChildCount()numberNumber of children
getChildElement(index)HTMLElement | nullCell wrapper element at index
setColumns(n)voidUpdate columns dynamically (number or "auto")
getState()GridLayoutStateSerialisable state snapshot
setState(state)voidRestore state (columns)
setContained(value)voidSet contained mode

GridLayoutState

interface GridLayoutState {
    columns: number;
    rows: number;
    childCount: number;
}

Composability

GridLayout implements the standard layout container contract. Any component with show(container) / hide() / destroy() can be used as a child. Plain HTMLElements are also supported. Duck-typed child mounting checks for setContained, show, getRootElement, and falls back to direct appendChild.

// Place gauges in a 2x2 grid
var grid = new GridLayout({
    columns: 2,
    gap: 16,
    aspectRatio: 1,
    children: [gaugeA, gaugeB, gaugeC, gaugeD]
});
grid.show(document.getElementById("dashboard"));

Global Exports

When loaded via <script> tag:

CSS Classes

ClassElementDescription
.gridlayoutRootCSS Grid container
.gridlayout-cellWrapperPer-child grid cell wrapper

GuidedTour

Product walkthrough component wrapping Driver.js.

Overview

GuidedTour creates in-app product tours using Driver.js (MIT, ~5KB, zero deps) with enterprise-themed popovers. It supports step progression, conditional visibility, localStorage persistence, and analytics hooks.

Usage

<!-- Driver.js CDN (required) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/driver.js@1.4.1/dist/driver.css" />
<script src="https://cdn.jsdelivr.net/npm/driver.js@1.4.1/dist/driver.js.iife.js"></script>

<!-- GuidedTour -->
<link rel="stylesheet" href="components/guidedtour/guidedtour.css" />
<script src="components/guidedtour/guidedtour.js"></script>
var tour = createGuidedTour({
    tourId: "onboarding",
    steps: [
        {
            target: "#sidebar",
            title: "Navigation",
            description: "Use the sidebar to browse between sections."
        },
        {
            target: "#search-box",
            title: "Search",
            description: "Search across all your projects and documents.",
            side: "bottom"
        },
        {
            target: "#user-menu",
            title: "Your Account",
            description: "Access settings, notifications, and sign out.",
            side: "left"
        }
    ],
    onTourStart: function() { console.log("Tour started"); },
    onStepView: function(i) { console.log("Viewing step", i); },
    onTourComplete: function() { console.log("Tour completed"); },
    onTourDismiss: function(i) { console.log("Tour dismissed at step", i); }
});

// Start the tour
if (tour && !tour.isCompleted()) {
    tour.start();
}

Factory

FunctionReturnsDescription
createGuidedTour(options)GuidedTourHandle | nullReturns null if Driver.js not loaded

Options

PropertyTypeDefaultDescription
tourIdstringRequiredUnique tour ID
stepsTourStep[]RequiredTour steps
showProgressbooleantrue"Step X of Y" counter
showSkipbooleantrueSkip Tour button
overlayColorstring"rgba(0,0,0,0.5)"Backdrop colour
animatebooleantrueAnimate transitions
onTourStart() => voidTour started
onStepView(index) => voidStep viewed
onStepSkip(index) => voidStep skipped
onTourComplete() => voidTour completed
onTourDismiss(index) => voidTour dismissed

TourStep

PropertyTypeRequiredDescription
targetstring | HTMLElementYesElement to highlight
titlestringYesStep title
descriptionstringYesStep description
sidestringNoPosition: top/bottom/left/right
visible() => booleanNoConditional visibility
onBeforeStep() => voidNoPre-step hook
onAfterStep() => voidNoPost-step hook

API

MethodDescription
start()Start or restart tour
next()Next step
previous()Previous step
goToStep(index)Jump to step
dismiss()Dismiss tour
isActive()Tour running?
isCompleted()Check localStorage
resetProgress()Clear completion
destroy()Clean up

Keyboard

KeyAction
Arrow Right/DownNext step
Arrow Left/UpPrevious step
EscapeDismiss tour

Persistence

Tour completion is stored in localStorage under the key guidedtour-{tourId}-complete. Call resetProgress() to clear.

Dependencies

Asset Paths

CSS: components/guidedtour/guidedtour.css
JS:  components/guidedtour/guidedtour.js

HelpDrawer

Right-side sliding panel for in-context documentation display.

Overview

HelpDrawer is a singleton component — one per page. It slides in from the right edge, renders markdown documentation via Vditor display mode, and supports topic history with a back button.

Usage

<!-- Vditor CDN (required for markdown rendering) -->
<link rel="stylesheet" href="https://unpkg.com/vditor@3.11.2/dist/index.css" />
<script src="https://unpkg.com/vditor@3.11.2/dist/index.min.js"></script>

<!-- HelpDrawer -->
<link rel="stylesheet" href="components/helpdrawer/helpdrawer.css" />
<script src="components/helpdrawer/helpdrawer.js"></script>
// Create (or get existing) singleton
var drawer = createHelpDrawer({ width: 420 });

// Open with inline markdown
drawer.open({
    id: "getting-started",
    title: "Getting Started",
    markdown: "# Welcome\n\nThis is the getting started guide."
});

// Open with URL
drawer.open({
    id: "api-reference",
    title: "API Reference",
    url: "/docs/api-reference.md"
});

// Navigate back
drawer.back();

// Close
drawer.close();

Factory

FunctionReturnsDescription
createHelpDrawer(options?)HelpDrawerHandleCreates or returns singleton
getHelpDrawer()HelpDrawerHandle | nullReturns existing instance

Options

PropertyTypeDefaultDescription
widthnumber400Initial width in px
minWidthnumber280Minimum resize width
maxWidthnumber600Maximum resize width
onClose() => voidClose callback
onNavigate(url: string) => voidURL navigation callback

API

MethodDescription
open(topic)Open with a HelpTopic
close()Close the drawer
isOpen()Returns open state
back()Go to previous topic
canGoBack()True if history has entries
getElement()Returns root element
destroy()Remove and clean up

HelpTopic

PropertyTypeRequiredDescription
idstringYesTopic identifier
titlestringYesHeader title
markdownstringNoInline markdown
urlstringNoURL to fetch markdown

Keyboard

KeyAction
EscapeClose drawer

Dependencies

CSS Classes

ClassDescription
.helpdrawerRoot element
.helpdrawer-openVisible state
.helpdrawer-headerDark header bar
.helpdrawer-bodyScrollable content

Asset Paths

CSS: components/helpdrawer/helpdrawer.css
JS:  components/helpdrawer/helpdrawer.js

HelpTooltip

A small ? icon that attaches to any element for in-context help.

Overview

HelpTooltip renders a 14px blue circle ? icon positioned relative to a target element. Hovering shows a plain-text tooltip (400ms delay); clicking opens the HelpDrawer with linked documentation.

Usage

<link rel="stylesheet" href="components/helptooltip/helptooltip.css" />
<script src="components/helptooltip/helptooltip.js"></script>
<!-- HelpDrawer must also be loaded for click behaviour -->
<script src="components/helpdrawer/helpdrawer.js"></script>
var tooltip = createHelpTooltip(document.getElementById("my-field"), {
    text: "Enter your project name here",
    topic: {
        id: "project-name",
        title: "Project Name",
        markdown: "# Project Name\n\nThe project name must be unique."
    },
    position: "top-right"
});

Factory

FunctionReturnsDescription
createHelpTooltip(target, options)HelpTooltipHandleMultiple instances allowed

Options

PropertyTypeDefaultDescription
textstringHover tooltip text
topicHelpTooltipTopicHelpDrawer topic
positionstring"top-right"Icon position
sizenumber14Icon diameter in px

Positions

API

MethodDescription
setText(text)Update hover text
setTopic(topic)Update drawer topic
show()Show icon
hide()Hide icon
getElement()Returns icon element
destroy()Remove and clean up

Keyboard

KeyAction
Enter / SpaceOpen HelpDrawer

Dependencies

Asset Paths

CSS: components/helptooltip/helptooltip.css
JS:  components/helptooltip/helptooltip.js

InlineToolbar

A compact inline toolbar that renders INSIDE a container element as a flex row. Designed for sidebars, panel headers, and other embedded contexts where a full docked toolbar is inappropriate.

Assets

AssetPath
CSScomponents/inlinetoolbar/inlinetoolbar.css
JScomponents/inlinetoolbar/inlinetoolbar.js
Typescomponents/inlinetoolbar/inlinetoolbar.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/inlinetoolbar/inlinetoolbar.css">
<script src="components/inlinetoolbar/inlinetoolbar.js"></script>
<script>
    var toolbar = createInlineToolbar({
        container: document.getElementById("sidebar-header"),
        items: [
            { id: "filter", icon: "funnel", tooltip: "Filter", type: "toggle" },
            { id: "sep1", icon: "", tooltip: "", type: "separator" },
            { id: "expand", icon: "arrows-expand", tooltip: "Expand All" },
            { id: "collapse", icon: "arrows-collapse", tooltip: "Collapse All" },
            { id: "refresh", icon: "arrow-clockwise", tooltip: "Refresh",
              onClick: function(item) { console.log("Refresh clicked"); } }
        ],
        size: "sm",
        compact: true
    });
</script>

API

Global Functions

FunctionReturnsDescription
createInlineToolbar(options)InlineToolbarCreate toolbar inside container

InlineToolbarOptions

OptionTypeDefaultDescription
containerHTMLElementrequiredParent element to render inside
itemsInlineToolbarItem[]requiredToolbar items
sizestring"sm"xs (24px), sm (28px), md (32px)
alignstring"left"left, center, right
compactbooleanfalseReduce gaps for tight spaces

InlineToolbarItem

OptionTypeDefaultDescription
idstringrequiredUnique item identifier
iconstringrequiredBootstrap icon name (without bi- prefix)
tooltipstringrequiredTooltip text (rendered as title attribute)
typestring"button"button, toggle, separator
activebooleanfalseInitial toggle state
disabledbooleanfalseDisable the item
onClickfunction--(item, active) => void

InlineToolbar Handle

MethodDescription
setItemDisabled(id, disabled)Enable or disable an item
setItemActive(id, active)Set toggle active state
show()Show the toolbar
hide()Hide the toolbar
destroy()Remove from DOM and clean up
getElement()Return root element

Features

Accessibility

See specs/explorer-inline-toolbar.req.md for the original requirement.


LatexEditor

A LaTeX equation editor component with two editing modes: Visual (WYSIWYG via MathLive) and Source (raw LaTeX textarea). Includes a live KaTeX-rendered preview, tabbed symbol palette with 12 categories (including chemistry via mhchem), and styling controls for colour, bold, size, and highlight.

Designed for embedding in DiagramEngine shapes, FormDialog popups, Sidebar tabs, and standalone page layouts.

Files

FilePurpose
latexeditor.tsComponent source
latexeditor.scssStyles (le- prefix)
latexeditor.test.tsVitest tests
README.mdThis file

CDN Dependencies

LibraryRoleLoading
KaTeXLive preview renderingInclude before component
MathLiveWYSIWYG visual editorOptional, loaded on demand
<!-- KaTeX (required for preview) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css">
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js"></script>

<!-- MathLive (optional, for visual editing mode) -->
<script src="https://cdn.jsdelivr.net/npm/mathlive@0.107.3/dist/mathlive.min.js"></script>

<!-- LatexEditor -->
<link rel="stylesheet" href="components/latexeditor/latexeditor.css">
<script src="components/latexeditor/latexeditor.js"></script>

Quick Start

var editor = createLatexEditor({
    container: "#my-editor",
    expression: "x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}",
    onChange: function(latex) { console.log("Updated:", latex); },
});

Factory

createLatexEditor(options): LatexEditor

Creates and mounts a LatexEditor instance.

Options

OptionTypeDefaultDescription
containerHTMLElement | stringrequiredContainer element or CSS selector
expressionstring""Initial LaTeX expression
editMode"visual" | "source""visual"Initial editing mode
displayModebooleantrueDisplay mode (block) or inline
showToolbarbooleantrueShow the styling toolbar
showSymbolPalettebooleantrueShow the symbol palette
showPreviewbooleantrueShow live preview pane
containedbooleanfalseContained mode (fills parent, no min size)
minWidthnumber400Minimum width in px
minHeightnumber300Minimum height in px
cssClassstringAdditional CSS class on root
readOnlybooleanfalseRead-only mode
enableChemistrybooleantrueEnable mhchem chemistry extension
enableCancelbooleantrueEnable cancel/strikethrough commands
onChange(latex: string) => voidCalled when expression changes
onConfirm(latex: string, mathml: string) => voidCalled on Ctrl+Enter

Public API

MethodReturnsDescription
getLatex()stringCurrent LaTeX expression
getMathML()stringMathML output (requires MathLive)
getValue(){ latex, mathml }Both formats
setExpression(latex)voidSet expression programmatically
setEditMode(mode)voidSwitch to "visual" or "source"
getEditMode()stringCurrent mode
insertAtCursor(latex)voidInsert LaTeX at cursor
setReadOnly(bool)voidToggle read-only
focus()voidFocus the editor
destroy()voidClean up and remove
getElement()HTMLElementRoot DOM element

Keyboard Shortcuts

ShortcutAction
Ctrl+EnterConfirm (fires onConfirm)
Ctrl+BBold selection (Phase 3)
Ctrl+Shift+MToggle visual/source mode (Phase 3)
Ctrl+/Insert fraction template (Phase 3)

Symbol Palette Categories

CategoryKey Symbols
Greekα β γ δ ε θ λ π σ φ ω Γ Δ Θ Σ Ω
Operators+ − × ÷ · ± ∓ ∪ ∩ ⊕ ⊗
Relations= ≠ ≡ ≈ < > ≤ ≥ ∈ ⊂ ⊆
Arrows→ ← ↔ ⇒ ⇐ ⇔ ↑ ↓ ↦
Brackets( ) [ ] { } ⟨ ⟩ ⌊ ⌋ ⌈ ⌉
Calculus∫ ∬ ∮ ∑ ∏ lim d∂ ∇
Structuresfrac, sqrt, matrix, cases, binom
Functionssin cos tan log ln exp det
Accentshat, bar, vec, dot, tilde
Chemistry\ce{H2O}, →, ⇌, bonds, states
Logic∀ ∃ ¬ ∧ ∨ ⊢ ℕ ℤ ℝ ℂ
Misc∞ ∅ ℏ ℓ ∂ spaces dots

Styling Controls

ButtonLaTeX CommandDescription
Color\textcolor{#hex}{...}Foreground colour
Bold\mathbf{...}Bold math
Size\large{...} etc.Font size
Highlight\colorbox{#hex}{...}Background colour
Boxed\boxed{...}Box around expression
Cancel\cancel{...}Diagonal strikethrough

DiagramEngine Embedding

// Register as embeddable component
engine.registerEmbeddableComponent("latexeditor", {
    factory: "createLatexEditor",
    label: "Equation",
    icon: "bi-calculator",
    category: "data",
    defaultWidth: 300,
    defaultHeight: 80,
});

In view mode, equations render as lightweight KaTeX HTML (no MathLive loaded). Double-click enters interactive edit mode with the full editor.


LayerLayout

A z-stack layout container where all children are simultaneously visible, layered in z-order. The container uses position: relative and each layer uses position: absolute, enabling overlapping content such as floating action buttons, overlays, watermarks, and heads-up displays.

Assets

AssetPath
CSScomponents/layerlayout/layerlayout.css
JScomponents/layerlayout/layerlayout.js
Typescomponents/layerlayout/layerlayout.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/layerlayout/layerlayout.css">
<script src="components/layerlayout/layerlayout.js"></script>

<script>
    // Background canvas with a floating action button in the bottom-right
    var stack = createLayerLayout({
        sizing: "fixed",
        width: "100%",
        height: "400px",
        layers: [
            { child: document.getElementById("canvas"), fill: true },
            { child: document.getElementById("fab"), align: "bottom-right", zIndex: 10 }
        ]
    });
</script>

How It Works

LayerLayout creates a position: relative container. Each layer is wrapped in a position: absolute div and positioned according to its configuration -- fill the entire container, anchor to specific edges, or snap to one of nine alignment points.

Container (position: relative)
+-------------------------------------------------------+
|  Layer 0: fill (background image)                     |
|  +---------------------------------------------------+|
|  |                                                   ||
|  |         Layer 1: align "center"                   ||
|  |              (watermark)                          ||
|  |                                                   ||
|  +---------------------------------------------------+|
|                                                       |
|                              Layer 2: align           |
|                              "bottom-right"           |
|                                   [FAB]               |
+-------------------------------------------------------+

All layers are rendered simultaneously. Higher z-index values appear on top of lower ones. Layers added later appear above earlier layers by default.

Options

LayerLayoutOptions

OptionTypeDefaultDescription
idstringautoCustom element ID
sizing"largest" | "fixed" | "fitContent""fitContent"Container sizing strategy
layersLayerConfig[][]Initial layers (bottom-to-top order)
paddingstring--Container padding (CSS value)
cssClassstring--Additional CSS class(es)
heightstring--Explicit height (CSS value)
widthstring--Explicit width (CSS value)
onLayoutChange(state) => void--Fired when layers are added or removed

LayerConfig

PropertyTypeDefaultDescription
childHTMLElement | Component--Child element or component
keystring--Optional string key for lookup
anchor{ top?, right?, bottom?, left? }--Anchor offsets from container edges (CSS values)
alignalignment string--Shorthand alignment (see table below)
zIndexnumber--Explicit z-index for this layer
fillbooleanfalseIf true, layer stretches to fill entire container

Alignment Shortcuts

The align property maps to CSS absolute positioning:

ValueCSS Position
"top-left"top: 0; left: 0
"top-center"top: 0; left: 50%; transform: translateX(-50%)
"top-right"top: 0; right: 0
"center-left"top: 50%; left: 0; transform: translateY(-50%)
"center"top: 50%; left: 50%; transform: translate(-50%, -50%)
"center-right"top: 50%; right: 0; transform: translateY(-50%)
"bottom-left"bottom: 0; left: 0
"bottom-center"bottom: 0; left: 50%; transform: translateX(-50%)
"bottom-right"bottom: 0; right: 0

Public API

MethodReturnsDescription
show(container?)voidAppend to container and display
hide()voidRemove from DOM (preserves state)
destroy()voidFull cleanup, destroy all layers
getRootElement()HTMLElement | nullThe root relative container
isVisible()booleanWhether the layout is displayed
setContained(value)voidSet contained mode
addLayer(config)voidAdd a layer on top of the stack
removeLayer(index)voidRemove a layer by index
removeLayerByKey(key)voidRemove a layer by its string key
setLayerZIndex(index, z)voidUpdate z-index of a layer
getLayerCount()numberNumber of mounted layers
getState()LayerLayoutStateSerialisable state snapshot
setState(state)voidRestore state (sizing only)

State

interface LayerLayoutState {
    layerCount: number;
    sizing: string;
}

Composability

LayerLayout implements the standard layout container contract. Any component with show(container) / hide() / destroy() can be used as a layer child. Plain HTMLElements are also supported.

// Overlay a loading spinner on top of a content area
var overlay = new LayerLayout({
    sizing: "fixed",
    width: "100%",
    height: "100%",
    layers: [
        { child: contentPanel, fill: true },
        { child: spinner, align: "center", zIndex: 100 }
    ]
});

overlay.show(document.getElementById("main"));

Global Exports

When loaded via <script> tag:

CSS Classes

ClassElementDescription
.layerlayoutRootRelative-positioned container
.layerlayout-layerWrapperAbsolute-positioned layer wrapper

LineEndingPicker

A dropdown picker that displays line ending (arrowhead / marker) styles with inline SVG previews, letting users select marker shapes for the start or end of lines in graph and drawing tools. Marker values are aligned with maxGraph native arrow types for direct interop with GraphCanvasMx.

Usage

<link rel="stylesheet" href="components/lineendingpicker/lineendingpicker.css">
<script src="components/lineendingpicker/lineendingpicker.js"></script>

<div id="my-ending-picker"></div>

<script>
var picker = createLineEndingPicker("my-ending-picker", {
    value: "classic",
    mode: "end",
    onChange: function(ending) {
        console.log("Selected:", ending.label, ending.value);
    }
});
</script>

Options

OptionTypeDefaultDescription
endingsLineEndingItem[]12 standard markersCustom endings list
valuestring--Initially selected ending value
mode"start" | "end""end"Which end of the line receives the marker
previewStrokeWidthnumber2Preview line thickness
size"mini" | "sm" | "default" | "lg""default"Size variant
disabledbooleanfalseDisable the picker
maxVisibleItemsnumber8Max items before scrolling
showERNotationbooleanfalseAppend ER notation endings to the default list
onChange(ending) => void--Fires on selection change
onOpen() => void--Fires when dropdown opens
onClose() => void--Fires when dropdown closes

Default Endings (maxGraph-aligned)

NameValueDescription
NonenoneNo marker, plain line end
BlockblockFilled triangle arrowhead
Block (Open)block-openUnfilled triangle outline
ClassicclassicArrow with notch (classic arrowhead)
Classic (Open)classic-openUnfilled classic arrowhead
OpenopenChevron (open, no fill)
DiamonddiamondFilled diamond shape
Diamond (Open)diamond-openUnfilled diamond outline
CircleovalFilled circle
Circle (Open)oval-openUnfilled circle outline
DashdashPerpendicular line
CrosscrossX mark

ER Notation Endings

When showERNotation: true is set (and no custom endings array is provided), the following Entity-Relationship endings are appended after the standard endings, with a visual group separator.

NameValueDescription
Oneer-oneSingle vertical bar
Mandatory Oneer-mandatory-oneDouble vertical bars
Many (Crow's Foot)er-manyThree lines radiating from a point
One to Manyer-one-to-manyBar + crow's foot
Zero to Oneer-zero-to-oneCircle + bar
Zero to Manyer-zero-to-manyCircle + crow's foot
// Enable ER notation endings alongside standard endings
var picker = createLineEndingPicker("er-picker", {
    value: "classic",
    showERNotation: true,
    onChange: function(ending) {
        console.log("Selected:", ending.value);
    }
});

API

MethodReturnsDescription
getValue()stringCurrent ending value
getSelectedEnding()LineEndingItem | nullFull selected item
setValue(value)voidSelect by ending value
setEndings(endings)voidReplace endings list
getMode()"start" | "end"Current marker mode
setMode(mode)voidChange mode and re-render trigger
open()voidOpen the dropdown
close()voidClose the dropdown
enable()voidEnable the picker
disable()voidDisable the picker
getElement()HTMLElement | nullRoot DOM element
destroy()voidTear down component

Mode

The mode option controls which end of the preview line displays the marker:

You can change the mode at runtime with setMode(), which re-renders the trigger preview.

// Create a start-of-line ending picker
var startPicker = createLineEndingPicker("start-picker", {
    value: "classic",
    mode: "start"
});

// Switch mode at runtime
startPicker.setMode("end");

Keyboard

KeyAction
ArrowDown / ArrowUpMove highlight / open dropdown
EnterConfirm selection
EscapeClose dropdown
Home / EndJump to first / last item
SpaceOpen dropdown (when trigger focused)

LineShapePicker

A dropdown picker that displays line shape/routing patterns with inline SVG previews, letting users select connector shapes for graph and drawing tools. Default shapes align with maxGraph edge routing styles.

Usage

<link rel="stylesheet" href="components/lineshapepicker/lineshapepicker.css">
<script src="components/lineshapepicker/lineshapepicker.js"></script>

<div id="my-shape-picker"></div>

<script>
var picker = createLineShapePicker("my-shape-picker", {
    value: "orthogonal",
    onChange: function(shape) {
        console.log("Selected:", shape.label, shape.value);
    }
});
</script>

Options

OptionTypeDefaultDescription
shapesLineShapeItem[]6 maxGraph routing stylesCustom shape list
valuestring--Initially selected shape value
previewStrokeWidthnumber2Preview line thickness
size"mini" | "sm" | "default" | "lg""default"Size variant
disabledbooleanfalseDisable the picker
maxVisibleItemsnumber8Max items before scrolling
onChange(shape) => void--Fires on selection change
onOpen() => void--Fires when dropdown opens
onClose() => void--Fires when dropdown closes

Default Shapes

NameValueDescription
StraightstraightSimple horizontal line
OrthogonalorthogonalStaircase with rounded corners
Segment (Bezier)segmentSmooth S-curve with draggable waypoints
ManhattanmanhattanSharp orthogonal staircase (no rounding)
ElbowelbowSingle right-angle bend
Entity RelationentityER connector with perpendicular midpoint turn

API

MethodReturnsDescription
getValue()stringCurrent shape value
getSelectedShape()LineShapeItem | nullFull selected item
setValue(value)voidSelect by shape value
setShapes(shapes)voidReplace shape list
open()voidOpen the dropdown
close()voidClose the dropdown
enable()voidEnable the picker
disable()voidDisable the picker
getElement()HTMLElement | nullRoot DOM element
destroy()voidTear down component

Keyboard

KeyAction
ArrowDown / ArrowUpMove highlight / open dropdown
EnterConfirm selection
EscapeClose dropdown
Home / EndJump to first / last item
SpaceOpen dropdown (when trigger focused)

LineTypePicker

A dropdown picker that displays line dash patterns with inline SVG previews, letting users select stroke styles for graph and drawing tools.

Usage

<link rel="stylesheet" href="components/linetypepicker/linetypepicker.css">
<script src="components/linetypepicker/linetypepicker.js"></script>

<div id="my-type-picker"></div>

<script>
var picker = createLineTypePicker("my-type-picker", {
    value: "dashed",
    onChange: function(type) {
        console.log("Selected:", type.label, type.value, type.dashArray);
    }
});
</script>

Options

OptionTypeDefaultDescription
typesLineTypeItem[]12 common patternsCustom type list
valuestringInitially selected type name (e.g. "dashed")
previewStrokeWidthnumber2Preview line thickness
size"mini" | "sm" | "default" | "lg""default"Size variant
disabledbooleanfalseDisable the picker
maxVisibleItemsnumber8Max items before scrolling
onChange(type) => voidFires on selection change
onOpen() => voidFires when dropdown opens
onClose() => voidFires when dropdown closes

Default Types

NameValueDasharray
Solidsolid(none)
Dotteddotted2 2
Dasheddashed6 4
Dash-Dotdash-dot6 4 2 4
Long Dashlong-dash12 4
Short Dashshort-dash4 2
Double Dotdouble-dot2 2 6 2
Double Dashdouble-dash6 2 6 2
Narrow Dotnarrow-dot1 2
Narrow Dashnarrow-dash3 2
Wide Dotwide-dot2 6
Wide Dashwide-dash8 6

LineTypeItem

FieldTypeDescription
labelstringDisplay name (e.g. "Dashed")
valuestringSemantic identifier (e.g. "dashed")
dashArraystringSVG stroke-dasharray (e.g. "6 4"). Empty string for solid.

API

MethodReturnsDescription
getValue()stringCurrent semantic type name (e.g. "dashed")
getSelectedType()LineTypeItem | nullFull selected item
setValue(value)voidSelect by type name (also accepts dasharray for compat)
setTypes(types)voidReplace type list
open()voidOpen the dropdown
close()voidClose the dropdown
enable()voidEnable the picker
disable()voidDisable the picker
getElement()HTMLElement | nullRoot DOM element
destroy()voidTear down component

Keyboard

KeyAction
ArrowDown / ArrowUpMove highlight / open dropdown
EnterConfirm selection
EscapeClose dropdown
Home / EndJump to first / last item
SpaceOpen dropdown (when trigger focused)

LineWidthPicker

A dropdown picker that displays line widths with visual CSS border previews, letting users select stroke thickness for graph and drawing tools.

Usage

<link rel="stylesheet" href="components/linewidthpicker/linewidthpicker.css">
<script src="components/linewidthpicker/linewidthpicker.js"></script>

<div id="my-width-picker"></div>

<script>
var picker = createLineWidthPicker("my-width-picker", {
    value: 2,
    onChange: function(width) {
        console.log("Selected:", width.label, width.value);
    }
});
</script>

Options

OptionTypeDefaultDescription
widthsLineWidthItem[]13 common widthsCustom width list
valuenumberInitially selected width
size"mini" | "sm" | "default" | "lg""default"Size variant
disabledbooleanfalseDisable the picker
maxVisibleItemsnumber8Max items before scrolling
onChange(width) => voidFires on selection change
onOpen() => voidFires when dropdown opens
onClose() => voidFires when dropdown closes

Default Widths

0.5, 1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10, 15, 20px

API

MethodReturnsDescription
getValue()numberCurrent width value
getSelectedWidth()LineWidthItem | nullFull selected item
setValue(value)voidSelect a width programmatically
setWidths(widths)voidReplace width list
open()voidOpen the dropdown
close()voidClose the dropdown
enable()voidEnable the picker
disable()voidDisable the picker
getElement()HTMLElement | nullRoot DOM element
destroy()voidTear down component

Keyboard

KeyAction
ArrowDown / ArrowUpMove highlight / open dropdown
EnterConfirm selection
EscapeClose dropdown
Home / EndJump to first / last item
SpaceOpen dropdown (when trigger focused)

LogConsole

A reusable in-app logging console for displaying high-level user actions and system events. Renders a scrollable, level-filterable log feed with dark/light theming, per-level colour customisation, Clear/Export actions, rAF-batched DOM updates, and FIFO eviction.

Features

Assets

AssetPath
CSScomponents/logconsole/logconsole.css
JScomponents/logconsole/logconsole.js
Typescomponents/logconsole/logconsole.d.ts

Requires: Bootstrap Icons CSS (optional, for action button icons). Does not require Bootstrap JS.

Quick Start

<link rel="stylesheet" href="components/logconsole/logconsole.css">
<script src="components/logconsole/logconsole.js"></script>
<script>
    var log = createLogConsole({ theme: "dark", maxEntries: 500 });
    document.getElementById("my-container").appendChild(log.getElement());

    log.info("Application started");
    log.warn("Retry attempt 2/3");
    log.error("Request failed (status 500)");
    log.success("Session saved");
</script>

API

Constructor

const log = new LogConsole(options?: LogConsoleOptions);

Creates the log console DOM element. Attach with getElement().

LogConsoleOptions

OptionTypeDefaultDescription
maxEntriesnumber500FIFO eviction threshold
theme"dark" | "light""dark"Colour scheme
showHeaderbooleantrueShow header bar with filters and actions
autoScrollbooleantrueAuto-scroll to newest entry
exportFilenamePrefixstring"logs"Download filename prefix
levelColorsLogConsoleLevelColorsPer-level badge colour overrides
fontFamilystringmonospace stackFont family for all text
fontSizestring"12px"Font size
backgroundColorstringtheme defaultRoot background colour
textColorstringtheme defaultPrimary text colour
headerBackgroundColorstringtheme defaultHeader bar background
entryBorderColorstringtheme defaultEntry row border colour
mutedTextColorstringtheme defaultTimestamp / muted text colour
heightstring"100%"Root element CSS height
cssClassstringAdditional CSS class(es)
containedbooleanfalseRelative positioning for embedding
onClearfunctionCalled after clear()
onExportfunctionCalled after export with text content
onEntryfunctionCalled after each entry is added

Default Level Colours

LevelColourHex
DEBUGGray#94a3b8
INFOBlue#3b82f6
WARNAmber#f59e0b
ERRORRed#ef4444
SUCCESSGreen#22c55e

Shorthand Methods

log.debug("Initialising...");
log.info("Session loaded");
log.warn("Retry attempt 2/3");
log.error("Request failed");
log.success("Diagram saved");

Each generates a timestamp automatically and calls log().

Structured Logging

log.log({
    timestamp: "10:53:04.123",
    level: "INFO",
    message: "Custom timestamp entry"
});

Methods

MethodDescription
debug(msg)Log a DEBUG message
info(msg)Log an INFO message
warn(msg)Log a WARN message
error(msg)Log an ERROR message
success(msg)Log a SUCCESS message
log(entry)Log a structured entry with custom timestamp
clear()Clear all entries from memory and DOM
exportAsText()Returns all entries (unfiltered) as formatted text
downloadAsText()Triggers a .txt download of all entries
setFilter(levels)Set which log levels are visible
getFilter()Get currently visible log levels
getElement()Returns the root DOM element for embedding
destroy()Remove from DOM and release all references

Convenience Functions

createLogConsole(options?)  // Create and return

Global Exports

window.LogConsole
window.createLogConsole

Theme Tokens

TokenDarkLight
Background#1e293b#f8fafc
Text#e2e8f0#1e293b
Entry border#334155#e2e8f0
Header bg#334155#f1f5f9
Muted text#94a3b8#64748b

All tokens can be overridden individually via options.

Integration with TabbedPanel

var logConsole = createLogConsole({ theme: "dark" });
var panel = createDockedTabbedPanel({
    tabs: [
        { id: "log", title: "Log Console", icon: "bi-terminal",
          content: logConsole.getElement() }
    ]
});

logConsole.info("Application started");

Export Format

[10:53:04.123] [INFO   ] Saving session data to backend...
[10:53:04.198] [INFO   ] Session data saved successfully
[10:53:06.829] [WARN   ] Retry attempt 2/3
[10:53:14.200] [ERROR  ] Request failed (status 500)

Filename: {prefix}-YYYY-MM-DDTHH-MM-SS.txt

Accessibility

See specs/2026-02-17-shared-log-console-requirements.md for the full specification.


LogUtility

A non-visual, centralised logging utility that replaces per-component logInfo/logWarn/logError/logDebug helper functions with a singleton providing named loggers, level filtering, timestamp control, and optional LogConsole routing.

Assets

AssetPath
JScomponents/logutility/logutility.js
Typescomponents/logutility/logutility.d.ts

Requirements

Quick Start

<script src="components/logutility/logutility.js"></script>
<script>
    // Create the singleton (or retrieve it if already created).
    var logUtil = createLogUtility({ level: "info" });

    // Create a named logger for a component.
    var log = logUtil.getLogger("MyComponent");

    log.info("initialised");
    log.warn("deprecation notice");
    log.error("failed to load", new Error("timeout"));
    log.debug("internal state", { count: 42 });
</script>

API

Factory Function

FunctionReturnsDescription
createLogUtility(options?)LogUtilityCreate or retrieve the singleton
resetLogUtility()voidDestroy and reset the singleton for a fresh start

LogUtilityOptions

OptionTypeDefaultDescription
level"debug" | "info" | "warn" | "error""debug"Minimum log level
timestampsbooleantrueInclude ISO timestamps in output
logConsoleLogConsoleHandlenullRoute user-visible events to a LogConsole
consoleOutputbooleantrueEnable or disable browser console output

LogUtility Methods

MethodDescription
getLogger(componentName)Create a named Logger for a component
setLevel(level)Change the minimum log level at runtime
setLogConsole(lc)Attach or detach a LogConsole for event routing
setConsoleOutput(enabled)Toggle browser console output
destroy()Release resources

Logger Methods

MethodDescription
trace(...args)Log a trace message (most verbose — DOM, render, events)
debug(...args)Log a debug message
info(...args)Log an informational message
warn(...args)Log a warning (always emitted)
error(...args)Log an error (always emitted)
event(message, data?)Log a user-visible event, routed to LogConsole if configured

Output Format

Each log message is formatted as:

[ISO-TIMESTAMP] [LEVEL] [ComponentName] message args...

Examples:

2026-03-29T14:30:00.000Z [INFO] [MyComponent] initialised
2026-03-29T14:30:00.001Z [WARN] [MyComponent] deprecation notice
2026-03-29T14:30:00.002Z [ERROR] [MyComponent] failed to load Error: timeout
2026-03-29T14:30:00.003Z [DEBUG] [MyComponent] internal state { count: 42 }

Timestamps can be disabled by passing timestamps: false.

Level Filtering

Log levels have a numeric hierarchy: trace (-1) < debug (0) < info (1) < warn (2) < error (3).

Setting the level to "warn" suppresses trace, debug, and info messages. Warnings and errors are never filtered.

var logUtil = createLogUtility({ level: "warn" });
var log = logUtil.getLogger("Filtered");

log.trace("suppressed");  // Not output (most verbose)
log.debug("suppressed");  // Not output
log.info("suppressed");   // Not output
log.warn("visible");      // Always output
log.error("always");      // Always output

Trace Level

Trace is the most verbose level — intended for very low-level output such as DOM mutations, render cycles, and event propagation. It uses console.debug for output.

var logUtil = createLogUtility({ level: "trace" });
var log = logUtil.getLogger("Renderer");

log.trace("DOM mutation", { target: el, type: "childList" });
log.trace("render cycle", { frameId: 42, objects: 15 });

LogConsole Integration

When a LogConsoleHandle is attached, calls to logger.event() are routed to the LogConsole in addition to the browser console. This is useful for displaying user-visible events in an in-app log panel.

var logUtil = createLogUtility();

// Attach a LogConsole later.
logUtil.setLogConsole(myLogConsoleInstance);

var log = logUtil.getLogger("Auth");
log.event("User logged in", { userId: 42 });
// Console: [INFO] [Auth] User logged in { userId: 42 }
// LogConsole: "[Auth] User logged in"

Console Output Toggle

Console output can be disabled entirely for production or testing scenarios.

logUtil.setConsoleOutput(false);  // Silence all console output
logUtil.setConsoleOutput(true);   // Re-enable console output

Singleton Behaviour

createLogUtility() returns the same instance on every call. The options argument is only used on the first call. To create a fresh instance with different options, call resetLogUtility() first.

var lu1 = createLogUtility({ level: "info" });
var lu2 = createLogUtility({ level: "error" });  // Same instance as lu1

resetLogUtility();

var lu3 = createLogUtility({ level: "error" });  // Fresh instance

Runtime Debug Flags

Global variables on window allow customer support or developers to enable verbose logging at runtime without reloading the page:

FlagEffect
window.__ebt_debug_logging = trueEnable DEBUG level output
window.__ebt_info_logging = trueEnable INFO level output
window.__ebt_trace_logging = trueEnable DEBUG level (alias)

WARN and ERROR are always enabled — they cannot be suppressed. Only INFO, DEBUG, and TRACE are controlled by these flags.

Flags are checked on every log call, so toggling them at runtime takes effect immediately:

// In the browser console:
window.__ebt_debug_logging = true;   // verbose debug output starts
window.__ebt_debug_logging = false;  // back to normal

The flags work as an override — if the LogUtility is configured with level: "error" but __ebt_debug_logging is true, debug messages will still be emitted.

Studio Apps

All 4 studio apps (Ribbon, Layout, Shape, Component) automatically enable all log levels by setting:

window.__ebt_debug_logging = true;
window.__ebt_info_logging = true;
window.__ebt_trace_logging = true;

Window Globals

GlobalTypeDescription
window.createLogUtilityfunctionFactory for the singleton
window.resetLogUtilityfunctionReset the singleton
window.LogUtilityclassThe implementation class
window.__ebt_debug_loggingbooleanRuntime debug flag
window.__ebt_info_loggingbooleanRuntime info flag
window.__ebt_trace_loggingbooleanRuntime trace flag

Magnifier

A cursor-following magnifying glass overlay that clones and scales the content of a target element within a circular lens. The lens tracks the mouse cursor and shows a magnified view of the area directly under the pointer.

Assets

AssetPath
CSScomponents/magnifier/magnifier.css
JScomponents/magnifier/magnifier.js
Typescomponents/magnifier/magnifier.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/magnifier/magnifier.css">
<script src="components/magnifier/magnifier.js"></script>

<div id="magnify-target" style="width: 600px; height: 400px; overflow: auto;">
    <p>Hover over this content to see it magnified in the lens.</p>
    <img src="sample-image.png" alt="Sample" style="width: 100%;">
</div>

<script>
    var magnifier = createMagnifier("my-container", {
        target: "magnify-target",
        zoom: 2.5,
        diameter: 180,
        showCrosshair: true,
        onMove: function(x, y) {
            console.log("Cursor at:", x, y);
        }
    });
</script>

Options (MagnifierOptions)

OptionTypeDefaultDescription
targetHTMLElement | stringdocument.bodyElement to magnify. A string value is treated as an element ID.
zoomnumber2Magnification factor.
diameternumber150Lens diameter in pixels.
borderColorstring"#868e96"Lens border colour (gray-600).
borderWidthnumber2Lens border width in pixels.
offset{ x: number; y: number }{ x: 20, y: 20 }Pixel offset from cursor to lens position.
showCrosshairbooleantrueDisplay crosshair lines at the centre of the lens.
disabledbooleanfalseStart with magnification disabled.
onMove(x: number, y: number) => void--Callback fired on each mouse move with cursor coordinates.

API

MethodReturnsDescription
getElement()HTMLElement | nullReturns the lens DOM element.
enable()voidEnable magnification tracking.
disable()voidDisable tracking and hide the lens.
setZoom(zoom)voidUpdate the magnification factor. Must be positive.
setDiameter(diameter)voidUpdate the lens diameter in pixels. Must be positive.
destroy()voidRemove the lens from the DOM and detach all event listeners.

Factory Function

var magnifier = createMagnifier("container-id", { zoom: 3, diameter: 200 });

The containerId parameter serves as the logical owner identifier. The lens element itself is appended to document.body because it uses position: fixed to follow the cursor across the viewport.

How It Works

  1. On mouseenter over the target, the component clones the target element's DOM subtree into the lens.
  2. The clone is scaled by the zoom factor using CSS transform: scale().
  3. On each mousemove, the lens repositions near the cursor (offset by offset.x and offset.y), and the clone is translated so that the area directly under the cursor appears centred in the lens.
  4. On mouseleave, the lens hides and the clone is removed.

Global Exports

window.Magnifier;          // Class constructor
window.createMagnifier;    // Factory function

Examples

Basic Usage with Default Settings

var mag = createMagnifier("app", { target: "photo-gallery" });

High Zoom with Large Lens

var mag = createMagnifier("app", {
    target: document.getElementById("blueprint"),
    zoom: 4,
    diameter: 250,
    borderColor: "#1c7ed6",
    borderWidth: 3,
    showCrosshair: true
});

Programmatic Control

var mag = createMagnifier("app", { target: "map-view", disabled: true });

// Enable on button click
document.getElementById("toggle-btn").addEventListener("click", function() {
    mag.enable();
});

// Change zoom dynamically
document.getElementById("zoom-slider").addEventListener("input", function(e) {
    mag.setZoom(parseFloat(e.target.value));
});

// Clean up
document.getElementById("remove-btn").addEventListener("click", function() {
    mag.destroy();
});

MarginsPicker

A dropdown component showing page margin presets with visual page thumbnails, modelled after Microsoft Word's Margins dropdown. Each option displays a small SVG page illustration with margin boundaries drawn, plus the preset name and exact Top/Bottom/Left/Right values in inches.

Usage

<link rel="stylesheet" href="components/marginspicker/marginspicker.css">
<script src="components/marginspicker/marginspicker.js"></script>

<div id="my-margins-picker"></div>

<script>
var picker = createMarginsPicker({
    container: "my-margins-picker",
    value: "Normal",
    onChange: function(preset) {
        console.log("Selected:", preset.name,
            "Top:", preset.top, "Bottom:", preset.bottom,
            "Left:", preset.left, "Right:", preset.right);
    }
});
</script>

Configuration

OptionTypeDefaultDescription
containerHTMLElement | string(required)Container element or ID
valuestring"Normal"Initial selected preset name
presetsMarginPreset[](6 built-in)Custom preset definitions
showCustombooleantrueShow "Custom Margins..." link
onChange(preset) => void-Callback when a preset is selected
onCustom() => void-Callback when "Custom Margins..." is clicked
ribbonModebooleantrueRender as ribbon-compatible dropdown

Default Presets

NameTopBottomLeftRight
Normal1"1"1"1"
Narrow0.5"0.5"0.5"0.5"
Moderate1"1"0.75"0.75"
Wide1"1"2"2"
Mirrored1"1"Inside: 1.25"Outside: 1"
None0"0"0"0"

Public API

interface MarginsPicker {
    getValue(): MarginPreset;
    setValue(presetName: string): void;
    setPresets(presets: MarginPreset[]): void;
    show(): void;
    hide(): void;
    destroy(): void;
    getElement(): HTMLElement;
}

Custom Presets

var picker = createMarginsPicker({
    container: "my-picker",
    presets: [
        { name: "A4 Standard", top: 1.5, bottom: 1.5, left: 1, right: 1 },
        { name: "Legal Tight", top: 0.75, bottom: 0.75, left: 0.5, right: 0.5 },
        { name: "Book Mirror", top: 1, bottom: 1, left: 1, right: 1,
          inside: 1.5, outside: 0.75 },
    ],
    showCustom: true,
    onCustom: function() {
        openCustomMarginsDialog();
    }
});

Keyboard Navigation

KeyAction
Enter / SpaceSelect focused preset or toggle dropdown
EscapeClose dropdown
TabMove focus; closes dropdown

Dark Mode

All colours use var(--theme-*) CSS custom properties and automatically adapt to dark mode when data-bs-theme="dark" is set on the <html> element.


MarkdownEditor

A Bootstrap 5-themed Markdown editor wrapper around Vditor with tab/side-by-side layout modes, collapsible panes, inline selection toolbar, export, and optional modal hosting.

Dependencies

This component requires external libraries loaded before the component script:

<!-- Vditor (>= 3.8.13 required for security fixes; 3.11.2 recommended) -->
<link rel="stylesheet" href="https://unpkg.com/vditor@3.11.2/dist/index.css" />
<script src="https://unpkg.com/vditor@3.11.2/dist/index.min.js"></script>

<!-- DOMPurify (strongly recommended for XSS protection) -->
<script src="https://unpkg.com/dompurify@3.2.4/dist/purify.min.js"></script>

<!-- Component CSS + JS -->
<link rel="stylesheet" href="components/markdowneditor/markdowneditor.css">
<script src="components/markdowneditor/markdowneditor.js"></script>

Quick Start

<div id="my-editor"></div>
<script>
    var editor = createMarkdownEditor("my-editor", {
        title: "My Document",
        value: "# Hello World\n\nStart writing...",
        onChange: function(value) { console.log("Content changed"); }
    });
</script>

Modal Usage

<script>
    showMarkdownEditorModal({
        modalTitle: "Edit Description",
        value: existingMarkdown,
        onSave: function(value) {
            console.log("Saved:", value);
        },
        onClose: function(value) {
            // value is null if cancelled
            if (value !== null) {
                console.log("Closed with content");
            }
        }
    });
</script>

Configuration Options

OptionTypeDefaultDescription
valuestring""Initial markdown content
mode"tabs" | "sidebyside" | "display" | "naked""tabs"Layout mode. "display" renders read-only markdown with no chrome — ideal for hovers and inline content. "naked" renders an editable Vditor surface with no chrome — feels like a textarea that understands markdown
editablebooleantrueReadwrite (true) or readonly (false). When false, the toolbar is hidden and the Preview tab is shown by default
titlestringHeader bar title
heightstring"70vh"Component height (CSS value)
widthstring"100%"Component width (CSS value)
minHeightnumber300Minimum height in pixels
minWidthnumber400Minimum width in pixels
showExportbooleantrueShow export dropdown
showFullscreenbooleantrueShow fullscreen toggle
showInlineToolbarbooleantrueShow inline formatting toolbar on selection
showCounterbooleanfalseShow character counter
placeholderstringPlaceholder text
vditorMode"ir" | "wysiwyg" | "sv""ir"Vditor editing mode
size"sm" | "md" | "lg""md"Size variant
disabledbooleanfalseDisabled state
isolatedbooleanfalseScope display-mode styles to prevent CSS bleed into parent
compactbooleanfalseTighter vertical spacing for sidebar/embedded contexts
theme"light" | "dark""light"Color theme for display mode
toolbarstring[]Default setCustom Vditor toolbar items
vditorOptionsobjectCustom Vditor options (merged)
onChange(value: string) => voidContent changed
onReady() => voidEditor ready (fires in all modes including display)
onSave(value: string) => voidSave triggered (Ctrl+Enter)
onModeChange(mode: string) => voidLayout mode switched

Display Mode

The "display" mode renders markdown as read-only HTML with no UI chrome — no header bar, toolbar, tabs, resize handle, or border. This is ideal for embedding rendered markdown in tooltips, popovers, hover cards, or any read-only container.

createMarkdownEditor("tooltip-content", {
    mode: "display",
    value: "**Status:** Approved\n\nSee [JIRA-1234](#) for details."
});

In display mode:

CSS Isolation (isolated: true)

When multiple display-mode instances are embedded in a sidebar or panel, Vditor's CSS (margins, heading sizes, code block styles) can bleed into the parent container. Setting isolated: true applies a CSS reset boundary:

createMarkdownEditor("sidebar-idea", {
    mode: "display",
    value: ideaMarkdown,
    isolated: true
});

Compact Spacing (compact: true)

Reduces vertical spacing for dense embedded contexts: tighter paragraph margins (0.25em), smaller headings (relative to container), reduced code block padding, collapsed list item spacing.

createMarkdownEditor("category-idea", {
    mode: "display",
    value: ideaMarkdown,
    compact: true
});

Dark Theme (theme: "dark")

Renders with light text on a dark background — suitable for dark tooltips, popovers, and panels. Code blocks use native syntax highlighting, and link colors contrast against dark backgrounds.

createMarkdownEditor("hover-popover", {
    mode: "display",
    value: nodeMarkdown,
    theme: "dark"
});

onReady in Display Mode

The onReady callback fires after the markdown has been rendered to HTML and inserted into the DOM. This allows consumers to measure content height, attach event listeners, or adjust layout:

createMarkdownEditor("idea-container", {
    mode: "display",
    value: ideaMarkdown,
    onReady: function() {
        var el = document.getElementById("idea-container");
        if (el.scrollHeight > el.clientHeight) {
            showOverflowIndicator(el);
        }
    }
});

Naked Mode

The "naked" mode renders a fully editable Vditor surface with no UI chrome — no header bar, toolbar, tabs, or mode toggle. It feels like a <textarea> that understands markdown. The editor has a form-control-style border and is resizable via CSS resize: both.

createMarkdownEditor("notes-field", {
    mode: "naked",
    value: "- [ ] Review PR\n- [x] Update docs",
    placeholder: "Type markdown here...",
    height: "200px",
    onChange: function(md) { console.log("Changed:", md.length); }
});

In naked mode:

Use naked mode for expanded detail panels, wiki-style paragraph editing, or any context where you want markdown editing power without the editor chrome. For per-row lightweight editing (todo items, checklist entries), use the RichTextInput component instead.

Modal Options

Extends all options above plus:

OptionTypeDefaultDescription
modalTitlestring"Edit Markdown"Modal title
showSavebooleantrueShow Save button
saveLabelstring"Save"Save button text
cancelLabelstring"Cancel"Cancel button text
onClose(value: string | null) => voidModal closed (null if cancelled)

The modal dialog is horizontally resizable — drag the right edge to adjust width (min 480 px, max 95 vw).

Public API

MethodReturnsDescription
getValue()stringGet current markdown
setValue(md)voidSet markdown content
getHTML()stringGet sanitised rendered HTML
setMode(mode)voidSwitch layout mode
setEditable(bool)voidSwitch readonly/readwrite
setTitle(title)voidUpdate header title
exportMarkdown()voidDownload as .md file
exportHTML()voidDownload as .html file
exportPDF()voidOpen print dialog (Save as PDF)
focus()voidFocus the editor
enable()voidEnable the editor
disable()voidDisable the editor
destroy()voidRemove component and clean up

Supported Markdown Features

Via Vditor, the editor supports:

Keyboard Shortcuts

ShortcutAction
Ctrl/Cmd + BBold
Ctrl/Cmd + IItalic
Ctrl/Cmd + UUnderline
Ctrl/Cmd + DStrikethrough
Ctrl/Cmd + ZUndo
Ctrl/Cmd + Shift + ZRedo
Ctrl/Cmd + KInsert link
Ctrl/Cmd + EnterSave (triggers onSave)
EscapeClose inline toolbar / Exit fullscreen

Security

Important: This component requires security-conscious deployment.

  1. Vditor version: Always use >= 3.8.13 (fixes CVE-2022-0341, CVE-2022-0350, CVE-2021-4103, CVE-2021-32855). Pin the version in production.
  2. DOMPurify: Always load DOMPurify alongside this component. All HTML output is sanitised through DOMPurify before DOM insertion or export.
  3. Content-Security-Policy: Configure CSP headers to restrict script-src to trusted CDN domains only.
  4. SRI hashes: Use Subresource Integrity attributes on CDN <script> and <link> tags in production.
  5. getHTML() is safe: The public getHTML() method returns sanitised HTML. If you bypass the component and call Vditor's getHTML() directly, you must sanitise the output yourself.

Window Globals

GlobalType
window.MarkdownEditorclass
window.createMarkdownEditorfunction(containerId, options): MarkdownEditor
window.showMarkdownEditorModalfunction(options): MarkdownEditor

MarkdownRenderer

Shared markdown-to-HTML rendering utility for the Enterprise Theme. Converts markdown to styled HTML using marked with optional extensions for code highlighting, math, and diagrams. Zero CSS injection — renders using the theme's native fonts and colours.

Dependencies

Required

LibraryGlobalCDNPurpose
markedwindow.markedhttps://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.jsMarkdown parsing

Optional (auto-detected)

These libraries are probed on window at render time. If present, they are used automatically. If absent, the corresponding features are silently skipped.

LibraryGlobalCDNPurpose
highlight.jswindow.hljshttps://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.jsCode syntax highlighting
KaTeXwindow.katexhttps://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.jsLaTeX/MathML math rendering
Mermaidwindow.mermaidhttps://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.jsMermaid diagrams
@viz-js/vizwindow.Vizhttps://cdn.jsdelivr.net/npm/@viz-js/viz@3.11.0/lib/viz-standalone.jsGraphviz/dot diagrams (client-side WASM, no server)

PlantUML (server-based)

PlantUML diagrams are rendered by sending the source text to a PlantUML server which returns SVG. No client-side library is needed.

The encoding uses native CompressionStream('deflate-raw') + PlantUML's custom base64 — no encoder library needed.

Usage

Basic (markdown only)

<script src="marked.min.js"></script>
<script src="markdownrenderer.js"></script>
<script>
var renderer = createMarkdownRenderer();
renderer.render("# Hello\n\nSome **bold** text.", targetElement);
</script>

With code highlighting

<link rel="stylesheet" href="highlight.js/styles/github.min.css">
<script src="marked.min.js"></script>
<script src="highlight.min.js"></script>
<script src="markdownrenderer.js"></script>
<script>
var renderer = createMarkdownRenderer();
renderer.render("```js\nconsole.log('hi');\n```", targetElement);
</script>

With all extensions

<link rel="stylesheet" href="highlight.js/styles/github.min.css">
<link rel="stylesheet" href="katex/dist/katex.min.css">
<script src="marked.min.js"></script>
<script src="highlight.min.js"></script>
<script src="katex.min.js"></script>
<script src="mermaid.min.js"></script>
<script src="viz-standalone.js"></script>
<script src="markdownrenderer.js"></script>
<script>
var renderer = createMarkdownRenderer({
    plantumlServer: "http://localhost:8080/svg/"
});
renderer.render(markdownContent, targetElement);
</script>

Configuration options

createMarkdownRenderer({
    highlight: true,        // Use highlight.js if available (default: true)
    math: true,             // Use KaTeX if available (default: true)
    mermaid: true,          // Use Mermaid if available (default: true)
    graphviz: true,         // Use @viz-js/viz if available (default: true)
    plantuml: true,         // Enable PlantUML server rendering (default: true)
    plantumlServer: "...",  // PlantUML server URL (default: public server)
});

Disabling specific features

// Only markdown + code highlighting, no diagrams or math
createMarkdownRenderer({
    math: false,
    mermaid: false,
    graphviz: false,
    plantuml: false,
});

API

createMarkdownRenderer(opts?): MarkdownRendererHandle

Creates a renderer instance. Registered on window.createMarkdownRenderer.

MarkdownRendererHandle

MethodDescription
render(md, target)Render markdown into a DOM element. Runs async post-processing for diagrams.
toHtml(md)Convert markdown to sanitised HTML string. Does not process diagrams (they require DOM).

Supported markdown features

Standard markdown

Headings, paragraphs, bold, italic, links, images, lists, blockquotes, horizontal rules, tables.

Code blocks

Fenced code blocks with language tags. Syntax highlighted when highlight.js is loaded.

```javascript
function greet() { console.log("hello"); }
```

Math (KaTeX)

Inline math with $E = mc^2$ and block math with:

$$
\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
$$

Mermaid diagrams

```mermaid
graph TD
    A[Start] --> B{Decision}
    B -->|Yes| C[OK]
    B -->|No| D[End]
```

Graphviz / dot diagrams

Rendered entirely client-side via @viz-js/viz (WASM). No server needed.

```dot
digraph G {
    A -> B -> C;
    B -> D;
}
```

Also supports the graphviz language tag:

```graphviz
digraph G { rankdir=LR; A -> B; }
```

PlantUML diagrams

Rendered via server. Source is deflate-compressed, base64-encoded, and fetched as SVG.

```plantuml
@startuml
Alice -> Bob: Hello
Bob --> Alice: Hi back
@enduml
```

Security

Architecture

The renderer is designed as a thin orchestration layer:

[marked]  ──parse──>  HTML string  ──sanitize──>  Safe HTML
                          │
              ┌───────────┼───────────┐
              │           │           │
         [highlight.js] [KaTeX]   [code blocks]
                                      │
                            ┌─────────┼─────────┐
                            │         │         │
                       [Mermaid]  [Viz.js]  [PlantUML]
                       (client)  (client)  (server)

Used by

Both components call window.createMarkdownRenderer() internally. Load markdownrenderer.js before their scripts.


MaskedEntry

A specialised input field that masks sensitive non-password data — API keys, tokens, SSNs, connection strings, and similar secrets. Provides a show/hide toggle, copy-to-clipboard, and configurable masking strategy.

Assets

AssetPath
CSScomponents/maskedentry/maskedentry.css
JScomponents/maskedentry/maskedentry.js
Typescomponents/maskedentry/maskedentry.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/maskedentry/maskedentry.css">
<script src="components/maskedentry/maskedentry.js"></script>
<script>
    var entry = createMaskedEntry("my-container", {
        value: "sk-abc123def456ghi789",
        label: "API Key",
        readonly: true
    });
</script>

Options

OptionTypeDefaultDescription
valuestring""The sensitive value to mask
placeholderstring""Placeholder text when the input is empty
maskMode"native" | "custom""native"Masking strategy
maskCharstring"•"Character used for custom mask mode
initiallyRevealedbooleanfalseWhether the value is visible on construction
showCopyButtonbooleantrueShow the copy-to-clipboard button
showToggleButtonbooleantrueShow the reveal/conceal toggle button
copyFeedbackDurationnumber2000Duration in ms to show "Copied!" feedback
disabledbooleanfalseDisable the entire control
readonlybooleanfalseMake the input read-only (buttons still active)
size"sm" | "default" | "lg""default"Bootstrap size variant
maxLengthnumberMaximum character length
labelstringOptional label text above the input
cssClassstringAdditional CSS classes
onChangefunctionFires when the user changes the value
onCopyfunctionFires after a successful clipboard copy
onRevealfunctionFires when reveal state changes

API

MethodReturnsDescription
show(containerId)voidAppend to a container by ID
hide()voidRemove from DOM, keep state
destroy()voidHide, clean up, null references
getValue()stringCurrent plaintext value
setValue(value)voidSet a new value and re-mask
isRevealed()booleanWhether the value is visible
reveal()voidShow the plaintext value
conceal()voidMask the value
toggleReveal()voidToggle between revealed and concealed
enable()voidEnable the input and buttons
disable()voidDisable the input and buttons
getElement()HTMLElementRoot DOM element

Convenience Function

createMaskedEntry(containerId, options)  // Create, show, and return

Global Exports

window.MaskedEntry
window.createMaskedEntry

Masking Modes

Native mode (maskMode: "native") toggles the input between type="password" and type="text". This leverages the browser's built-in masking and is the default.

Custom mode (maskMode: "custom") keeps the input as type="text" at all times. When concealed, the component replaces the display value with repeated mask characters. The actual value is stored internally.

Accessibility

Examples

Read-only API key display

var entry = createMaskedEntry("api-key-container", {
    value: "sk-abc123def456ghi789jkl012mno345",
    label: "API Key",
    readonly: true,
    onCopy: function() { console.log("Key copied"); }
});

Custom mask character

var entry = createMaskedEntry("token-container", {
    value: "ghp_xxxxxxxxxxxxxxxxxxxx",
    maskMode: "custom",
    maskChar: "*",
    label: "GitHub Token"
});

Small size variant

var entry = createMaskedEntry("inline-container", {
    value: "bearer_token_value",
    size: "sm",
    showToggleButton: false
});

See specs/maskedentry.prd.md for the complete specification.


MultiselectCombo

A multi-select combo box that allows users to choose multiple items from a filterable dropdown list. Selected items appear as removable chips or a compact count badge. Each dropdown item has a checkbox that toggles selection without closing the dropdown.

Assets

AssetPath
CSScomponents/multiselectcombo/multiselectcombo.css
JScomponents/multiselectcombo/multiselectcombo.js
Typescomponents/multiselectcombo/multiselectcombo.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/multiselectcombo/multiselectcombo.css">
<script src="components/multiselectcombo/multiselectcombo.js"></script>
<script>
    var combo = createMultiselectCombo({
        items: [
            { value: "a", label: "Apple" },
            { value: "b", label: "Banana" },
            { value: "c", label: "Cherry" },
            { value: "d", label: "Date" }
        ],
        placeholder: "Pick fruits...",
        onChange: function(values) { console.log("Selected:", values); }
    }, "my-container");
</script>

API

Interfaces

ComboItem

PropertyTypeDefaultDescription
valuestringrequiredProgrammatic identifier
labelstringrequiredDisplay text in dropdown and chips
groupstringGrouping label for group headers
iconstringBootstrap Icons class(es)
disabledbooleanfalseItem is shown but not selectable
dataobjectArbitrary consumer data (not rendered)

MultiselectComboOptions

OptionTypeDefaultDescription
itemsComboItem[]requiredItems to display in the dropdown
selectedstring[][]Initial selected values
placeholderstring"Select..."Placeholder when nothing selected
maxSelectionsnumber0Max selections (0 = unlimited)
showSelectAllbooleantrueShow Select All checkbox
showChipsbooleantrueShow chips (false = count badge)
chipRemovablebooleantrueShow remove button on chips
maxChipsVisiblenumber5Max chips before "+N more" badge
filterPlaceholderstring"Filter items..."Filter input placeholder
noResultsTextstring"No results found"Empty filter message
showFilterbooleantrueShow filter input in dropdown
disabledbooleanfalseDisable the component
readonlybooleanfalseAllow viewing but not changing
sizestring"default""sm", "default", or "lg"
cssClassstringAdditional CSS class(es)
onSelectfunctionCalled when item selected
onDeselectfunctionCalled when item deselected
onChangefunctionCalled after any selection change
onFilterChangefunctionCalled when filter text changes
onOpenfunctionCalled when dropdown opens
onClosefunctionCalled when dropdown closes

Class: MultiselectCombo

MethodReturnsDescription
new MultiselectCombo(options)instanceCreates DOM tree (does not attach)
show(containerId)voidAppends to container by ID
hide()voidRemoves from DOM, preserves state
destroy()voidRemoves and cleans up everything
getElement()HTMLElementReturns root element
getSelectedValues()string[]Currently selected values
getSelectedItems()ComboItem[]Currently selected items
setSelected(values)voidReplace selection
selectAll()voidSelect all non-disabled items
deselectAll()voidClear all selections
addItem(item)voidAdd item to dropdown
removeItem(value)voidRemove item by value
setItems(items)voidReplace all items
open()voidOpen dropdown
close()voidClose dropdown
enable()voidEnable component
disable()voidDisable component
focus()voidFocus input area

Convenience Function

FunctionDescription
createMultiselectCombo(options, containerId?)Create, optionally show, return instance

Features

Keyboard

KeyAction
Enter / SpaceOpen dropdown (when closed); toggle highlighted item (when open)
ArrowDownOpen dropdown or highlight next item
ArrowUpHighlight previous item
EscapeClose dropdown, return focus to input area
TabClose dropdown, move focus naturally
BackspaceRemove last selected chip (when filter is empty)
HomeHighlight first item
EndHighlight last item
Ctrl+ASelect all / deselect all visible items

Accessibility

See specs/multiselectcombo.prd.md for the complete specification.


Notification Center (In-App Bell)

Aggregated notification panel with bell trigger, unread badge, category filters, read/unread state, dismiss per item, date grouping, and deep-link navigation.

Assets

AssetPath
CSScomponents/notificationcenter/notificationcenter.css
JScomponents/notificationcenter/notificationcenter.js
Typescomponents/notificationcenter/notificationcenter.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/notificationcenter/notificationcenter.css">
<script src="components/notificationcenter/notificationcenter.js"></script>
<script>
    var nc = createNotificationCenter({
        container: document.getElementById("bell-slot"),
        notifications: [
            { id: "1", title: "New comment", message: "Alice mentioned you", category: "mentions", timestamp: new Date() }
        ],
        categories: [
            { id: "mentions", label: "Mentions", icon: "bi-at" },
            { id: "alerts", label: "Alerts", icon: "bi-exclamation-triangle" }
        ],
        onNotificationClick: function(n) { console.log("Open:", n.id); }
    });

    // Push a new notification
    nc.addNotification({ id: "2", title: "Build complete", timestamp: new Date() });
</script>

API

createNotificationCenter(options): NotificationCenterHandle

NotificationCenterOptions

OptionTypeDefaultDescription
containerHTMLElementrequiredMount target for bell trigger
notificationsNotificationItem[][]Initial notifications
categoriesNotificationCategory[][]Filter tabs (auto-prepends "All")
panelWidthnumber380Panel width in px
size"sm" | "md" | "lg""md"Size variant
cssClassstringExtra CSS class(es) on bell
onNotificationClick(n) => voidClick callback for notification items
onDismiss(id) => voidCallback when item is dismissed
onUnreadCountChange(count) => voidCallback when unread count changes
onMarkAllRead() => voidCallback when "Mark all read" is clicked
emptyMessagestring"No notifications"Empty state text

NotificationItem

PropertyTypeDescription
idstringUnique identifier (required)
titlestringNotification title (required)
messagestringDetail message text
categorystringCategory key for filtering
iconstringBootstrap Icons class
avatarUrlstringAvatar image URL (alternative to icon)
timestampstring | DateISO string or Date (required)
readbooleanRead state. Default: false
dataunknownArbitrary payload

NotificationCategory

PropertyTypeDescription
idstringUnique key matching NotificationItem.category
labelstringDisplay label
iconstringBootstrap Icons class

NotificationCenterHandle

MethodReturnsDescription
addNotification(n)voidPush a new notification (prepends to list)
removeNotification(id)voidRemove by ID
markRead(id)voidMark single notification as read
markAllRead()voidMark all notifications as read
getUnreadCount()numberCurrent unread count
setNotifications(items)voidReplace all notifications
getNotifications()NotificationItem[]Return all notifications
setCategories(cats)voidReplace category tabs
open() / close() / toggle()voidPanel visibility
getElement()HTMLElementReturn bell button element
destroy()voidTear down DOM and listeners

Keyboard

KeyAction
Enter / SpaceToggle panel from bell
EscapeClose panel
Arrow Down / Arrow UpNavigate notification items
DeleteDismiss focused notification

Accessibility


OrientationPicker

A simple dropdown picker for selecting page orientation (Portrait or Landscape). Each option displays a small SVG page icon in the corresponding orientation. Clicking an option selects it and fires the onChange callback. The trigger button shows the current selection with a chevron indicator.

Usage

<link rel="stylesheet" href="components/orientationpicker/orientationpicker.css">
<script src="components/orientationpicker/orientationpicker.js"></script>

<div id="my-orientation"></div>

<script>
var picker = createOrientationPicker({
    container: "my-orientation",
    value: "portrait",
    onChange: function(orientation) {
        console.log("Orientation:", orientation);
    }
});
</script>

Using an HTMLElement Container

<div id="toolbar-orient"></div>

<script>
var el = document.getElementById("toolbar-orient");
var picker = createOrientationPicker({
    container: el,
    value: "landscape",
    onChange: function(orientation) {
        console.log("Changed to:", orientation);
    }
});
</script>

Options

OptionTypeDefaultDescription
containerHTMLElement | stringrequiredContainer element or ID
value"portrait" | "landscape""portrait"Initial orientation
onChange(orientation) => void--Fires when selection changes
ribbonModebooleanfalseRibbon-compatible mode

API

MethodReturnsDescription
getValue()"portrait" | "landscape"Current orientation
setValue(orientation)voidSet orientation programmatically
show()voidOpen the dropdown panel
hide()voidClose the dropdown panel
destroy()voidTear down and remove from DOM
getElement()HTMLElementRoot DOM element

Keyboard

KeyContextAction
ArrowDown / Enter / SpaceTrigger buttonOpen dropdown
Enter / SpaceDropdown itemSelect item
EscapeDropdown itemClose dropdown

Visual

Each orientation displays a small SVG page icon:

The selected item shows a checkmark indicator.


PeoplePicker

Searchable person selector for share dialogs, assignment fields, and permission lists. Provides a dropdown with frequent contacts, async API lookup, and PersonChip integration. Supports single-select and multi-select modes.

Usage

HTML

<link rel="stylesheet" href="components/personchip/personchip.css">
<link rel="stylesheet" href="components/peoplepicker/peoplepicker.css">

<div id="my-picker"></div>

<script src="components/personchip/personchip.js"></script>
<script src="components/peoplepicker/peoplepicker.js"></script>

JavaScript — Multi-Select with Frequent Contacts

var picker = createPeoplePicker("my-picker", {
    frequentContacts: [
        { id: "u1", name: "Alice Chen", email: "alice@acme.com", status: "online" },
        { id: "u2", name: "Bob Smith", email: "bob@acme.com" },
        { id: "u3", name: "Carol Davis", avatarUrl: "https://i.pravatar.cc/56?u=carol" }
    ],
    onSearch: function(query) {
        return fetch("/api/people?q=" + encodeURIComponent(query))
            .then(function(r) { return r.json(); });
    },
    onChange: function(selected) {
        console.log("Selected:", selected);
    }
});

JavaScript — Single-Select (Assign To)

var assignPicker = createPeoplePicker("assign-to", {
    multiple: false,
    frequentContacts: contacts,
    onSelect: function(person) {
        console.log("Assigned to:", person.name);
    }
});

JavaScript — URL-Based Search

var urlPicker = createPeoplePicker("url-picker", {
    searchUrl: "https://api.example.com/people",
    debounceMs: 500
});

Options

OptionTypeDefaultDescription
multiplebooleantrueMulti-select or single-select mode
maxSelectionsnumber0Max selections (0 = unlimited)
selectedPersonData[][]Pre-selected people
frequentContactsPersonData[][]Shown on focus before typing
onSearch(q) => Promise<PersonData[]>Async search callback
searchUrlstringURL-based search (appends ?q=)
debounceMsnumber300Search debounce delay
minSearchCharsnumber2Min chars before searching
placeholderstring"Search people..."Input placeholder
maxChipsVisiblenumber5Visible chips before overflow
noResultsTextstring"No results found"Empty state text
size"sm" | "md" | "lg""md"Size variant
cssClassstringExtra CSS class
disabledbooleanfalseDisabled state
readonlybooleanfalseReadonly state
onSelect(person) => voidCalled on selection
onDeselect(person) => voidCalled on removal
onChange(selected) => voidCalled on any change
onOpen() => voidDropdown opened
onClose() => voidDropdown closed
onSearchError(error) => voidSearch error handler
keyBindingsRecord<string, string>Key binding overrides

PersonData

interface PersonData {
    id: string;
    name: string;
    email?: string;
    avatarUrl?: string;
    role?: string;
    status?: "online" | "offline" | "busy" | "away";
    metadata?: Record<string, string>;
}

Public API

MethodReturnsDescription
show(containerId)voidMount into container
getElement()HTMLElementRoot DOM element
getSelected()PersonData[]Current selections
setSelected(people)voidReplace all selections
addPerson(person)voidAdd one person
removePerson(id)voidRemove by ID
clearSelection()voidClear all
hasSelection(id)booleanCheck if selected
enable()voidEnable the component
disable()voidDisable the component
setReadonly(flag)voidToggle readonly
setFrequentContacts(contacts)voidUpdate frequent list
focus()voidFocus the input
destroy()voidCleanup

Keyboard Navigation

KeyDropdown ClosedDropdown Open
ArrowDownOpen + highlight firstMove down
ArrowUpMove up
EnterSelect highlighted
EscapeClear inputClose dropdown
BackspaceRemove last chipRemove last chip
TabNormal focusClose + tab
HomeJump to first
EndJump to last

Dropdown Positioning

The dropdown is appended to document.body with position: fixed to avoid containing-block traps from ancestor transform, will-change, or filter properties. This means the dropdown is not a child of the .peoplepicker root element in the DOM. The aria-owns attribute on the combobox root links the dropdown for assistive technology.

This architecture ensures the dropdown renders correctly when the PeoplePicker is placed inside FormDialog, modals, or any container with CSS transforms or overflow: hidden.

Accessibility

PersonChip Integration

PeoplePicker uses PersonChip for rich person display in dropdown rows (md size) and selected chips (sm size). Load personchip.js before peoplepicker.js. If PersonChip is not loaded, falls back to simple span elements with initials.


PeriodPicker

Coarse time-period selector for enterprise project planning. Select months, quarters, halves, or years (e.g., "Q1 2026", "H2 2028") with automatic date resolution based on start/end mode.

Usage

CSS

<link rel="stylesheet" href="components/periodpicker/periodpicker.css">

JavaScript

<script src="components/periodpicker/periodpicker.js"></script>

Basic

<div id="my-period-picker"></div>

<script>
const picker = createPeriodPicker("my-period-picker", {
    mode: "start",
    onSelect: function(value) {
        console.log("Selected:", value.period, value.year, value.date);
    }
});
</script>

End Mode (Quarters Only)

<div id="quarter-picker"></div>

<script>
const picker = createPeriodPicker("quarter-picker", {
    mode: "end",
    granularities: ["quarter", "year"],
    onSelect: function(value) {
        console.log("End date:", value.date);
    }
});
</script>

Options

OptionTypeDefaultDescription
mode"start" | "end""start"Resolve to first or last day of period
granularitiesPeriodGranularity[]["month","quarter","half","year"]Which period types to show
valuePeriodValue | nullnullInitial selected value
minYearnumbercurrentYear - 10Earliest navigable year
maxYearnumbercurrentYear + 10Latest navigable year
size"sm" | "md" | "lg""md"Size variant
disabledbooleanfalseDisable the component
readonlybooleanfalseRead-only mode
placeholderstring"Select period…"Input placeholder
onSelect(value: PeriodValue) => voidFires on selection
onChange(value: PeriodValue | null) => voidFires on value change
onOpen() => voidFires when dropdown opens
onClose() => voidFires when dropdown closes
keyBindingsRecord<string, string>Override default key combos

PeriodValue

interface PeriodValue {
    year: number;           // e.g., 2026
    period: string;         // "Jan", "Q1", "H1", "2026"
    type: PeriodGranularity; // "month" | "quarter" | "half" | "year"
    date: Date;             // Resolved date based on mode
    monthIndex?: number;    // 0-11 for months
}

Date Resolution

SelectionStart ModeEnd Mode
Jan 20262026-01-012026-01-31
Q1 20262026-01-012026-03-31
H2 20282028-07-012028-12-31
20262026-01-012026-12-31

Public API

MethodReturnsDescription
getValue()PeriodValue | nullGet current selection
setValue(value)voidSet selection programmatically
getFormattedValue()stringGet display string (e.g., "Q1 2026")
open()voidOpen dropdown
close()voidClose dropdown
enable()voidEnable component
disable()voidDisable component
setReadonly(flag)voidToggle read-only
setMode(mode)voidSwitch start/end mode
setYear(year)voidNavigate to year
destroy()voidClean up DOM and listeners

Keyboard

KeyAction
ArrowDownOpen dropdown / Move focus down
ArrowUpMove focus up
ArrowLeftMove focus left
ArrowRightMove focus right
Enter / SpaceSelect focused period
EscapeClose dropdown
PageUpPrevious year
PageDownNext year

Dropdown Positioning

The dropdown is portaled to document.body with position: fixed and z-index: 2050, ensuring it renders above FormDialog overlays (z-index 2001).


PermissionMatrix

Interactive RBAC permission matrix with roles as columns and grouped permissions as rows. Features tri-state checkboxes, inheritance resolution, bulk operations, search/filter, change tracking, sticky headers, and JSON export.

Usage

<link rel="stylesheet" href="components/permissionmatrix/permissionmatrix.css">
<script src="components/permissionmatrix/permissionmatrix.js"></script>
const matrix = createPermissionMatrix({
    roles: [
        { id: "admin", name: "Admin", description: "Full access" },
        { id: "editor", name: "Editor", inheritsFrom: "viewer" },
        { id: "viewer", name: "Viewer" }
    ],
    groups: [
        {
            id: "content",
            name: "Content",
            permissions: [
                { id: "content.read", name: "Read" },
                { id: "content.write", name: "Write" },
                { id: "content.delete", name: "Delete" }
            ]
        }
    ],
    cells: [
        { roleId: "admin", permissionId: "content.read", state: "granted" },
        { roleId: "admin", permissionId: "content.write", state: "granted" },
        { roleId: "viewer", permissionId: "content.read", state: "granted" }
    ],
    onChange: (change) => console.log("Changed:", change)
}, "my-container");

Options

OptionTypeDefaultDescription
rolesRole[][]Role columns
groupsPermissionGroup[][]Permission groups with nested permissions
cellsMatrixCell[][]Initial state for each role-permission pair
readOnlybooleanfalseDisable all editing
showFilterbooleantrueShow search filter input
filterDebounceMsnumber250Filter debounce delay (ms)
showExportbooleantrueShow export button
showResetbooleantrueShow reset button
showChangeCounterbooleantrueShow pending changes counter
showInheritancebooleantrueResolve and display inherited permissions
showChangeHighlightbooleantrueHighlight cells with pending changes
enableGroupBulkbooleantrueEnable bulk grant/deny per group row
heightstring"400px"Container height

API

MethodDescription
show(containerId)Mount to container
hide()Remove from DOM
destroy()Full cleanup
setRoles(roles)Replace role columns
setGroups(groups)Replace permission groups
setCells(cells)Replace cell states
getState()Get current state map
getPendingChanges()Get array of pending changes
resetChanges()Revert all pending changes
exportData(format)Export matrix data as JSON
refresh()Re-render the grid

Permission States

StateIconColourDescription
grantedCheck circleGreenExplicitly granted
deniedX circleGrayExplicitly denied
inheritedArrow down-rightBlueInherited from parent role
noneDashed boxGrayNo explicit state

Keyboard

KeyAction
Arrow keysNavigate between cells
Space / EnterToggle cell state
Tab / Shift+TabMove focus between controls

PersonChip

A compact inline element displaying a person's identity: circular avatar (image or deterministic initials), name, optional status dot, and email/role detail at large size. Visual style matches the UserMenu trigger — transparent background at rest, subtle gray on hover, zero border-radius.

Designed for share dialogs, assignment fields, permission lists, and future PeoplePicker dropdown rows.

Assets

AssetPath
CSScomponents/personchip/personchip.css
JScomponents/personchip/personchip.js
Typescomponents/personchip/personchip.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/personchip/personchip.css">
<script src="components/personchip/personchip.js"></script>
<script>
    var chip = createPersonChip({
        name: "Alice Chen",
        email: "alice@acme.com",
        status: "online"
    });
    document.getElementById("container").appendChild(chip.getElement());
</script>

API

Global Functions

FunctionReturnsDescription
createPersonChip(options)PersonChipCreate a PersonChip instance

PersonChip Class

MethodReturnsDescription
getElement()HTMLElementGet the root DOM element
setName(name)voidUpdate name, initials, and tooltip
setEmail(email)voidUpdate tooltip and lg detail
setAvatarUrl(url)voidSwap avatar image or fall back to initials
setStatus(status)voidChange or remove status dot
setRole(role)voidUpdate tooltip and lg detail
destroy()voidRemove listeners, DOM, null refs

PersonChipOptions

PropertyTypeRequiredDefaultDescription
namestringYesDisplay name
emailstringNoShown in tooltip (+ lg detail)
avatarUrlstringNoImage URL for avatar
rolestringNoRole/title in tooltip (+ lg detail)
status"online"|"offline"|"busy"|"away"NoStatus dot indicator
size"sm"|"md"|"lg"No"md"Size variant
clickablebooleanNofalseEnable pointer + click/keyboard
hrefstringNoRender as <a> tag
tooltipstringNoautoOverride auto-generated tooltip
avatarOnlybooleanNofalseRender only avatar circle + status dot
cssClassstringNoAdditional CSS class
metadataRecord<string,string>Nodata-* attributes for integration
onClick(chip) => voidNoClick callback
onHover(chip, event) => voidNoMouseenter callback
onHoverOut(chip, event) => voidNoMouseleave callback

Size Variants

SizeAvatarFontDetail Shown
sm20px$font-size-smNo (tooltip only)
md28px0.875remNo (tooltip only)
lg36px0.875remYes (email/role inline)

Status Indicators

StatusColour
onlineGreen ($green-600)
busyRed ($red-600)
awayYellow ($yellow-500)
offlineGray ($gray-400)

Accessibility

Keyboard

KeyAction
EnterActivate clickable chip
SpaceActivate clickable chip
TabMove focus to/from chip

Examples

Basic with initials

var chip = createPersonChip({ name: "Bob Smith" });
container.appendChild(chip.getElement());

With avatar image and status

var chip = createPersonChip({
    name: "Alice Chen",
    email: "alice@acme.com",
    avatarUrl: "https://example.com/alice.jpg",
    status: "online",
    size: "lg"
});
container.appendChild(chip.getElement());

Clickable with callback

var chip = createPersonChip({
    name: "Carol Davis",
    clickable: true,
    onClick: function(c) { console.log("Clicked:", c.getElement().title); }
});
container.appendChild(chip.getElement());

Share dialog composition

var people = ["Alice Chen", "Bob Smith", "Carol Davis"];
var row = document.createElement("div");
row.style.display = "flex";
row.style.flexWrap = "wrap";
row.style.gap = "4px";

people.forEach(function(name) {
    var chip = createPersonChip({ name: name, status: "online" });
    row.appendChild(chip.getElement());
});
container.appendChild(row);

ADR

ADR-036: PersonChip replicates UserMenu trigger style as standalone reusable element.


Pill

A reusable inline pill element for mentions, issues, documents, tags, and other entity references. Designed to flow naturally within text content, following the rounded-end style of Google Docs and GitHub.

Note: The rounded border-radius (9999px) is an explicit exception to the theme's zero-radius rule (see ADR-034).

Assets

AssetPath
CSScomponents/pill/pill.css
JScomponents/pill/pill.js
Typescomponents/pill/pill.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/pill/pill.css">
<script src="components/pill/pill.js"></script>
<script>
    var pill = createPill({ label: "@Alice Chen", icon: "bi bi-person" });
    document.getElementById("container").appendChild(pill.getElement());

    var dismissible = createPill({
        label: "PROJ-101",
        color: "green",
        dismissible: true,
        onDismiss: function(p) { console.log("Dismissed"); }
    });
    document.getElementById("container").appendChild(dismissible.getElement());
</script>

API

Global Functions

FunctionReturnsDescription
createPill(options)PillCreate a pill instance

Pill Class

MethodReturnsDescription
getElement()HTMLElementGet the root DOM element
setLabel(text)voidUpdate label text and aria-label
setColor(color)voidSwap colour preset class
setStyle(overrides)voidApply inline style overrides via CSS custom properties
destroy()voidRemove listeners, remove from DOM, null refs

PillOptions

OptionTypeDefaultDescription
labelstringrequiredDisplay text
iconstringBootstrap icon class, e.g. "bi bi-person"
colorPillColor"blue"Preset: blue, gray, green, red, purple, orange
backgroundstringCSS override (takes precedence over preset)
foregroundstringCSS text colour override
borderColorstringCSS border-color override
borderRadiusstringCSS override, default 9999px
sizestring"md""sm", "md", "lg"
dismissiblebooleanfalseShow close button
clickablebooleanfalsePointer cursor + click/keyboard activation
hrefstringRender as <a> instead of <span>
tooltipstringNative title attribute
cssClassstringAdditional CSS class(es)
metadataRecord<string, string>data-* attributes
onClick(pill) => voidClick/keyboard activation callback
onDismiss(pill) => voidDismiss button callback
onHover(pill, event) => voidmouseenter callback
onHoverOut(pill, event) => voidmouseleave callback

PillStyleOverrides

PropertyTypeDescription
backgroundstringSets --pill-bg
foregroundstringSets --pill-fg
borderColorstringSets --pill-border-color
borderRadiusstringSets --pill-border-radius

Colour Presets

PresetBackgroundText
blue (default)rgba($blue-500, 0.10)$blue-700
gray$gray-100$gray-700
greenrgba($green-600, 0.10)$green-800
redrgba($red-600, 0.10)$red-800
purplergba(139, 92, 246, 0.10)rgb(91, 33, 182)
orangergba($orange-500, 0.10)#c2410c

Size Variants

SizePaddingFont Size
sm1px 6px$font-size-sm * 0.9
md2px 8px$font-size-sm
lg3px 10px$font-size-base

CSS Custom Properties

All visual properties use CSS custom properties for runtime override:

.pill {
    --pill-bg: ...;
    --pill-fg: ...;
    --pill-border-color: ...;
    --pill-border-radius: ...;
    --pill-padding: ...;
    --pill-font-size: ...;
    --pill-max-width: ...;
}

Override via JavaScript:

pill.setStyle({ background: "pink", borderRadius: "4px" });

Or via inline style:

pill.getElement().style.setProperty("--pill-max-width", "300px");

STIE Integration

Pill is designed to complement the Smart Text Input Engine (STIE). Apps wanting full Pill features in STIE tokens use both class sets:

className: "stie-token-pill pill pill-blue"

When Pill CSS is loaded alongside STIE CSS, the Pill's custom properties layer on top.

Accessibility

DOM Structure

<span class="pill pill-blue pill-md" role="status" aria-label="Alice Chen">
    <i class="bi bi-person pill-icon" aria-hidden="true"></i>
    <span class="pill-label">@Alice Chen</span>
    <button class="pill-dismiss" aria-label="Remove" tabindex="-1">
        <i class="bi bi-x" aria-hidden="true"></i>
    </button>
</span>

PresenceIndicator

A compact overlapping avatar stack showing who is actively viewing or editing a shared resource. Collapsed state shows overlapping circles with white ring borders and a "+N" overflow badge. Click to expand and reveal full PersonChip instances with names.

Designed for document headers, toolbar corners, and collaborative editing UIs similar to Google Docs' presence avatars.

Assets

AssetPath
CSScomponents/presenceindicator/presenceindicator.css
JScomponents/presenceindicator/presenceindicator.js
Typescomponents/presenceindicator/presenceindicator.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/personchip/personchip.css">
<link rel="stylesheet" href="components/presenceindicator/presenceindicator.css">
<script src="components/personchip/personchip.js"></script>
<script src="components/presenceindicator/presenceindicator.js"></script>
<script>
    var indicator = createPresenceIndicator("container-id", {
        people: [
            { id: "u1", name: "Alice Chen", status: "online" },
            { id: "u2", name: "Bob Smith", status: "busy" },
            { id: "u3", name: "Carol Davis", status: "away" }
        ]
    });
</script>

API

Global Functions

FunctionReturnsDescription
createPresenceIndicator(containerId, options)PresenceIndicatorCreate and optionally mount an instance

PresenceIndicator Class

MethodReturnsDescription
show(containerId)voidMount into container
getElement()HTMLElementRoot DOM element
destroy()voidCleanup chips, listeners, DOM
setPeople(people)voidReplace all people
addPerson(person)voidAdd one person
removePerson(id)voidRemove by ID
getPeople()PersonData[]Current list
expand()voidSwitch to expanded view
collapse()voidSwitch to collapsed view
toggle()voidToggle expanded/collapsed
isExpanded()booleanState query

PresenceIndicatorOptions

PropertyTypeRequiredDefaultDescription
peoplePersonData[]No[]Initial people list
maxVisiblenumberNo4Max avatars before overflow badge
size"sm"|"md"|"lg"No"md"Size variant
expandablebooleanNotrueAllow expand/collapse
expandedbooleanNofalseInitial expanded state
cssClassstringNoAdditional CSS class
disabledbooleanNofalseDisable interaction
onClick(person) => voidNoPerson click callback (expanded)
onExpand() => voidNoExpand callback
onCollapse() => voidNoCollapse callback

PersonData

PropertyTypeRequiredDescription
idstringYesUnique identifier
namestringYesDisplay name
emailstringNoEmail address
avatarUrlstringNoImage URL for avatar
rolestringNoRole or title
status"online"|"offline"|"busy"|"away"NoStatus dot indicator
metadataRecord<string,string>NoData attributes

Size Variants

SizeAvatar DiameterOverlapOverflow Badge
sm24px-8px24px
md32px-10px32px
lg40px-12px40px

Accessibility

Keyboard

KeyAction
EnterToggle expand/collapse
SpaceToggle expand/collapse
EscapeCollapse if expanded

Examples

Basic collapsed

var indicator = createPresenceIndicator("my-container", {
    people: [
        { id: "1", name: "Alice Chen", status: "online" },
        { id: "2", name: "Bob Smith" },
        { id: "3", name: "Carol Davis", status: "away" }
    ]
});

With overflow

createPresenceIndicator("container", {
    people: sevenPeople,
    maxVisible: 4   // Shows 4 avatars + "+3" badge
});

Programmatic control

var pi = createPresenceIndicator(null, { people: initialPeople });
document.getElementById("target").appendChild(pi.getElement());

pi.addPerson({ id: "new", name: "Dave Wilson", status: "online" });
pi.expand();
pi.removePerson("new");

ADR

ADR-038: PresenceIndicator composes PersonChip avatarOnly mode with runtime bridge pattern.


ProgressModal

A modal dialog for displaying progress of long-running operations.

Quick Start

<link rel="stylesheet" href="https://theme.priyavijai-kalyan2007.workers.dev/components/progressmodal/progressmodal.css">
<script src="https://theme.priyavijai-kalyan2007.workers.dev/components/progressmodal/progressmodal.js"></script>

<script>
    // Indeterminate spinner
    var modal = showProgressModal({ title: "Processing..." });
    modal.logInfo("Starting operation...");
    modal.setStatus("Connecting...");
    // ... later
    modal.logSuccess("Done!");
    modal.complete("Operation finished.");

    // Stepped progress
    var stepped = showSteppedProgressModal("Uploading", 5);
    stepped.setStep(1);
    stepped.logInfo("Uploading file 1...");
    // ... later
    stepped.setStep(5);
    stepped.complete();
</script>

Configuration Options

OptionTypeDefaultDescription
titlestringRequiredModal title
mode"indeterminate" | "determinate""indeterminate"Progress mode
statusTextstringInitial status message
totalStepsnumberTotal steps (enables step counter)
showTimestampsbooleantrueTimestamps in log
showDetailLogbooleantrueShow scrollable log
showCopyLogbooleantrueCopy log button
autoClosenumber0Auto-close delay (ms) on success
allowBackdropClosebooleanfalseBackdrop dismissible when done
widebooleanfalseWide layout
onCancel() => voidCancel callback (shows button)
onRetry() => voidRetry callback (shows on error)
onClose() => voidClose callback

Instance Methods

MethodDescription
show()Show the modal
setStatus(text)Update status text
setProgress(0.0–1.0)Set progress (switches to determinate)
setStep(n)Set current step (calculates %)
setIndeterminate()Switch to spinner mode
log(entry)Add log entry
logInfo(msg)Add info entry
logSuccess(msg)Add success entry
logError(msg)Add error entry
logWarning(msg)Add warning entry
complete(msg?)Mark as complete
fail(msg?)Mark as failed
close()Close (after complete/fail)
getLog()Get all entries
getLogText()Get log as text
getState()"running" / "completed" / "failed" / "closed"
isVisible()Check visibility
destroy()Remove from DOM

Log Entry Levels

LevelIconColour
infobi-info-circleDefault
successbi-check-circle-fillGreen
errorbi-x-circle-fillRed
warningbi-exclamation-triangle-fillYellow
progressMini spinnerPrimary

Dependencies


PromptTemplateManager

Two-pane CRUD interface for creating, editing, organising, and testing prompt templates with {{variable}} extraction, preview, tags, categories, and import/export.

Assets

FilePurpose
prompttemplatemanager.tsTypeScript source
prompttemplatemanager.scssComponent styles
prompttemplatemanager.jsCompiled JS (IIFE-wrapped)
prompttemplatemanager.cssCompiled CSS

Usage

<link rel="stylesheet" href="components/prompttemplatemanager/prompttemplatemanager.css">
<script src="components/splitlayout/splitlayout.js"></script>
<script src="components/prompttemplatemanager/prompttemplatemanager.js"></script>

Basic

var manager = createPromptTemplateManager({
    templates: [
        {
            id: "tpl-1",
            name: "Customer Support",
            content: "You are a support agent for {{company}}. Help {{customer}} with: {{issue}}",
            category: "Support",
            tags: ["support", "customer"],
            version: 1
        }
    ],
    categories: ["Support", "Sales", "Engineering"],
    onSave: function(tpl) {
        console.log("Saving:", tpl);
        return Promise.resolve(tpl);
    },
    onDelete: function(id) {
        console.log("Deleting:", id);
        return Promise.resolve(true);
    }
}, "my-container");

Read-Only Mode

var manager = createPromptTemplateManager({
    templates: myTemplates,
    readOnly: true
}, "container");

Interfaces

PromptVariable

interface PromptVariable {
    name: string;
    defaultValue?: string;
    description?: string;
    type?: "text" | "number" | "select" | "textarea";
    options?: string[];
    required?: boolean;
}

PromptTemplate

interface PromptTemplate {
    id: string;
    name: string;
    description?: string;
    content: string;
    category?: string;
    tags?: string[];
    variables?: PromptVariable[];
    version?: number;
    createdAt?: string;
    updatedAt?: string;
    author?: string;
    metadata?: Record<string, unknown>;
}

PromptTemplateManagerOptions

interface PromptTemplateManagerOptions {
    templates?: PromptTemplate[];
    categories?: string[];
    showPreview?: boolean;           // Default: true
    showVariableEditor?: boolean;    // Default: true
    showImportExport?: boolean;      // Default: true
    showSearch?: boolean;            // Default: true
    editorHeight?: string;           // Default: "300px"
    listWidth?: number;              // Default: 300
    height?: string;                 // Default: "600px"
    readOnly?: boolean;              // Default: false
    cssClass?: string;
    onSave?: (template: PromptTemplate) => Promise<PromptTemplate>;
    onDelete?: (templateId: string) => Promise<boolean>;
    onDuplicate?: (template: PromptTemplate) => Promise<PromptTemplate>;
    onPreview?: (content: string, variables: Record<string, string>) => Promise<string>;
    onLoadTemplates?: () => Promise<PromptTemplate[]>;
    onSelect?: (template: PromptTemplate) => void;
    onChange?: (template: PromptTemplate) => void;
}

API

MethodDescription
show(containerId)Mount into a container element
hide()Remove from DOM, preserve state
destroy()Full cleanup
getElement()Returns the root DOM element
getTemplates()Returns a copy of all templates
setTemplates(templates)Replace all templates
getSelectedTemplate()Returns the selected template or null
selectTemplate(id)Select a template by ID
createTemplate()Create and select a new blank template
deleteTemplate(id)Delete a template by ID
duplicateTemplate(id)Duplicate a template by ID
exportTemplates()Export all templates as JSON download
importTemplates(json)Import templates from JSON string
refresh()Reload via onLoadTemplates callback
getPreviewContent(variables?)Get substituted preview text
createPromptTemplateManager(opts, id)Create, show, and return instance

Keyboard

KeyAction
Ctrl+SSave current template
Ctrl+NCreate new template
Ctrl+PToggle preview
Enter / CommaAdd tag (in tags input)
BackspaceRemove last tag (when tags input empty)
EscapeClear search / return focus to list

Variable Extraction

Templates use {{variableName}} syntax. Variables are extracted automatically from content using /\{\{(\w+)\}\}/g. Detected variables appear as editable fields below the prompt editor.

Dependencies


Property Inspector (Slide-out Drawer)

Non-modal right-side panel for viewing and editing entity details without navigating away from the parent list. Supports tabbed sections, resize handle, header actions, and footer.

Assets

AssetPath
CSScomponents/propertyinspector/propertyinspector.css
JScomponents/propertyinspector/propertyinspector.js
Typescomponents/propertyinspector/propertyinspector.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/propertyinspector/propertyinspector.css">
<script src="components/propertyinspector/propertyinspector.js"></script>
<script>
    var inspector = createPropertyInspector({
        container: document.getElementById("main-content"),
        onClose: function() { console.log("Closed"); },
        onAction: function(id) { console.log("Action:", id); }
    });

    // Open with content
    var content = document.createElement("div");
    content.textContent = "Entity details here...";

    inspector.open({
        title: "Task #1234",
        subtitle: "High Priority",
        icon: "bi-clipboard-check",
        content: content,
        actions: [{ id: "edit", label: "Edit", icon: "bi-pencil" }]
    });
</script>

API

createPropertyInspector(options): PropertyInspectorHandle

PropertyInspectorOptions

OptionTypeDefaultDescription
containerHTMLElementrequiredParent element to scope drawer within
widthnumber380Drawer width in px
resizablebooleantrueAllow drag-to-resize
showBackdropbooleanfalseShow overlay behind drawer
size"sm" | "md" | "lg""md"Size variant
cssClassstringExtra CSS class(es)
onClose() => voidCalled when drawer closes
onAction(actionId, data) => voidHeader action callback
onTabChange(tabId) => voidTab change callback
onResize(width) => voidResize callback

InspectorOpenOptions

PropertyTypeDescription
titlestringHeader title (required)
subtitlestringHeader subtitle
iconstringHeader Bootstrap Icons class
actionsInspectorAction[]Header action buttons
contentHTMLElementBody content (if no tabs)
tabsInspectorTab[]Tabbed sections (overrides content)
activeTabstringInitial active tab ID
footerHTMLElementFooter element
dataunknownPayload for action callbacks

PropertyInspectorHandle

MethodReturnsDescription
open(options)voidShow drawer with content
close()voidHide drawer
isOpen()booleanCheck visibility
setTitle(title, subtitle?)voidUpdate header text
setContent(el)voidReplace body content
setTabs(tabs)voidReplace tabs
setActiveTab(id)voidSwitch active tab
setFooter(el)voidReplace footer content
getElement()HTMLElementRoot drawer element
destroy()voidTear down DOM and listeners

Keyboard

KeyAction
EscapeClose the drawer
TabNavigate within drawer content

Accessibility


ReasoningAccordion

Collapsible accordion for displaying AI chain-of-thought reasoning steps with status indicators, shimmer animation, timing metadata, confidence bars, and Vditor markdown rendering.

Assets

FilePurpose
reasoningaccordion.tsTypeScript source
reasoningaccordion.scssComponent styles
reasoningaccordion.jsCompiled JS (IIFE-wrapped)
reasoningaccordion.cssCompiled CSS

Usage

<link rel="stylesheet" href="components/reasoningaccordion/reasoningaccordion.css">
<script src="components/reasoningaccordion/reasoningaccordion.js"></script>

Basic — Static Steps

var acc = createReasoningAccordion("container-id", {
    title: "Reasoning",
    steps: [
        { id: "s1", title: "Analyzing input", status: "complete", duration: 820, content: "Parsed query entities." },
        { id: "s2", title: "Querying sources", status: "complete", duration: 1540, content: "Retrieved 12 documents." },
        { id: "s3", title: "Generating response", status: "complete", duration: 950, content: "Synthesized final answer." }
    ]
});

Streaming — Add Steps Over Time

var acc = createReasoningAccordion("container-id", {
    title: "AI Reasoning",
    autoExpandActive: true,
    autoCollapseCompleted: true
});

// Step 1 starts thinking
acc.addStep({ id: "s1", title: "Parsing query", status: "thinking" });

// Later — step 1 completes, step 2 starts
acc.updateStep("s1", { status: "complete", duration: 800, content: "Parsed OK." });
acc.addStep({ id: "s2", title: "Searching", status: "thinking" });

Interfaces

ReasoningStep

interface ReasoningStep {
    id: string;
    title: string;
    content?: string;               // Markdown rendered via Vditor
    status: "pending" | "thinking" | "complete" | "error";
    duration?: number;               // Milliseconds
    confidence?: number;             // 0-1
    icon?: string;                   // Bootstrap icon class override
    metadata?: Record<string, unknown>;
}

ReasoningAccordionOptions

interface ReasoningAccordionOptions {
    id?: string;
    steps?: ReasoningStep[];
    title?: string;                  // Default: "Reasoning"
    expandAll?: boolean;
    showTimings?: boolean;           // Default: true
    showStepNumbers?: boolean;       // Default: true
    showConfidence?: boolean;        // Default: false
    showExpandAllButton?: boolean;   // Default: true
    autoExpandActive?: boolean;      // Default: true
    autoCollapseCompleted?: boolean; // Default: false
    size?: "sm" | "default" | "lg"; // Default: "default"
    cssClass?: string;
    onStepClick?: (step: ReasoningStep) => void;
    onExpandChange?: (stepId: string, expanded: boolean) => void;
}

API

MethodDescription
show(containerId)Mount into a container element
hide()Remove from DOM (preserves state)
destroy()Full cleanup
getElement()Returns root HTMLElement
addStep(step)Append a new step
updateStep(stepId, changes)Partial update
removeStep(stepId)Remove and renumber
getSteps()Returns copy of steps array
clear()Remove all steps
expandStep(stepId)Expand a step panel
collapseStep(stepId)Collapse a step panel
expandAll()Expand all steps
collapseAll()Collapse all steps
setStepStatus(stepId, status)Update step status
setStepContent(stepId, markdown)Set/replace step markdown content
getTotalDuration()Sum of all step durations (ms)

Keyboard

KeyAction
Enter / SpaceToggle expand/collapse on focused step
Arrow DownFocus next step header
Arrow UpFocus previous step header
HomeFocus first step header
EndFocus last step header

Status States

StatusIconColourAnimation
pendingbi-circleGrayNone
thinkingbi-arrow-repeatBlueSpin + shimmer header
completebi-check-circle-fillGreenNone
errorbi-exclamation-triangle-fillRedNone

Size Variants

Dependencies


RelationshipManager

Component for creating, viewing, and managing typed relationships between entities. Embeddable in any resource detail view as a "Relationships" section.

Usage

<link rel="stylesheet" href="components/relationshipmanager/relationshipmanager.css" />
<script src="components/relationshipmanager/relationshipmanager.js"></script>
<!-- Optional: TypeBadge for rich type display -->
<script src="components/typebadge/typebadge.js"></script>
const rm = createRelationshipManager({
    container: document.getElementById("relationships"),
    resourceId: "proj-123",
    resourceType: "work.project",
    resourceDisplayName: "Payment Redesign",
    relationshipDefinitions: [
        {
            key: "aligns_to",
            displayName: "aligns to",
            inverseName: "aligned by",
            targetTypeKeys: ["strategy.okr", "strategy.goal"],
            sourceTypeKeys: null,
            cardinality: "MANY_TO_MANY"
        }
    ],
    relationships: [
        {
            id: "rel-1",
            relationshipKey: "aligns_to",
            relationshipDisplayName: "aligns to",
            direction: "outbound",
            otherResourceId: "okr-456",
            otherResourceDisplayName: "Q2 Revenue Growth OKR",
            otherResourceType: "strategy.okr",
            properties: {},
            provenance: "manual",
            createdAt: "2026-02-15"
        }
    ],
    onCreateRelationship: async (req) => { /* POST to API */ },
    onDeleteRelationship: async (id) => { /* DELETE from API */ },
    onSearchResources: async (query, types) => { /* GET search results */ },
    onNavigate: (resourceId) => { /* Navigate to resource */ }
});

Features

Options

OptionTypeDefaultDescription
containerHTMLElementMount point
resourceIdstringCurrent resource ID
resourceTypestringCurrent resource type key
resourceDisplayNamestringDisplay name
relationshipDefinitionsRelationshipDefinitionSummary[]Available relationship types
relationshipsRelationshipInstance[]Current relationships
readOnlybooleanfalseDisable add/delete
showAddButtonbooleaninferredShow the "+Add" button (see below)
allowedRelationshipKeysstring[]allRestrict creatable types
showProvenancebooleantrueShow provenance badges
showConfidencebooleantrueShow AI confidence %
groupByTypebooleantrueGroup by relationship type

Add Button Visibility

By default, the "+Add" button is shown only when onCreateRelationship is provided and readOnly is false. Use the explicit showAddButton option to override inference:

show = showAddButton ?? (typeof onCreateRelationship === 'function')
effective = !readOnly && show
ScenarioResult
onCreateRelationship provided, no overrideButton visible
No onCreateRelationship, no overrideButton hidden
showAddButton: false with callbackButton hidden
showAddButton: true without callbackButton visible (openAddPanel warns)
readOnly: trueAlways hidden regardless

Public API

Global

window.createRelationshipManager(options)

Ribbon

A Microsoft Office-style tabbed toolbar for organizing commands and controls into logical groups. Each tab reveals a panel of grouped controls arranged in rows, supporting large, small, and mini button sizes. Includes an optional traditional menu bar, Quick Access Toolbar (QAT), contextual tabs, backstage panel, adaptive group collapsing, multi-level KeyTips, and inline gallery controls.

Features

Assets

AssetPath
CSScomponents/ribbon/ribbon.css
JScomponents/ribbon/ribbon.js
Typescomponents/ribbon/ribbon.d.ts

Requires: Bootstrap CSS (for SCSS variables), Bootstrap Icons CSS. Does not require a JavaScript framework.

Quick Start

<link rel="stylesheet" href="components/ribbon/ribbon.css">
<script src="components/ribbon/ribbon.js"></script>
<script>
    var ribbon = createRibbon({
        tabs: [
            {
                id: "home",
                label: "Home",
                keyTip: "H",
                groups: [
                    {
                        id: "clipboard",
                        label: "Clipboard",
                        collapsePriority: 30,
                        controls: [
                            { type: "button", id: "paste", label: "Paste", icon: "bi bi-clipboard", size: "large", keyTip: "V" },
                            { type: "button", id: "cut", label: "Cut", icon: "bi bi-scissors", size: "small", keyTip: "X" },
                            { type: "button", id: "copy", label: "Copy", icon: "bi bi-files", size: "small", keyTip: "C" }
                        ]
                    },
                    {
                        id: "font",
                        label: "Font",
                        collapsePriority: 50,
                        controls: [
                            { type: "dropdown", id: "font-family", options: [{ value: "Arial", label: "Arial" }, { value: "Times New Roman", label: "Times New Roman" }], value: "Arial", width: "120px" },
                            { type: "dropdown", id: "font-size", options: [{ value: "11", label: "11" }, { value: "12", label: "12" }, { value: "14", label: "14" }], value: "11", width: "55px" },
                            { type: "button", id: "bold", label: "Bold", icon: "bi bi-type-bold", size: "mini", toggle: true, keyTip: "B" },
                            { type: "button", id: "italic", label: "Italic", icon: "bi bi-type-italic", size: "mini", toggle: true, keyTip: "I" }
                        ]
                    }
                ]
            }
        ],
        qat: [
            { id: "save", icon: "bi bi-floppy", tooltip: "Save", keyTip: "1" },
            { id: "undo", icon: "bi bi-arrow-counterclockwise", tooltip: "Undo", keyTip: "2" },
            { id: "redo", icon: "bi bi-arrow-clockwise", tooltip: "Redo", keyTip: "3" }
        ]
    }, "ribbon-container");
</script>

API

Factory

createRibbon(options: RibbonOptions, containerId?: string): Ribbon

Creates a Ribbon instance. If containerId is provided, the ribbon is shown immediately.

RibbonOptions

PropertyTypeDefaultDescription
tabsRibbonTab[]requiredArray of tab definitions
activeTabIdstringFirst tabInitially active tab ID
menuBarRibbonMenuBarItem[]Optional menu bar items
qatRibbonQATItem[]Quick Access Toolbar items
qatPosition"above" | "below""above"QAT position relative to tab bar
panelHeightnumber86Panel height in pixels
groupOverflow"visible" | "hidden""visible"Overflow for hosted components
collapsiblebooleantrueAllow ribbon collapse
collapsedbooleanfalseStart collapsed
adaptivebooleantrueEnable adaptive group collapse
keyTipsbooleantrueEnable multi-level KeyTips
onTabChange(tabId) => voidTab switch callback
onCollapse(collapsed) => voidCollapse/expand callback
onQATCustomize(items) => voidQAT customize callback
onControlClick(controlId) => voidAny control click callback
statusBarHTMLElement | () => HTMLElementRight-aligned status area in the tab bar (user info, entity name, version badge, etc.)
autoCollapseDelaynumber0Auto-collapse delay in ms after temp-expanding (0 = disabled, min 5000)
cssClassstringAdditional CSS class on root
keyBindingsRecord<string, string>Custom key bindings
Colour optionsstringSee Colour Configuration below

RibbonTab

PropertyTypeDescription
idstringUnique tab identifier
labelstringTab display label
groupsRibbonGroup[]Groups of controls in this tab
contextualbooleanHidden by default, shown via API
accentColorstringContextual tab accent colour
backstagebooleanOpens backstage instead of panel
backstageContentHTMLElement | () => HTMLElementBackstage main content
backstageSidebarRibbonBackstageItem[]Backstage sidebar items
keyTipstringKeyTip letter for this tab

RibbonGroup

PropertyTypeDescription
idstringUnique group identifier
labelstringGroup label (shown below controls)
controlsRibbonControl[]Array of controls
collapsePrioritynumberLower collapses first (default 50)
collapseStagesRibbonCollapseStage[]Custom collapse stages

Control Types

button

PropertyTypeDescription
type"button"Control type
idstringUnique ID
labelstringButton label
iconstringIcon CSS classes — Bootstrap Icons ("bi bi-type-bold") or Font Awesome ("fa-solid fa-home"). Any space-separated classes work.
size"large" | "small" | "mini"Button size
togglebooleanEnable toggle behaviour
activebooleanInitial toggle state
tooltipstringTooltip text
keyTipstringKeyTip letter
disabledbooleanDisable the button
onClick(btn, active) => voidClick callback

split-button

Same properties as button plus:

PropertyTypeDescription
type"split-button"Control type
menuItemsRibbonSplitMenuItem[]Dropdown menu items

gallery

PropertyTypeDescription
type"gallery"Control type
optionsRibbonGalleryOption[]Gallery items
selectedIdstringCurrently selected option ID
columnsnumberGrid columns in popup (default 4)
layout"grid" | "list"Popup layout
inlineCountnumberItems shown inline (default 3)
onSelect(option) => voidSelection callback

dropdown

PropertyTypeDescription
type"dropdown"Control type
options{ value, label }[]Dropdown options
valuestringCurrent value
widthstringCSS width
onChange(value) => voidChange callback

input

PropertyTypeDescription
type"input"Control type
placeholderstringPlaceholder text
valuestringCurrent value
widthstringCSS width
onInput(value) => voidInput callback
onSubmit(value) => voidEnter key callback

color

PropertyTypeDescription
type"color"Control type
valuestringHex colour value
showLabelbooleanShow hex value text
onChange(value) => voidChange callback

number

PropertyTypeDescription
type"number"Control type
valuenumberCurrent value
minnumberMinimum value
maxnumberMaximum value
stepnumberStep increment
suffixstringUnit suffix text
widthstringCSS width
onChange(value) => voidChange callback

checkbox / toggle

PropertyTypeDescription
type"checkbox" | "toggle"Control type
checkedbooleanCurrent state
onChange(checked) => voidChange callback

separator / row-break / label / custom

PropertyTypeDescription
type"separator"Visual divider
type"row-break"Invisible layout break — starts a new horizontal row within the group (see Row Layout below)
type"label"Non-interactive text with optional icon and colour
type"custom"Consumer-provided element: HTMLElement | () => HTMLElement
widthstringCSS width for the custom control wrapper (e.g. "120px")

Instance Methods

MethodDescription
show(containerId?)Mount and display the ribbon
hide()Hide the ribbon
destroy()Remove from DOM, clean up listeners
setActiveTab(tabId)Switch to a tab
getActiveTab()Get active tab ID
addTab(tab, index?)Add a tab dynamically
removeTab(tabId)Remove a tab
showContextualTab(tabId)Show a contextual tab
hideContextualTab(tabId)Hide a contextual tab
setControlDisabled(id, disabled)Enable/disable a control
setControlHidden(id, hidden)Show/hide a control
setControlActive(id, active)Set toggle active state
getControlValue(id)Get control value
setControlValue(id, value)Set control value
getControlState(id)Get { disabled, active, visible } or null if unknown ID
addQATItem(item)Add a QAT button
removeQATItem(id)Remove a QAT button
collapse()Minimize ribbon to tab bar
expand()Expand ribbon
toggleCollapse()Toggle collapse state
isCollapsed()Check collapsed state
openBackstage()Open backstage panel
closeBackstage()Close backstage panel
setStatusBar(element)Set, replace, or remove (null) the status bar at runtime
getStatusBarElement()Get the .ribbon-tabbar-status wrapper element (or null)
setAutoCollapseDelay(ms)Set auto-collapse delay in ms (0 to disable)
getAutoCollapseDelay()Get current auto-collapse delay
getState()Get serialisable snapshot of current ribbon UI state
restoreState(state)Restore ribbon state from a (partial) snapshot
setColors(colors)Update colours at runtime
getElement()Get root DOM element

Status Bar

The optional statusBar slot places a right-aligned area in the tab bar between the tabs and the collapse button. The Ribbon provides the container; consumers provide the content (user chip, entity name, version badge, PresenceIndicator, etc.).

var ribbon = createRibbon({
    tabs: [ /* ... */ ],
    statusBar: function() {
        var bar = document.createElement("div");
        bar.style.display = "inline-flex";
        bar.style.alignItems = "center";
        bar.style.gap = "8px";

        var user = document.createElement("span");
        user.textContent = "Jane Doe";
        bar.appendChild(user);

        var badge = document.createElement("span");
        badge.textContent = "v2.4.1";
        bar.appendChild(badge);
        return bar;
    }
}, "container");

// Update at runtime
ribbon.setStatusBar(newElement);

// Remove
ribbon.setStatusBar(null);

// Get the wrapper element
var wrapper = ribbon.getStatusBarElement();

Keyboard

KeyContextAction
AltAnywhereToggle KeyTips
EscapeKeyTips activeGo back one level or dismiss
EscapeMenu/popup openClose
EscapeBackstageClose backstage
ArrowLeft/RightTab barNavigate tabs
Enter/SpaceTab barActivate tab
ArrowDownMenu triggerOpen menu
ArrowRightSubmenu triggerOpen submenu
ArrowLeftSubmenuClose submenu
Ctrl+F1AnywhereToggle ribbon collapse

Colour Configuration

All visual properties are configurable via options and setColors():

PropertyCSS VariableDefault
backgroundColor--ribbon-bg$gray-100
tabBarBackgroundColor--ribbon-tab-bar-bg$gray-200
tabTextColor--ribbon-tab-color$gray-700
tabActiveTextColor--ribbon-tab-active-color$gray-900
tabActiveBackgroundColor--ribbon-tab-active-bg$gray-100
panelBackgroundColor--ribbon-panel-bg$gray-100
groupLabelColor--ribbon-group-label-color$gray-500
groupBorderColor--ribbon-group-border-color$gray-300
controlColor--ribbon-control-color$gray-800
controlHoverColor--ribbon-control-hover-bg$gray-200
controlActiveColor--ribbon-control-active-bg$blue-100
qatBackgroundColor--ribbon-qat-bg$gray-200
menuBarBackgroundColor--ribbon-menubar-bg$gray-200
ribbon.setColors({
    backgroundColor: "#1a1a2e",
    tabBarBackgroundColor: "#16213e",
    tabTextColor: "#a0a0c0",
    tabActiveTextColor: "#ffffff",
    controlColor: "#e0e0f0"
});

Row Layout

By default, small/mini controls stack into vertical columns of 3. Use row-break controls to arrange items into explicit horizontal rows instead — ideal for Office-style groups like Font or Paragraph.

controls: [
    { type: "row-break", id: "r1" },        // start row 1
    { type: "dropdown", id: "font-family", ... },
    { type: "dropdown", id: "font-size", ... },
    { type: "row-break", id: "r2" },        // start row 2
    { type: "button", id: "bold", size: "mini", ... },
    { type: "button", id: "italic", size: "mini", ... },
    { type: "button", id: "underline", size: "mini", ... }
]

This produces:

Row 1: [Arial ▼] [11 ▼]
Row 2: [B] [I] [U]

All rows are wrapped in a vertical .ribbon-stack container so they tile top-to-bottom within the horizontal group flow.

Stack Alignment

Stacks use CSS subgrid to align labels and controls across rows. When multiple labeled controls are stacked (e.g. three dropdowns or a mix of dropdowns, inputs, and custom controls), all labels share one column width and all controls share another — producing uniform alignment. The width property on custom controls is applied as min-width so it sets a floor without conflicting with grid column sizing. In mini collapse, labels are hidden and the stack collapses to a single column.

Adaptive Collapse

Groups progressively collapse as the ribbon narrows:

  1. Full — all controls at configured sizes
  2. Medium — large buttons become small
  3. Small — all buttons become small
  4. Mini — all buttons become icon-only
  5. Overflow — entire group collapses into a single dropdown

Groups with lower collapsePriority collapse first. A ResizeObserver triggers recalculation (debounced 150ms).

Layout Integration

The Ribbon uses position: relative (not fixed), making it embeddable in any layout container:

LayoutSlotUsage
DockLayouttoolbardock.setToolbar(ribbon.getElement())
BorderLayoutnorthborder.setNorth(ribbon.getElement())
FlexGridLayoutRow 0rows: ["auto", "1fr"]
BoxLayoutChild 0flex: 0 (natural height)
StandaloneAny divribbon.show("container-id")

Component Hosting

The custom control type embeds existing components inside ribbon groups:

{
    type: "custom",
    id: "workspace",
    label: "Workspace",
    size: "large",
    element: function() {
        var div = document.createElement("div");
        var ws = new WorkspaceSwitcher({ workspaces: [...], size: "sm" });
        ws.show(div);
        return div;
    }
}

The optional label property renders a text label whose position depends on size: small/mini place the label on the left (row layout, matching built-in controls), while large places it below (column layout). The optional width property sets the wrapper's minimum width (e.g. width: "120px"), useful for components like FontDropdown or Slider that need space at mini height. Inside stacks the grid controls the actual width; the width value acts as a floor. The ribbon uses overflow: visible on group content so hosted component dropdowns are not clipped.

Icon Support

The icon property on controls accepts any space-separated CSS class string. The Ribbon applies each class individually, so any CSS icon library works:

Ensure the corresponding icon library CSS is loaded in the page.

DOM Structure

div.ribbon
├── div.ribbon-qat [role="toolbar"]
├── div.ribbon-menubar [role="menubar"]
│   └── div.ribbon-menu-item
│       ├── button.ribbon-menu-trigger
│       └── div.ribbon-menu-dropdown [role="menu"]
├── div.ribbon-tabbar [role="tablist"]
│   ├── button.ribbon-tab [role="tab"]
│   ├── div.ribbon-tabbar-status          ← optional right-aligned status slot
│   │   └── [consumer HTMLElement]
│   └── button.ribbon-collapse-btn
├── div.ribbon-panel [role="tabpanel"]
│   └── div.ribbon-tab-content
│       ├── div.ribbon-group [role="group"]
│       │   ├── div.ribbon-group-content
│       │   │   ├── button.ribbon-btn-large
│       │   │   ├── div.ribbon-stack
│       │   │   │   └── button.ribbon-btn-small
│       │   │   └── div.ribbon-custom.ribbon-custom-{size}
│       │   │       ├── span.ribbon-custom-label    ← before element for small/mini
│       │   │       ├── [consumer HTMLElement]
│       │   │       └── span.ribbon-custom-label    ← after element for large
│       │   └── div.ribbon-group-label
│       └── div.ribbon-group-separator
├── div.ribbon-backstage [role="dialog"]
└── div.ribbon-keytip-layer [aria-hidden]

Auto-Collapse

When the ribbon is collapsed and a user clicks a tab, the panel temporarily expands. With autoCollapseDelay set to a positive value (minimum 5000 ms), the panel automatically collapses after the specified delay of inactivity. Any interaction (click, tap) inside the ribbon resets the timer.

var ribbon = createRibbon({
    tabs: [ /* ... */ ],
    collapsed: true,
    autoCollapseDelay: 8000  // auto-hide after 8 seconds
}, "container");

// Change at runtime
ribbon.setAutoCollapseDelay(5000);

// Disable
ribbon.setAutoCollapseDelay(0);

State Persistence

Save and restore the ribbon's UI state across sessions using getState() and restoreState(). The returned RibbonState object is JSON-serialisable.

// Save
var state = ribbon.getState();
localStorage.setItem("ribbon-state", JSON.stringify(state));

// Restore (on next load)
var saved = JSON.parse(localStorage.getItem("ribbon-state"));
if (saved) { ribbon.restoreState(saved); }

restoreState() accepts a Partial<RibbonState>, so consumers can restore only the fields they saved. The state includes: activeTabId, collapsed, contextualTabs visibility, controlValues, and autoCollapseDelay.

Deferred State

State methods (setControlDisabled, setControlActive, setControlHidden, setControlValue) work on controls in any tab — even tabs not yet rendered. The Ribbon queues changes internally and applies them when the tab is first activated. Multiple calls before render are supported; the last value wins.

getControlValue and getControlState also return queued state for unrendered controls, so consuming apps never need to track parallel state variables.

When setControlActive is called on a toggle: true button, the Ribbon syncs the internal toggle state so the next click always produces the correct opposite value — whether the state was set by click or by API.

Accessibility


RibbonBuilder

Visual WYSIWYG editor for composing Ribbon toolbar layouts via drag-and-drop. Exports Markdown specs consumable by coding agents and JSON configs for direct use with createRibbon().

Usage

HTML

<link rel="stylesheet" href="components/ribbonbuilder/ribbonbuilder.css">
<script src="components/ribbon/ribbon.js"></script>
<script src="components/ribbonbuilder/ribbonbuilder.js"></script>

<div id="my-ribbon-builder"></div>

Enhanced Icon Picker

When the SymbolPicker component is loaded, the icon picker auto-discovers all Bootstrap Icons and Font Awesome icons from loaded stylesheets (hundreds of icons). Falls back to a built-in curated set of 62 icons if SymbolPicker is not loaded.

<!-- Optional: load for enhanced icon picker -->
<link rel="stylesheet" href="components/symbolpicker/symbolpicker.css">
<script src="components/symbolpicker/symbolpicker.js"></script>

JavaScript

var builder = createRibbonBuilder({
    onChange: function(config) {
        console.log("Config updated:", config);
    },
    onExport: function(markdown) {
        console.log("Markdown exported:", markdown);
    }
}, "my-ribbon-builder");

Options

OptionTypeDefaultDescription
initialConfigPartial<RibbonOptions>starter layoutPre-loaded config to edit
containerHTMLElement | stringContainer element or ID
previewHeightnumber96Live preview min-height (px)
treeWidthnumber240Structure tree panel width (px)
onChange(config) => voidFires on every config change
onExport(markdown) => voidFires on Markdown export
cssClassstringExtra CSS class on root

Handle API

MethodReturnsDescription
show(containerId?)voidMount into a container
destroy()voidTear down component
getConfig()RibbonOptionsDeep copy of current config
setConfig(config)voidReplace the current config
exportMarkdown()stringExport as structured Markdown
exportJSON()stringExport as formatted JSON
importJSON(json)voidImport from JSON string
getElement()HTMLElementRoot DOM element

Layout

The editor has three panels:

  1. Toolbar (top) — Add Tab, Add Group, Add Control dropdown, Delete, Export MD, Export JSON, Import JSON
  2. Live Preview (middle) — Real-time Ribbon preview rebuilt on every change (250ms debounce)
  3. Bottom Split — Structure tree (left, resizable) and Property panel with Icon picker (right)

Keyboard Shortcuts

KeyAction
Delete / BackspaceRemove selected node
F2Focus the first property field (rename)
ArrowUp / ArrowDownMove selection in tree

Drag-and-Drop

Export Formats

Markdown

Structured tables per group with columns: #, Type, ID, Label, Icon, Size, Extra. Full JSON appended at end.

JSON

Standard RibbonOptions object — paste directly into createRibbon() calls.

Dependencies


RichTextInput

A lightweight contenteditable-based rich text input that composes STIE and Pill for per-row editing contexts — todo items, checklist entries, inline detail fields. Fills the gap between a plain <input> and the full MarkdownEditor. No external dependencies; suitable for 20–50 instances on a single page.

Assets

AssetPath
CSScomponents/richtextinput/richtextinput.css
JScomponents/richtextinput/richtextinput.js
Typescomponents/richtextinput/richtextinput.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/richtextinput/richtextinput.css">
<script src="components/richtextinput/richtextinput.js"></script>
<script>
    var editor = createRichTextInput({
        placeholder: "Type here...",
        formatting: true,
        onChange: function(html) { console.log("Content:", html); }
    });
    document.getElementById("container").appendChild(editor.getElement());
</script>

API

Global Functions

FunctionReturnsDescription
createRichTextInput(options)RichTextInputCreate a rich text input instance

RichTextInput Class

MethodReturnsDescription
show(container)voidAppend into container (ID string or element)
getElement()HTMLElementGet root DOM element
getValue()stringContent in configured format (html or markdown)
getHtml()stringAlways returns HTML
getPlainText()stringStripped plain text
setValue(html)voidSet content from HTML string
setDisabled(flag)voidToggle disabled state
setReadonly(flag)voidToggle readonly state
focus()voidProgrammatic focus
isEmpty()booleanWhether content is empty/whitespace-only
getStieEngine()object|nullReturns STIE engine if attached
execFormat(action)voidProgrammatic formatting
destroy()voidFull cleanup

RichTextInputOptions

OptionTypeDefaultDescription
valuestringundefinedInitial HTML content
placeholderstringundefinedPlaceholder text
disabledbooleanfalseDisable editing
readonlybooleanfalseRead-only mode
size"sm"|"default"|"lg""default"Bootstrap size variant
cssClassstringundefinedAdditional CSS class(es)
maxLengthnumber0Plain-text char limit (0 = unlimited)
showCounterbooleanfalseShow character counter
minHeightstring"auto"CSS min-height
maxHeightstring"none"CSS max-height
resizablebooleantrueCSS resize: vertical
formattingbooleantrueEnable bold/italic/strike/link/code
listsbooleanfalseEnable ordered/unordered/task lists
showInlineToolbarbooleantrueShow toolbar on selection
toolbarActionsFormattingAction[]See belowToolbar button set
triggersStieCompatTriggerDefinition[]undefinedSTIE trigger definitions
stieOptionsRecord<string, unknown>undefinedOptions for STIE engine
outputFormat"html"|"markdown""html"getValue() output format
onChange(value: string) => voidundefinedContent changed (debounced)
onFocus() => voidundefinedFocus callback
onBlur() => voidundefinedBlur callback
onSubmit(value: string) => voidundefinedCtrl+Enter callback
submitOnCtrlEnterbooleanfalseEnable Ctrl+Enter submit

FormattingAction Type

type FormattingAction =
    | "bold" | "italic" | "strikethrough" | "link" | "code"
    | "orderedList" | "unorderedList" | "taskList";

Default toolbar actions: ["bold", "italic", "strikethrough", "link", "code"]

Keyboard Shortcuts

ShortcutActionCondition
Ctrl+BBoldHas focus
Ctrl+IItalicHas focus
Ctrl+DStrikethroughHas focus
Ctrl+KInsert linkHas focus + text selected
Ctrl+EInline codeHas focus
Ctrl+Shift+8Unordered listlists: true
Ctrl+Shift+7Ordered listlists: true
Ctrl+Shift+9Task listlists: true
Ctrl+EnterSubmitsubmitOnCtrlEnter: true

CSS Custom Properties

Override these on .richtextinput to customize appearance:

PropertyDefaultDescription
--rti-bg$input-bgBackground
--rti-border-color$input-border-colorBorder
--rti-focus-border-color$input-focus-border-colorFocus border
--rti-focus-shadow$input-focus-box-shadowFocus shadow
--rti-color$gray-900Text colour
--rti-placeholder-color$input-placeholder-colorPlaceholder
--rti-toolbar-bg$gray-100Toolbar background
--rti-toolbar-border$gray-300Toolbar border

STIE Integration

When STIE and Pill are loaded before RichTextInput, pass trigger definitions to enable inline entity references:

<script src="components/smarttextinput/smarttextinput.js"></script>
<script src="components/pill/pill.js"></script>
<script src="components/richtextinput/richtextinput.js"></script>
<script>
    var mentionTrigger = {
        trigger: "@",
        name: "mention",
        activation: { precedingChar: "whitespace", minLength: 1, maxLength: 30 },
        dataSource: {
            query: function(text) {
                return Promise.resolve([
                    { id: "u1", label: "Alice Chen", sublabel: "Engineering" }
                ].filter(function(u) {
                    return u.label.toLowerCase().includes(text.toLowerCase());
                }));
            }
        },
        tokenRenderer: { display: "label", className: "pill pill-blue", icon: "bi bi-person" },
        tokenSerializer: {
            prefix: "@",
            pattern: /^@\[(.+?)\]\(user:(\w+)\)$/,
            serialize: function(t) { return "@[" + t.label + "](user:" + t.id + ")"; },
            deserialize: function(m) { return { id: m[2], label: m[1] }; }
        }
    };

    var editor = createRichTextInput({
        placeholder: "Type @ to mention someone...",
        triggers: [mentionTrigger],
        stieOptions: {
            queryDebounceMs: 100,
            onTriggerQuery: function(ev) { /* show popover */ }
        }
    });
    document.getElementById("container").appendChild(editor.getElement());
</script>

If STIE is not loaded, triggers are silently ignored and the component works as plain rich text.


Ruler

A canvas-based calibrated ruler with cursor tracking, multiple unit systems, and DPI-aware rendering. Supports horizontal and vertical orientations with configurable tick marks, labelling, and real-time cursor position tracking.

Assets

AssetPath
CSScomponents/ruler/ruler.css
JScomponents/ruler/ruler.js
Typescomponents/ruler/ruler.d.ts

Requirements

Quick Start

Horizontal Pixel Ruler

<link rel="stylesheet" href="components/ruler/ruler.css">
<script src="components/ruler/ruler.js"></script>

<div id="my-ruler" style="width: 800px;"></div>
<script>
    var ruler = createRuler("my-ruler", {
        orientation: "horizontal",
        unit: "px",
        showCursor: true
    });
</script>

Vertical Centimetre Ruler

<div id="cm-ruler" style="height: 600px;"></div>
<script>
    var cmRuler = createRuler("cm-ruler", {
        orientation: "vertical",
        unit: "cm",
        markingSide: "right",
        cursorColor: "#228be6"
    });
</script>

Inch Ruler with Custom Origin

<div id="inch-ruler" style="width: 600px;"></div>
<script>
    var inchRuler = createRuler("inch-ruler", {
        unit: "in",
        origin: 96,
        showCursor: true
    });
</script>

Custom Unit Ruler

<div id="unit-ruler" style="width: 500px;"></div>
<script>
    // 1 custom unit = 50 CSS pixels
    var unitRuler = createRuler("unit-ruler", {
        unit: "unit",
        unitScale: 50,
        majorInterval: 2
    });
</script>

API

Factory Function

FunctionReturnsDescription
createRuler(containerId, options?)RulerCreate a ruler inside the container

The Ruler class is also available globally for direct instantiation:

var ruler = new Ruler("container-id", { unit: "cm" });

RulerOptions

OptionTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"Ruler orientation
markingSide"top" | "bottom" | "left" | "right"Depends on orientationSide where tick marks appear. Horizontal defaults to "bottom", vertical defaults to "right".
unit"px" | "unit" | "cm" | "mm" | "in""px"Measurement unit
unitScalenumber1CSS pixels per unit (only used when unit is "unit")
lengthnumberContainer sizeRuler length in CSS px. Defaults to the container width (horizontal) or height (vertical).
majorIntervalnumberAutoOverride the auto-calculated major tick interval (in current unit)
showCursorbooleantrueShow the cursor tracking line
cursorColorstring"#e03131"Colour of the cursor tracking line
originnumber0Offset in CSS pixels where the 0 mark starts
disabledbooleanfalseDisable mouse interaction

Instance Methods

MethodReturnsDescription
getElement()HTMLElement | nullReturns the wrapper element, or null if destroyed
setUnit(unit)voidChange the measurement unit and re-render
setOrientation(orientation)voidChange the orientation and re-render
setOrigin(origin)voidChange the origin offset and re-render
setCursorPosition(px)voidSet cursor position (CSS px) along the ruler axis
calibrate()voidRe-measure DPI and re-render
resize()voidRecalculate canvas size from container, re-render
destroy()voidRemove DOM elements and clean up event listeners

Unit Systems

The ruler supports five unit systems with automatic tick subdivision:

UnitMajor TickMinor TickSub TickDescription
pxEvery 100pxEvery 50pxEvery 10pxCSS pixels
unitEvery 1 unitEvery 0.5 unitEvery 0.1 unitCustom unit (scaled by unitScale)
inEvery 1"Every 0.5"Every 0.25"Inches (DPI-aware)
cmEvery 1cmEvery 0.5cmEvery 0.1cmCentimetres (DPI-aware)
mmEvery 10mmEvery 5mmEvery 1mmMillimetres (DPI-aware)

When majorInterval is provided, it overrides the default major tick interval. Minor ticks are placed at half the major interval and sub-minor ticks at one-tenth.

DPI Awareness

The ruler measures the physical DPI of the display using a hidden 1-inch DOM element. This allows cm, mm, and in units to render at accurate physical sizes. The measurement accounts for window.devicePixelRatio to render sharp lines on high-DPI (Retina) displays.

Call calibrate() after display changes (e.g., moving the window to a different monitor).

Cursor Tracking

When showCursor is true (the default), the ruler draws a coloured line that follows the mouse position. The cursor automatically tracks when the mouse is over the ruler canvas.

External code can also drive the cursor position programmatically:

// Sync cursor with mouse position over another element
document.getElementById("workspace").addEventListener("mousemove", function(e) {
    ruler.setCursorPosition(e.clientX - workspaceRect.left);
});

CSS Classes

ClassDescription
.rulerBase wrapper
.ruler-canvasThe <canvas> element
.ruler-horizontalHorizontal orientation (28px tall, full width)
.ruler-verticalVertical orientation (28px wide, full height)
.ruler-disabledDisabled state (reduced opacity, no pointer events)

Accessibility

See specs/ruler.prd.md for the complete specification.


SearchBox

A debounced search input with search icon, clear button, loading spinner, and optional suggestions dropdown. Designed for embedding in app shells, sidebars, and toolbars. Supports static and async suggestion sources, full keyboard navigation, and screen reader announcements.

Assets

AssetPath
CSScomponents/searchbox/searchbox.css
JScomponents/searchbox/searchbox.js
Typescomponents/searchbox/searchbox.d.ts

Requirements

Quick Start (Script Tag)

<link rel="stylesheet" href="components/searchbox/searchbox.css">
<script src="components/searchbox/searchbox.js"></script>

<div id="search-container"></div>

<script>
    // Basic search box
    var sb = createSearchBox("search-container", {
        placeholder: "Search items...",
        onSearch: function(query) {
            console.log("Search:", query);
        },
        onSubmit: function(query) {
            console.log("Submit:", query);
        }
    });
</script>

Quick Start with Suggestions

<div id="search-suggestions"></div>

<script>
    // Static suggestions
    var sb = createSearchBox("search-suggestions", {
        placeholder: "Search fruits...",
        suggestions: ["Apple", "Apricot", "Banana", "Blueberry", "Cherry", "Date"],
        onSearch: function(query) {
            console.log("Search:", query);
        }
    });

    // Async suggestions
    var sbAsync = createSearchBox("search-async", {
        placeholder: "Search users...",
        suggestions: function(query) {
            return fetch("/api/users?q=" + encodeURIComponent(query))
                .then(function(r) { return r.json(); })
                .then(function(data) { return data.map(function(u) { return u.name; }); });
        },
        minChars: 3,
        debounceMs: 500
    });
</script>

Quick Start (ES Module)

import { createSearchBox, SearchBox } from "./components/searchbox/searchbox.js";

const sb = createSearchBox("my-container", {
    placeholder: "Search...",
    onSearch: (query) => console.log("Search:", query),
    suggestions: ["Alpha", "Beta", "Gamma"]
});

// Programmatic control
sb.setValue("Beta");
sb.focus();
sb.clearValue();

API

createSearchBox(containerId, options)

Creates a SearchBox and appends it to the specified container. Returns the SearchBox instance.

SearchBox Methods

MethodReturnsDescription
show(containerId)voidAppend to container, attach listeners
hide()voidRemove from DOM (preserves state)
destroy()voidFull cleanup, release all references
getElement()HTMLElementReturns the root element
getValue()stringReturns the current input value
setValue(value)voidSets the input value and triggers search
focus()voidFocuses the input element
clearValue()voidClears the input, hides clear button, fires onSearch("")
setDisabled(disabled)voidToggles the disabled state

SearchBoxOptions

OptionTypeDefaultDescription
placeholderstring"Search..."Input placeholder text
debounceMsnumber300Debounce delay in milliseconds
onSearchfunction--Called after debounce with query string
onSubmitfunction--Called when Enter is pressed
suggestionsstring[] | function--Static list or async function returning suggestions
minCharsnumber2Minimum characters before showing suggestions
size"mini" | "sm" | "md" | "lg""md"Size variant
disabledbooleanfalseInitial disabled state
cssClassstring--Additional CSS class(es) for the root element
keyBindingsobject--Override default key combos

Features

Keyboard Shortcuts

KeyAction
EscapeClose suggestions if open; clear input if suggestions closed
EnterSelect highlighted suggestion; or fire onSubmit
ArrowDownOpen suggestions if closed (with enough chars); or move highlight down
ArrowUpMove highlight up in suggestions

All key bindings can be overridden via the keyBindings option:

var sb = createSearchBox("container", {
    keyBindings: {
        clear: "Escape",
        submit: "Enter",
        nextSuggestion: "ArrowDown",
        prevSuggestion: "ArrowUp"
    }
});

Accessibility


ShareDialog

A modal dialog for sharing resources with configurable access levels. Composes PeoplePicker for person search/selection and PersonChip for existing access display. Returns a diff of added, changed, and removed access when the user clicks Done.

Assets

AssetPath
CSScomponents/sharedialog/sharedialog.css
JScomponents/sharedialog/sharedialog.js
Typescomponents/sharedialog/sharedialog.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="css/custom.css">
<link rel="stylesheet" href="components/personchip/personchip.css">
<link rel="stylesheet" href="components/peoplepicker/peoplepicker.css">
<link rel="stylesheet" href="components/sharedialog/sharedialog.css">

<script src="components/personchip/personchip.js"></script>
<script src="components/peoplepicker/peoplepicker.js"></script>
<script src="components/sharedialog/sharedialog.js"></script>
<script>
    async function shareDocument() {
        const result = await showShareDialog({
            title: "Share Document",
            accessLevels: [
                { id: "viewer", label: "Viewer" },
                { id: "commenter", label: "Commenter" },
                { id: "editor", label: "Editor" },
            ],
            defaultAccessLevelId: "viewer",
            existingAccess: [
                { person: { id: "1", name: "Alice Smith", email: "alice@example.com" }, accessLevelId: "editor" },
            ],
            frequentContacts: [
                { id: "2", name: "Bob Jones", email: "bob@example.com" },
                { id: "3", name: "Carol Lee", email: "carol@example.com" },
            ],
            onSearch: async (query) => {
                const resp = await fetch(`/api/people?q=${encodeURIComponent(query)}`);
                return resp.json();
            },
        });

        if (result) {
            console.log("Added:", result.added);
            console.log("Changed:", result.changed);
            console.log("Removed:", result.removed);
        }
    }
</script>

API

Global Functions

FunctionReturnsDescription
showShareDialog(options)Promise<ShareDialogResult | null>Show share dialog; resolves to result diff on Done, null on Cancel
createShareDialog(options)ShareDialogCreate instance for imperative control; call .show() to display

Class: ShareDialog

const dialog = createShareDialog({ title: "Share", accessLevels: [...] });
const result = await dialog.show();

ShareDialogOptions

OptionTypeDefaultDescription
titlestringrequiredDialog title text
accessLevelsAccessLevel[]requiredAvailable access levels
defaultAccessLevelIdstringFirst levelDefault access level for new additions
existingAccessSharedPerson[][]People who already have access
frequentContactsPersonData[][]Frequent contacts for PeoplePicker
onSearch(query) => Promise<PersonData[]>--Async search callback
searchUrlstring--URL-based search for PeoplePicker
onShare(result) => void | Promise<void>--Callback when user clicks Done
onCancel() => void--Callback when user cancels
sizestring"md""sm", "md", "lg"
cssClassstring--Additional CSS class on the dialog
closeOnBackdropbooleantrueClose when clicking outside
closeOnEscapebooleantrueClose on Escape key
keyBindingsRecord<string, string>--Override default key bindings
onRemoveConfirm(person: { id: string; name: string }) => Promise<boolean>--Confirmation callback before removing a person's access. Return true to proceed, false to cancel. If omitted, removal is immediate.

Data Types

AccessLevel

{ id: string; label: string; description?: string }

SharedPerson

{ person: PersonData; accessLevelId: string }

ShareDialogResult

{
    added: SharedPerson[];    // newly added people
    changed: SharedPerson[];  // people whose access level changed
    removed: string[];        // IDs of removed people
}

Keyboard

KeyAction
EscapeCancel (resolve null)
TabCycle focus within dialog (focus trap)
EnterActivate focused button

Key bindings can be overridden via the keyBindings option:

showShareDialog({
    title: "Share",
    accessLevels: [...],
    keyBindings: { close: "Ctrl+Escape" },
});

Accessibility

DOM Structure

div.sharedialog-overlay [z-index 2000]
  div.sharedialog-backdrop
  div.sharedialog.sharedialog-{size} [role="dialog" aria-modal="true"]
    div.sharedialog-header
      h2.sharedialog-title
      button.sharedialog-close [aria-label="Close"]
    div.sharedialog-body
      div.sharedialog-add-section
        div.sharedialog-picker-row
          div.sharedialog-picker-wrap [PeoplePicker mounts here]
          select.sharedialog-access-select.form-select
        button.sharedialog-add-btn.btn.btn-primary
      div.sharedialog-divider
      div.sharedialog-existing-section
        h3.sharedialog-section-label "People with access"
        div.sharedialog-access-list
          div.sharedialog-access-row [data-person-id] x N
            div.sharedialog-access-person [PersonChip md]
            select.sharedialog-access-level.form-select
            button.sharedialog-access-remove
    div.sharedialog-footer
      span.sharedialog-status
      div.sharedialog-actions
        button.sharedialog-cancel-btn.btn.btn-secondary
        button.sharedialog-done-btn.btn.btn-primary
    div.visually-hidden [aria-live="polite"]

Features


Sidebar

A dockable, floatable, resizable sidebar panel component that acts as a container for other components. Supports docking to left/right viewport edges, free-positioned floating with drag-based positioning, collapsing to a 40px icon strip, resizing via drag handles, tab grouping when multiple sidebars share the same dock edge, and drag-to-dock with visual drop zones.

Assets

AssetPath
CSScomponents/sidebar/sidebar.css
JScomponents/sidebar/sidebar.js
Typescomponents/sidebar/sidebar.d.ts

Requirements

Usage (Script Tag)

<link rel="stylesheet" href="components/sidebar/sidebar.css">
<script src="components/sidebar/sidebar.js"></script>
<script>
    // Docked sidebar
    var explorer = createDockedSidebar({
        title: "Explorer",
        icon: "bi-folder",
        dockPosition: "left",
        width: 280
    });
    explorer.getContentElement().innerHTML = "<p style='padding:1rem'>Content here</p>";

    // Floating sidebar
    var tools = createFloatingSidebar({
        title: "Tools",
        icon: "bi-tools",
        floatX: 400,
        floatY: 100,
        width: 300,
        height: 400
    });
</script>

Usage (ES Module)

import { createSidebar, createDockedSidebar } from "./components/sidebar/sidebar.js";

const sb = createDockedSidebar({
    title: "Explorer",
    icon: "bi-folder",
    dockPosition: "left",
    onModeChange: (mode, sidebar) => console.log("Mode:", mode),
    onResize: (w, h) => console.log("Resized:", w, h)
});

Options

OptionTypeDefaultDescription
idstringautoUnique identifier
titlestringrequiredTitle bar text
iconstringBootstrap Icons class (e.g., "bi-folder")
mode"docked" | "floating""docked"Initial mode
dockPosition"left" | "right""left"Dock edge
widthnumber280Panel width in pixels
minWidthnumber180Minimum resize width
maxWidthnumber50% of viewportMaximum resize width (defaults to Math.round(window.innerWidth * 0.5))
heightnumber400Floating height in pixels
minHeightnumber200Minimum floating height
maxHeightnumber800Maximum floating height
floatXnumber100Initial floating X position
floatYnumber100Initial floating Y position
collapsedbooleanfalseStart collapsed
collapsedWidthnumber40Width when collapsed
backgroundColorstringCSS background colour override
opacitynumberOpacity (0-1)
borderColorstringCSS border colour override
borderWidthstringCSS border width override
zIndexnumberCSS z-index override
cssClassstringAdditional CSS classes
resizablebooleantrueEnable resize handles
draggablebooleantrueEnable floating drag
collapsiblebooleantrueEnable collapse
showTitleBarbooleantrueShow title bar
onModeChangefunctionMode change callback
onResizefunctionResize complete callback
onCollapseTogglefunctionCollapse toggle callback
onBeforeClosefunctionBefore close callback (return false to cancel)
onClosefunctionAfter close callback

API

Sidebar

MethodReturnsDescription
show()voidAppend to DOM and register
hide()voidRemove from DOM (preserves state)
destroy()voidHide, unregister, and release references
dock(position)voidSwitch to docked mode
float(x?, y?)voidSwitch to floating mode
collapse()voidCollapse to icon strip
expand()voidExpand from collapsed
toggleCollapse()voidToggle collapse state
setTitle(title)voidUpdate title text
setIcon(iconClass)voidUpdate title icon
setWidth(w)voidSet width (clamped)
setHeight(h)voidSet height (clamped, floating only)
getContentElement()HTMLElementContent div for appending children
getMode()stringCurrent mode
getDockPosition()stringCurrent dock position
getWidth()numberCurrent width
getHeight()numberCurrent height
isVisible()booleanWhether sidebar is in the DOM
isCollapsed()booleanWhether sidebar is collapsed

SidebarManager

MethodReturnsDescription
getInstance()SidebarManagerSingleton accessor
register(sidebar)voidRegister for tab grouping
unregister(sidebar)voidRemove from management
getSidebars(position?)Sidebar[]Query registered sidebars
getActiveTab(position)SidebarActive sidebar at a dock position
setActiveTab(sidebar)voidActivate a specific tab

Convenience Functions

FunctionDescription
createSidebar(options)Create and show a sidebar
createDockedSidebar(options)Create a docked sidebar (mode defaults to "docked")
createFloatingSidebar(options)Create a floating sidebar (mode defaults to "floating")

CSS Custom Properties

The component sets these properties on <html> for layout integration:

PropertyDescription
--sidebar-left-widthTotal width of docked left sidebar(s)
--sidebar-right-widthTotal width of docked right sidebar(s)

Consumers can offset main content with:

.main-content {
    margin-left: var(--sidebar-left-width, 0px);
    margin-right: var(--sidebar-right-width, 0px);
}

Tab Grouping

When multiple sidebars dock to the same edge, they automatically form a tab group. Only one sidebar is visible at a time; the others are hidden. A tab bar appears at the top of the dock zone.

Drag-to-Dock

In floating mode, dragging the sidebar near a viewport edge (within 40px) shows a translucent drop-zone indicator. Releasing within the zone docks the sidebar to that edge.

Z-Index Layering

ElementZ-Index
Docked sidebar1035
Floating sidebar1036
Drop zone overlays1037
StatusBar1040
Bootstrap modals1050+

Accessibility


SizesPicker

A dropdown listing page and frame sizes with proportional page thumbnails and dimensions. Each item shows a small proportional SVG rectangle, the size name, and width by height. Items are grouped by category (Paper, Screen, Mobile, Tablet). An optional "More Paper Sizes..." link at the bottom allows integration with custom size dialogs.

Usage

<link rel="stylesheet" href="components/sizespicker/sizespicker.css">
<script src="components/sizespicker/sizespicker.js"></script>

<div id="my-sizes-picker"></div>

<script>
var picker = createSizesPicker({
    container: "my-sizes-picker",
    value: "A4",
    onChange: function(size) {
        console.log("Selected:", size.name, size.width, "x", size.height);
    }
});
</script>

With Custom Sizes

<div id="custom-sizes"></div>

<script>
var picker = createSizesPicker({
    container: "custom-sizes",
    sizes: [
        { name: "Poster", width: 2400, height: 3600, category: "Print", displayWidth: '25"', displayHeight: '37.5"' },
        { name: "Banner", width: 4800, height: 1200, category: "Print", displayWidth: '50"', displayHeight: '12.5"' },
    ],
    onCustom: function() {
        alert("Open custom size dialog");
    }
});
</script>

Filtered by Category

<div id="paper-only"></div>

<script>
var picker = createSizesPicker({
    container: "paper-only",
    category: "Paper",
    onChange: function(size) {
        console.log("Paper size:", size.name);
    }
});
</script>

Options

OptionTypeDefaultDescription
containerHTMLElement | stringrequiredContainer element or ID
valuestringFirst sizeInitial selected preset name
sizesSizePreset[]Built-in defaultsCustom size definitions
categorystringFilter to one category
showCustombooleantrueShow "More Paper Sizes..." link
onChange(size: SizePreset) => voidFires when a size is selected
onCustom() => voidFires when "More Paper Sizes..." is clicked
ribbonModebooleantrueRender as ribbon-compatible dropdown

API

MethodReturnsDescription
getValue()SizePresetCurrent selected size (returns a copy)
setValue(name)voidSet selection by size name (case-insensitive)
setSizes(sizes)voidReplace the size list and re-render
show()voidShow the picker
hide()voidHide the picker
destroy()voidTear down and remove from DOM
getElement()HTMLElementRoot DOM element

Default Sizes

NameWidthHeightCategory
Letter816px (8.5")1056px (11")Paper
Legal816px (8.5")1344px (14")Paper
A4794px (8.27")1123px (11.69")Paper
A5559px (5.83")794px (8.27")Paper
B5 (JIS)693px (7.17")979px (10.12")Paper
Executive696px (7.25")1008px (10.5")Paper
Full HD1920px1080pxScreen
iPhone 15393px852pxMobile
iPad Air820px1180pxTablet

SizePreset Interface

interface SizePreset {
    name: string;
    width: number;          // pixels at 96 DPI
    height: number;
    category?: string;      // "Paper", "Screen", "Mobile", etc.
    displayWidth?: string;  // human-readable width
    displayHeight?: string; // human-readable height
}

Keyboard

KeyAction
Enter / SpaceSelect focused item
TabMove focus between items

SVG Thumbnails

Each size item renders a proportional page rectangle as an inline SVG. The tallest page in the current list is scaled to 40px; all other widths and heights scale proportionally relative to the tallest entry. The rectangle uses a white fill with a #dee2e6 border.


SkeletonLoader

Animated placeholder component that mimics content layout during loading. CSS shimmer animation with six presets: text, avatar, card, table, paragraph, and custom.

Assets

AssetPath
CSScomponents/skeletonloader/skeletonloader.css
JScomponents/skeletonloader/skeletonloader.js
Typescomponents/skeletonloader/skeletonloader.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/skeletonloader/skeletonloader.css">
<script src="components/skeletonloader/skeletonloader.js"></script>
<script>
    var skeleton = createSkeletonLoader(
        { preset: "card" },
        "my-container"
    );
    // Later when data loads:
    skeleton.destroy();
</script>

Options (SkeletonLoaderOptions)

OptionTypeDefaultDescription
presetstring"text"text, avatar, card, table, paragraph, custom
linesnumber3Number of lines (text/paragraph)
rowsnumber5Number of rows (table)
columnsnumber4Number of columns (table)
widthstring"100%"CSS width (custom/avatar)
heightstring"1rem"CSS height (custom/avatar)
gapstring"0.5rem"Gap between elements
animatebooleantrueEnable shimmer animation
borderRadiusstring"0"CSS border-radius
circlebooleanfalseCircle shape (avatar only)
cssClassstringAdditional CSS class(es)

API

MethodReturnsDescription
show(containerId?)voidAppend to container (or body)
hide()voidRemove from DOM, keep state
destroy()voidHide, clean up, null references
getElement()HTMLElementRoot DOM element

Global Exports

window.SkeletonLoader
window.createSkeletonLoader

Accessibility

See specs/skeletonloader.prd.md for the complete specification.


Slider

A range input component with single-value and dual-thumb range modes, optional tick marks, value labels, keyboard navigation, and size variants. Supports both horizontal and vertical orientations.

Purpose and Use Cases

Quick Start

<!-- Dependencies -->
<link rel="stylesheet" href="css/custom.css">
<link rel="stylesheet" href="components/slider/slider.css">

<!-- Component -->
<div id="my-slider"></div>
<script src="components/slider/slider.js"></script>
<script>
    createSlider("my-slider", {
        min: 0,
        max: 100,
        value: 50,
        onChange: (v) => console.log("Value:", v)
    });
</script>

Range Mode

<div id="range-slider"></div>
<script>
    createSlider("range-slider", {
        mode: "range",
        min: 0,
        max: 1000,
        valueLow: 200,
        valueHigh: 800,
        formatValue: (v) => "$" + v,
        onChange: (r) => console.log("Range:", r.low, "-", r.high)
    });
</script>

Configuration Options

OptionTypeDefaultDescription
mode"single" | "range""single"Single thumb or dual-thumb range
minnumber0Minimum value
maxnumber100Maximum value
stepnumber1Step increment
valuenumber50Current value (single mode)
valueLownumber25Low value (range mode)
valueHighnumber75High value (range mode)
labelstringundefinedLabel text above the slider
showValuebooleantrueShow value label below the slider
showTicksbooleanfalseShow tick marks along the track
tickIntervalnumberSame as stepInterval between tick marks
formatValue(v: number) => stringString(v)Custom value formatter
disabledbooleanfalseDisable the slider
orientation"horizontal" | "vertical""horizontal"Slider orientation
size"mini" | "sm" | "default" | "lg""default"Size variant
onChangefunctionundefinedFired on value change
onSlideStartfunctionundefinedFired when drag begins
onSlideEndfunctionundefinedFired when drag ends

Instance Methods

MethodReturnsDescription
getValue()numberCurrent value (single mode)
getRange(){ low, high }Current range values
setValue(v)voidSet value (single mode)
setRange(low, high)voidSet range values
setMin(v)voidUpdate minimum bound
setMax(v)voidUpdate maximum bound
setStep(v)voidUpdate step increment
enable()voidEnable the slider
disable()voidDisable the slider
getElement()HTMLElementRoot DOM element
destroy()voidRemove from DOM and clean up

Keyboard Interactions

KeyAction
ArrowRight / ArrowUpIncrease by one step
ArrowLeft / ArrowDownDecrease by one step
PageUpIncrease by 10x step
PageDownDecrease by 10x step
HomeJump to minimum
EndJump to maximum

In vertical orientation, ArrowUp/Down are the primary directional keys.

Size Variants

<div id="sm-slider"></div>
<div id="lg-slider"></div>
<script>
    createSlider("sm-slider", { size: "sm", value: 30 });
    createSlider("lg-slider", { size: "lg", value: 70 });
</script>

Accessibility

Dependencies

DependencyRequiredNotes
Bootstrap 5 CSSYesFor base styling variables
Bootstrap 5 JSNoNot used
Enterprise Theme CSSYesFor theme variable overrides

SmartTextInput

A behavioral middleware engine (non-UI) that attaches to text inputs and provides trigger-based inline references such as @mentions, #resources, and $formulas. The host application is responsible for rendering the popover/dropdown UI and supplying data sources; the engine handles trigger detection, keyboard delegation, token lifecycle, serialization, and accessibility announcements.

Quick Start

<link rel="stylesheet" href="components/smarttextinput/smarttextinput.css">
<script src="components/smarttextinput/smarttextinput.js"></script>

<textarea id="my-input"></textarea>
<script>
    var engine = createSmartTextInput({
        queryDebounceMs: 150,
        delegateKeyboard: true
    });

    // Register an @mention trigger
    engine.register({
        trigger: "@",
        name: "mention",
        activation: {
            requireWhitespaceBefore: true,
            minQueryLength: 0,
            maxQueryLength: 50,
            cancelChars: [" ", "\n"],
            escapeChar: "\\",
            suppressIn: ["codeBlock", "inlineCode", "url"]
        },
        dataSource: { query: (text) => fetchUsers(text) },
        tokenRenderer: {
            type: "pill",
            display: (token) => "@" + token.label,
            className: "stie-token-pill"
        },
        tokenSerializer: {
            serialize: (token) => "[@" + token.label + "](user:" + token.id + ")",
            deserialize: (raw) => parseMentions(raw)
        }
    });

    // Attach to the input element
    engine.attach(document.getElementById("my-input"));

    // Listen for trigger lifecycle events
    engine.on("trigger:open", function(e) {
        showPopover(e.position, e.triggerName);
    });

    engine.on("trigger:query", function(e) {
        updatePopoverResults(e.queryText);
    });

    engine.on("trigger:close", function(e) {
        hidePopover(e.reason);
    });

    // When the user picks an item from the host popover:
    engine.resolve({ id: "42", label: "Jane Doe" });
</script>

Configuration Options

SmartTextInputOptions passed to createSmartTextInput():

OptionTypeDefaultDescription
queryDebounceMsnumber150Debounce delay before emitting trigger:query
delegateKeyboardbooleantrueWhen true, engine intercepts ArrowUp/Down/Enter/Escape during active sessions
atomicTokenDeletionbooleanfalseDelete entire token on single Backspace
showTriggerCharInTokenbooleanfalseInclude the trigger character in token display text
onTriggerOpenfunction--Callback when a trigger session opens
onTriggerQueryfunction--Callback when query text changes
onTriggerClosefunction--Callback when a trigger session closes
onTokenInsertedfunction--Callback after a token is inserted
onTokenRemovedfunction--Callback after a token is removed
onTokenClickedfunction--Callback when a token is clicked
onNavigatefunction--Callback for ArrowUp/Down during active session
onSelectfunction--Callback for Enter during active session
onDismissfunction--Callback for Escape during active session
onContentChangefunction--Callback when input content changes

Trigger Definition

Each trigger registered via engine.register(def) conforms to TriggerDefinition:

FieldTypeDescription
triggerstringCharacter(s) that activate this trigger (e.g. "@", "#", "::")
namestringUnique name used in events and token lookups
activationTriggerActivationRules governing how the trigger activates (see below)
dataSourceTriggerDataSourceObject with an async query(text) method returning DataSourceResult[]
tokenRendererTokenRendererControls visual rendering of inserted tokens
tokenSerializerTokenSerializerSerialize/deserialize tokens to/from raw text
allowedInputTypesInputAdapterType[]Optional adapter type restriction

TriggerActivation

FieldTypeDefaultDescription
requireWhitespaceBeforeboolean--Trigger only fires after whitespace or at start of input
minQueryLengthnumber0Minimum characters typed after trigger before querying
maxQueryLengthnumber50Cancel session if query exceeds this length
cancelCharsstring[]--Characters that cancel the session (e.g. [" ", "\n"])
escapeCharstring | nullnullPreceding character that suppresses trigger (e.g. "\\")
suppressInSuppressContext[][]Suppress triggers inside: codeBlock, inlineCode, url, email, quotation

Adapter Types

The engine auto-detects the adapter from the attached element. You can also pass an explicit type to attach().

AdapterElement TypesToken Storage
plaintext<input>, <textarea>Serialized syntax inline in element value
contenteditable[contenteditable="true"]Non-editable <span> elements in the DOM
prosemirrorProseMirror viewHost-implemented adapter
codemirrorCodeMirror viewHost-implemented adapter
monacoMonaco EditorHost-implemented adapter

The plaintext and contenteditable adapters are built in. The others require the host to implement the InputAdapter interface.

Public API

MethodReturnsDescription
attach(element, adapterType?)voidAttach engine to a DOM element
detach()voidDetach from the current element
destroy()voidPermanently destroy the engine
register(trigger)voidRegister a TriggerDefinition
unregister(triggerName)voidRemove a trigger by name
getTriggers()TriggerDefinition[]List all registered triggers
resolve(item)voidResolve active session with a DataSourceResult
cancel()voidCancel the active trigger session
getTokens()ResolvedToken[]All tokens in the engine
getTokensByType(triggerName)ResolvedToken[]Tokens filtered by trigger name
removeToken(instanceId)voidRemove a token by instance ID
replaceToken(instanceId, partial)voidMerge new data into an existing token
getSerializedContent()stringRaw content with serialized tokens
setSerializedContent(content)voidLoad serialized content into the adapter
getPlainTextContent()stringPlain text without token markup
on(event, handler)UnsubscribeSubscribe to an engine event
setOptions(options)voidMerge new options at runtime
static renderTokens(container, content, serializers, renderers)voidRender serialized content into a read-only container with styled tokens

Events

Subscribe via engine.on(eventName, handler). Each returns an Unsubscribe function.

EventPayloadDescription
trigger:open{ triggerName, triggerDef, queryText, position }A trigger character was detected
trigger:query{ triggerName, queryText, position }Query text changed (debounced)
trigger:close{ triggerName, reason }Session ended; reason: "resolved", "cancelled", "blur", "escape"
token:insertedResolvedTokenA token was inserted into the input
token:removedResolvedTokenA token was removed
token:clickedResolvedTokenA token was clicked
navigate{ direction: "up" | "down" }ArrowUp/Down pressed during active session
select{}Enter pressed during active session
dismiss{}Escape pressed during active session
content:change{ content }Input content changed

Token Model

ResolvedToken represents an inserted token:

FieldTypeDescription
instanceIdstringUnique ID for this token instance
triggerNamestringName of the trigger that created it
idstringEntity ID from the data source
labelstringDisplay label
sublabelstring?Optional secondary label
iconstring?Optional icon class
metadataRecord<string, unknown>Arbitrary data from the data source
sourceRange{ start, end }Character offsets in the input

Token Rendering Types

The TokenRenderer.type field controls visual appearance. Built-in CSS classes are provided for each type:

TypeCSS ClassDescription
pill.stie-token-pillRounded badge with background, border, optional icon -- for @mentions
link.stie-token-linkUnderlined link text, renders as <a> in contenteditable -- for #resources
computed.stie-token-computedMonospace badge with green tint -- for $formulas
inline-text.stie-token-inline-textItalic inline text, minimal chrome -- for lightweight cross-refs
custom.stie-token-customNo built-in styling; use render() callback for full control

Serialization

Tokens are serialized to and from raw text using TokenSerializer. This allows content with tokens to be stored, transmitted, and restored.

The default pattern uses a markdown-link-inspired format:

[@Jane Doe](user:42)
[#PROJ-100](issue:100)
[$revenue](formula:rev-2024)

Each trigger's serializer defines serialize(token) (token to string) and deserialize(rawContent) (string to DeserializedToken[]). The engine's setSerializedContent() runs all registered deserializers to reconstruct tokens from stored content.

Use SmartTextInputEngine.renderTokens() to render serialized content into a read-only container with styled token spans.

Keyboard Behavior

When delegateKeyboard is true (default) and a trigger session is active:

KeyAction
ArrowUpEmits navigate with "up" -- host moves popover highlight
ArrowDownEmits navigate with "down" -- host moves popover highlight
EnterEmits select -- host calls engine.resolve(item)
EscapeCancels session, emits trigger:close with reason "escape"

All four keys call preventDefault() during an active session to avoid interfering with the input's default behavior. When no session is active, keys pass through normally.

Accessibility

The engine creates a visually-hidden ARIA live region (role="status", aria-live="polite") that announces:

The host popover should implement the WAI-ARIA combobox pattern (role="combobox" on the input, role="listbox" on the results list, aria-activedescendant for the highlighted item) to provide full screen reader support.

Security

Window Globals

GlobalType
window.SmartTextInputEngineclass
window.createSmartTextInputfunction(options?): SmartTextInputEngine

SpacingPicker

A dropdown showing line/paragraph spacing presets with visual SVG thumbnails. Each option displays a small box with horizontal lines at different vertical spacings to illustrate the visual difference. Designed for use in Ribbon toolbars, standalone toolbars, and property panels.

Usage

<link rel="stylesheet" href="components/spacingpicker/spacingpicker.css">
<script src="components/spacingpicker/spacingpicker.js"></script>

<div id="my-spacing"></div>

<script>
var picker = createSpacingPicker({
    container: "my-spacing",
    value: "1.5",
    onChange: function(preset) {
        console.log("Line height:", preset.lineHeight);
        console.log("After paragraph:", preset.afterParagraph, "px");
    }
});
</script>

Options

OptionTypeDefaultDescription
containerHTMLElement | stringrequiredContainer element or ID
valuestring"1.15"Initial selected preset name
presetsSpacingPreset[]built-in 6Custom preset definitions
showCustombooleantrueShow "Custom Spacing..." link
onChange(preset) => voidCallback on selection
onCustom() => voidCallback for custom link
ribbonModebooleantrueRibbon-compatible rendering

Default Presets

NameLine HeightBeforeAfter
Single1.000
1.151.1508
1.51.508
Double2.008
Compact1.004
Relaxed1.6812

API

MethodDescription
getValue()Returns the currently selected SpacingPreset
setValue(name)Select a preset by name
setPresets(presets)Replace all presets
show()Open the dropdown
hide()Close the dropdown
destroy()Remove from DOM and clean up
getElement()Get the root DOM element

Keyboard

KeyAction
Arrow Down / Arrow UpNavigate items
Enter / SpaceSelect focused item
EscapeClose dropdown

CSS Classes

ClassPurpose
.spacingpickerRoot container
.spacingpicker-triggerDropdown button
.spacingpicker-panelDropdown panel
.spacingpicker-itemPreset item
.spacingpicker-item--selectedSelected state
.spacingpicker-thumbSVG thumbnail
.spacingpicker-customCustom spacing link

SpineMap

Interactive SVG capability/feature map with a central spine, branching sub-nodes, four layout algorithms, zoom/pan, status color coding, cross-branch connections, and integrated editing.

Assets

AssetPath
JScomponents/spinemap/spinemap.js
CSScomponents/spinemap/spinemap.css

Quick Start

<link rel="stylesheet" href="components/spinemap/spinemap.css">
<script src="components/spinemap/spinemap.js"></script>

<div id="my-map" style="width:100%; height:600px;"></div>

<script>
const map = createSpineMap({
    container: document.getElementById("my-map"),
    layout: "vertical",
    data: {
        hubs: [
            {
                id: "admin",
                label: "Tenant Admin",
                status: "available",
                branches: [
                    { id: "rbac", label: "RBAC", status: "available" },
                    { id: "fgac", label: "FGAC", status: "planned", timeframe: "Q3 2026" }
                ]
            },
            {
                id: "apps",
                label: "Apps",
                status: "available",
                branches: [
                    { id: "diagrams", label: "Diagrams", status: "available" },
                    { id: "todo", label: "ToDo", status: "in-progress" },
                    { id: "checklists", label: "Checklists", status: "available" },
                    { id: "workitems", label: "Work Items", status: "planned" }
                ]
            }
        ],
        connections: [
            { from: "fgac", to: "rbac", type: "depends-on" }
        ]
    }
});
</script>

Global Functions

FunctionReturnsDescription
createSpineMap(options)SpineMapCreates and mounts a new SpineMap

Class API

Data

MethodDescription
loadData(data)Replace all hubs and connections
getData()Get current SpineMapData (deep copy)
importJSON(json)Parse JSON string and load as data

Nodes

MethodDescription
addHub(hub, index?)Add a hub to the spine
addBranch(branch, parentId)Add a branch under a hub or another branch
updateNode(nodeId, changes)Update node properties
removeNode(nodeId)Remove node and all children
reparentNode(nodeId, newParentId)Move node to a different parent
getNode(nodeId)Get node data by ID

Connections

MethodDescription
addConnection(conn)Add a cross-branch connection
removeConnection(fromId, toId)Remove a connection
getConnections()Get all connections (deep copy)

Layout

MethodDescription
setLayout(layout)Switch layout: vertical, horizontal, radial, winding
getLayout()Get current layout mode
relayout()Recompute positions without changing layout mode

Zoom & Pan

MethodDescription
zoomIn() / zoomOut()Step zoom
zoomTo(level)Set exact zoom level
fitToView()Auto-fit all nodes in viewport
panTo(nodeId)Center viewport on a node
getZoom()Get current zoom level

Selection

MethodDescription
selectNode(nodeId)Select and highlight a node
clearSelection()Clear selection
getSelectedNode()Get selected node ID

Export

MethodDescription
exportSVG()Returns SVG markup string
exportPNG()Returns Promise<Blob>
exportJSON()Returns JSON data string

Lifecycle

MethodDescription
refresh()Recompute layout and re-render
destroy()Remove from DOM, clean up events
getElement()Get root HTMLElement

Options

OptionTypeDefaultDescription
containerHTMLElementRequired. Mount target
dataSpineMapDataInitial data
layoutstring"vertical"vertical, horizontal, radial, winding
hubSpacingnumber180Pixels between hub centers
branchSpacingnumber50Pixels between branch nodes
branchLengthnumber140Pixels from hub to first branch
editablebooleanfalseEnable all editing modes
sidebarPositionstring"right"left, right, none
sidebarWidthnumber320Sidebar width in pixels
statusColorsobjectOverride default status colors
minZoomnumber0.1Minimum zoom level
maxZoomnumber3.0Maximum zoom level
showToolbarbooleantrueShow toolbar
showConnectionsbooleantrueShow cross-branch connections
showStatusLegendbooleantrueShow status color legend
fitOnLoadbooleantrueAuto-fit on initial render
sizestring"md"sm, md, lg
cssClassstringAdditional CSS class on root
popoverFieldsSpinePopoverFieldConfig[]Custom field configuration for the node popover. When omitted, uses the default fields (Status, Timeframe, Link, Description, Connections)
popoverWidthnumber300Popover width in pixels (overrides size-variant defaults)

Popover Fields

The popoverFields option lets consumers configure which fields appear in the node popover, their order, and what widget renders each field.

SpinePopoverFieldConfig

{
    key: string;             // Node property name or metadata key
    label: string;           // Display label
    type: SpinePopoverFieldType;  // Widget type (see below)
    source?: "property" | "metadata";  // Where to read/write the value
    showInView?: boolean;    // Show in view mode (default: true)
    showInEdit?: boolean;    // Show in edit mode (default: true)
    componentOptions?: Record<string, unknown>;  // Forwarded to component factory
    selectOptions?: { value: string; label: string }[];  // For "select" type
    serialize?: (value: unknown) => string;    // Custom serializer
    deserialize?: (stored: string) => unknown; // Custom deserializer
    renderView?: (value: unknown, node) => HTMLElement;  // Custom view renderer
    required?: boolean;
    hint?: string;           // Help text below field in edit mode
    cssClass?: string;       // Extra CSS class on field wrapper
    width?: "compact" | "full";  // "full" forces min-height for editors
}

Supported Field Types

CategoryTypes
HTML nativetext, number, email, url, tel, textarea, select, checkbox, range
Library pickersdatepicker, timepicker, durationpicker, cronpicker, timezonepicker, periodpicker, sprintpicker, colorpicker, symbolpicker, fontdropdown
Library text/contentrichtextinput, maskedentry, editablecombobox
Library multi-valuemultiselectcombo, tagger, peoplepicker
Library editorscodeeditor, markdowneditor
Built-in specialstatus, connections, link
Fully customcustom

Library component types require the corresponding component JS to be loaded. If unavailable, the field falls back to a plain text input with a console warning.

Source Auto-Detection

If source is not specified, known node properties (label, status, timeframe, link, description, statusLabel, statusColor) default to "property". All other keys default to "metadata" and are stored in node.metadata.

Default Configuration

When popoverFields is omitted, the popover uses this default (identical to previous behavior):

[
    { key: "status",      label: "Status",      type: "status"      },
    { key: "timeframe",   label: "Timeframe",   type: "text"        },
    { key: "link",        label: "Link",        type: "link"        },
    { key: "description", label: "Description", type: "textarea"    },
    { key: "connections", label: "Connections",  type: "connections" }
]

Example: Custom Fields with Metadata

const map = createSpineMap({
    container: document.getElementById("my-map"),
    editable: true,
    popoverFields: [
        { key: "status",   label: "Status",   type: "status"   },
        { key: "timeframe", label: "Timeframe", type: "periodpicker" },
        { key: "priority", label: "Priority",  type: "select",
          source: "metadata",
          selectOptions: [
              { value: "high", label: "High" },
              { value: "medium", label: "Medium" },
              { value: "low", label: "Low" }
          ]
        },
        { key: "owner", label: "Owner", type: "text",
          source: "metadata", hint: "Assignee name" },
        { key: "description", label: "Description",
          type: "richtextinput", width: "full" },
        { key: "connections", label: "Connections",
          type: "connections" }
    ],
    popoverWidth: 380,
    data: { hubs: [ /* ... */ ] }
});

Callbacks

CallbackArgumentsDescription
onNodeClick(node)Node clicked
onNodeDoubleClick(node)Node double-clicked
onNodeHover(node | null)Mouse enter/leave
onNodeAdd(node, parentId)Node added
onNodeChange(nodeId, changes)Node updated
onNodeRemove(nodeId)Node removed
onNodeReparent(nodeId, newParentId)Node moved to new parent
onConnectionAdd(conn)Connection added
onConnectionRemove(connId)Connection removed
onLayoutChange(layout)Layout mode changed
onZoomChange(zoom)Zoom level changed

Data Types

SpineHub

{
    id: string;
    label: string;
    status?: "available" | "in-progress" | "planned" | "not-supported" | "deprecated" | "custom";
    statusLabel?: string;
    statusColor?: string;
    link?: string;
    timeframe?: string;
    description?: string;
    metadata?: Record<string, string>;
    branches: SpineBranch[];
}

SpineBranch

Same as SpineHub but with children?: SpineBranch[] instead of branches.

SpineConnection

{
    from: string;           // node id
    to: string;             // node id
    type: "depends-on" | "works-with" | "blocks" | "enhances";
    label?: string;
}

Keyboard Shortcuts

KeyAction
+ / =Zoom in
-Zoom out
0Fit to view
Arrow keysPan viewport
Shift + arrowsFast pan
EscapeClose popover
Click nodeSelect + show popover
Double-clickOpen popover in edit mode (editable)
Shift + dragDraw connection between nodes (editable)
Drag nodeReposition node (editable)

Accessibility

Connection Types

TypeLine StyleColorUse
depends-onDashedOrangeB requires A
works-withDottedBlueComplementary features
blocksSolidRedA blocks B
enhancesDash-dotGreenA enhances B

Layout Modes

ModeDescription
verticalSpine top-to-bottom, branches left/right
horizontalSpine left-to-right, branches up/down
radialHubs in a circle, branches radiate outward
windingSerpentine S-curve path, branches up/down

Editing Modes

Sidebar (TreeGrid)

When editable: true, a sidebar with a TreeGrid shows the full hierarchy. Edit labels, status, and timeframe inline. Drag rows to reparent nodes. Requires TreeGrid JS to be loaded; falls back to a simple tree list otherwise.

Popover

Click any node to see its details. In editable mode, use Edit/Add Child/Remove buttons. Double-click to open directly in edit mode.

Visual

Drag nodes to reposition. Shift+drag from one node to another to create a connection. Click a connection path to remove it.

Export Formats

FormatMethodDescription
SVGexportSVG()Standalone SVG markup
PNGexportPNG()2x resolution raster via canvas
JSONexportJSON()Full SpineMapData for backup/import

SplitLayout

A split layout container that divides available space into two or more panes separated by draggable dividers. Supports horizontal/vertical orientation, pane collapsing, nested layouts, and state persistence via localStorage.

Assets

AssetPath
CSScomponents/splitlayout/splitlayout.css
JScomponents/splitlayout/splitlayout.js
Typescomponents/splitlayout/splitlayout.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/splitlayout/splitlayout.css">
<script src="components/splitlayout/splitlayout.js"></script>
<script>
    var layout = createSplitLayout({
        orientation: "horizontal",
        panes: [
            { id: "left", initialSize: "30%", minSize: 200, collapsible: true },
            { id: "center", initialSize: "1fr" },
            { id: "right", initialSize: 300, collapsible: true }
        ]
    }, "my-container");

    // Add content to panes
    layout.getPaneElement("left").textContent = "Left pane";
    layout.getPaneElement("center").textContent = "Center pane";
    layout.getPaneElement("right").textContent = "Right pane";
</script>

Options (SplitLayoutOptions)

OptionTypeDefaultDescription
orientation"horizontal" | "vertical"requiredSplit direction
panesSplitPaneConfig[]requiredPane definitions (minimum 2)
dividerSizenumber4Divider thickness in pixels
dividerStyle"line" | "dots" | "handle""line"Visual style of dividers
gutterColorstringCSS colour override for dividers
cssClassstringAdditional CSS classes
persistKeystringlocalStorage key for automatic state persistence
onResizefunctionCalled with current sizes after resize
onCollapsefunctionCalled when a pane is collapsed/expanded
onDividerDblClickfunctionCalled on divider double-click

Pane Config (SplitPaneConfig)

OptionTypeDefaultDescription
idstringrequiredUnique pane identifier
contentHTMLElementInitial content element
initialSizenumber | stringequal splitPixels, "30%", or "1fr"
minSizenumber50Minimum pane size in pixels
maxSizenumberInfinityMaximum pane size in pixels
collapsiblebooleanfalseAllow collapse via double-click or API
collapsedbooleanfalseStart collapsed

API

MethodReturnsDescription
show(containerId?)voidAppend to container (or body)
hide()voidRemove from DOM, keep state
destroy()voidHide, clean up, null references
getElement()HTMLElementRoot DOM element
getPaneElement(paneId)HTMLElementContent div for a pane
setPaneContent(paneId, el)voidReplace pane content
collapsePane(paneId)voidCollapse a pane
expandPane(paneId)voidExpand a collapsed pane
togglePane(paneId)voidToggle collapse state
getSizes()Record<string, number>Current sizes in pixels
setSizes(sizes)voidApply sizes (clamped to min/max)
setOrientation(dir)voidSwitch horizontal/vertical
addPane(config, index?)voidInsert a new pane
removePane(paneId)voidRemove a pane (min 2 required)
getState()SplitLayoutStateSerialisable snapshot
setState(state)voidRestore from snapshot

Convenience Function

createSplitLayout(options, containerId?)  // Create, show, and return

Global Exports

window.SplitLayout
window.createSplitLayout

Size Calculation

Initial sizes are resolved in priority order:

  1. Persisted state from persistKey (if available)
  2. Explicit initialSize values from pane config
  3. Equal distribution of remaining space

Supports pixels (200), percentages ("30%"), and fractional units ("1fr").

Keyboard Accessibility

KeyAction
Left/Right ArrowMove divider 10px (horizontal)
Up/Down ArrowMove divider 10px (vertical)
HomeCollapse the left/top adjacent pane
EndCollapse the right/bottom adjacent pane
EnterToggle collapse on the smaller adjacent pane

Accessibility

Examples

Two-pane editor layout

var layout = createSplitLayout({
    orientation: "horizontal",
    panes: [
        { id: "explorer", initialSize: 250, minSize: 180, collapsible: true },
        { id: "editor", initialSize: "1fr", minSize: 300 }
    ],
    persistKey: "editor-layout"
}, "app-container");

Nested three-pane IDE layout

var outer = createSplitLayout({
    orientation: "horizontal",
    panes: [
        { id: "sidebar", initialSize: "20%", collapsible: true },
        { id: "main", initialSize: "1fr" }
    ]
}, "ide-container");

var inner = new SplitLayout({
    orientation: "vertical",
    panes: [
        { id: "editor", initialSize: "70%", minSize: 200 },
        { id: "terminal", initialSize: "30%", minSize: 100, collapsible: true }
    ]
});

outer.setPaneContent("main", inner.getElement());
inner.show();

See specs/splitlayout.prd.md for the complete specification.


SprintPicker

Agile sprint selector with list and calendar views. Computes sprints from anchor date, sprint length, and week start day. Supports configurable naming and colored sprint band overlays.

Usage

CSS

<link rel="stylesheet" href="components/sprintpicker/sprintpicker.css">

JavaScript

<script src="components/sprintpicker/sprintpicker.js"></script>

Basic (List View)

<div id="my-sprint-picker"></div>

<script>
const picker = createSprintPicker("my-sprint-picker", {
    anchorDate: new Date(2026, 0, 5), // Mon Jan 5 2026
    sprintLength: 2,
    onSelect: function(value) {
        console.log("Selected:", value.sprintName, value.date);
    }
});
</script>

Calendar View

<div id="calendar-sprint"></div>

<script>
const picker = createSprintPicker("calendar-sprint", {
    anchorDate: "2026-01-05",
    sprintLength: 1,
    viewMode: "calendar",
    onSelect: function(value) {
        console.log("Sprint:", value.sprintName);
    }
});
</script>

Custom Naming

<div id="custom-sprint"></div>

<script>
const picker = createSprintPicker("custom-sprint", {
    anchorDate: new Date(2026, 0, 5),
    sprintLength: 2,
    sprintNaming: function(index, start, end) {
        return "Iteration " + (index + 1);
    }
});
</script>

Options

OptionTypeDefaultDescription
anchorDateDate | stringrequiredFirst sprint start date
sprintLength1–82Sprint duration in weeks
weekStartDay0–61 (Mon)Week start day
mode"start" | "end""start"Date resolution mode
maxSprintsnumber26Maximum sprints to compute
sprintNamingSprintNaming"sprint"Naming mode
viewMode"list" | "calendar""list"Initial view
size"sm" | "md" | "lg""md"Size variant
disabledbooleanfalseDisable the component
readonlybooleanfalseRead-only mode
placeholderstring"Select sprint…"Input placeholder
onSelect(value: SprintValue) => voidFires on selection
onChange(value: SprintValue | null) => voidFires on value change
onOpen() => voidFires when dropdown opens
onClose() => voidFires when dropdown closes
keyBindingsRecord<string, string>Override default key combos

Sprint Naming Modes

ModeExample Output
"sprint"Sprint 1, Sprint 2, …
"short"S1, S2, …
"monthly"Jan Sprint 1, Feb Sprint 2, …
callbackCustom function (index, start, end) => string

SprintValue

interface SprintValue {
    sprintIndex: number;   // 0-based
    sprintName: string;    // Display name
    startDate: Date;       // Sprint start (Monday)
    endDate: Date;         // Sprint end (Friday)
    date: Date;            // Resolved date based on mode
}

Public API

MethodReturnsDescription
getValue()SprintValue | nullGet current selection
setValue(value)voidSet selection programmatically
getFormattedValue()stringGet display string
open()voidOpen dropdown
close()voidClose dropdown
enable()voidEnable component
disable()voidDisable component
setReadonly(flag)voidToggle read-only
setMode(mode)voidSwitch start/end mode
setSprintLength(weeks)voidChange sprint length (recomputes)
setAnchorDate(date)voidChange anchor date (recomputes)
getSprintAtDate(date)SprintInfo | nullFind sprint containing date
getSprints()SprintInfo[]Get all computed sprints
destroy()voidClean up DOM and listeners

Keyboard

List View

KeyAction
ArrowUpPrevious sprint
ArrowDownNext sprint
HomeFirst sprint
EndLast sprint
Enter / SpaceSelect focused sprint
vToggle to calendar view
EscapeClose dropdown

Calendar View

KeyAction
ArrowLeft/RightPrevious/next day
ArrowUp/DownPrevious/next week
PageUpPrevious month
PageDownNext month
Enter / SpaceSelect sprint at focused day
vToggle to list view
EscapeClose dropdown

Sprint Computation

Sprints are computed from the anchor date:

start[i] = anchorDate + (i × sprintLength × 7) days
end[i]   = start[i] + (sprintLength × 7 - 3) days  // Friday

Example (2-week sprints, Mon Jan 5 2026):

Dropdown Positioning

The dropdown is portaled to document.body with position: fixed and z-index: 2050, ensuring it renders above FormDialog overlays (z-index 2001).


StackLayout

A vertically stacked panel layout where each panel has a collapsible header (with icon, title, and chevron toggle) and a content area. Draggable dividers between panels allow resizing. When a panel is collapsed, only its 28px header is visible and the remaining panels expand proportionally.

Assets

AssetPath
CSScomponents/stacklayout/stacklayout.css
JScomponents/stacklayout/stacklayout.js
Typescomponents/stacklayout/stacklayout.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/stacklayout/stacklayout.css">
<script src="components/stacklayout/stacklayout.js"></script>
<script>
    var propsEl = document.createElement("div");
    propsEl.textContent = "Properties content";

    var relsEl = document.createElement("div");
    relsEl.textContent = "Relationships content";

    var stack = createStackLayout({
        container: document.getElementById("sidebar-content"),
        panels: [
            { id: "properties", title: "Properties", icon: "bi-gear", content: propsEl },
            { id: "relationships", title: "Relationships", icon: "bi-diagram-3", content: relsEl, collapsed: true },
        ],
        resizable: true,
        onResize: function(sizes) { console.log("Sizes:", sizes); },
        onCollapse: function(id, collapsed) { console.log(id, collapsed); },
    });
</script>

Options (StackLayoutOptions)

OptionTypeDefaultDescription
containerHTMLElementrequiredContainer element to render into
panelsStackedPanelConfig[]requiredPanel definitions, in order
resizablebooleantrueWhether dividers are draggable
orientation"vertical""vertical"Stack direction (vertical only for now)
onResizefunction--Called with panel size percentages after resize
onCollapsefunction--Called when a panel is collapsed or expanded

Panel Config (StackedPanelConfig)

OptionTypeDefaultDescription
idstringrequiredUnique panel identifier
titlestringrequiredHeader title text
iconstring--Bootstrap Icons class (e.g. "bi-gear")
contentHTMLElementrequiredContent element
collapsedbooleanfalseStart collapsed
collapsiblebooleantrueWhether panel can be collapsed
minHeightnumber50Minimum panel height in pixels

API

MethodReturnsDescription
getPanel(id)PanelHandle | nullGet a panel sub-handle
collapsePanel(id)voidCollapse a panel by ID
expandPanel(id)voidExpand a panel by ID
setSizes(percentages)voidSet expanded panel sizes as percentages
destroy()voidRemove from DOM, clean up
getElement()HTMLElementRoot DOM element

Panel Handle

Returned by getPanel(id):

MethodDescription
setContent(el)Replace panel content
collapse()Collapse this panel
expand()Expand this panel

Global Export

window.createStackLayout

Keyboard Accessibility

KeyAction
Enter / SpaceToggle collapse on focused panel header
TabMove focus between panel headers and dividers

Accessibility

Visual Layout

+---------------------+
| > Properties    [-] |  <-- Collapsible header with chevron
|                     |
|  Name: Deploy Plan  |  <-- Panel 1 content
|  Type: Checklist    |
|                     |
+- - - - - - - - - - -+  <-- 4px draggable divider
| > Relationships [-] |  <-- Collapsible header
|                     |
|  derived_from (1)   |  <-- Panel 2 content
|  owned_by (1)       |
|                     |
+---------------------+

When a panel is collapsed, only its 28px header row is visible. The remaining expanded panels redistribute to fill available space.

Composition with Sidebar

var rightSidebar = createDockedSidebar({ position: "right", width: 380 });
var stack = createStackLayout({
    container: rightSidebar.getContentElement(),
    panels: [
        { id: "properties", title: "Properties", content: propsEl },
        { id: "relationships", title: "Relationships", content: relsEl, collapsed: true },
    ],
    resizable: true,
});

See specs/stackable-sidebars.req.md for the full requirement document.


StatusBadge

Colour-coded pills/dots communicating process or system state with animated pulse and click-for-detail support. Supports seven built-in statuses plus a custom mode, three visual variants, and four sizes.

Assets

AssetPath
CSScomponents/statusbadge/statusbadge.css
JScomponents/statusbadge/statusbadge.js

Quick Start

<link rel="stylesheet" href="components/statusbadge/statusbadge.css">
<script src="components/statusbadge/statusbadge.js"></script>

<div id="status-container"></div>

<script>
    // Factory function — creates and shows in one call
    var badge = createStatusBadge("status-container", {
        status: "operational",
        variant: "indicator"
    });

    // Update status dynamically
    badge.setStatus("degraded");

    // Update label text
    badge.setLabel("Service Degraded");

    // Toggle pulse animation
    badge.setPulse(false);
</script>

API

createStatusBadge(containerId, options)

Creates a StatusBadge instance and appends it to the specified container. Returns the instance for further manipulation.

var badge = createStatusBadge("my-container", {
    status: "in-progress",
    variant: "pill",
    clickable: true,
    onClick: function() { console.log("Clicked!"); }
});

StatusBadgeOptions

OptionTypeDefaultDescription
statusstringRequiredOne of: "operational", "degraded", "down", "in-progress", "failed", "maintenance", "unknown", "custom"
variantstring"indicator"Visual variant: "dot", "pill", or "indicator"
labelstringAutoOverride the auto-generated label text
customColorstringCSS colour for status="custom"
pulsebooleanAutoEnable/disable pulse animation. Auto-enables for operational and in-progress
clickablebooleanfalseWhether the badge responds to click events
onClickfunctionCallback invoked on click
tooltipstringTooltip text (title attribute)
sizestring"md"Size variant: "xs", "sm", "md", "lg"
cssClassstringAdditional CSS class(es)

Instance Methods

MethodDescription
show(containerId)Append the badge to a container element
hide()Remove from DOM (preserves state)
destroy()Remove from DOM and release all references
getElement()Returns the root DOM element (or null)
setStatus(status)Update the displayed status
setLabel(label)Update the label text
setPulse(enabled)Enable or disable pulse animation

Status Values

StatusColourDefault LabelAuto-Pulse
operationalGreen ($green-600)OperationalYes
degradedYellow ($yellow-500)DegradedNo
downRed ($red-600)DownNo
in-progressBlue ($blue-600)In ProgressYes
failedDark Red ($red-800)FailedNo
maintenanceOrange ($orange-500)MaintenanceNo
unknownGrey ($gray-400)UnknownNo
customUser-definedUser-definedNo

Variants

Dot (variant: "dot")

Displays only the coloured dot with no text label. Useful for compact status indicators in tables or lists.

Pill (variant: "pill")

Displays only the label text with a coloured background. The label inherits the status colour as its background and uses light text for contrast.

Indicator (variant: "indicator")

Default variant. Displays both the coloured dot and the text label side-by-side. Provides the most informative visual representation.

Features

Accessibility

Dependencies


StatusBar

A fixed-to-bottom viewport status bar with configurable label/value regions separated by pipe dividers. Text is natively selectable for Ctrl+C copying.

Quick Start

<link rel="stylesheet" href="https://theme.priyavijai-kalyan2007.workers.dev/components/statusbar/statusbar.css">
<script src="https://theme.priyavijai-kalyan2007.workers.dev/components/statusbar/statusbar.js"></script>

<script>
    var bar = createStatusBar({
        regions: [
            { id: "status", icon: "bi-circle-fill", value: "Connected" },
            { id: "env", label: "Environment:", value: "Production" },
            { id: "user", label: "User:", value: "jsmith" },
            { id: "version", value: "v2.4.1" }
        ]
    });

    // Update a value dynamically
    bar.setValue("user", "adoe");

    // Read a value
    var user = bar.getValue("user"); // "adoe"

    // Get all text (including dividers)
    var text = bar.getAllText();
    // "Connected | Environment: Production | User: adoe | v2.4.1"
</script>

Configuration Options

OptionTypeDefaultDescription
regionsStatusBarRegion[]RequiredRegions to display
size"sm" | "md" | "lg""md"Height variant (24/28/34px)
backgroundColorstring$gray-800Background colour
textColorstring$gray-300Text colour
labelColorstring$gray-400Label colour
fontSizestringFont size override
zIndexnumber1040CSS z-index
cssClassstringAdditional CSS class(es)
showDividersbooleantrueShow pipe dividers
dividerCharstring"|"Divider character

Region Definition

PropertyTypeDescription
idstringRequired. Unique identifier
labelstringOptional semi-bold label
valuestringOptional value text
iconstringOptional Bootstrap Icons class (e.g., "bi-circle-fill")
minWidthstringOptional minimum width (CSS value)

Instance Methods

MethodDescription
show()Append to body, set --statusbar-height
hide()Remove from DOM, clear CSS property
destroy()Hide and release all references
setValue(id, value)Update region value text
getValue(id)Get region value text
setIcon(id, cls)Update region icon class
getAllText()Get full bar text with dividers
addRegion(region, index?)Add a region dynamically
removeRegion(id)Remove a region by ID
isVisible()Check visibility state
getElement()Returns the root DOM element (or null)

Size Variants

SizeHeightFont Size
sm24px~11.5px
md28px12.25px
lg34px14px (base)

CSS Custom Property

When visible, the bar sets --statusbar-height on <html>. Other components can use:

.my-fixed-element {
    bottom: var(--statusbar-height, 0px);
}

Accessibility

Dependencies


Multi-Stage Stepper (Wizard)

Linear or non-linear step progression UI for complex multi-step processes with validation gates, save-as-draft, step summary, and completion percentage.

Assets

AssetPath
CSScomponents/stepper/stepper.css
JScomponents/stepper/stepper.js
Typescomponents/stepper/stepper.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/stepper/stepper.css">
<script src="components/stepper/stepper.js"></script>
<script>
    var stepper = createStepper({
        container: document.getElementById("my-wizard"),
        steps: [
            { label: "Account", description: "Create your account", content: step1El },
            { label: "Profile", description: "Set up your profile", content: step2El },
            { label: "Confirm", description: "Review and submit", content: step3El }
        ],
        onFinish: function() { console.log("Done!"); }
    });
</script>

API

createStepper(options): StepperHandle

StepperOptions

OptionTypeDefaultDescription
containerHTMLElementrequiredMount target element
stepsStepConfig[]requiredStep definitions
orientation"horizontal" | "vertical""horizontal"Layout direction
nonLinearbooleanfalseAllow clicking any step freely
showProgressbooleantrueShow completion percentage bar
showSaveAsDraftbooleanfalseShow "Save as Draft" button
finishLabelstring"Finish"Label for final step's button
size"sm" | "md" | "lg""md"Size variant
cssClassstringExtra CSS class(es)
onStepChange(from, to) => voidStep change callback
onFinish() => voidFinal step completion callback
onSaveAsDraft() => voidSave-as-draft callback

StepConfig

PropertyTypeDescription
labelstringDisplay label (required)
descriptionstringDescription text below label
iconstringBootstrap Icons class for indicator
contentHTMLElementContent element for this step's body
summarystringSummary text shown when step is completed
validate() => boolean | Promise<boolean>Validation gate before advancing
optionalbooleanWhether the step can be skipped

StepperHandle

MethodReturnsDescription
goToStep(index)Promise<boolean>Navigate to step (validates in linear mode)
nextStep()Promise<boolean>Advance to next step (validates)
prevStep()voidGo back one step
getActiveStep()numberCurrent step index
setStepState(i, state)voidSet step to pending/active/completed/error/skipped
getCompletionPercent()numberPercentage of completed steps
getElement()HTMLElementRoot DOM element
destroy()voidTear down DOM

Step States

StateMarkerDescription
pendingGrey numberNot yet reached
activeBlue filledCurrent step
completedGreen checkSuccessfully completed
errorRed exclamationValidation failed
skippedGrey dashedSkipped (optional step)

Keyboard

KeyAction
Enter / SpaceClick focused step indicator
TabMove focus between step indicators and buttons

Accessibility


SymbolPicker

A grid-based symbol and icon picker for inserting Unicode characters and Bootstrap Icons. Includes categorized tabs, search filtering, recently-used tracking via localStorage, hover preview, and full keyboard navigation. Supports inline and popup display modes.

Purpose and Use Cases

Quick Start

Inline Mode

<!-- Dependencies -->
<link rel="stylesheet" href="css/custom.css">
<link rel="stylesheet" href="icons/bootstrap-icons.css">
<link rel="stylesheet" href="components/symbolpicker/symbolpicker.css">

<!-- Component -->
<div id="my-symbols"></div>
<script src="components/symbolpicker/symbolpicker.js"></script>
<script>
    createSymbolPicker("my-symbols", {
        inline: true,
        mode: "both",
        onInsert: (sym) => console.log("Inserted:", sym.char, sym.name)
    });
</script>

Popup Mode

<button id="sym-trigger">Insert Symbol</button>
<div id="sym-host"></div>
<script src="components/symbolpicker/symbolpicker.js"></script>
<script>
    createSymbolPicker("sym-host", {
        inline: false,
        triggerElement: document.getElementById("sym-trigger"),
        onInsert: (sym) => editor.insertText(sym.char)
    });
</script>

ES Module

import { createSymbolPicker } from "./components/symbolpicker/symbolpicker.js";

const picker = createSymbolPicker("my-symbols", {
    mode: "unicode",
    showRecent: true,
    onInsert: (sym) => insertCharacter(sym.char)
});

Configuration Options

OptionTypeDefaultDescription
mode"unicode" | "icons" | "both""both"Which symbol sets to display
categoriesSymbolCategory[]Built-in setOverride default categories
valuestringundefinedInitially selected symbol code
showRecentbooleantrueShow recently used section
maxRecentnumber20Maximum recent items to remember
showPreviewbooleantrueShow enlarged preview on hover/select
showSearchbooleantrueShow the search input
columnsnumber12Number of grid columns
cellSizenumber32Cell size in pixels
inlinebooleanfalseRender inline (true) or as popup (false)
popupPosition"bottom-start" | "bottom-end" | "top-start" | "top-end""bottom-start"Popup position relative to trigger
triggerElementHTMLElementundefinedCustom trigger element for popup mode
size"mini" | "sm" | "default" | "lg""default"Size variant
disabledbooleanfalseDisable the component
onSelectfunctionundefinedCalled when a symbol is highlighted
onInsertfunctionundefinedCalled when a symbol is inserted (double-click or button)
onOpenfunctionundefinedCalled when the popup opens
onClosefunctionundefinedCalled when the popup closes
keyBindingsPartial<Record<string, string>>undefinedOverride default keyboard bindings

SymbolItem

PropertyTypeDescription
charstringThe character ("α") or icon class ("bi-house")
namestringHuman-readable name ("Greek Small Letter Alpha")
codestringUnicode point ("U+03B1") or icon class ("bi-house")
categorystringCategory id this item belongs to ("greek", "common")

SymbolCategory

PropertyTypeDescription
idstringMachine id used for filtering
labelstringHuman-readable label for the tab
iconstringOptional Bootstrap Icon class for the tab
itemsSymbolItem[]Items belonging to this category

Instance Methods

MethodReturnsDescription
show(containerId?)voidMount into a container element
hide()voidRemove from DOM, keep state
destroy()voidRemove from DOM and clean up all resources
open()voidOpen the popup panel
close()voidClose the popup panel
getValue()stringSelected symbol's code, or "" if none
getSelectedSymbol()SymbolItem | nullSelected symbol object, or null
setMode(mode)voidSwitch to "unicode" or "icons" mode
isOpen()booleanWhether the popup is currently open
enable()voidEnable the component
disable()voidDisable the component; closes popup if open
getElement()HTMLElement | nullRoot DOM element

Built-in Categories

Unicode Categories (~500 symbols)

CategoryIdDescription
Latin ExtendedlatinExtended Latin characters and diacritics
Greek LettersgreekGreek alphabet characters
Math SymbolsmathMathematical operators and notation
ArrowsarrowsDirectional arrow characters
CurrencycurrencyCurrency signs from around the world
PunctuationpunctuationSpecial punctuation marks
Box & GeometricboxBox-drawing and geometric shapes
Emoji & DingbatsemojiCommon emoji and dingbat characters

Bootstrap Icons Categories (~200 icons)

CategoryIdDescription
Common Actionsico-commonFrequently used action icons
Arrows & Navigationico-arrowsDirectional and navigation icons
Files & Foldersico-filesFile and folder management icons
Communicationico-commChat, mail, and messaging icons
Mediaico-mediaAudio, video, and playback icons
People & Socialico-peopleUser and social interaction icons
Charts & Dataico-chartsData visualization icons
Alerts & Statusico-alertsWarning, info, and status icons
Technologyico-techHardware and technology icons
Miscellaneousico-miscOther general-purpose icons

Icon Auto-Discovery

The SymbolPicker automatically scans all loaded CSS stylesheets at initialization to discover available icons. This works for both Bootstrap Icons and Font Awesome icons.

How It Works

  1. On first instantiation, the picker scans document.styleSheets for CSS rules matching .bi-*::before and .fa-*::before selectors.
  2. Discovered icons are automatically categorized into heuristic groups (Arrows, Files, Communication, Media, People, Charts, Alerts, Technology, Commerce, Places, General).
  3. Results are cached at the module level — subsequent picker instances reuse the cache.
  4. If no icon stylesheets are detected (e.g., Bootstrap Icons CSS is not loaded), the picker falls back to the built-in curated set of ~178 icons.

Font Awesome Support

When Font Awesome CSS is loaded, the picker detects the available style class (fa-solid, fa-regular, fa-brands, fa-light, or fa-thin) by probing the DOM. Icons render with the correct style prefix automatically.

<!-- Load Font Awesome alongside Bootstrap Icons -->
<link rel="stylesheet" href="icons/bootstrap-icons.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">

<script>
    // SymbolPicker will auto-discover both BI and FA icons
    createSymbolPicker("my-picker", { mode: "icons" });
</script>

Discovery Limitations

Keyboard Interactions

KeyAction
ArrowLeftMove to previous cell
ArrowRightMove to next cell
ArrowUpMove up one row
ArrowDownMove down one row
EnterInsert the highlighted symbol
EscapeClose the popup
HomeJump to first cell
EndJump to last cell
Ctrl+FFocus the search input

Key bindings can be overridden via the keyBindings option using action names: moveLeft, moveRight, moveUp, moveDown, confirmInsert, closePopup, jumpToFirst, jumpToLast, focusSearch.

Recently Used

When showRecent: true (the default), the picker displays a "Recently Used" row above the category grid. Selections are persisted to localStorage under the key symbolpicker-recent. Configure the maximum count with maxRecent (default 20).

Size Variants

<script>createSymbolPicker("sm-picker", { inline: true, size: "sm" });</script>
<script>createSymbolPicker("default-picker", { inline: true });</script>
<script>createSymbolPicker("lg-picker", { inline: true, size: "lg" });</script>

Size classes applied: symbolpicker-sm, symbolpicker-lg.

Accessibility

Dependencies

DependencyRequiredNotes
Bootstrap 5 CSSYesFor base styling variables
Bootstrap 5 JSNoNot used by this component
Bootstrap IconsYesFor tab icons and the icons mode categories
Enterprise Theme CSSYesFor theme variable overrides

TabbedPanel

A dockable, collapsible, resizable tabbed panel component for grouping related content into tabs. Supports docking to top/bottom viewport edges, free-positioned floating with drag-based positioning, collapsing to a 32px strip, resizing via drag handles, configurable tab bar position (top/left/bottom/right), and drag-to-dock with visual drop zones.

Assets

AssetPath
CSScomponents/tabbedpanel/tabbedpanel.css
JScomponents/tabbedpanel/tabbedpanel.js
Typescomponents/tabbedpanel/tabbedpanel.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/tabbedpanel/tabbedpanel.css">
<script src="components/tabbedpanel/tabbedpanel.js"></script>
<script>
    var panel = createTabbedPanel({
        tabs: [
            { id: "terminal", title: "Terminal", icon: "bi-terminal" },
            { id: "output", title: "Output", icon: "bi-journal-text" },
            { id: "problems", title: "Problems", icon: "bi-exclamation-triangle" }
        ],
        dockPosition: "bottom",
        height: 250,
        onTabSelect: function(tabId) { console.log("Selected:", tabId); }
    });
</script>

Options

OptionTypeDefaultDescription
idstringautoUnique identifier
tabsTabDefinition[][]Initial tab definitions
tabBarPosition"top" | "left" | "bottom" | "right""top"Tab bar position within the panel
tabTitleMode"icon" | "text" | "icon-text""icon-text"Tab display style
mode"docked" | "floating""docked"Positioning mode
dockPosition"top" | "bottom""bottom"Dock edge
collapsiblebooleantrueEnable collapse to strip
resizablebooleantrueEnable resize handles
draggablebooleantrueEnable floating drag
heightnumber250Panel height (docked/floating)
minHeightnumber100Minimum resize height
maxHeightnumber600Maximum resize height
widthnumber500Floating width
minWidthnumber300Minimum floating width
maxWidthnumber1200Maximum floating width
collapsedHeightnumber32Height when collapsed
titlestringPanel title (shown in title bar and collapsed strip). When omitted, the active tab's title is used as fallback.
showTitleBarbooleanautoShow title bar (defaults to true for floating, false for docked)
collapsedbooleanfalseStart collapsed
floatXnumber100Initial floating X position
floatYnumber100Initial floating Y position
backgroundstringCSS background colour override
foregroundstringCSS text colour override
fontFamilystringCSS font family override
fontSizestringCSS font size override
cssClassstringAdditional CSS classes
zIndexnumberCSS z-index override
onTabSelectfunctionTab selection callback
onTabDeselectfunctionTab deselection callback
onTabClosefunctionTab close callback (return false to cancel)
onCollapsefunctionCollapse/expand callback
onResizefunctionResize complete callback
onModeChangefunctionMode change callback
onBeforeClosefunctionBefore close callback (return false to cancel)
onClosefunctionAfter close callback

TabDefinition

interface TabDefinition {
    id: string;          // Unique tab identifier
    title: string;       // Display title
    icon?: string;       // Bootstrap Icons class (e.g., "bi-terminal")
    closable?: boolean;  // Show close button (default: false)
    content?: HTMLElement | string;  // Initial tab content
    cssClass?: string;   // Additional CSS class for the tab panel
    data?: Record<string, unknown>;  // User data passed to callbacks
    disabled?: boolean;  // Disabled state (default: false)
}

API

Tab Management

MethodReturnsDescription
addTab(tab)voidAdd a tab dynamically
removeTab(tabId)voidRemove a tab (fires onTabClose)
selectTab(tabId)voidActivate a tab
getActiveTabId()string | nullCurrent active tab ID
getTabCount()numberNumber of tabs
getTabDefinition(tabId)TabDefinition | undefinedGet tab config
getTabContentElement(tabId)HTMLElement | nullGet tab panel DOM element
setTabTitle(tabId, title)voidUpdate tab title text
setTabIcon(tabId, icon)voidUpdate tab icon class
setTabDisabled(tabId, disabled)voidEnable or disable a tab

Mode & Position

MethodReturnsDescription
dock(position)voidSwitch to docked mode at top or bottom
float(x?, y?)voidSwitch to floating mode
getMode()stringCurrent mode
getDockPosition()stringCurrent dock position

Collapse

MethodReturnsDescription
collapse()voidCollapse to strip
expand()voidExpand from strip
toggleCollapse()voidToggle collapse state
isCollapsed()booleanWhether collapsed

Size

MethodReturnsDescription
setHeight(h)voidSet panel height
setWidth(w)voidSet panel width (floating)
getHeight()numberCurrent height
getWidth()numberCurrent width

Lifecycle

MethodReturnsDescription
show(container?)voidMount and display
hide()voidRemove from DOM, keep state
destroy()voidFull cleanup
isVisible()booleanWhether visible
getId()stringPanel identifier
getRootElement()HTMLElementRoot DOM element
setTitle(title)voidUpdate panel title
getTitle()stringResolved title (explicit title > active tab title > "Panel")

Convenience Functions

createTabbedPanel(options, container?)       // Create, show, and return
createDockedTabbedPanel(options, container?)  // Shorthand for docked mode
createFloatingTabbedPanel(options, container?) // Shorthand for floating mode

Global Exports

window.TabbedPanel
window.TabbedPanelManager
window.createTabbedPanel
window.createDockedTabbedPanel
window.createFloatingTabbedPanel

CSS Custom Properties

When docked, the panel sets a CSS custom property on <html>:

PropertySet When
--tabbedpanel-top-heightDocked top
--tabbedpanel-bottom-heightDocked bottom

Properties are cleared when the panel is hidden or undocked.

Keyboard Accessibility

KeyAction
TabMove focus into the tab bar
Arrow Left/RightNavigate between tabs (horizontal bar)
Arrow Up/DownNavigate between tabs (vertical bar)
Home / EndFirst / last tab
Enter / SpaceActivate tab, expand collapsed strip
EscapeCollapse panel
Arrow keys on resize handleResize by 10px per press

Examples

Docked bottom with icon-text tabs

var panel = createTabbedPanel({
    tabs: [
        { id: "terminal", title: "Terminal", icon: "bi-terminal" },
        { id: "output", title: "Output", icon: "bi-journal-text" },
        { id: "problems", title: "Problems", icon: "bi-exclamation-triangle", closable: true }
    ],
    dockPosition: "bottom",
    height: 250,
    onTabSelect: function(tabId) { console.log("Selected:", tabId); }
});
// Add content to a tab
var termEl = panel.getTabContentElement("terminal");
termEl.style.padding = "0.5rem";
termEl.textContent = "$ npm run build";

Floating panel with title bar

var props = createFloatingTabbedPanel({
    title: "Properties",
    tabs: [
        { id: "props", title: "Properties", icon: "bi-sliders" },
        { id: "events", title: "Events", icon: "bi-lightning" },
        { id: "styles", title: "Styles", icon: "bi-palette" }
    ],
    floatX: 400,
    floatY: 120,
    width: 360,
    height: 300
});

Dynamic tab management

// Add a tab at runtime
panel.addTab({
    id: "debug-console",
    title: "Debug Console",
    icon: "bi-bug",
    closable: true
});

// Prevent tab close with confirmation
var panel = createTabbedPanel({
    tabs: [ /* ... */ ],
    onTabClose: function(tabId) {
        return confirm("Close " + tabId + "?");
    }
});

// Remove a tab
panel.removeTab("debug-console");

Collapse and expand

panel.collapse();   // Collapse to 32px strip
panel.expand();     // Restore full size
panel.toggleCollapse();

panel.onCollapse = function(collapsed) {
    console.log(collapsed ? "Panel collapsed" : "Panel expanded");
};

Tab bar position variants

// Tab bar on the left (vertical)
createTabbedPanel({
    tabBarPosition: "left",
    tabTitleMode: "icon",
    tabs: [ /* ... */ ]
});

// Tab bar on the bottom
createTabbedPanel({
    tabBarPosition: "bottom",
    tabs: [ /* ... */ ]
});

Drag-to-dock

// Start floating, drag to top/bottom edge to dock
var panel = createFloatingTabbedPanel({
    title: "Dockable Panel",
    tabs: [ /* ... */ ],
    onModeChange: function(mode) {
        console.log("Mode changed to:", mode);
    }
});

See specs/tabbedpanel.prd.md for the complete specification.


Tagger

Combined freeform and controlled-vocabulary tag input with autocomplete, colored chips, taxonomy categories, and validation.

Quick Start

<link rel="stylesheet" href="components/tagger/tagger.css">
<script src="components/tagger/tagger.js"></script>
<script>
    var tagger = createTagger("my-container", {
        taxonomy: [
            { id: "priority", label: "Priority", color: "#c92a2a", values: ["High", "Medium", "Low"] },
            { id: "status", label: "Status", color: "#2b8a3e", values: ["Open", "Closed", "Pending"] }
        ],
        allowFreeform: true,
        onAdd: function(tag) { console.log("Added:", tag); },
        onRemove: function(tag) { console.log("Removed:", tag); }
    });
</script>

Assets

AssetPath
CSScomponents/tagger/tagger.css
JScomponents/tagger/tagger.js
Typescomponents/tagger/tagger.d.ts

Requires: Bootstrap CSS, Bootstrap Icons CSS.

Options (TaggerOptions)

PropertyTypeDefaultDescription
tagsTagItem[][]Initial tags to display
taxonomyTagCategory[][]Taxonomy categories with allowed values
allowFreeformbooleantrueAllow creating tags not in taxonomy
maxTagsnumberundefinedGlobal maximum number of tags
placeholderstring"Add tag..."Input placeholder text
colorMode"category" | "hash" | "none""category"Chip colouring strategy
validator(value: string) => boolean | stringundefinedCustom validation function
duplicateMode"reject" | "ignore""reject"How to handle duplicate tags
showCategoryBadgebooleantrueShow category badge on chips
size"sm" | "default" | "lg""default"Size variant
disabledbooleanfalseDisable all interaction
readonlybooleanfalseRead-only mode (shows chips, no editing)
cssClassstringundefinedAdditional CSS class for root element
maxDropdownItemsnumber50Max items shown in dropdown
minFilterLengthnumber0Min chars before dropdown opens
filterDebounceMsnumber150Debounce delay for filter input (ms)
onAdd(tag: TagItem) => voidundefinedCallback when a tag is added
onRemove(tag: TagItem) => voidundefinedCallback when a tag is removed
onChange(tags: TagItem[]) => voidundefinedCallback when tags change
onValidationError(value: string, error: string) => voidundefinedCallback on validation error

Interfaces

TagCategory

PropertyTypeDescription
idstringUnique category identifier
labelstringDisplay label
colorstringOptional colour for badge and border
valuesstring[]Allowed tag values in this category
allowFreeformbooleanOverride freeform permission per-category
iconstringOptional Bootstrap Icon class
maxTagsnumberOptional per-category tag limit

TagItem

PropertyTypeDescription
valuestringTag value text
categorystringOptional category ID
colorstringResolved display colour
dataRecord<string, unknown>Optional custom data

API Methods

MethodSignatureDescription
show(containerId: string) => voidRender into a container element
hide() => voidRemove from DOM, keep state
destroy() => voidFull teardown, remove listeners
addTag(value: string, category?: string) => booleanProgrammatically add a tag
removeTag(value: string, category?: string) => booleanProgrammatically remove a tag
getTags() => TagItem[]Get current tags array
setTags(tags: TagItem[]) => voidReplace all tags
clearTags() => voidRemove all tags
hasTag(value: string, category?: string) => booleanCheck if a tag exists
getTagsByCategory(categoryId: string) => TagItem[]Get tags in a category
enable() => voidEnable the component
disable() => voidDisable the component
focus() => voidFocus the input

Color Modes

Keyboard Interaction

KeyAction
ArrowDownOpen dropdown / highlight next item
ArrowUpOpen dropdown / highlight previous item
EnterSelect highlighted item or create freeform tag
EscapeClose dropdown
Backspace (input empty)Remove last tag
HomeHighlight first item (dropdown open)
EndHighlight last item (dropdown open)
TabClose dropdown and move focus

Accessibility

Size Variants

SizeContainer PaddingChip FontInput Font
sm3px 4px0.75rem$font-size-sm
default4px 6px$font-size-sm$font-size-base
lg6px 8px$font-size-base$font-size-lg

Paste Handling

Pasting comma-separated values (e.g., "tag1, tag2, tag3") automatically splits and adds each value through the validation pipeline.

Example: Full-Featured

<div id="full-tagger"></div>
<script>
    var tagger = createTagger("full-tagger", {
        tags: [
            { value: "High", category: "priority" },
            { value: "custom-label" }
        ],
        taxonomy: [
            { id: "priority", label: "Priority", color: "#c92a2a",
              values: ["Critical", "High", "Medium", "Low"], maxTags: 1 },
            { id: "type", label: "Type", color: "#1c7ed6",
              values: ["Bug", "Feature", "Enhancement", "Docs"] }
        ],
        maxTags: 10,
        allowFreeform: true,
        colorMode: "category",
        duplicateMode: "reject",
        size: "default",
        validator: function(v) {
            return v.length >= 2 || "Tag must be at least 2 characters";
        },
        onChange: function(tags) {
            console.log("Tags:", tags);
        }
    });
</script>

See specs/tagger.prd.md for the full specification.


ThemeToggle

Compact three-state theme switcher — Light, Auto (OS preference), and Dark. Sets data-bs-theme on <html> for Bootstrap 5 dark mode. Does not persist preferences; the host application controls storage via defaultTheme and the onChange callback.

Assets

AssetPath
CSScomponents/themetoggle/themetoggle.css
JScomponents/themetoggle/themetoggle.js
Typescomponents/themetoggle/themetoggle.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/themetoggle/themetoggle.css">
<script src="components/themetoggle/themetoggle.js"></script>

<div id="theme-toggle"></div>

<script>
    var toggle = createThemeToggle({
        container: document.getElementById("theme-toggle"),
        defaultTheme: "auto",
        onChange: function(theme, mode) {
            console.log("Theme:", theme, "Mode:", mode);
            // Persist `mode` to localStorage, cookie, or server
        }
    });
</script>

FOUC Prevention

Place this inline script in <head> before any CSS to prevent a flash of unstyled content:

<script>
(function() {
    var mode = localStorage.getItem("theme-mode") || "auto";
    var dark = mode === "dark" ||
               (mode === "auto" && matchMedia("(prefers-color-scheme: dark)").matches);
    if (dark) document.documentElement.setAttribute("data-bs-theme", "dark");
})();
</script>

API

createThemeToggle(options): ThemeToggleHandle

OptionTypeDefaultDescription
containerHTMLElementRequired. Element to render the toggle into.
defaultTheme"light" | "dark" | "auto""auto"Initial mode. "auto" reads OS preference.
onChange(theme, mode) => voidFires when resolved theme changes. theme is "light" or "dark"; mode is the selected mode.

ThemeToggleHandle

MethodReturnsDescription
getTheme()"light" | "dark"Current resolved theme.
getMode()"light" | "dark" | "auto"Current selected mode.
setTheme(mode)voidProgrammatically change mode.
destroy()voidRemove DOM and event listeners.

Keyboard

KeyAction
TabMove focus between toggle buttons
Enter / SpaceActivate focused button

Accessibility


Timeline

A horizontal event timeline component for displaying point and span events along a time axis. Supports grouped rows, collapsible sections, viewport panning, a "now" marker, and item selection.

Features

Assets

AssetPathDescription
TypeScript sourcecomponents/timeline/timeline.tsComponent source code
Compiled JScomponents/timeline/timeline.jsIIFE-wrapped JavaScript
Type declarationscomponents/timeline/timeline.d.tsTypeScript declaration file
SCSS sourcecomponents/timeline/timeline.scssComponent styles
Compiled CSScomponents/timeline/timeline.cssCompiled stylesheet

Requires: Bootstrap CSS (for SCSS variables). Does not require Bootstrap JS or Bootstrap Icons.

Quick Start

<link rel="stylesheet" href="components/timeline/timeline.css">
<script src="components/timeline/timeline.js"></script>

<div id="my-timeline"></div>

<script>
    var timeline = createTimeline({
        containerId: "my-timeline",
        start: new Date("2026-01-01"),
        end: new Date("2026-12-31"),
        items: [
            {
                id: "release-1",
                type: "point",
                start: new Date("2026-03-15"),
                label: "v2.0 Release"
            },
            {
                id: "sprint-4",
                type: "span",
                start: new Date("2026-04-01"),
                end: new Date("2026-04-14"),
                label: "Sprint 4",
                color: "#2b8a3e"
            }
        ],
        onItemClick: function(item) { console.log("Clicked:", item.id); }
    });
</script>

API Reference

5.1 Types

type TimelineItemType = "point" | "span";
type TimelineSize = "sm" | "md" | "lg";
type TickIntervalPreset = "1min" | "5min" | "10min" | "15min" | "30min" | "1h" | "3h" | "6h" | "12h" | "1d";

5.2 TimelineItem Interface

FieldTypeRequiredDefaultDescription
idstringYes--Unique identifier for the item
typeTimelineItemTypeYes--"point" for a single moment or "span" for a duration
startDateYes--Start date and time
endDateFor spans--End date and time (required when type is "span")
labelstringYes--Display label rendered on or beside the item
tooltipstringNo--Native browser tooltip via the title attribute
colorstringNo"#0d6efd"CSS colour applied to the item
cssClassstringNo--Additional CSS class added to the item element
groupstringNo--Group ID that this item belongs to
dataunknownNo--Arbitrary user data passed through to callbacks

5.3 TimelineGroup Interface

FieldTypeRequiredDefaultDescription
idstringYes--Unique group identifier
labelstringYes--Display label shown in the group label column
collapsiblebooleanNotrueWhether the group can be collapsed
collapsedbooleanNofalseWhether the group is initially collapsed
ordernumberNo0Sort order; lower values appear first
cssClassstringNo--Additional CSS class on the group row

5.4 TimelineOptions Interface

FieldTypeRequiredDefaultDescription
containerIdstringYes--DOM container element ID
startDateYes--Viewport start date
endDateYes--Viewport end date
itemsTimelineItem[]No[]Initial items
groupsTimelineGroup[]No[]Group definitions
maxVisibleRowsnumberNo8Max rows before scroll
showHeaderbooleanNotrueShow time axis header
showGroupLabelsbooleanNotrueShow group label column
showNowMarkerbooleanNofalseShow "now" marker line
pointSizenumberNo10Point marker diameter (px)
spanHeightnumberNo24Span bar height (px)
rowGapnumberNo4Vertical row gap (px)
groupLabelWidthnumberNo120Label column width (px)
collapsedBandHeightnumberNo6Collapsed band height (px)
collapsedBandColorstringNo"#adb5bd"Collapsed band colour
sizeTimelineSizeNo"md"Density variant
heightstringNo--CSS height (e.g., "400px")
widthstringNo"100%"CSS width
cssClassstringNo--Extra CSS class on root
selectedItemIdstringNonullInitially selected item ID
disabledbooleanNofalseDisable all interactions
timezonestringNoBrowser localIANA timezone for display labels (e.g. "UTC", "America/New_York")
showTimezoneSelectorbooleanNofalseShow timezone badge/dropdown in header
tickIntervalnumber | TickIntervalPreset | "auto"No"auto"Tick interval in ms, named preset, or auto-select
pannablebooleanNofalseEnable drag-to-pan on body and axis
onItemClick(item) => voidNo--Item click callback
onItemSelect(item | null) => voidNo--Selection change callback
onItemVisible(items[]) => voidNo--Visible items change callback
onViewportChange(start, end) => voidNo--Viewport change callback
onGroupToggle(group, collapsed) => voidNo--Group toggle callback
onTimezoneChange(timezone) => voidNo--Timezone change callback

5.5 Methods

Item API

MethodDescription
addItem(item)Add a single item and re-render.
addItems(items)Add multiple items in one batch.
removeItem(id)Remove an item by ID.
updateItem(item)Replace item matching item.id.
getItems()Return a copy of all items.

Selection API

MethodDescription
selectItem(id)Select by ID, or pass null to clear.
getSelectedItem()Return selected item or null.

Group API

MethodDescription
addGroup(group)Add a group definition.
removeGroup(id)Remove a group by ID.
updateGroup(group)Replace group matching group.id.
toggleGroup(id)Toggle collapsed state.
collapseAll()Collapse all groups.
expandAll()Expand all groups.

Viewport API

MethodDescription
setViewport(start, end)Set the visible date range.
getViewport()Return { start, end } dates.
scrollToDate(date)Centre viewport on date.

Timezone API

MethodDescription
setTimezone(tz)Set display timezone (IANA string). Re-renders labels.
getTimezone()Return current timezone string.

Tick Interval API

MethodDescription
setTickInterval(interval)Set tick interval (ms, preset, or "auto").
getTickInterval()Return current tick interval setting.

Lifecycle

MethodDescription
show()Render into the container element.
hide()Remove from DOM without destroying state.
destroy()Hide and release all references and listeners.
setDisabled(disabled)Enable or disable interactions.

Convenience Functions

createTimeline(options)    // Create, show, and return

Global Exports

window.Timeline
window.createTimeline

Examples

Grouped timeline with collapsible sections

var timeline = createTimeline({
    containerId: "project-timeline",
    start: new Date("2026-01-01"),
    end: new Date("2026-06-30"),
    groups: [
        { id: "backend", label: "Backend", order: 1 },
        { id: "frontend", label: "Frontend", order: 2 },
        { id: "milestones", label: "Milestones", order: 0, collapsible: false }
    ],
    items: [
        { id: "api", type: "span", start: new Date("2026-01-15"),
          end: new Date("2026-02-28"), label: "API Design", group: "backend" },
        { id: "ui", type: "span", start: new Date("2026-02-01"),
          end: new Date("2026-04-15"), label: "UI Build", group: "frontend", color: "#2b8a3e" },
        { id: "launch", type: "point", start: new Date("2026-05-01"),
          label: "Launch", group: "milestones", color: "#e67700" }
    ],
    showNowMarker: true,
    onGroupToggle: function(group, collapsed)
    {
        console.log(group.label, collapsed ? "collapsed" : "expanded");
    }
});

Dynamic item management

timeline.addItem({ id: "hotfix-1", type: "point", start: new Date("2026-03-10"), label: "Hotfix deployed" });

timeline.addItems([
    { id: "testing", type: "span", start: new Date("2026-03-01"),
      end: new Date("2026-03-14"), label: "QA Testing", group: "backend" },
    { id: "freeze", type: "point", start: new Date("2026-03-15"), label: "Code Freeze" }
]);

timeline.updateItem({ id: "hotfix-1", type: "point", start: new Date("2026-03-12"),
    label: "Hotfix (rescheduled)", color: "#dc3545" });

timeline.removeItem("hotfix-1");

Viewport control and selection

timeline.setViewport(new Date("2026-04-01"), new Date("2026-06-30"));
timeline.scrollToDate(new Date());
timeline.selectItem("sprint-4");
timeline.selectItem(null);

var vp = timeline.getViewport();
console.log("Showing:", vp.start, "to", vp.end);

Timezone and tick interval control

var timeline = createTimeline({
    containerId: "tz-timeline",
    start: new Date("2026-02-13T00:00:00Z"),
    end: new Date("2026-02-14T00:00:00Z"),
    timezone: "UTC",
    showTimezoneSelector: true,
    tickInterval: "1h",
    onTimezoneChange: function(tz) { console.log("Now showing:", tz); }
});

// Switch timezone programmatically
timeline.setTimezone("America/New_York");

// Change tick interval
timeline.setTickInterval("15min");
timeline.setTickInterval("auto");

Drag-to-pan

var timeline = createTimeline({
    containerId: "pan-timeline",
    start: new Date("2026-02-10"),
    end: new Date("2026-02-17"),
    pannable: true,
    onViewportChange: function(start, end)
    {
        console.log("Viewport:", start, "to", end);
    }
});

Drag left/right on the body or axis to scroll through time. Shift+mouse wheel also pans horizontally.

Callbacks for application integration

var timeline = createTimeline({
    containerId: "ops-timeline",
    start: new Date("2026-01-01"),
    end: new Date("2026-12-31"),
    onItemClick: function(item) { showDetailPanel(item.data); },
    onItemSelect: function(item) { item ? highlightRelated(item.id) : clearHighlights(); },
    onViewportChange: function(start, end)
    {
        fetchEventsForRange(start, end).then(function(events) { timeline.addItems(events); });
    },
    onItemVisible: function(items) { console.log(items.length, "items visible"); }
});

Accessibility

FeatureImplementation
Region landmarkRoot element uses role="region" with aria-label.
Group headingsInteractive headings that toggle collapse state.
Item rolesrole="button" with aria-label containing label and date.
Selection stateSelected items indicated with aria-pressed="true".
Disabled stateRoot element has aria-disabled="true"; items become inert.
Tooltipstooltip field maps to native title attribute.
KeyAction
TabFocus timeline / navigate between groups
Arrow Left / RightNavigate between items in a row
Arrow Up / DownNavigate between rows or groups
Enter / SpaceSelect or activate focused item
EscapeClear selection
Home / EndFirst or last item in current row

Security

The Timeline component applies the following measures to prevent cross-site scripting (XSS):

See specs/timeline.prd.md for the complete specification.


TimePicker

A time-of-day picker with spinner columns and optional timezone selector.

Quick Start

<link rel="stylesheet" href="https://theme.priyavijai-kalyan2007.workers.dev/components/timepicker/timepicker.css">
<script src="https://theme.priyavijai-kalyan2007.workers.dev/components/timepicker/timepicker.js"></script>

<div id="my-time"></div>
<script>
    var picker = createTimePicker("my-time", {
        clockMode: "24",
        showSeconds: true,
        onSelect: function(time) { console.log("Selected:", time); }
    });
</script>

Configuration Options

OptionTypeDefaultDescription
valueTimeValueCurrent timeInitial time
clockMode"12" | "24""24"Clock mode
showSecondsbooleantrueShow seconds column
formatstring"HH:mm:ss"Display format
minuteStepnumber1Minute step (1, 5, 15, 30)
secondStepnumber1Second step
minTimeTimeValueEarliest selectable time
maxTimeTimeValueLatest selectable time
showNowButtonbooleantrueShow Now button
showTimezonebooleanfalseShow timezone selector
timezonestring"UTC"IANA timezone or "local"
showFormatHintbooleantrueFormat hint below input
showFormatHelpbooleantrueHelp icon and tooltip
disabledbooleanfalseDisable the component
readonlybooleanfalseRead-only input
size"mini" | "sm" | "default" | "lg""default"Size variant
onSelect(time: TimeValue) => voidSelection callback
onChange(time: TimeValue | null) => voidChange callback
onTimezoneChange(tz: string) => voidTimezone change callback

TimeValue Interface

interface TimeValue {
    hours: number;   // 0–23
    minutes: number; // 0–59
    seconds?: number; // 0–59
}

Instance Methods

MethodReturnsDescription
getValue()TimeValue | nullCurrent time
getFormattedValue()stringFormatted time string
getTimezone()stringCurrent IANA timezone
setValue(time)voidSet time programmatically
setTimezone(tz)voidSet timezone
open() / close()voidToggle dropdown
enable() / disable()voidToggle state
destroy()voidRemove from DOM

Dependencies


TimezonePicker

A searchable dropdown selector for IANA timezones with grouped regions, UTC offset display, and live current-time preview.

Quick Start

<link rel="stylesheet" href="components/timezonepicker/timezonepicker.css">
<script src="components/timezonepicker/timezonepicker.js"></script>

<div id="my-tz"></div>
<script>
    var picker = createTimezonePicker("my-tz", {
        timezone: "America/New_York",
        onSelect: function(tz) { console.log("Selected:", tz); }
    });
</script>

Configuration Options

OptionTypeDefaultDescription
timezonestring"UTC"Initial IANA timezone or "local"
showTimePreviewbooleantrueLive clock in dropdown footer
showFormatHintbooleantrueIANA identifier below input
showFormatHelpbooleantrueHelp icon and tooltip
formatHelpTextstringCustom help tooltip text
disabledbooleanfalseDisable the component
readonlybooleanfalseRead-only mode
size"mini" | "sm" | "default" | "lg""default"Size variant
placeholderstring"Select a timezone..."Input placeholder
onSelect(tz: string) => voidSelection callback
onChange(tz: string) => voidChange callback
onOpen() => voidDropdown open callback
onClose() => voidDropdown close callback

Public API

MethodReturnsDescription
getValue()stringCurrent IANA timezone
setValue(tz)voidSet timezone programmatically
getOffset()stringCurrent UTC offset (e.g., "GMT-5")
open()voidOpen dropdown
close()voidClose dropdown
enable()voidEnable component
disable()voidDisable component
destroy()voidRemove from DOM

Features

Keyboard Shortcuts

KeyAction
ArrowDownOpen dropdown or move highlight down
ArrowUpMove highlight up
EnterSelect highlighted timezone
EscapeClose dropdown

Dependencies


Toast

A transient, non-blocking notification system with stacking, auto-dismiss, progress bar, and action support. Toasts appear at configurable viewport corners and stack newest-on-top.

Assets

AssetPath
CSScomponents/toast/toast.css
JScomponents/toast/toast.js
Typescomponents/toast/toast.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/toast/toast.css">
<script src="components/toast/toast.js"></script>
<script>
    showSuccessToast("Changes saved successfully", "Saved");
    showErrorToast("Failed to save changes", "Error");

    showToast({
        message: "Item deleted",
        variant: "warning",
        actionLabel: "Undo",
        onAction: function() { console.log("Undo clicked"); }
    });
</script>

API

Global Functions

FunctionReturnsDescription
showToast(options)ToastHandleShow toast with full options
showInfoToast(msg, title?)ToastHandleInfo variant shortcut
showSuccessToast(msg, title?)ToastHandleSuccess variant shortcut
showWarningToast(msg, title?)ToastHandleWarning variant shortcut
showErrorToast(msg, title?)ToastHandleError variant shortcut
clearAllToasts()voidDismiss all and clear queue
configureToasts(options)voidSet position, max visible, etc.

ToastOptions

OptionTypeDefaultDescription
messagestringrequiredMain notification text
titlestringBold heading text
variantstring"info"info, success, warning, error
iconstringBootstrap Icons class (auto per variant)
durationnumber5000Auto-dismiss ms (0 = persistent)
dismissiblebooleantrueShow close button
showProgressbooleantrueShow countdown bar
actionLabelstringAction button label
onActionfunctionAction button callback
onDismissfunctionCalled when dismissed

ToastContainerOptions

OptionTypeDefaultDescription
positionstring"top-right"Viewport corner
maxVisiblenumber5Max simultaneous toasts
gapnumber8Gap between toasts (px)
zIndexnumber1070CSS z-index

Features

Accessibility

See specs/toast.prd.md for the complete specification.


Toolbar

A programmable action bar component for grouping tools and actions into labelled regions. Inspired by the Microsoft Office Ribbon but adapted to the enterprise Bootstrap 5 aesthetic — single strip, no tabs.

Features

Assets

AssetPath
CSScomponents/toolbar/toolbar.css
JScomponents/toolbar/toolbar.js
Typescomponents/toolbar/toolbar.d.ts

Requires: Bootstrap CSS (for SCSS variables), Bootstrap Icons CSS, Bootstrap JS (optional, for tooltips). Does not require a JavaScript framework.

Quick Start

<link rel="stylesheet" href="components/toolbar/toolbar.css">
<script src="components/toolbar/toolbar.js"></script>
<script>
    var toolbar = createToolbar({
        label: "Document formatting",
        regions: [
            {
                id: "formatting",
                title: "Formatting",
                items: [
                    { id: "bold", icon: "bi-type-bold", tooltip: "Bold", toggle: true },
                    { id: "italic", icon: "bi-type-italic", tooltip: "Italic", toggle: true },
                    { type: "separator" },
                    { id: "align-left", icon: "bi-text-left", tooltip: "Align left" }
                ]
            }
        ]
    });
</script>

API

Constructor

const toolbar = new Toolbar(options: ToolbarOptions);

Creates the toolbar DOM but does not attach to the page.

ToolbarOptions

OptionTypeDefaultDescription
idstringautoUnique identifier
labelstringrequiredARIA label for accessibility
titlestring | ToolbarTitleVisible title at the left edge (text, icon, custom colours)
regionsToolbarRegion[]requiredRegions containing tool items
rightContentHTMLElementCustom HTML rendered right-aligned, after all regions
orientation"horizontal" | "vertical""horizontal"Toolbar orientation
mode"docked" | "floating""docked"Positioning mode
dockPosition"top" | "bottom" | "left" | "right""top"Dock edge
style"icon" | "icon-label" | "label""icon"Default tool display style
toolSizenumber32Tool button size in pixels
iconSizenumber16Icon size in pixels
overflowbooleantrueEnable overflow menu
resizablebooleantrueEnable resize handle
minSizenumber120Minimum size in orientation axis (px)
maxSizenumberviewportMaximum size in orientation axis (px)
draggablebooleantrueEnable floating drag
dragToDockbooleantrueEnable drag-to-dock snapping
dragToDockThresholdnumber40Dock zone threshold in px
keyTipsbooleantrueEnable KeyTip badges on Alt press
persistLayoutbooleanfalseEnable localStorage persistence
persistKeystring"toolbar-layout-"localStorage key prefix
floatXnumber100Initial floating X position
floatYnumber100Initial floating Y position
onToolClickfunctionGlobal tool click handler
onResizefunctionCalled when toolbar is resized
onModeChangefunctionCalled when mode changes
onOrientationChangefunctionCalled when orientation changes

Methods

MethodDescription
show()Appends to document.body, sets CSS custom properties
hide()Removes from DOM without destroying state
destroy()Hides, releases all references and event listeners
dock(position)Switches to docked mode at the given edge
float(x?, y?)Switches to floating mode at optional coordinates
setOrientation(o)Changes orientation (horizontal/vertical)
addRegion(region, index?)Adds a region at optional position
removeRegion(regionId)Removes a region by ID
getRegion(regionId)Returns the region configuration
addTool(regionId, tool, index?)Adds a tool to a region
removeTool(toolId)Removes a tool by ID
getTool(toolId)Returns the tool configuration
setToolState(toolId, state)Updates tool properties
getToolState(toolId)Returns current tool state
setTitle(cfg)Sets, updates, or removes (null) the visible title
getTitle()Returns the current title config or null
setRightContent(el)Sets or removes (null) the right-content slot
getRightContent()Returns the right-content container or null
setStyle(style)Changes default tool display style
setToolSize(size)Changes tool button size
recalculateOverflow()Forces overflow recalculation
setGallerySelection(galleryId, optionId)Selects a gallery option
getGallerySelection(galleryId)Returns selected gallery option ID
setSplitMenuItems(splitId, items)Updates split button menu items
showKeyTips()Shows KeyTip badges
hideKeyTips()Hides KeyTip badges
saveLayout()Saves layout state to localStorage
restoreLayout()Restores layout state from localStorage
getLayoutState()Returns current layout state
applyLayoutState(state)Applies a layout state object
getMode()Returns current mode
getOrientation()Returns current orientation
getDockPosition()Returns current dock position
isVisible()Returns visibility state
getElement()Returns the root DOM element

Convenience Functions

createToolbar(options)         // Create, show, and return
createDockedToolbar(options)   // Shorthand for docked mode
createFloatingToolbar(options) // Shorthand for floating mode

Global Exports

window.Toolbar
window.createToolbar
window.createDockedToolbar
window.createFloatingToolbar

Title

The toolbar can display a non-interactive title at the left edge (or top, in vertical orientation). Pass a string for text-only, or a ToolbarTitle object for full control.

// Text-only
createToolbar({ label: "My App", title: "My App", regions: [...] });

// Icon + text with custom colours
createToolbar({
    label: "My App",
    title: {
        text: "My App",
        icon: "bi-app",
        backgroundColor: "#1864ab",
        color: "#ffffff"
    },
    regions: [...]
});

ToolbarTitle

PropertyTypeDescription
textstringDisplay text
iconstringBootstrap Icons class (e.g., "bi-app")
backgroundColorstringBackground colour (CSS value)
colorstringText / icon colour (CSS value)
cssClassstringAdditional CSS class(es)
widthstringFixed width (CSS value, e.g. "200px", "12rem")

Use setTitle() to update or remove at runtime:

toolbar.setTitle({ text: "New Title", icon: "bi-star" });
toolbar.setTitle(null);  // remove

Region Alignment

Regions can be aligned to the left (default) or right side of the toolbar. Right-aligned regions are pushed to the trailing edge with flexible space between the two groups.

createToolbar({
    label: "My App",
    title: { text: "My App", icon: "bi-app", backgroundColor: "#1864ab", color: "#fff", width: "200px" },
    regions: [
        {
            id: "editing",
            title: "Editing",
            items: [
                { id: "cut", icon: "bi-scissors", tooltip: "Cut" },
                { id: "copy", icon: "bi-clipboard", tooltip: "Copy" },
                { id: "paste", icon: "bi-clipboard-check", tooltip: "Paste" }
            ]
        },
        {
            id: "user",
            title: "User",
            align: "right",
            items: [
                { id: "settings", icon: "bi-gear", tooltip: "Settings" },
                { id: "help", icon: "bi-question-circle", tooltip: "Help" }
            ]
        }
    ]
});

In vertical orientation, "left" maps to top and "right" maps to bottom.

ToolbarRegion

PropertyTypeDefaultDescription
idstringrequiredUnique identifier
titlestringRegion title text
showTitlebooleantrueShow/hide region title
itemsArrayrequiredTool items, split buttons, galleries, separators
styleToolStyleDefault tool style for this region
hiddenbooleanfalseHidden state
cssClassstringAdditional CSS class(es)
align"left" | "right""left"Alignment within the toolbar

Tool Types

Standard Tool

{ id: "bold", icon: "bi-type-bold", tooltip: "Bold", toggle: true }

Split Button

{
    type: "split-button",
    id: "paste",
    icon: "bi-clipboard",
    tooltip: "Paste",
    menuItems: [
        { id: "paste-plain", icon: "bi-file-text", label: "Paste as plain text" },
        { id: "paste-special", icon: "bi-file-earmark-code", label: "Paste special" }
    ]
}

Gallery Control

{
    type: "gallery",
    id: "font-color",
    icon: "bi-palette",
    tooltip: "Font colour",
    layout: "grid",
    columns: 4,
    options: [
        { id: "red", label: "Red", color: "#dc2626" },
        { id: "blue", label: "Blue", color: "#1c7ed6" }
    ],
    onSelect: function(option, gallery) { console.log("Selected:", option.id); }
}

Input

{
    type: "input",
    id: "search",
    placeholder: "Search...",
    icon: "bi-search",
    width: "200px",
    onInput: function(value) { console.log("Searching:", value); },
    onSubmit: function(value) { console.log("Submit:", value); }
}

Dropdown

{
    type: "dropdown",
    id: "zoom",
    tooltip: "Zoom level",
    value: "100",
    width: "80px",
    options: [
        { value: "50", label: "50%" },
        { value: "100", label: "100%" },
        { value: "200", label: "200%" }
    ],
    onChange: function(value) { console.log("Zoom:", value); }
}

Label

{
    type: "label",
    id: "status",
    text: "Ready",
    icon: "bi-check-circle",
    color: "#40c057"
}

Separator

{ type: "separator" }

Checkbox

{
    type: "checkbox",
    id: "show-grid",
    label: "Show Grid",
    tooltip: "Toggle grid visibility",
    checked: true,
    onChange: function(checked) { console.log("Grid:", checked); }
}

Toggle Switch

{
    type: "switch",
    id: "live-preview",
    label: "Live Preview",
    tooltip: "Toggle live preview",
    checked: false,
    onChange: function(checked) { console.log("Preview:", checked); }
}

Number Spinner

{
    type: "number",
    id: "font-size",
    tooltip: "Font size",
    value: 14,
    min: 8,
    max: 72,
    step: 1,
    width: "80px",
    suffix: "px",
    onChange: function(value) { console.log("Size:", value); }
}

Color Picker

{
    type: "color",
    id: "fg-color",
    tooltip: "Foreground color",
    value: "#333333",
    showLabel: true,
    onChange: function(value) { console.log("Color:", value); },
    onInput: function(value) { /* live drag updates */ }
}

CSS Custom Properties

When docked, the toolbar sets a CSS custom property on <html>:

PropertySet When
--toolbar-top-sizeDocked top
--toolbar-bottom-sizeDocked bottom
--toolbar-left-sizeDocked left
--toolbar-right-sizeDocked right

Properties are cleared when the toolbar is hidden or undocked.

Overflow

When tools exceed the available space, the Priority+ algorithm hides excess tools into a dropdown menu. The overflow button ("...") appears inline, right after the last visible tool in each group.

Set overflowPriority on each tool to control which tools collapse first:

{ id: "cut", icon: "bi-scissors", tooltip: "Cut", overflowPriority: "never" }
{ id: "help", icon: "bi-question-circle", tooltip: "Help", overflowPriority: "low" }

Disable overflow with overflow: false in toolbar options.

Keyboard Accessibility

KeyAction
TabMove focus into toolbar
Arrow keysNavigate between tools
Home / EndFirst / last tool
Enter / SpaceActivate tool
EscapeClose dropdown / gallery / overflow / KeyTips
AltToggle KeyTip badges

See specs/toolbar.prd.md for the complete specification.


ToolColorPicker

A visual colour picker that displays colours as tool icons (pens, markers, pencils, highlighters, brushes). Each colour renders as the tool icon filled with that colour. The tool shape is configurable at creation and runtime.

Usage

Row Layout (default)

<link rel="stylesheet" href="components/toolcolorpicker/toolcolorpicker.css">
<script src="components/toolcolorpicker/toolcolorpicker.js"></script>

<div id="my-color-picker"></div>

<script>
var picker = createToolColorPicker({
    container: "my-color-picker",
    tool: "pen",
    onChange: function(color) {
        console.log("Selected:", color.label, color.hex);
    }
});
</script>

Grid Layout

<div id="brush-colors"></div>

<script>
var picker = createToolColorPicker({
    container: "brush-colors",
    tool: "brush",
    colors: createToolColorPicker.BRUSH_COLORS,
    layout: "grid",
    gridColumns: 4,
    onChange: function(color) {
        console.log("Brush:", color.hex);
    }
});
</script>

Highlighter with Alpha

<div id="highlighter-picker"></div>

<script>
var picker = createToolColorPicker({
    container: "highlighter-picker",
    tool: "highlighter",
    colors: createToolColorPicker.HIGHLIGHTER_COLORS,
    onChange: function(color) {
        console.log("Highlight:", color.hex, "alpha:", color.alpha);
    }
});
</script>

Options

OptionTypeDefaultDescription
containerHTMLElement | string(required)Container element or ID
tool"pen" | "marker" | "pencil" | "highlighter" | "brush""pen"Tool icon shape
colorsToolColor[]Built-in pack for toolAvailable colours
valuestringFirst colour's hexInitially selected colour hex
onChange(color: ToolColor) => void--Fires on colour selection
layout"row" | "grid""row"Layout mode
gridColumnsnumber6Columns in grid layout
showTooltipsbooleantrueShow colour name tooltip on hover

API

MethodReturnsDescription
getValue()ToolColorCurrently selected colour
setValue(hex)voidSet selected colour by hex
setColors(colors)voidReplace the colour palette
setTool(tool)voidChange the tool icon shape
getElement()HTMLElement | nullRoot DOM element
destroy()voidTear down and remove from DOM

Built-in Color Packs

Access via static properties on the factory function:

PackPropertyCountAlpha
PencreateToolColorPicker.PEN_COLORS71.0
MarkercreateToolColorPicker.MARKER_COLORS60.6
HighlightercreateToolColorPicker.HIGHLIGHTER_COLORS60.4
PencilcreateToolColorPicker.PENCIL_COLORS61.0
BrushcreateToolColorPicker.BRUSH_COLORS81.0

Tool Icons

Each tool renders a distinct 24x36px SVG icon:

ToolVisual
PenThin triangular nib at bottom, cylindrical body with clip at top
MarkerWide chisel-tip at bottom, thick rectangular body with ridge
PencilHexagonal body, eraser at top, sharpened point at bottom
HighlighterWide flat tip, thick body with grip section
BrushRound bristles at bottom, thin wooden handle with ferrule

Keyboard

KeyAction
Tab / Shift+TabNavigate between swatches
Enter / SpaceSelect focused swatch

ToolColor Interface

interface ToolColor {
    hex: string;      // CSS hex colour
    label: string;    // Human-readable name
    alpha?: number;   // 0-1 opacity (default: 1)
}

TreeGrid

A highly configurable tree-grid hybrid component for displaying hierarchical data with multi-column tabular views. Supports expandable tree structure in the first column, inline cell editing, column sorting, resizing, drag-and-drop, context menu, virtual scrolling for large datasets, and full WAI-ARIA grid pattern keyboard navigation.

Assets

AssetPath
CSScomponents/treegrid/treegrid.css
JScomponents/treegrid/treegrid.js
Typescomponents/treegrid/treegrid.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/treegrid/treegrid.css">
<script src="components/treegrid/treegrid.js"></script>

<div id="my-grid" style="height: 400px"></div>

<script>
    var grid = createTreeGrid({
        containerId: "my-grid",
        label: "Project Files",
        nodes: [
            {
                id: "1",
                label: "src",
                icon: "bi-folder",
                data: { size: "4.2 MB", modified: "2026-02-10" },
                children: [
                    {
                        id: "2",
                        label: "main.ts",
                        icon: "bi-file-code",
                        data: { size: "1.5 KB", modified: "2026-02-12" }
                    },
                    {
                        id: "3",
                        label: "utils.ts",
                        icon: "bi-file-code",
                        data: { size: "890 B", modified: "2026-02-11" }
                    }
                ],
                expanded: true
            },
            {
                id: "4",
                label: "package.json",
                icon: "bi-filetype-json",
                data: { size: "512 B", modified: "2026-02-09" }
            }
        ],
        columns: [
            { id: "size", label: "Size", width: 100, sortable: true, align: "right" },
            { id: "modified", label: "Modified", width: 150, sortable: true }
        ],
        treeColumnLabel: "Name",
        treeColumnWidth: 250,
        stickyHeader: true,
        rowStriping: true,
        onRowSelect: function(node, selected) {
            console.log(node.label, selected ? "selected" : "deselected");
        }
    });
</script>

Options

OptionTypeDefaultDescription
containerIdstringrequiredDOM element ID for the container
labelstringrequiredAccessible label for the grid (ARIA)
nodesTreeGridNode[]requiredRoot-level tree nodes
columnsTreeGridColumn[]requiredColumn definitions for data columns
treeColumnLabelstring"Name"Label for the first (tree) column
treeColumnWidthnumber250Initial width of tree column in pixels
treeColumnMinWidthnumber120Minimum width of tree column
treeColumnResizablebooleantrueAllow tree column resizing
selectionMode"single" | "multi" | "none""single"Selection behaviour
enableDragDropbooleanfalseEnable drag-and-drop row reordering
enableContextMenubooleanfalseEnable right-click context menu
contextMenuItemsTreeGridContextMenuItem[][]Context menu item definitions
indentPxnumber20Per-level indent in tree column (pixels)
rowStripingbooleanfalseAlternate row background colors
stickyHeaderbooleantrueFix header row during vertical scroll
rowHeightnumber32Fixed row height in pixels (required for virtual scrolling)
heightstring"100%"Component height CSS value
widthstring"100%"Component width CSS value
cssClassstringAdditional CSS class on root element
emptyMessagestring"No items to display"Message shown when grid is empty
virtualScrolling"auto" | "enabled" | "disabled""auto"Virtual scrolling mode. "auto" enables above 5000 visible rows
scrollBuffernumber50Number of rows rendered above/below viewport in virtual mode
enableColumnReorderbooleanfalseEnable drag-to-reorder columns by dragging header cells
showColumnPickerbooleanfalseShow a gear icon button in the header that opens a dropdown checklist for showing/hiding columns
externalSortbooleanfalseWhen true, the grid does not sort internally. The app must sort data in the onColumnSort callback and call refresh(). Useful when sort logic requires server-side or complex comparisons

Column Definition

PropertyTypeDescription
idstringUnique column identifier (maps to node.data keys)
labelstringColumn header label
widthnumberInitial column width in pixels (default: 150)
minWidthnumberMinimum column width (default: 60)
maxWidthnumberMaximum column width (optional)
resizablebooleanAllow column resizing (default: true)
sortablebooleanAllow column sorting (default: false)
sortDirection"asc" | "desc"Initial sort direction (optional)
valueAccessor(data) => unknownCustom value accessor function (default: data[column.id])
renderer(cell, node, value) => voidCustom cell renderer (default: textContent)
editablebooleanAllow inline cell editing (default: false)
editorType"text" | "number" | "select" | "date" | "custom"Editor type for inline editing
editorOptionsArray<{value, label}>Options for select editor
align"left" | "center" | "right"Cell text alignment (default: "left")
hiddenbooleanHide column (default: false)
cssClassstringAdditional CSS class for cells
aggregate(values) => unknownAggregate function for parent rows (e.g., sum, count)
comparator(a: unknown, b: unknown) => numberCustom comparator for sorting. Receives raw cell values; return negative/zero/positive. Overrides the built-in type-aware default

Node Definition

PropertyTypeDescription
idstringUnique node identifier
labelstringDisplay text in tree column
iconstringBootstrap Icons class (e.g., "bi-folder")
dataRecord<string, unknown>Column data keyed by column ID
childrenTreeGridNode[] | nullChild nodes. null = lazy-loadable
expandedbooleanInitially expanded (default: false)
lazybooleanWhen true and children is null, expand triggers onLoadChildren
selectablebooleanCan be selected (default: true)
draggablebooleanCan be dragged (default: true)
disabledbooleanCannot be interacted with (default: false)
tooltipstringNative tooltip text

Callbacks

CallbackParametersDescription
onRowSelect(node, selected)Row selected or deselected
onSelectionChange(nodes)Full selection set changed
onRowActivate(node)Row double-clicked or Enter pressed on tree cell
onRowToggle(node, expanded)Tree node expanded or collapsed
onLoadChildren(node) => Promise<TreeGridNode[]>Async loader for lazy children
onColumnResize(column, newWidth)Column resized by user
onColumnReorder(columns)Columns reordered by drag-drop (future)
onColumnSort(column, direction)Column sort direction changed
onEditStart(node, column, currentValue) => boolean | voidCell edit started. Return false to cancel
onEditCommit(node, column, oldValue, newValue)Cell edit committed
onEditCancel(node, column)Cell edit cancelled
onContextMenuAction(actionId, node)Context menu action clicked
onDragValidate(sources[], target, position) => booleanValidate drop operation
onDrop(sources[], target, position)Rows dropped
onExternalDrop(dataTransfer, target, position)External content dropped
onRefreshComplete()Programmatic refresh completed

Keyboard Navigation

KeyAction
Arrow DownMove focus to next row (same column)
Arrow UpMove focus to previous row (same column)
Arrow RightMove focus to next column / expand collapsed parent in tree column
Arrow LeftMove focus to previous column / collapse expanded parent in tree column
HomeMove to first cell in row (Ctrl+Home = first cell in grid)
EndMove to last cell in row (Ctrl+End = last cell in grid)
SpaceToggle row selection in tree column
EnterActivate row (tree column) / start editing (data column)
F2Start inline edit on focused cell
EscapeCancel inline edit / close context menu
TabCommit edit and move to next editable cell
Shift+TabCommit edit and move to previous editable cell

Public API

MethodReturnsDescription
addNode(node, parentId)voidAdd a node under parent (null = root)
removeNode(nodeId)voidRemove a node and descendants
updateNode(nodeId, updates)voidUpdate node properties (label, icon, data, etc.)
expandNode(nodeId)voidExpand a node
collapseNode(nodeId)voidCollapse a node
expandAll()voidExpand all parent nodes
collapseAll()voidCollapse all nodes
selectNode(nodeId)voidSelect and focus a node
clearSelection()voidClear selection
getSelectedNodes()TreeGridNode[]Get selected nodes
scrollToNode(nodeId)voidScroll to node, expanding ancestors
setColumns(columns)voidReplace column definitions
updateColumn(columnId, updates)voidUpdate a single column's properties at runtime. Takes a column ID and a Partial<TreeGridColumn> object. Triggers minimal rebuild
getColumns()TreeGridColumn[]Get current column definitions
showColumn(columnId)voidShow a hidden column
hideColumn(columnId)voidHide a visible column
refresh()voidRe-render grid
destroy()voidFull cleanup
getElement()HTMLElement | nullRoot DOM element
getId()stringInstance ID

Convenience Functions

FunctionDescription
createTreeGrid(options)Create a TreeGrid instance

Global Exports

When loaded via <script> tag:

Performance

The TreeGrid is optimized for large datasets with 1M+ total nodes:

Virtual Scrolling

Virtual scrolling activates automatically when visible row count exceeds 5000, or can be forced via virtualScrolling: "enabled". In virtual mode, only visible rows are rendered in the DOM, with spacer elements maintaining scroll height.

Requirements:

Large Dataset Example

var grid = createTreeGrid({
    containerId: "large-grid",
    label: "Enterprise Data",
    virtualScrolling: "auto",
    rowHeight: 32,
    scrollBuffer: 50,
    stickyHeader: true,
    nodes: largeDataSet,
    columns: [
        { id: "col1", label: "Column 1", sortable: true },
        { id: "col2", label: "Column 2", sortable: true }
    ],
    onLoadChildren: function(node) {
        return fetch("/api/children/" + node.id)
            .then(function(r) { return r.json(); });
    }
});

Examples

Lazy Loading Tree

var grid = createTreeGrid({
    containerId: "lazy-grid",
    label: "File System",
    nodes: [
        {
            id: "root",
            label: "C:\\",
            icon: "bi-hdd",
            data: { type: "Drive" },
            children: null,
            lazy: true
        }
    ],
    columns: [
        { id: "type", label: "Type", width: 100 },
        { id: "size", label: "Size", width: 100, align: "right" }
    ],
    treeColumnLabel: "Path",
    onLoadChildren: function(node) {
        return fetch("/api/fs/children?path=" + encodeURIComponent(node.id))
            .then(function(r) { return r.json(); });
    }
});

Inline Editing

var grid = createTreeGrid({
    containerId: "editable-grid",
    label: "Product Catalog",
    nodes: [
        {
            id: "1",
            label: "Electronics",
            icon: "bi-lightning",
            data: { stock: 150, price: 0 },
            children: [
                {
                    id: "2",
                    label: "Laptop",
                    icon: "bi-laptop",
                    data: { stock: 25, price: 1299.99 }
                },
                {
                    id: "3",
                    label: "Mouse",
                    icon: "bi-mouse",
                    data: { stock: 125, price: 29.99 }
                }
            ]
        }
    ],
    columns: [
        {
            id: "stock",
            label: "Stock",
            width: 100,
            editable: true,
            editorType: "number",
            align: "right"
        },
        {
            id: "price",
            label: "Price",
            width: 100,
            editable: true,
            editorType: "number",
            align: "right",
            renderer: function(cell, node, value) {
                if (typeof value === "number") {
                    cell.textContent = "$" + value.toFixed(2);
                }
            }
        }
    ],
    treeColumnLabel: "Category / Item",
    onEditCommit: function(node, column, oldValue, newValue) {
        console.log("Updated", node.label, column.id, "from", oldValue, "to", newValue);
        // Persist to server
        fetch("/api/products/" + node.id, {
            method: "PATCH",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ [column.id]: newValue })
        });
    }
});

Sortable Columns with Custom Renderer

var grid = createTreeGrid({
    containerId: "sorted-grid",
    label: "Team Members",
    nodes: [
        {
            id: "eng",
            label: "Engineering",
            icon: "bi-code-slash",
            data: { count: 12, budget: 1200000 },
            children: [
                { id: "e1", label: "Alice", data: { role: "Senior", salary: 125000 } },
                { id: "e2", label: "Bob", data: { role: "Junior", salary: 85000 } }
            ]
        },
        {
            id: "des",
            label: "Design",
            icon: "bi-palette",
            data: { count: 5, budget: 500000 },
            children: [
                { id: "d1", label: "Carol", data: { role: "Lead", salary: 110000 } }
            ]
        }
    ],
    columns: [
        {
            id: "role",
            label: "Role",
            width: 120,
            sortable: true
        },
        {
            id: "salary",
            label: "Salary",
            width: 120,
            sortable: true,
            align: "right",
            renderer: function(cell, node, value) {
                if (typeof value === "number") {
                    cell.textContent = "$" + value.toLocaleString();
                }
            }
        }
    ],
    treeColumnLabel: "Department / Name",
    rowStriping: true,
    onColumnSort: function(column, direction) {
        console.log("Sorting by", column.label, direction);
    }
});

Context Menu

var grid = createTreeGrid({
    containerId: "context-grid",
    label: "Project Tasks",
    enableContextMenu: true,
    contextMenuItems: [
        { id: "add-child", label: "Add Subtask", icon: "bi-plus-circle" },
        { id: "edit", label: "Edit", icon: "bi-pencil", shortcutHint: "F2" },
        { id: "sep1", label: "", separator: true },
        { id: "delete", label: "Delete", icon: "bi-trash" }
    ],
    nodes: [
        {
            id: "t1",
            label: "Phase 1",
            data: { status: "In Progress", due: "2026-03-01" },
            children: [
                { id: "t2", label: "Design mockups", data: { status: "Done", due: "2026-02-15" } }
            ]
        }
    ],
    columns: [
        { id: "status", label: "Status", width: 120, sortable: true },
        { id: "due", label: "Due Date", width: 120, sortable: true }
    ],
    treeColumnLabel: "Task",
    onContextMenuAction: function(actionId, node) {
        console.log("Action:", actionId, "on", node.label);
        if (actionId === "delete") {
            grid.removeNode(node.id);
        }
    }
});

Column Management

The TreeGrid supports runtime column management:

// Toggle a column's editability at runtime
grid.updateColumn("status", { editable: false });

// Change column width
grid.updateColumn("estimate", { width: 200 });

// Hide a column
grid.updateColumn("priority", { hidden: true });

Sorting

Columns with sortable: true support click-to-sort (ascending → descending → none). The built-in sort is type-aware: numbers are compared numerically, strings lexicographically (case-insensitive), and nulls sort to the end.

Per-Column Comparator

Override the default sort logic for a specific column:

columns: [
    {
        id: "estimate",
        label: "Est. Hours",
        sortable: true,
        comparator: function(a, b) {
            // Custom numeric comparison with null handling
            if (a == null && b == null) return 0;
            if (a == null) return 1;
            if (b == null) return -1;
            return Number(a) - Number(b);
        }
    }
]

External Sort (Server-Side)

When the application needs full control over sorting (e.g., server-side sort, complex multi-column sort), set externalSort: true. The grid updates sort indicators and fires the callback, but does not rearrange data:

var grid = createTreeGrid({
    containerId: "server-sorted-grid",
    label: "Server Data",
    externalSort: true,
    columns: [
        { id: "name", label: "Name", sortable: true },
        { id: "created", label: "Created", sortable: true }
    ],
    nodes: serverData,
    onColumnSort: function(column, direction) {
        // Application fetches sorted data from server
        fetch("/api/data?sort=" + column.id + "&dir=" + direction)
            .then(function(r) { return r.json(); })
            .then(function(sorted) {
                grid.setNodes(sorted);
                grid.refresh();
            });
    }
});

TreeView

A highly configurable, generic tree view component for representing multi-tree structured data. Supports lazy loading, multi-select (Ctrl+Click / Shift+Click), drag and drop (internal + cross-tree + external), context menu, inline rename (F2), search with mark highlighting, starred/favourites group, sort modes, extensible node types with badges, toolbar actions, and full WAI-ARIA tree pattern keyboard navigation.

Assets

AssetPath
CSScomponents/treeview/treeview.css
JScomponents/treeview/treeview.js
Typescomponents/treeview/treeview.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/treeview/treeview.css">
<script src="components/treeview/treeview.js"></script>

<div id="my-tree" style="height: 400px"></div>

<script>
    var tree = createTreeView({
        containerId: "my-tree",
        roots: [
            { id: "1", label: "Documents", kind: "folder", children: [
                { id: "2", label: "Report.pdf", kind: "leaf", icon: "bi-file-earmark-pdf" },
                { id: "3", label: "Notes.txt", kind: "leaf", icon: "bi-file-text" }
            ]},
            { id: "4", label: "Images", kind: "folder", children: [] }
        ],
        nodeTypes: {
            folder: { kind: "folder", icon: "bi-folder", isParent: true },
            leaf: { kind: "leaf", icon: "bi-file-earmark" }
        },
        onSelect: function(node, selected) {
            console.log(node.label, selected ? "selected" : "deselected");
        }
    });
</script>

Options

OptionTypeDefaultDescription
containerIdstringrequiredDOM element ID for the container
rootsTreeNode[]requiredRoot-level tree nodes
nodeTypesRecord<string, TreeNodeTypeDescriptor>{}Registry of node type descriptors keyed by kind
selectionMode"single" | "multi" | "none""single"Selection behaviour
sortMode"alpha-asc" | "alpha-desc" | "newest" | "oldest" | "custom""alpha-asc"Sort mode for siblings
sortComparator(a, b) => numberCustom sort comparator (when sortMode is "custom")
showStarredbooleanfalseShow the starred/favourites group
starredLabelstring"Starred"Label for the starred group
showSearchbooleanfalseShow search box in toolbar
searchDebounceMsnumber300Search input debounce delay (ms)
toolbarActionsTreeToolbarAction[][]Toolbar action buttons
enableDragDropbooleanfalseEnable drag and drop
enableInlineRenamebooleanfalseEnable F2 inline rename
enableContextMenubooleanfalseEnable right-click context menu
indentPxnumber20Per-level indent in pixels
heightstring"100%"Component height CSS value
widthstring"100%"Component width CSS value
cssClassstringAdditional CSS class on root element
emptyMessagestring"No items to display"Message shown when tree is empty
rowHeightnumber28Fixed row height in pixels (required for virtual scrolling)
virtualScrolling"auto" | "enabled" | "disabled""auto"Virtual scrolling mode. "auto" enables above 5000 visible nodes
scrollBuffernumber50Number of rows rendered above/below viewport in virtual mode
searchAsyncThresholdnumber5000Node count above which async/chunked search is used

Callbacks

CallbackParametersDescription
onSelect(node, selected)Node selected or deselected
onSelectionChange(nodes)Full selection set changed
onActivate(node)Node double-clicked or Enter pressed
onToggle(node, expanded)Node expanded or collapsed
onLoadChildren(node) => Promise<TreeNode[]>Async loader for lazy children
onRename(node, newLabel) => Promise<boolean> | booleanInline rename handler
onStarToggle(node, starred)Star state toggled
onDragValidate(sources[], target, position) => booleanValidate drop
onDrop(sources[], target, position)Nodes dropped
onExternalDrop(dataTransfer, target, position)External content dropped
onContextMenuAction(actionId, node)Context menu action clicked
onRefreshComplete()Programmatic refresh completed
onSearchAsync(query: string) => Promise<string[]>Server-side search returning matching node IDs

TreeNode Interface

PropertyTypeDescription
idstringUnique identifier
labelstringDisplay text
kindstringNode kind (matches nodeTypes registry)
iconstringBootstrap Icons class override
childrenTreeNode[] | nullChild nodes. null = lazy-loadable
lazybooleanWhen true and children is null, expand triggers onLoadChildren
expandedbooleanInitially expanded
disabledbooleanCannot be interacted with
starredbooleanAppears in starred group
tooltipstringNative tooltip text
badgesTreeBadge[]Badges after label
dataunknownArbitrary consumer data

Public API

MethodReturnsDescription
addNode(parentId, node, index?)voidAdd a node under parent (null = root)
removeNode(nodeId)voidRemove a node and descendants
updateNode(nodeId, updates)voidUpdate node properties
getNodeById(nodeId)TreeNode | undefinedFind a node by ID
expandNode(nodeId)voidExpand a node
collapseNode(nodeId)voidCollapse a node
expandAll()voidExpand all parent nodes
collapseAll()voidCollapse all nodes
selectNode(nodeId)voidSelect and focus a node
deselectAll()voidClear selection
getSelectedNodes()TreeNode[]Get selected nodes
scrollToNode(nodeId)voidScroll to node, expanding ancestors
setSort(mode)voidChange sort mode
setRoots(roots)voidReplace all roots
clearSearch()voidClear search filter
refresh()voidRe-render tree
destroy()voidFull cleanup
getElement()HTMLElement | nullRoot DOM element
getId()stringInstance ID

Convenience Functions

FunctionDescription
createTreeView(options)Create a TreeView instance

Global Exports

When loaded via <script> tag:

Keyboard Navigation

KeyAction
Arrow DownMove to next visible node
Arrow UpMove to previous visible node
Arrow RightExpand collapsed parent / move to first child
Arrow LeftCollapse expanded parent / move to parent
HomeMove to first node
EndMove to last visible node
SpaceToggle selection
EnterActivate node (fires onActivate)
F2Start inline rename (when enabled)
*Expand all siblings
Ctrl+ASelect all visible nodes (multi-select mode)
EscapeClose context menu / cancel rename
Type characterJump to next node starting with that character

Drag and Drop

Internal drag uses MIME type application/x-treeview with a JSON payload containing the source tree ID and node IDs, enabling cross-tree drag between multiple TreeView instances. External drops (files, URLs) are passed to onExternalDrop with the raw DataTransfer object.

Drop position is determined by mouse Y within the target row thirds: top third = "before", middle third = "inside" (parent nodes only), bottom third = "after".

Performance

The TreeView is optimized for trees with 1M+ total nodes:

Virtual Scrolling

Virtual scrolling activates automatically when visible node count exceeds 5000, or can be forced via virtualScrolling: "enabled". In virtual mode, the tree uses a flat <div> structure with explicit aria-level instead of nested <ul>/<li>. DOM elements are recycled from a pool during scroll.

Requirements:

Large Tree Example

var tree = createTreeView({
    containerId: "large-tree",
    virtualScrolling: "auto",
    rowHeight: 28,
    scrollBuffer: 50,
    roots: largeDataSet,
    nodeTypes: { folder: { kind: "folder", icon: "bi-folder", isParent: true } },
    onSearchAsync: function(query) {
        return fetch("/api/search?q=" + encodeURIComponent(query))
            .then(function(r) { return r.json(); });
    }
});

Examples

Lazy Loading

var tree = createTreeView({
    containerId: "lazy-tree",
    roots: [
        { id: "root", label: "Projects", kind: "folder", children: null, lazy: true }
    ],
    nodeTypes: {
        folder: { kind: "folder", icon: "bi-folder", isParent: true }
    },
    onLoadChildren: function(node) {
        return fetch("/api/children/" + node.id)
            .then(function(r) { return r.json(); });
    }
});

Multi-Select with Starred

var tree = createTreeView({
    containerId: "starred-tree",
    selectionMode: "multi",
    showStarred: true,
    roots: myNodes,
    onStarToggle: function(node, starred) {
        console.log(node.label, starred ? "starred" : "unstarred");
    }
});

Context Menu

var tree = createTreeView({
    containerId: "ctx-tree",
    enableContextMenu: true,
    nodeTypes: {
        folder: {
            kind: "folder", icon: "bi-folder", isParent: true,
            contextMenuItems: [
                { id: "new-folder", label: "New Folder", icon: "bi-folder-plus" },
                { id: "rename", label: "Rename", shortcutHint: "F2" },
                { id: "sep", label: "", separator: true },
                { id: "delete", label: "Delete", icon: "bi-trash" }
            ]
        }
    },
    onContextMenuAction: function(actionId, node) {
        console.log("Action:", actionId, "on", node.label);
    }
});

TypeBadge

Small inline chip/badge that visually identifies an ontology type via icon, color, and label.

Usage

<link rel="stylesheet" href="components/typebadge/typebadge.css" />
<script src="components/typebadge/typebadge.js"></script>
const badge = createTypeBadge({
    typeKey: "strategy.okr",
    icon: "crosshair",
    color: "#C0392B",
    size: "sm",
    variant: "subtle"
});
document.getElementById("container").appendChild(badge);

Options

OptionTypeDefaultDescription
typeKeystringOntology type key, e.g. "strategy.okr"
displayNamestringextracted from typeKeyOverride display text
iconstringBootstrap icon name (without bi bi- prefix)
colorstring"#475569"Hex color
size"sm" | "md" | "lg""sm"Size variant (20/28/32px height)
variant"filled" | "outlined" | "subtle""subtle"Visual variant
showNamespacebooleanfalseShow namespace prefix in label
clickablebooleanfalseEnable click/keyboard activation
onClick() => voidClick callback

Sizes

Variants

Global

window.createTypeBadge(options)

UserMenu

Avatar-triggered dropdown menu for user account actions. Shows user avatar (image or initials), name, role, status dot, and a dropdown menu with grouped items, dividers, headers, and a sign-out action.

Assets

AssetPath
TypeScript sourcecomponents/usermenu/usermenu.ts
SCSS sourcecomponents/usermenu/usermenu.scss
Compiled JScomponents/usermenu/usermenu.js
Compiled CSScomponents/usermenu/usermenu.css

Quick Start

Include Assets

<link rel="stylesheet" href="components/usermenu/usermenu.css">
<script src="components/usermenu/usermenu.js"></script>

With Image Avatar

var menu = createUserMenu("my-container", {
    userName: "John Smith",
    userRole: "Administrator",
    avatarUrl: "/img/john.png",
    status: "online",
    menuItems: [
        { id: "settings", label: "Settings", icon: "bi-gear" },
        { id: "profile",  label: "Profile",  icon: "bi-person" },
        { id: "divider-1", label: "", type: "divider" },
        { id: "sign-out", label: "Sign Out", icon: "bi-box-arrow-right", danger: true },
    ],
    onItemClick: function(itemId) { console.log("Clicked:", itemId); },
    onSignOut: function() { console.log("Signing out"); },
});

With Initials Avatar

var menu = createUserMenu("my-container", {
    userName: "Jane Doe",
    userRole: "Developer",
    status: "busy",
    menuItems: [
        { id: "account-header", label: "Account", type: "header" },
        { id: "settings", label: "Settings", icon: "bi-gear" },
        { id: "profile",  label: "Profile",  icon: "bi-person" },
        { id: "divider-1", label: "", type: "divider" },
        { id: "help-header", label: "Help", type: "header" },
        { id: "docs",     label: "Documentation", icon: "bi-book" },
        { id: "support",  label: "Support",       icon: "bi-life-preserver" },
        { id: "divider-2", label: "", type: "divider" },
        { id: "sign-out", label: "Sign Out", icon: "bi-box-arrow-right", danger: true },
    ],
    onItemClick: function(itemId) { console.log("Clicked:", itemId); },
});

Programmatic Status Change

// Update status dot
menu.setStatus("away");

// Update name and role
menu.setUserName("John A. Smith");
menu.setUserRole("Senior Administrator");

// Switch to image avatar
menu.setAvatarUrl("/img/john-new.png");

// Replace menu items
menu.setMenuItems([
    { id: "settings", label: "Settings", icon: "bi-gear" },
    { id: "sign-out", label: "Sign Out", icon: "bi-box-arrow-right", danger: true },
]);

API

Factory Function

FunctionDescription
createUserMenu(containerId, options)Create a UserMenu and mount it into the specified container

Public Methods

MethodDescription
show(containerId)Mount the component into a container element
hide()Remove from DOM and detach listeners
destroy()Full cleanup, nullify all references
getElement()Return the root HTMLElement (or null if destroyed)
open()Programmatically open the dropdown
close()Programmatically close the dropdown
isOpen()Returns true if the dropdown is currently open
setStatus(status)Update the status dot ("online", "offline", "busy", "away")
setUserName(name)Update the displayed user name
setUserRole(role)Update the displayed role in the dropdown header
setAvatarUrl(url)Switch the avatar to an image
setMenuItems(items)Replace the entire menu item list

UserMenuOptions

OptionTypeDefaultDescription
userNamestringRequiredDisplay name
userRolestring""Role label shown in dropdown header
avatarUrlstring-Image URL for avatar
avatarInitialsstringAuto-derivedOverride initials text
avatarColorstringAuto-derivedOverride initials background colour
statusstring-Status dot: "online", "offline", "busy", "away"
menuItemsUserMenuItem[]RequiredArray of menu items
onItemClick(itemId: string) => void-Callback when any item is clicked
onSignOut() => void-Convenience callback for the "sign-out" item ID
size"sm" | "md" | "lg""md"Size variant
cssClassstring-Additional CSS class(es) on root
keyBindingsPartial<Record<string, string>>-Override default key combos

UserMenuItem

PropertyTypeDefaultDescription
idstringRequiredUnique identifier
labelstringRequiredDisplay text
iconstring-Bootstrap icon class (e.g. "bi-gear")
type"item" | "divider" | "header""item"Entry type
disabledbooleanfalseGreyed out, non-interactive
dangerbooleanfalseRed styling for destructive actions

Status Values

StatusDot ColourDescription
onlineGreenUser is active and available
offlineGreyUser is disconnected
busyRedUser is occupied, do not disturb
awayYellowUser is temporarily away

Keyboard Shortcuts

When Trigger is Focused

KeyAction
Enter / SpaceToggle dropdown open/close
Arrow DownOpen dropdown and focus first item

When Dropdown is Open

KeyAction
Arrow DownFocus next item (wraps, skips disabled)
Arrow UpFocus previous item (wraps, skips disabled)
HomeFocus first item
EndFocus last item
EnterActivate focused item
EscapeClose dropdown, return focus to trigger

All key bindings can be overridden via the keyBindings option. See DEFAULT_KEY_BINDINGS in the source for action names.

Accessibility


VisualTableEditor

A compact, embeddable table component for editing and viewing styled tabular data. Unlike DataGrid (which targets data management with sorting, filtering, and pagination), VisualTableEditor is a visual-first component for presenting and editing cell-level styled tables -- analogous to table widgets in PowerPoint, Figma, draw.io, or Notion. Supports per-cell formatting, inline rich content (bold/italic/links/images), cell merging, presets, live aggregate summaries, undo/redo, and clipboard operations.

Assets

AssetPath
CSScomponents/visualtableeditor/visualtableeditor.css
JScomponents/visualtableeditor/visualtableeditor.js
Typescomponents/visualtableeditor/visualtableeditor.d.ts

Requirements

Quick Start

<link rel="stylesheet" href="components/visualtableeditor/visualtableeditor.css">
<script src="components/visualtableeditor/visualtableeditor.js"></script>
<script>
    var table = createVisualTableEditor({
        container: "#my-container",
        mode: "edit",
        preset: "blue-header",
        data: {
            columns: [
                { id: "name", width: 180, align: "left" },
                { id: "role", width: 140, align: "left" },
                { id: "status", width: 100, align: "center" }
            ],
            rows: [
                {
                    id: "hdr",
                    cells: {
                        name: { value: "Name", style: { bold: true } },
                        role: { value: "Role", style: { bold: true } },
                        status: { value: "Status", style: { bold: true } }
                    }
                },
                {
                    id: "r1",
                    cells: {
                        name: { value: "Alice Chen" },
                        role: { value: "Engineer" },
                        status: { value: "Active", style: { color: "#198754" } }
                    }
                }
            ]
        }
    });
</script>

API

Factory

function createVisualTableEditor(options: VisualTableEditorOptions): VisualTableEditor;

Options

OptionTypeDefaultDescription
containerHTMLElement | stringrequiredContainer element or CSS selector
mode"edit" | "view""edit"Initial mode
dataVisualTableDataInitial table data (empty 3x3 if omitted)
pageSizenumber0Rows per page (0 = auto-paginate at 500)
showToolbarbooleantrueShow formatting toolbar in edit mode
showRowNumbersbooleanfalseShow row number gutter column
resizableColumnsbooleantrueAllow column resize by dragging
resizableRowsbooleanfalseAllow row resize by dragging
allowMergebooleantrueAllow cell merging
allowStructureEditbooleantrueAllow adding/removing rows
allowColumnEditbooleantrueAllow adding/removing columns
allowReorderbooleanfalseAllow drag-reorder of rows and columns
presetstringNamed table preset (see Presets)
compactbooleanfalseCompact mode -- reduced padding/font
containedbooleanfalseContained mode -- no viewport docking
minWidthnumber200Minimum table width in px
cssClassstringAdditional CSS class on root element

Callbacks

CallbackSignatureDescription
onCellChange(row, col, oldValue, newValue) => voidCell value changed
onStyleChange(row, col, style) => voidCell style changed
onStructureChange(action, detail) => voidRow/column add/remove/resize/reorder
onModeChange(mode) => voidMode switched
onChange(data) => voidTable data changed (debounced 300ms)
onHeaderClick(columnId, event) => voidHeader cell clicked (host sorting)
onHeaderContextMenu(columnId, event) => voidHeader cell right-clicked
onSelectionChange(selection) => voidSelection changed
onAggregateChange(aggregates) => voidAggregates recomputed on selection

Methods

MethodReturnsDescription
setMode(mode)voidSwitch between "edit" and "view"
getMode()stringCurrent mode
getData()VisualTableDataExport full table data as JSON
setData(data)voidImport table data (replaces current)
clear()voidReset to empty table
getCellValue(row, col)stringGet cell text
setCellValue(row, col, value)voidSet cell text
getCellStyle(row, col)VisualTableCellStyleGet cell style
setCellStyle(row, col, style)voidSet cell style (merge with existing)
getSelection()CellRange[]Current selection
setSelection(range)voidSelect a contiguous range
addToSelection(range)voidAdd range to non-contiguous selection
selectRow(rowIndex)voidSelect entire row
selectColumn(colIndex)voidSelect entire column
selectAll()voidSelect all cells
clearSelection()voidDeselect all
insertRow(index?)stringInsert row, returns row ID
removeRow(rowId)voidRemove a row
insertColumn(index?)stringInsert column, returns column ID
removeColumn(colId)voidRemove a column
setColumnWidth(colId, width)voidSet column width
setRowHeight(rowId, height)voidSet row height
moveRow(from, to)voidReorder a row
moveColumn(from, to)voidReorder a column
mergeCells(range)voidMerge cells in range
unmergeCells(row, col)voidUnmerge a merged cell
applyStyleToSelection(style)voidApply style to all selected cells
applyPreset(name)voidApply a named table preset
setHeaderRows(count)voidSet number of header rows
setAlternatingRows(enabled, color?)voidToggle alternating row colours
sortRows(comparator)voidReorder rows using comparator
filterRows(predicate)voidShow/hide rows by predicate
clearFilter()voidShow all rows
getAggregates(range?)AggregateResultCompute aggregates for range or selection
setFooterAggregate(type)voidSet table-level footer aggregate
setColumnAggregate(colId, type)voidSet per-column footer aggregate
showSummaryBar(show)voidToggle summary bar visibility
show()voidShow the component
hide()voidHide the component
destroy()voidRemove DOM and release resources
refresh()voidRe-render the table

Data Model

VisualTableData

PropertyTypeDescription
metaVisualTableMetaTable-level metadata (header rows, alternating, borders, etc.)
columnsVisualTableColumn[]Column definitions (id, width, alignment)
rowsVisualTableRow[]Row data with cells keyed by column ID

VisualTableCell

PropertyTypeDefaultDescription
valuestring | VisualTableCellContent[]Plain text or rich content segments
styleVisualTableCellStylePer-cell style overrides
colspannumber1Column span
rowspannumber1Row span

VisualTableCellContent

Rich content segment for mixed formatting within a cell (e.g., "Total: **$1,250**").

PropertyTypeDescription
type"text" | "link" | "image"Content type
textstringText content (for text/link)
urlstringURL (for link href or image src)
altstringAlt text (for image)
widthnumberImage width in px
heightnumberImage height in px
styleobjectInline style: bold, italic, underline, color, fontSize

VisualTableCellStyle

PropertyTypeDescription
backgroundstringBackground colour
colorstringText colour
fontFamilystringFont family
fontSizenumberFont size in px
boldbooleanBold
italicbooleanItalic
underlinebooleanUnderline
align"left" | "center" | "right"Horizontal alignment
valign"top" | "middle" | "bottom"Vertical alignment
wrapbooleanText wrapping (default: true)
paddingnumberCell padding in px

Example JSON

{
    "meta": { "headerRows": 1, "alternatingRows": true, "bordered": true },
    "columns": [
        { "id": "name", "width": 180 },
        { "id": "role", "width": 140 },
        { "id": "status", "width": 100, "align": "center" }
    ],
    "rows": [
        {
            "id": "hdr",
            "cells": {
                "name": { "value": "Name", "style": { "bold": true, "background": "#0d6efd", "color": "#fff" } },
                "role": { "value": "Role", "style": { "bold": true, "background": "#0d6efd", "color": "#fff" } },
                "status": { "value": "Status", "style": { "bold": true, "background": "#0d6efd", "color": "#fff" } }
            }
        },
        { "id": "r1", "cells": { "name": { "value": "Alice" }, "role": { "value": "Engineer" }, "status": { "value": "Active", "style": { "color": "#198754" } } } },
        { "id": "r2", "cells": { "name": { "value": "Bob" }, "role": { "value": "Designer" }, "status": { "value": "Away", "style": { "color": "#fd7e14" } } } }
    ]
}

Presets

Six built-in presets applied via applyPreset(name) or the preset option.

PresetHeader BGAlt RowDescription
"blue-header"#0d6efd#e7f1ffClassic blue header, light blue stripes
"dark-header"#212529#f8f9faDark header, subtle grey stripes
"green-accent"#198754#e8f5e9Green header, light green stripes
"warm"#fd7e14#fff3e0Warm orange header, cream stripes
"minimal"transparentnoneNo fills, bottom borders only
"striped"var(--bs-tertiary-bg)var(--bs-tertiary-bg)Subtle grey, standard striped

Presets set meta and header-row styles without overwriting per-cell overrides. The "minimal" and "striped" presets use CSS variables and adapt to dark mode automatically.

Aggregates

Live aggregate computations on the current selection, displayed in an optional summary bar.

AggregateDescription
SumTotal of numeric values
AverageArithmetic mean
CountNon-empty cells (all types)
Count NumbersCells with parseable numeric values
Min / MaxSmallest / largest numeric value
MedianMiddle value when sorted
ModeMost frequent numeric value
Std DevPopulation standard deviation
RangeMax minus Min

Aggregates are unit-aware: only computed when all numeric cells share the same unit signature (e.g., all $-prefixed or all %-suffixed). Mixed units produce no result. Results reattach the unit prefix/suffix for display (e.g., Sum: $1,250).

Footer aggregate rows can be configured per-table or per-column. They are non-editable and recompute automatically on cell value changes.

Keyboard Shortcuts

KeyAction
Arrow keysMove selection
Shift+ArrowExtend contiguous range
Ctrl+ClickToggle cell in non-contiguous selection
Tab / Shift+TabNext / previous cell
Enter / F2Begin editing selected cell
EscapeCancel editing / clear selection
Delete / BackspaceClear selected cell(s)
Ctrl+B / I / UToggle bold / italic / underline
Ctrl+ASelect all cells
Ctrl+C / VCopy / paste (TSV format)
Ctrl+ZUndo
Ctrl+YRedo

Accessibility

Window Globals

GlobalType
window.createVisualTableEditorfunction(options): VisualTableEditor

See specs/visualtableeditor.prd.md for the complete specification.


WorkspaceSwitcher

Dropdown or modal control for switching between organisational workspaces and tenants.

Usage

<link rel="stylesheet" href="components/workspaceswitcher/workspaceswitcher.css">
<script src="components/workspaceswitcher/workspaceswitcher.js"></script>
const switcher = createWorkspaceSwitcher({
    workspaces: [
        { id: "1", name: "Acme Corp", icon: "bi-building", role: "Owner" },
        { id: "2", name: "Beta Industries", role: "Admin", memberCount: 8 },
        { id: "3", name: "Gamma Retail", avatarUrl: "/img/gamma.png", role: "Member" },
    ],
    activeWorkspaceId: "1",
    mode: "dropdown",
    onSwitch: (ws) => console.log("Switched to:", ws.name),
    onCreate: () => console.log("Create workspace"),
}, "my-container");

Options

OptionTypeDefaultDescription
workspacesWorkspace[]RequiredAvailable workspaces
activeWorkspaceIdstringRequiredCurrently active workspace ID
mode"dropdown" | "modal""dropdown"Display mode
showSearchbooleanAuto (>5)Show search input
showCreateButtonbooleantrueShow create workspace button
showMemberCountbooleanfalseShow member count
showRolebooleantrueShow user role badge
showPlanbooleanfalseShow plan badge
size"sm" | "default" | "lg""default"Size variant
onSwitch(ws) => void-Workspace switched callback
onCreate() => void-Create button callback
onSearch(q) => Promise<Workspace[]>-Server-side search

API

MethodDescription
show(containerId)Mount to container
hide()Remove from DOM
destroy()Full cleanup
open()Programmatic open
close()Programmatic close
isOpen()Check open state
getActiveWorkspace()Get active workspace
setActiveWorkspace(id)Set active workspace
setWorkspaces(ws[])Replace workspace list
addWorkspace(ws)Add a workspace
removeWorkspace(id)Remove a workspace

Keyboard

KeyAction
Enter / SpaceOpen/select
EscapeClose
Arrow Up/DownNavigate items
Home / EndFirst/last item