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.