@teammanager/ui (0.11.0)
Installation
@teammanager:registry=npm install @teammanager/ui@0.11.0"@teammanager/ui": "0.11.0"About this package
TeamManager UI
Shared React visual primitives for TeamManager Web and Race Engineer.
The package owns a small, versionable visual foundation: design tokens, accessible buttons and dialogs, structural panels, explicit status treatment, and workflow orientation. It is deliberately not a race-strategy engine, data client, application shell, or Wails/browser abstraction.
Authority boundary
- TeamManager Server owns Team Event strategy and revisions.
- TeamManager Web is the Team Event planning and decision surface.
- Race Engineer owns local/solo strategy, SafetyGate, telemetry interpretation, and every simulator-control decision.
- This package only renders values and dispatches ordinary UI interactions.
Consumer contract
Consumers import @teammanager/ui/styles.css once and wrap their app in
ThemeProvider: both cockpit and web use the restrained dark operational
token surface. It keeps CSS variables on both the visible app frame and
portaled Radix controls. Consumers retain ownership of data, routing,
mutations, i18n, authority labels, and product-specific tests.
@teammanager/ui/tailwind.css contains the package's compiled base layers and
semantic focus rules. It is not an arbitrary consumer-utility bundle. A
consumer compiles its own application layouts with the published
@teammanager/ui/tailwind-preset in its Tailwind configuration and its own
content globs. This gives Web and Race Engineer the same semantic tokens
without copying a primitive library or depending on a stale static class list.
import "@teammanager/ui/styles.css";
import { Button, Panel, Status, ThemeProvider } from "@teammanager/ui";
export function App() {
return <ThemeProvider theme="web"><Status label="Evidence current" /></ThemeProvider>;
}
Component catalogue
| Need | Primitives |
|---|---|
| Foundation | Button, Input, Label, Select, Checkbox, Switch, Dialog, Tabs, Accordion, Collapsible, DropdownMenu, Tooltip, Popover, Progress, Separator, ScrollArea, Table, Badge, Alert, Panel, Status, Metric, SegmentedControl, Field |
| Structural orientation | ContextBar, TopNavigation, WorkspaceMenu, WorkflowRail, Tabs |
| Explicit operator attention | Attention |
| App frame (0.10.0) | AppShell (sidebar, top bar, mobile drawer), ConnectionIndicator, AccountMenu, PageHeader |
| Selection and lists (0.10.0) | Combobox, ExpandableTable, List / ListRow, FilterChips, StatusChip, StatStrip, Heatmap, IconButton |
The catalogue deliberately has no strategy calculator, mutable dashboard layout, telemetry projection, chart engine, routing, or state store. Existing visual-only domain compositions remain exported for consumer compatibility, but are not the foundation for the new Event Workspace. New planning sections belong in Web. A product adds a generic extension only after a concrete operator question, typed data contract, and validation scenario exist.
Race-operations primitives
Metric, StintTimeline, PitWindow, RejoinForecast,
StrategyComparison, RaceIntelligenceList, and TrackMap are visual-only
helpers for a Pitwall or plan review. StintTimeline receives pre-computed
axis positions. PitWindow receives an explicit open/close state.
RejoinForecast receives a supplied range, adjacent cars, and assumptions; it
does not estimate a rejoin itself. StrategyComparison presents supplied
baseline and candidate values without selecting a winner. RaceIntelligenceList
uses native disclosures for supplied opponent/race evidence, including an
explicit unavailable state. TrackMap receives circuit SVG geometry or
normalized progress and explicit marker positions, including an optional
projected marker. None of these components projects telemetry, derives a stint,
or labels evidence by itself.
RelativeGapList presents a supplied class-relative order, gap, and own-car
marker. LiveScenarioRequest is a native form frame for a human to request a
what-if evaluation; the consumer validates and submits its fields, then renders
the separate authority-owned candidate. It never calculates a candidate or
issues a simulator command.
RaceTimingTable is the concrete, read-only class timing table for Pitwall.
Consumers supply its already ordered rows, formatted timing labels, class and
pit/status state, own-car marker, plus source, freshness, availability, and
confidence labels. Its native Evidence disclosure keeps row rationale
keyboard-operable. The table never sorts, calculates a gap or interval,
subscribes to timing, infers pit/opponent state, or replaces unavailable values
with zero; narrow screens retain its dense table in a scrollable region.
PlannerBoard is the controlled planning interaction primitive. It receives
named driver lanes and stints and emits only
{ stintID, fromDriverID, toDriverID, toIndex }. Pointer drag is an optional
enhancement for reordering and lane transfers; keyboard drag reorders within a
driver lane. The native move controls provide the keyboard-safe, operation-
equivalent lane transfer by choosing the destination and exact insertion index
before emitting the same intent. A request is announced
as a request, not as a saved or accepted plan, until the consumer reflects the
new controlled lanes. The consumer owns revision checks, conflicts, explicit
save, and persistence. disabled disables movement only, so supplied card
content remains selectable and editable. Supplied form content must be
controlled by its owner because transferring a card remounts it.
Workspace menus
WorkspaceMenu is built on @radix-ui/react-dropdown-menu. The dependency
provides keyboard navigation, focus management, escape handling, and safe
nested submenus for the application-level workspaces that a pitwall needs. It
does not own routing, permissions, saved layouts, or persistence; each product
provides those through items and onItemSelect.
Data binding
The package receives formatted, validated props from an owning product. It has no API client, cache, query library, event stream, or calculation layer. The current TeamManager Web and Server contracts, their safe component mappings, and example owner-side projections are documented in data-bindings.md.
Private registry
The Git repository remains private. The package is published only to the
private Forgejo npm registry under @teammanager; private: false is required
by npm to permit that registry publish and does not make the Git repository
public. Consumers configure their own Forgejo owner token outside version
control, following Forgejo's npm-registry documentation.
Each package change is independently reviewed in Forgejo before its owner-authorised
merge. The next release is a manual Forgejo Actions dispatch of Publish TeamManager UI package from main. Enter the required expected version 0.6.2; the workflow
checks both the branch and package.json, checks out
without persisted credentials, runs npm ci, check, test, build, and
pack:check, then uses TEAMMANAGER_PACKAGE_WRITE_TOKEN only for the final
publish step. It publishes to the existing publishConfig.registry; it does
not create tags, publish images, or deploy.
Verify that @teammanager/ui@0.6.2 is absent from the private registry before
the dispatch. npm package versions are immutable: never retry a successful
version or overwrite one that is already present. Stop on an ambiguous
registry result and decide the next version through the normal release change
before changing the workflow's exact release guard.
Storybook catalogue
Install dependencies with npm ci, then run npm run storybook and open
http://127.0.0.1:6006. Browse the sidebar from Foundation, Forms and
interaction, Orientation and workspace, Planning and review, Pitwall evidence,
and Live strategy contracts; Race operations/Compositions remains a useful
whole-workspace reference.
Run npm run build-storybook to check the static catalogue build. Run
npm run test:visual for deterministic Playwright screenshots; update an
intentional baseline only after inspecting the affected desktop, ultrawide,
and narrow views. npm run check, npm test, npm run build, and
npm run pack:check remain the package release checks.
If the sidebar seems empty, stop any old Storybook process, run the command
from this package root, and reload without cache. Confirm the running server is
on port 6006 and that its startup output includes the stories/**/*.stories.tsx
files. A stale server commonly lacks newly added catalogue entries.
Every story is fixed mock evidence, not an Alpha session, authenticated Server integration, live iRacing feed, strategy calculation, or simulator-control path. Live strategy samples keep an accepted baseline distinct from a pending, rejected, or evidence-expired candidate and show only the supported review path that the consuming product owns.
Canonical lime palette (0.9.0)
The default theme is the canonical TeamManager palette (as on teammanager.cc):
dark blue-graphite surfaces with a lime accent (#b7f46b) for action, focus
and information, a distinct success green (#39df7b), and amber/red warning
and danger colours. Web and cockpit
share it. Consumers must not re-declare --tm-ui-* palette tokens. Readable
text and focus indicators remain explicit. The Foundation/Shared palette story
shows both consumers, all button variants and the original WeatherIcon SVGs.
Applications must consume --tm-ui-* tokens (including --tm-ui-font-mono
for monospace text) instead of maintaining another colour palette. Non-React
document surfaces can use tm-ui-theme--web on the
HTML element alongside the exported stylesheet. React portals retain their
ThemeProvider scope. Important alternate actions (such as Solo planning) use
Button variant="secondary"; quiet intentionally remains a text action.
WeatherIcon accepts a consumer-supplied condition and optional accessible
label. Omit the label when adjacent text describes the condition. The component
does not fetch or infer weather; unknown is available for missing evidence.
Adoption requires publishing the package and updating the exact version and lockfile in both Web and Race Engineer. A package merge alone does not update installed applications.
Help hint (0.11.0)
HelpHintis a real<button type="button">showing?, namedHelp: <label>(labelprop, orlabelledByIdwhen the name lives in another element).aria-expandedreflects the pinned state only; hover and focus open the tip without changing it. Hover or keyboard focus shows the tip; click, tap, Enter or Space pins it open; Esc, an outside click or moving focus away closes it. The visual size is small; the hit area is 44px wide by 24px high (WCAG 2.5.8 AA), so it never overlaps the control below it. Inside a modal dialog passcontainer(the dialog element) so the tip stays in the focus scope; Tab from the learn-more link continues to the next control after the trigger.- The tip is plain text (
children: string). It is also rendered in a visually hidden span, so passingdescribedByIdand settingaria-describedbyon the field exposes it without opening. The only interactive content is the optional trailing link (learnMoreHref, labellearnMoreLabel, defaultLearn more); when the tip is pinned, Tab from the trigger moves to that link. Fieldtakes an optionalhelpprop (children,describedById,learnMoreHref,learnMoreLabel,label) that renders aHelpHintbeside the label; the plainhintprop is unchanged. The trigger is named from the field label througharia-labelledby; sethelp.labelexplicitly when you want different text. pass your owndescribedByIdand use it inaria-describedbyon the control; without it the hidden copy is only reachable in browse mode.- Max width matches
.tm-ui-tooltip-content(28ch). Options-style content (Race Engineer's{ what, options, defaultText }) is composed by the consumer into the text. - Stories:
Help hint. Interaction and baselines:visual-tests/help-hint.spec.ts(update new baselines withnpx playwright test help-hint --update-snapshots=missing).
Adoption requires publishing the package and bumping the exact version and lockfile in Web and Race Engineer; Race Engineer can then drop its local HelpTip.
App shell primitives (0.10.0)
AppShellowns layout only: routing stays with the consumer (href, orrenderLinkfor a router link;PageHeader.backandAccountMenuitems takerenderLinktoo), there is no team selector, and the scope switch (Team | Solo | League) is aSegmentedControlin page content. A stringtitlerenders as apby default soPageHeaderremains the page's onlyh1; usetitleAs="h1"on pages without aPageHeader. PassmainAs="div"when the app supplies its ownmainlandmark.Combobox,ExpandableTable,Heatmap,ConnectionIndicatorand the shell take all copy (labels, empty and legend text, accessible names) as props; the package ships no strings.ExpandableTabletherefore requiresexpandLabelanddetailsHeaderLabelwheneverrenderDetailis set.ExpandableTablestickyHeaderrequiresmaxHeight(a bounded scroll region); without it the header cannot stick. Default row height is 44px (density="comfortable"); the 36px table row of the system sheet isdensity="compact". Sortable headers render as buttons only whenonSortChangeis given. Select-all acts on the visible rows and keeps selections hidden by a filter.FilterChipsare independent toggle buttons. For exactly-one-of-many choices useSegmentedControl.- Stories:
App shell,Data display. Interaction checks:visual-tests/shell.spec.ts.
Breaking visual changes (0.10.0)
Button now follows the system sheet. Consumers must check screens and snapshots that contain buttons:
- Default height 36px becomes 32px;
compact32px becomes 26px; newsize="icon"(32px square). - Primary weight is 650, other variants 500.
dangeris outlined (coral text and border, soft coral hover) instead of a solid coral fill.- The base button no longer sets
white-space: nowrap, so long labels wrap;compactandiconbuttons stay on one line. - Icons directly inside a button default to 16px only when the
svghas nowidthattribute. lucide always sets one, so passsize={16}explicitly. --tm-ui-danger-actionand--tm-ui-danger-action-hoverare still exported but deprecated: no shipped component uses them forButtonany more.
Dependencies
Dependencies
| ID | Version |
|---|---|
| @dnd-kit/dom | ^0.5.0 |
| @dnd-kit/helpers | ^0.5.0 |
| @dnd-kit/react | ^0.5.0 |
| @radix-ui/react-accordion | ^1.2.20 |
| @radix-ui/react-checkbox | ^1.3.11 |
| @radix-ui/react-collapsible | ^1.1.20 |
| @radix-ui/react-dialog | ^1.1.23 |
| @radix-ui/react-dropdown-menu | ^2.1.24 |
| @radix-ui/react-popover | ^1.1.23 |
| @radix-ui/react-progress | ^1.1.16 |
| @radix-ui/react-scroll-area | ^1.2.18 |
| @radix-ui/react-separator | ^1.1.15 |
| @radix-ui/react-switch | ^1.3.7 |
| @radix-ui/react-tabs | ^1.1.13 |
| @radix-ui/react-tooltip | ^1.2.16 |
| class-variance-authority | ^0.7.1 |
| clsx | ^2.1.1 |
| lucide-react | ^1.31.0 |
| tailwindcss | ^3.4.19 |
Development dependencies
| ID | Version |
|---|---|
| @playwright/test | ^1.62.1 |
| @storybook/addon-a11y | ^10.5.0 |
| @storybook/react-vite | ^10.5.0 |
| @types/react | ^19.2.18 |
| @types/react-dom | ^19.2.4 |
| postcss | ^8.5.26 |
| react | ^19.1.1 |
| react-dom | ^19.1.1 |
| storybook | ^10.5.0 |
| tsup | ^8.5.1 |
| tsx | ^4.23.1 |
| typescript | ^5.9.3 |
| vite | ^8.1.5 |
Peer dependencies
| ID | Version |
|---|---|
| react | ^19.0.0 |
| react-dom | ^19.0.0 |