stareSDK v0.2.21

Elite Command Design System

@kineticlogic/staresdk is the React component library, theming and design-token set behind OpenStare and OpenTrack. Every example on this page is the real component from this release, running live. Use the theme button in the header to see each one in dark and light.

Overview#

The package ships the components, ThemeProvider/useTheme (dark and light), the chromeTheme.ts token set, and optional map, graph, chart and code-editor views. Components take their data as props. Routing, data fetching and app state stay in your app.

Getting started#

Install#

npm install @kineticlogic/staresdk react react-dom react-icons framer-motion react-markdown remark-gfm \
  mdast-util-find-and-replace @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities

The optional views need their own peer dependencies. Install them only if you use those views:

Entry pointInstall
@kineticlogic/staresdk/map-viewmaplibre-gl
@kineticlogic/staresdk/graph-view@xyflow/react @dagrejs/dagre
@kineticlogic/staresdk/chartuplot
@kineticlogic/staresdk/code-editor@codemirror/* and @lezer/highlight
Mermaid diagrams in MDTextmermaid

Usage#

Import the stylesheet once at your application's entry point, wrap your tree in ThemeProvider (and ToastProvider if you use Toast/useToast), then import components by name:

import '@kineticlogic/staresdk/styles.css'
import { ThemeProvider, ToastProvider, Button, Modal, useTheme } from '@kineticlogic/staresdk'

function App() {
  return (
    <ThemeProvider>
      <ToastProvider>
        <YourApp />
      </ToastProvider>
    </ThemeProvider>
  )
}

Theming#

ThemeProvider resolves the active theme ('dark' | 'light', default 'dark') onto <html data-theme="dark"|"light"> and exposes it through useTheme(). Every component reads its colours from the --color-*/--surface-*/--status-* custom properties in styles.css, so once the stylesheet and provider are mounted, components theme correctly with no further wiring.

const { theme, setTheme, commitTheme } = useTheme()
setTheme('light')     // paint only: a live preview
commitTheme('light')  // paint and remember the choice (localStorage 'theme')
Example useTheme()

Design contract#

DESIGN-CONTRACT.md says how an app built on stareSDK is laid out and which component to use for what. It is a contract, not a style suggestion. Read it before building a page. In short:

Rules that are never broken#

  1. Tokens only. Every colour, font, radius, shadow and spacing value comes from a var(--…) token (or chromeTheme.ts on render paths that cannot read CSS).
  2. Use the component that exists. One component per pattern. Don't hand-roll modals, badges, sliders, tab bars or panels.
  3. No unrequested controls. Never add a button, menu item, toggle or advisory copy nobody asked for.
  4. Icons are Tabler. Every icon comes from react-icons/tb.
  5. No placeholder states. Every view handles loading, empty and error explicitly, with real copy.
  6. Compositor-safe motion only. Animate transform, opacity and filter.
  7. Tight, not oversized. Components are compact by default; let tables and panels take the width.

Page anatomy#

Every page is a full-height column: one PageHeader, a Tabs variant="bar" for primary views, then stacked CollapsiblePanels. Detail opens in a right SideNav. The example below is that page, built from the real components in a small frame.

Example PageHeader + Tabs + CollapsiblePanel + DataTable
<DockProvider>
  <div style={{ position: 'fixed', inset: 0, display: 'flex', flexDirection: 'column',
                background: 'var(--color-bg-primary)', fontFamily: 'var(--font-sans)' }}>
    <PageHeader title="OpenTrack" appName="OpenTrack" actions={…} />
    <Tabs variant="bar" aria-label="Workspaces" tabs={…} value={view} onChange={setView} />
    <main style={{ flex: 1, overflowY: 'auto', padding: 'var(--space-lg)',
                   marginRight: 'var(--assistant-width, 0px)' }}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-md)' }}>
        <CollapsiblePanel title="Sources" badge="4">…</CollapsiblePanel>
      </div>
    </main>
  </div>
</DockProvider>

Which component, when#

NeedUseNot
Page title barPageHeadera custom header div
Primary views of a pageTabs variant="bar"buttons styled as tabs
A section of a pageCollapsiblePanelbordered sections, cards
Detail of a selected itemSideNava split column, a modal
Confirm / short formModal, useToast().confirm()window.confirm
Transient result or erroruseToast().toast()inline banners that never clear
Tabular dataDataTablehand-built <table>
Status, count, categoryBadgecoloured text, custom pills
ActionsButton (one primary per view)links styled as buttons
Save with dirty/saving/saved statesSaveButtonad-hoc state text
Text / number fieldLabel + Inputbare <input>
Pick one of a listFieldSelect, Selectbare <select>
Pick a unit or conventionUnitSelectinline unit math
On/off settingTogglecheckboxes for settings
Numeric rangeSlider, OpacitySlider, ZoomRangeSlider<input type="range">
Multi-step flowSteppernumbered headings
Paging a long listPaginationinfinite scroll
Explaining a fieldInfoTiphelper text under every field
Row actions menuPopoverMenuButton / ContextMenu
File inputFileDropZonebare file input
JSON / text editingCodeEditor<textarea>
Points on a mapMapViewa second map library
Lineage or a pipelineGraphViewhand-drawn SVG
Values over timeTimeSeriesCharta second chart library
Rendered MarkdownMDText

Foundations#

Chrome colours come from two places, matched to where they are read. In CSS and inline style={{}}, use var(--token-name), resolved from :root (dark) and [data-theme="light"] in styles.css. On JS, Canvas and other non-DOM render paths, use chromeTheme.ts's DARK_THEME / LIGHT_THEME, or themeFor(name). Never call getComputedStyle from a render path. Each swatch below shows the live token in the current theme, with its dark and light values. Click a token name to copy var(--…).

Canonical roles#

The eight canonical token roles. Every surface, text run and accent in the system is one of these.

Surfaces, text and brand#

Status colours go through Badge colours success, warning, blue and danger. Links are always --color-link and underlined; the accent green is never a link.

Intelligence layer#

For charts, maps and overlays only, never for buttons or chrome. These are theme-invariant.

Badge fills#

BADGE_BG is one of the two deliberate raw-hex exceptions: Badge's contrastText() parses these as WCAG luminance values, so a var() string would not work.

Severity ramp#

chromeTheme.ts's severityRamp, for topology, map and HUD-style consumers, is the other raw-hex exception. Its hues stay distinct from the --status-* set.

Classification markings#

Domain colours, not chrome. They read the same in both themes, so a marking never changes with the reader's setting.

Typography#

Montserrat (--font-sans) for all chrome, Oswald (--font-header) for wordmarks only, and --font-mono for data values only.

Type scale

Spacing, radii and shadows#

Page padding --space-lg; gap between panels --space-md; between related controls --space-sm. Panels use --radius-lg, inputs and buttons --radius-sm/--radius-md.

Scales

Icons#

Every icon a stareSDK component renders comes from react-icons/tb (Tabler). Use the same set for anything you build around them. No other icon set, and no emoji or Unicode glyphs as affordances.

Example react-icons/tb
import { TbMap2, TbRadar2, TbSettings } from 'react-icons/tb'
<Button variant="ghost" size="xs" icon={<TbSettings />} aria-label="Settings" title="Settings" />

Components#

Each component below renders live. Interact with it, switch the theme, and copy the code underneath. Everything is imported from @kineticlogic/staresdk unless the section names a subpath.

Actions and controls

Button Available in 0.2.0+#

Variants primary (at most one per panel or drawer), secondary, ghost (icon-only) and danger; sizes xs (square, icon-only) to xl. Use size="sm" in panel headers and drawers. Icon-only buttons always carry aria-label and title.

Example Button
<Button variant="primary" icon={<TbPlus />}>Add source</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="danger" icon={<TbTrash />}>Delete</Button>
<Button variant="ghost" size="xs" icon={<TbRefresh />} aria-label="Refresh" title="Refresh" />
<Button variant="secondary" active>Selected</Button>

ButtonPalette Available in 0.2.0+#

Groups related buttons with even spacing, horizontally or vertically, on an optional glass surface. For map tool palettes and layout switchers.

Example ButtonPalette
<ButtonPalette ariaLabel="Map tools">
  <Button variant="ghost" size="xs" icon={<TbPointer />} aria-label="Select" title="Select" active />
  <Button variant="ghost" size="xs" icon={<TbRuler2 />} aria-label="Measure" title="Measure" />
</ButtonPalette>

ButtonPalettePopover Available in 0.2.15+#

The flyout a palette button opens: rendered in place beside its trigger (to the right of a vertical palette by default, left, or top/bottom of a horizontal one), flipped to the other side when there is no room, lined up with the trigger (or, with alignTo, with the whole palette), and kept clear of the classification banners and inside any container that clips its overflow. It wears the palette's glass surface and closes on Escape or a press outside. A drawing tool passes closeOnOutsideClick={false} so it stays open while the user works on the map. isolatePointer stops pointer events at the flyout when it sits over a canvas that listens above it. Use Popover for menus hung below a control.

Example ButtonPalette + ButtonPalettePopover
<ButtonPalette ariaLabel="Map tools" orientation="vertical">
  <ButtonPalettePopover open={open} onClose={() => setOpen(false)} closeOnOutsideClick={false}
    ariaLabel="Measure" title="Measure" width={200}
    trigger={<Button variant="ghost" size="xs" icon={<TbRuler2 />} aria-label="Measure" title="Measure"
      active={open} aria-expanded={open} onClick={() => setOpen(o => !o)} />}>
    <RadioGroup ariaLabel="Measure" size="sm" value={mode} onChange={setMode} options={modes} />
  </ButtonPalettePopover>
</ButtonPalette>

SaveButton Available in 0.2.0+#

A Button with built-in dirty, saving and saved states. It stays disabled until there is something to save.

Example SaveButton
<SaveButton dirty={dirty} saving={saving} saved={saved} onSave={save} />

PopoverMenuButton Available in 0.2.0+#

A full-width, left-aligned menu item for kebab and popover menus.

Example PopoverMenuButton
<div role="menu">
  <PopoverMenuButton role="menuitem" icon={<TbPencil />} onClick={rename}>Rename</PopoverMenuButton>
  <PopoverMenuButton role="menuitem" icon={<TbTrash />} onClick={remove}
    style={{ color: 'var(--color-destructive)' }}>Delete</PopoverMenuButton>
</div>
Forms and input

Label and Input Available in 0.2.0+#

Every text or number field is a Label (uppercase, secondary) above an Input. error turns the border destructive.

Example Label + Input
<Label htmlFor="callsign">Callsign</Label>
<Input id="callsign" value={value} onChange={e => setValue(e.target.value)} placeholder="e.g. VIPER 21" />
<Input error value="12.3.4" aria-invalid />

Select Available in 0.2.0+#

Pick one value from a list of options, with an optional "nothing chosen" placeholder.

id, name, required, title, ref: 0.2.13+

Example Select
<Select ariaLabel="Domain" placeholder="Any domain" value={domain} onChange={setDomain}
  options={[{ value: 'air', label: 'Air' }, { value: 'sea', label: 'Sea' }, { value: 'land', label: 'Land' }]} />

FieldSelect Available in 0.2.0+#

Pick one field by name. allowNone adds a leading "None"; options can be disabled with a reason.

Example FieldSelect
<FieldSelect ariaLabel="Label field" allowNone value={field} onChange={setField}
  fields={[{ name: 'callsign' }, { name: 'mmsi' }, { name: 'geometry', disabled: true, disabledReason: 'Not a text field' }]} />

MultiSelect Available in 0.2.0+#

Choose several values from a dropdown checklist. locked options are always on.

icon trigger: 0.2.6+

Example MultiSelect
<MultiSelect ariaLabel="Roles" value={roles} onChange={setRoles}
  options={[{ value: 'viewer', label: 'Viewer', locked: true }, { value: 'operator', label: 'Operator' }, …]} />

TagInput Available in 0.2.20+#

Type values and press Enter or comma to add each as a chip; paste a comma-separated list to add them all. Backspace in the empty input removes the last chip, and each chip has its own ×. Values are trimmed and never added twice; normalize (e.g. lower-casing) runs first, and a value validate rejects is not added — its message shows under the field. suggestions lists matching values as you type (free entry still works), maxItems caps the count, and readOnly shows the chips alone.

A comma from a touch keyboard or autofill ends a value, and the placeholder shows only while there are no chips: 0.2.21+

Example TagInput
<Label htmlFor="elnots">ELNOTs</Label>
<TagInput id="elnots" ariaLabel="ELNOTs" value={elnots} onChange={setElnots}
  normalize={v => v.toUpperCase()}
  validate={v => (/^[0-9A-Z]{5}$/.test(v) ? null : 'An ELNOT is five letters or digits')} />
<TagInput ariaLabel="Tags" value={tags} onChange={setTags} suggestions={['maritime', 'air', 'land']} />

UnitSelect Available in 0.2.0+#

Pick a unit, orientation convention or confidence level. The family picks the list; conversions live in units.ts.

speed (m/s, km/h, knots, mph) and time (seconds, minutes, hours, days) families: 0.2.18+

Example UnitSelect
<UnitSelect family="distance" value={unit} onChange={setUnit} ariaLabel="Distance unit" />
distanceToMeters(12, 'nmi') // 22224
speedToMetersPerSecond(20, 'kn') // 10.29
durationToSeconds(2, 'h') // 7200

Checkbox Available in 0.2.12+#

A real checkbox with a label, the accent tint, and an indeterminate state.

Example Checkbox
<Checkbox label="Show empty fields" checked={show} onChange={setShow} />

RadioGroup Available in 0.2.15+#

One choice from a short list the reader should see side by side (use Select when the list is long). Real radios in a radiogroup: one tab stop, and the arrow keys move and choose. Options take a description and disabled; orientation="horizontal" lays them in a row.

Example RadioGroup
<RadioGroup ariaLabel="Layer source" label="Layer source" value={path} onChange={setPath}
  options={[{ value: 'existing', label: 'Use an existing layer' },
            { value: 'template', label: 'Create from template' },
            { value: 'import', label: 'Import a file', disabled: true }]} />
<RadioGroup ariaLabel="Clearance" value={level} onChange={setLevel}
  options={levels.map(l => ({ value: l.id, label: l.name, description: l.id }))} />

Toggle Available in 0.2.0+#

On/off settings. It renders no label of its own, so compose one beside it. onColor can mark a warning state.

Example Toggle
<Toggle value={on} onChange={setOn} aria-label="Show labels" />
<Toggle size="sm" value={airgap} onChange={setAirgap} onColor="var(--status-warning)" aria-label="Disconnected mode" />

Slider Available in 0.2.0+#

The standard single-handle numeric slider, for any min, max and step.

Example Slider
<Slider min={0.5} max={4} step={0.25} value={scale} onChange={setScale}
  aria-label="Symbol scale" formatValue={v => `${v}×`} />

OpacitySlider Available in 0.2.0+#

The single opacity control, 0–100.

Example OpacitySlider
<OpacitySlider value={opacity} onChange={setOpacity} aria-label="Layer opacity" />

ZoomRangeSlider Available in 0.2.0+#

A dual-handle zoom range that never crosses. With currentZoom, it can set either end from the map's zoom.

Example ZoomRangeSlider
<ZoomRangeSlider minValue={min} maxValue={max} currentZoom={9}
  onChange={(lo, hi) => { setMin(lo); setMax(hi) }} />

ColorPicker Available in 0.2.0+#

A swatch popover with a custom hex field. Its value is always a resolved hex, never a token.

Example ColorPicker
<ColorPicker value={hex} onChange={setHex} aria-label="Track colour" />

RampPicker Available in 0.2.0+#

Pick a curated ColorBrewer ramp (graduated or categorized), with a reverse toggle.

Example RampPicker
<RampPicker kind="sequential-diverging" rampId={ramp} onChange={setRamp}
  reversed={reversed} onReverseChange={setReversed} />

TypeaheadPicker Available in 0.2.0+#

The dropdown shell for search-and-pick from many: glass panel, highlight, loading, error and empty states. It owns no fetching. Render the input as its sibling inside a position: relative wrapper.

Example TypeaheadPicker
<div style={{ position: 'relative' }}>
  <Input value={query} onChange={…} onKeyDown={onKeyDown} />
  <TypeaheadPicker open={open} items={matches} getKey={s => s.id}
    renderRow={s => s.name} onSelect={pick} onDismiss={close}
    highlightIndex={hi} onHighlightChange={setHi} emptyCopy="No matching sites" />
</div>

FileDropZone Available in 0.2.0+#

Drag-and-drop or browse, with extension checks on both paths and optional progress.

Example FileDropZone
<FileDropZone inputId="layer-file" label="Layer file" accept=".geojson,.kml"
  acceptedExtensions={['geojson', 'kml']} file={file} onFileChange={setFile}
  onReject={message => toast({ variant: 'error', message })} hint="GeoJSON or KML, up to 50 MB" />

CodeEditor Available in 0.2.0+#

@kineticlogic/staresdk/code-editor. A CodeMirror 6 JSON, HTML or text editor, token-themed, with parse errors, caller-supplied diagnostics and caller-supplied completions. Try breaking the JSON, or type { in the template to pick a field.

language="html", completions: 0.2.10+

Example CodeEditor
import { CodeEditor } from '@kineticlogic/staresdk/code-editor'

<CodeEditor aria-label="Source config" language="json" value={json} onChange={setJson} minHeight={160}
  diagnostics={[{ line: 4, message: 'Port 80 needs root on most hosts', severity: 'warning' }]} />

<CodeEditor aria-label="Popup template" language="html" value={html} onChange={setHtml}
  completions={({ text, pos }) => /\{\w*$/.test(text.slice(0, pos))
    ? { from: text.lastIndexOf('{', pos) + 1, options: fields.map(f => ({ label: f.name, detail: f.type, apply: `${f.name}}` })) }
    : null} />
Overlays and menus

For decisions, not browsing: confirmations and short create or edit forms. AdminModal is the same dialog for admin-only forms. Esc and the backdrop close it.

Example Modal + AdminModal
{open && (
  <Modal title="Rename source" onClose={close} width={420}>
    <Label htmlFor="name">Name</Label>
    <Input id="name" defaultValue="AIS — Channel" />
    <Button size="sm" onClick={save}>Save</Button>
  </Modal>
)}

ClassifyModal Available in 0.2.0+#

Picks a full classification marking (level, controls, releasability, NOFORN) with favourites and a live banner preview. The host supplies the options and the banner text.

Example ClassifyModal
<ClassifyModal options={options} value={marking} bannerFor={bannerFor}
  favorites={favorites} onToggleFavorite={toggleFavorite}
  onConfirm={setMarking} onClose={close} />

DraggablePopup Available in 0.2.0+#

A floating, draggable and resizable glass popup whose position is remembered under storageKey.

Example DraggablePopup
<DraggablePopup storageKey="popup:feature" title="Feature" getDefaultPos={() => ({ x: 120, y: 120 })}
  headerRight={<Button variant="ghost" size="xs" icon={<TbX />} aria-label="Close" title="Close" onClick={close} />}>
  …
</DraggablePopup>

Popover Available in 0.2.12+#

A panel hung from a button (or a point): portaled, flipped and clamped to the viewport, kept clear of the classification banners, closed by Escape, an outside press or the anchor scrolling away.

maxHeight: 0.2.13+ · Placement by rendered width: 0.2.14+

Example Popover
<Popover open={open} onClose={close} anchorRef={button} ariaLabel="Layer options">
  <Checkbox label="Show labels" checked={labels} onChange={setLabels} />
</Popover>

ContextMenu Available in 0.2.0+#

A right-click menu at the pointer, flipped to stay inside the viewport. Right-click the frame. The first item takes focus; the arrow keys, Home and End move between enabled items, Enter or Space runs one, and Escape or Tab closes the menu and returns focus to where it was. size="sm" is the compact menu — small text, tight rows, as wide as its content — for a map's short menus.

Keyboard navigation: 0.2.11+ · size="sm": 0.2.16+

Example ContextMenu + ContextMenuItem
{menu && (
  <ContextMenu x={menu.x} y={menu.y} onClose={close} ariaLabel="Track actions" header="TRK-0412">
    <ContextMenuItem onClick={follow}><TbFocus2 /> Follow</ContextMenuItem>
    <ContextMenuItem danger onClick={drop}><TbTrash /> Drop track</ContextMenuItem>
  </ContextMenu>
)}

Flyout Available in 0.2.0+#

A tool menu anchored to a trigger, with an optional filter box, shortcuts, hints, disabled reasons and favourites.

Example Flyout
<Flyout open={open} onClose={close} anchorRef={triggerRef} ariaLabel="Tools" filterable
  onSelect={run} onToggleFavourite={toggleFavourite}
  items={[{ id: 'measure', label: 'Measure', icon: <TbRuler2 />, shortcut: 'M', hint: 'Distance and bearing' }, …]} />
Navigation and layout

The glass page title bar, one per page, first in the column. It sets document.title. Optional leading, center and actions slots take the home button, clocks, assistant and user chip.

Example PageHeader + HomeButton + ClockBadges + AssistantButton + Avatar
<PageHeader title="Track database" appName="OpenTrack"
  leading={<HomeButton onNavigate={navigate} />}
  center={<ClockBadges clocks={[{ timezone: 'UTC', label: 'Zulu' }, { timezone: 'America/New_York', label: 'HQ' }]} />}
  actions={<><AssistantButton open={open} onToggle={toggle} /><Avatar size={28} username="jdoe" /></>}>
  <Badge size="sm" uppercase color="success">Live</Badge>
</PageHeader>

Tabs Available in 0.2.0+#

variant="bar" is the page navigation directly under the header: uppercase, no icons. The inline variant switches views inside a panel or drawer, with optional icons, counts and notification bubbles. Both are WAI-ARIA tabs with arrow-key navigation; pair them with TabPanel.

Example Tabs + TabPanel
<Tabs variant="bar" aria-label="Workspaces" value={view} onChange={setView}
  tabs={[{ id: 'sources', label: 'Sources' }, { id: 'tracks', label: 'Tracks' }, { id: 'schema', label: 'Schema' }]} />

<Tabs aria-label="Track detail" idPrefix="detail" value={tab} onChange={setTab}
  tabs={[{ id: 'summary', label: 'Summary', icon: <TbInfoCircle /> },
         { id: 'contacts', label: 'Contacts', count: 214 },
         { id: 'reviews', label: 'Reviews', badge: 3, badgeLabel: '3 pending' }]} />
<TabPanel id={tab} idPrefix="detail">…</TabPanel>

SideNav Available in 0.2.0+#

The drawer for detail views, docked left or right, resizable, stacking with other drawers. It reserves page width through --assistant-width / --dock-left-width and must sit inside a DockProvider.

Example SideNav + DockProvider
<DockProvider>
  <SideNav open={selected != null} side="right" ariaLabel="Track detail" storageKey="sidenav:track">
    …
  </SideNav>
</DockProvider>

CollapsiblePanel Available in 0.2.0+#

The titled, collapsible section every page is built from. titleActions (directly after the title) and actions (right-aligned) hold the panel's own compact controls; clicks there never toggle it. persistKey remembers the open state.

Example CollapsiblePanel
<CollapsiblePanel title="Sources" badge="4" persistKey="panel:sources"
  titleActions={<Input placeholder="Search" style={{ width: 180, height: 26 }} />}
  actions={<Button size="sm" variant="secondary" icon={<TbPlus />}>Add</Button>}>
  …
</CollapsiblePanel>

Disclosure Available in 0.2.0+#

An inline expander. Its content renders only while open.

Example Disclosure
<Disclosure label="sensors (3)">…</Disclosure>

Stepper Available in 0.2.0+#

A compact wizard step header. Completed or failed steps can be revisited; upcoming ones cannot.

Example Stepper
<Stepper aria-label="Onboard a source" onStepClick={setStep} steps={[
  { id: 'connect', label: 'Connect', status: 'complete' },
  { id: 'map', label: 'Map fields', status: 'current' },
  { id: 'review', label: 'Review', status: 'upcoming' },
]} />

AppCard Available in 0.2.0+#

The landing-page app tile. It renders a real link, so Ctrl/⌘-click still opens a new tab; pass onNavigate to route a plain click through your router. HomeButton, AssistantButton and ClockBadges appear in the PageHeader example.

Example AppCard
<AppCard label="Track DB" icon={<TbRadar2 />} href="/tracks" description="Fused tracks and their sources" onNavigate={navigate} />
<AppCard label="Imagery" icon={<TbPhoto />} href="/imagery" disabled disabledTooltip="Ask an administrator for access" />
Data display

DataTable Available in 0.2.0+#

Sortable columns (stable, nulls last), row selection with keyboard activation, and a sticky header with virtualisation when maxHeight is set. Header rows stay neutral: weight, uppercase and a bottom border, no fill and no accent.

manualSort, pinFirstColumn, onColumnResize, onRowDoubleClick: 0.2.5+ · headerDividers: 0.2.7+ · onColumnReorder: 0.2.8+ · selection: 0.2.9+ · onRowContextMenu: 0.2.11+

Example DataTable
<DataTable aria-label="Tracks" rows={tracks} rowKey={t => t.id} maxHeight={260}
  selectedKey={selected} onRowClick={t => setSelected(t.id)}
  defaultSort={{ key: 'updated', direction: 'desc' }} empty="No tracks yet."
  columns={[
    { key: 'id', header: 'Track', mono: true, width: 110, sortValue: t => t.id },
    { key: 'callsign', header: 'Callsign', sortValue: t => t.callsign },
    { key: 'status', header: 'Status', render: t => <Badge size="sm" uppercase color={…}>{t.status}</Badge> },
    { key: 'speed', header: 'Speed (kn)', align: 'right', mono: true, sortValue: t => t.speed },
  ]} />

For a server-paged attribute table: manualSort leaves row order to the caller (header clicks only report the next sort), a column with sortable: false loses its sort affordance and shows sortDisabledReason as its tooltip, pinFirstColumn keeps the first column in place while the table scrolls sideways, onColumnResize adds a drag handle to each header edge (reported once, on release; widths stay controlled through each column's width), and onRowDoubleClick opens a row. The single highlighted row is selectedKey. selection adds a checkbox column for a caller-owned multi-row selection, onColumnReorder lets the reader drag headers into a new order, headerDividers separates headings that run together, and onRowContextMenu opens a row menu (right-click, Shift+F10 or the ContextMenu key).

Example DataTable as an attribute table
<DataTable aria-label="Track attributes" rows={page} rowKey={t => t.id} maxHeight={220}
  manualSort sort={sort} onSortChange={setSort} pinFirstColumn
  onColumnResize={(key, width) => setWidths(w => ({ ...w, [key]: width }))}
  onRowDoubleClick={t => open(t.id)} headerDividers
  onColumnReorder={(key, toIndex) => move(key, toIndex)}
  selection={{ isSelected, onToggle, header, onToggleAll }}
  columns={fields.map(f => ({ key: f.name, header: f.name, width: widths[f.name] ?? 160,
    sortable: f.sortable, sortDisabledReason: f.sortable ? undefined : 'Text fields cannot be sorted' }))} />

Pagination Available in 0.2.0+#

Offset/limit paging that matches ?offset= and ?limit=. It handles an unknown total and a backend offset ceiling. Optionally, showEnds adds First/Last buttons, pageSizes with onLimitChange adds a page-size select, and label="page" reads "Page X of Y · N features" (noun changes the last word).

showEnds, pageSizes, label="page": 0.2.5+

Example Pagination
<Pagination offset={offset} limit={50} total={1284} itemCount={rows.length} onOffsetChange={setOffset} />
<Pagination offset={offset} limit={limit} total={1284} itemCount={rows.length} onOffsetChange={setOffset}
  showEnds label="page" pageSizes={[50, 100, 250]} onLimitChange={l => { setLimit(l); setOffset(0) }} />

Tree Available in 0.2.0+#

A hierarchical list with expand and collapse, in none, single or multi selection modes. A node's description shows in a tooltip while its row is hovered or focused.

description: 0.2.17+

Example Tree
<Tree nodes={nodes} selectable="multi"
  checkedIds={checked} onCheckedChange={(id, on) => …}
  expandedIds={expanded} onExpandedChange={(id, open) => …} />

Facets Available in 0.2.0+#

A group of filter chips with counts, for narrowing a catalogue or search results.

Example Facets
<Facets label="Domain" icon={<TbTag />} options={[{ value: 'air', label: 'Air', count: 412 }, …]}
  selected={selected} onToggle={toggle} onClear={() => setSelected([])} />

SortableList Available in 0.2.0+#

Drag-to-reorder by a grip handle (or the whole row with dragMode="row"), keyboard-accessible through dnd-kit.

Example SortableList
<SortableList items={layers} getId={l => l.id} onReorder={setLayers} itemLabel="layer"
  renderItem={l => <span>{l.name}</span>} />

VirtualList Available in 0.2.0+#

A windowed list for long, fixed-height rows. Only the visible window renders.

Example VirtualList
<VirtualList items={contacts} rowHeight={28} maxHeight={200} ariaLabel="Contacts"
  getKey={c => c.id} renderRow={c => <div>{c.id}</div>} emptyState="No contacts." />

MDText Available in 0.2.0+#

Rendered Markdown with GFM (tables, task lists, strikethrough), in the wiki, chat or tool style. Mermaid code blocks render as diagrams when mermaid is installed.

Example MDText
<MDText markdown={article} />
Status and feedback

Badge Available in 0.2.0+#

Status, count or category. grey is the neutral default; brand is for brand affordances, not status. Use size="sm" and uppercase for states. Status badges always carry text.

Example Badge
<Badge size="sm" uppercase color="success">Live</Badge>
<Badge size="sm" uppercase color="warning" icon={<TbAlertTriangle />}>Lagging</Badge>
<Badge size="sm" outline color="blue">12 tracks</Badge>

Avatar Available in 0.2.0+#

A circular avatar. Missing or failing images fall back to an initials circle.

Example Avatar
<Avatar size={32} username="jdoe" firstName="Jane" lastName="Doe" src={user.avatarUrl} />

Tooltip Available in 0.2.0+#

The hover label for a map feature, anchored to viewport coordinates. It sits below a Modal. Move the pointer over the frame.

id: 0.2.17+

Example Tooltip
{hover && <Tooltip x={hover.x} y={hover.y} content={hover.feature.name} />}

InfoTip Available in 0.2.0+#

An ⓘ beside a label that explains a field, setting or heading. It shows above a Modal.

With interactive, the bubble can hold links: it stays open while the pointer moves into it, a click pins it open, and Escape or a press outside closes it.

interactive: 0.2.19+

Example InfoTip
<Label>Gate distance <InfoTip label="Gate distance">Contacts further than this from a track's
  predicted position start a new track.</InfoTip></Label>

ItemClassificationBar Available in 0.2.0+#

The per-item classification strip, coloured by level. null renders CLASSIFICATION NOT SET.

Example ItemClassificationBar
<ItemClassificationBar banner="UNCLASSIFIED//REL TO USA, FVEY" level="UNCLASSIFIED" />
<ItemClassificationBar banner={null} />

ClassificationBanner Available in 0.2.0+#

Fixed top and bottom strips above everything. Reserve BANNER_HEIGHT_PX through --banner-height. Shown here inside a frame.

Example ClassificationBanner
<ClassificationBanner enabled text="UNCLASSIFIED"
  background="var(--classification-unclassified-bg)" color="var(--classification-unclassified-text)" />

Toast Available in 0.2.0+#

useToast().toast() raises a transient result. Errors never auto-dismiss. useToast().confirm() replaces window.confirm and always names the object. Needs a ToastProvider.

Example useToast()
const { toast, confirm } = useToast()
toast({ variant: 'success', message: 'Source saved.' })
toast({ variant: 'error', title: 'Layer ingest failed', message: 'The file has no geometry column.' })
if (await confirm('Delete source "AIS — Channel"?', { confirmLabel: 'Delete' })) remove()
Maps and charts

MapView Available in 0.2.0+#

@kineticlogic/staresdk/map-view. A MapLibre GL map of points and polylines on the offline basemap style (theme-aware), with selection, fit-to-points and parent-driven fitTo. Point and line colours are data colours and stay the same in both themes. Click a point.

Example MapView
import { MapView } from '@kineticlogic/staresdk/map-view'
import 'maplibre-gl/dist/maplibre-gl.css'

<MapView aria-label="Tracks" height={320} outlines={countriesUrl}
  points={tracks} lines={trails} selectedId={selected} onSelect={setSelected} fitKey="initial" />

Set MapLibre's worker URL once in your app: setWorkerUrl(maplibreWorkerUrl) with import maplibreWorkerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url'.

TimeSeriesChart Available in 0.2.0+#

@kineticlogic/staresdk/chart. A compact uPlot line chart over time. Colours come from tokens and update when the theme changes. Each series picks a tone, never a colour, and the legend shows values under the cursor.

Example TimeSeriesChart
import { TimeSeriesChart } from '@kineticlogic/staresdk/chart'
import 'uplot/dist/uPlot.min.css'

<TimeSeriesChart aria-label="Ingest rate" times={times} unit="/min"
  series={[{ label: 'Contacts', values: contacts, tone: 'accent' }, { label: 'Rejected', values: rejected, tone: 'danger' }]} />

ProfileChart Available in 0.2.0+#

Height against distance along a path, in plain SVG with token colours. null heights break the line; an optional dashed sight line and obstruction marker show line of sight.

Example ProfileChart
<ProfileChart aria-label="Terrain profile" samples={samples}
  sightLine={[{ distance_m: 0, height_m: 412 }, { distance_m: 18000, height_m: 260 }]}
  obstruction={{ distance_m: 9400, height_m: 351 }} />

GraphView Available in 0.2.0+#

@kineticlogic/staresdk/graph-view. A directed graph for lineage, provenance and pipelines: React Flow rendering, dagre layout and token-styled nodes. ended edges are dashed, and animated edges move to show data flowing (drawn still under reduced motion).

Example GraphView
import { GraphView } from '@kineticlogic/staresdk/graph-view'
import '@xyflow/react/dist/base.css'

<GraphView aria-label="Pipeline" height={240} layerGap={48}
  nodes={[{ id: 'ais', label: 'AIS', sublabel: 'source', width: 120 }, { id: 'fuse', label: 'Fuse', tone: 'accent', width: 120 }, …]}
  edges={[{ id: 'e1', source: 'ais', target: 'fuse', animated: true }, …]} />

Reference#

Hardcoded-hex exceptions#

Two places in the package keep raw hex literals instead of var() references, mirroring the same two exceptions in OpenStare: badgeTokens.ts's BADGE_BG (parsed for WCAG contrast) and chromeTheme.ts's severityRamp. Map feature, track, tactical and data-viz colours inside OpenStare's layer rendering are deliberately theme-invariant and outside this package's scope.

Peer dependencies#

stareSDK declares its runtime dependencies as peerDependencies, so your app controls the installed versions: react, react-dom, react-icons, framer-motion, @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities, react-markdown, remark-gfm, mdast-util-find-and-replace, and mermaid (optional, loaded with a dynamic import() only when MDText renders a Mermaid block).

Lint#

The raw-colour-literal gate (eslint/colorLiteralGate.js) is exported for consuming apps, so colours stay on the tokens:

import { RAW_COLOR_LITERAL_SELECTORS } from '@kineticlogic/staresdk/eslint/colorLiteralGate.js'

export default [{ files: ['src/**/*.{ts,tsx}'], rules: { 'no-restricted-syntax': ['error', ...RAW_COLOR_LITERAL_SELECTORS] } }]

Development#

npm install --legacy-peer-deps
npm run check     # typecheck, lint (including the colour-literal gate) and tests
npm run build     # dist/
npm run docs      # this guide, into docs/

To release, bump version in package.json and publish a GitHub release tagged v<version>. The publish workflow then publishes the package to npm with provenance.

License#

MIT. © 2026 kineticlogic.io. For consulting or support, email admin@kineticlogic.io.