Utilities

Plain functions exported alongside the components. They have no side effects (except toasts and dialogs) and are covered by unit tests.

Toasts

import { toasts } from '@ruckc/truss-ui';

toasts.success('Saved');                       // intent helpers: success, warning, danger
toasts.push('Queued', { intent: 'primary', timeout: 8000 });   // timeout: 0 keeps it until dismissed
const id = toasts.danger('Build failed', 0);
toasts.dismiss(id);

toasts.push('Message sent', {
  action: { label: 'View', run: () => openThread() }   // a button; running it dismisses the toast
});

toasts.undoable({                              // runs now, then offers Undo
  message: 'Deleted 3 files',
  run: () => deleteFiles(ids),
  undo: () => restoreFiles(ids),
  timeout: 8000,                                 // default; undo is offered until the toast times out
});

Toasts that have an action get a button before the Dismiss control. The button is keyboard reachable and its label is part of the announced message. Auto-dismiss pauses while the pointer is over a toast or focus is inside it, and resumes with the time that was left when both leave. toasts.undoable() returns the toast id.

run and undo may return a Promise. While an async run is in flight the toast is already on screen with aria-busy="true", a spinner in place of its icon and a disabled Undo button, and it does not time out; the timeout starts when run resolves. If run rejects the toast is replaced by a danger toast with the error message. Pressing Undo with an async undo keeps the toast busy until it settles and then dismisses it; if undo rejects, an error toast follows. errorMessage customises the error text, and toasts.undoableAsync() takes the same options and returns a promise of whether run succeeded. Synchronous callers behave exactly as before. An action's run passed to toasts.push() may be async in the same way.

toasts.undoable({
  message: 'Deleted 3 files',
  run: () => api.deleteFiles(ids),                 // pending state until it settles
  undo: () => api.restoreFiles(ids),               // Undo is busy until it settles
  errorMessage: (e) => `Could not delete: ${(e as Error).message}`
});

const ok = await toasts.undoableAsync({ message: 'Archived', run: archive, undo: unarchive });   // true, or false after the error toast

Types: Toast, ToastIntent and ToastAction are exported.

Mount <Toaster /> once in your root layout.

Dialogs

Premade, promise-based replacements for the browser's alert(), confirm() and prompt(). Mount <DialogHost /> once in your root layout, then await them anywhere. Calls are queued and shown one at a time. Escape, the close button and a backdrop click all count as dismissing: confirm resolves false and prompt resolves null, just like the native versions.

import { dialogs } from '@ruckc/truss-ui';

await dialogs.alert('Your changes were saved.');

if (await dialogs.confirm('Delete 3 files?', { intent: 'danger', okLabel: 'Delete' })) {
  // destructive confirmations focus Cancel, not Delete
}

const name = await dialogs.prompt('Name the project:', {
  label: 'Name',
  defaultValue: 'Untitled',
  validate: (v) => (v.trim() ? undefined : 'A name is required')   // return a message to keep the dialog open
});   // string, or null when dismissed

To do the work before closing, pass onconfirm to confirm or prompt (which receives the value). It may return a Promise: the dialog stays open with the OK button busy, and Cancel, Escape, the close button and a backdrop click are disabled until it settles. If it throws or rejects, the dialog stays open and shows the error inline (role="alert") so the user can retry or cancel. On success the dialog closes and the call resolves true (for prompt, the value). Without onconfirm nothing changes.

const deleted = await dialogs.confirm('Delete 3 files?', {
  intent: 'danger', okLabel: 'Delete',
  onconfirm: () => api.deleteFiles(ids)    // keeps the dialog open and busy; an error is shown inside it
});

const name = await dialogs.prompt('Rename the project:', {
  label: 'Name',
  onconfirm: (value) => api.rename(value)
});

Options: title, intent (primary, warning, danger), okLabel, cancelLabel; for prompts also label, defaultValue, placeholder, type, multiline and validate; for confirm and prompt onconfirm. dialogs.clear() dismisses everything pending.

Hotkeys and commands

A single window keydown listener (installed with the first binding) drives every shortcut. mod means ⌘ on Apple platforms and Ctrl elsewhere; a space separates the steps of a sequence such as g h, which resets after a second or on a stray key. Shortcuts are ignored while the user types in an input, textarea, select or contenteditable element unless the binding sets allowInInput. Global bindings pause while a modal scope (the palette, the shortcut sheet) is on top. Binding the same combination twice in one scope logs a warning in development.

import { registerCommand, hotkeys } from '@ruckc/truss-ui';

// a command: listed in the palette, and bound to its shortcut while registered
const off = registerCommand({
  id: 'go-home', label: 'Go to home', group: 'Navigate', keywords: ['start'],
  shortcut: 'g h', run: () => goto('/')
});
off();   // unregister (call it from onDestroy or an effect cleanup)

// a bare shortcut
hotkeys.register('mod+shift+p', () => togglePanel(), { description: 'Toggle panel', group: 'View' });

// hold the global bindings while your own modal is open
const release = hotkeys.pushScope('my-modal');

Mount <CommandPalette /> (opens with mod+k) and <ShortcutSheet /> (opens with ?) once in your root layout. The palette fuzzy-filters the registered commands, lists recently run ones first, and can query async providers for extra results such as search hits. It is a combobox over a listbox, and announces the result count through a polite live region.

<CommandPalette providers={[{
  id: 'docs', label: 'Documents',
  search: async (q, signal) => (await find(q, signal)).map((d) => ({ id: d.id, label: d.title, run: () => goto(d.url) }))
}]} />
<ShortcutSheet />

Key caps

<Kbd keys="mod+shift+p" /> renders one <kbd> per key. Tooltip and Button take a shortcut prop that shows it in the tooltip (part of its accessible description) and sets aria-keyshortcuts; it is display only, so bind the key with registerCommand or hotkeys. The helpers are exported too.

import { formatCombo, matches, parseCombo } from '@ruckc/truss-ui';

formatCombo('mod+shift+p', 'apple');   // ['⇧', '⌘', 'P']
formatCombo('mod+shift+p', 'other');   // ['Ctrl', 'Shift', 'P']
matches(event, 'mod+k');               // does this KeyboardEvent match the combo?

Durations

import { parseDuration, formatDuration } from '@ruckc/truss-ui';

parseDuration('1h 30m');            // 5400
parseDuration('1:30');              // 5400   (h:mm)
parseDuration('PT1H30M');           // 5400   (ISO 8601)
parseDuration('90', ['m', 's']);    // 90     (a bare number uses the smallest enabled unit)
parseDuration('banana');            // null
formatDuration(5400, ['d', 'h', 'm']);   // '1h 30m'

Colour

import { parseHex, toHex, contrast } from '@ruckc/truss-ui';

parseHex('#1f5fbf');                        // { r: 31, g: 95, b: 191, a: 1 }
parseHex('#f008');                          // 4-digit shorthand with alpha
toHex({ r: 255, g: 0, b: 0 }, 0.5);        // '#ff000080'
contrast(rgbA, rgbB);                       // WCAG contrast ratio, 1–21

Graph

import { autoLayout, routeEdge, portAnchor, defaultPorts } from '@ruckc/truss-graph';

// Layered layout (cycle-safe); returns new nodes
nodes = autoLayout(nodes, edges, { direction: 'LR', gapX: 90, gapY: 28 });

// The router behind <Graph>, usable on its own
const { d, mid } = routeEdge('avoid', a, b, obstacles);   // an SVG path and its midpoint

Dates and times (used by the pickers)

Dates are YYYY-MM-DD strings and times are HH:MM, so they compare lexicographically. The pickers also accept typed forms such as 3/5/2026, 2:30 pm, 9a and 1430, normalising them on commit.

/docs/utilities Truss UI · MIT