Frontend design system¶
SCSS tokens, shared layouts, and UI conventions for the SPA (frontend/src/styles/ + design-system React components). Code is the source of truth; this page is the map.
Entry¶
frontend/src/index.scss loads, in order: design-system/_css-variables (var(--*) tokens) → design-system/_global (reset, links, .btn*) → design-system/_utilities (shared modifiers — prefer over TSX style={{}} for static chrome).
Design system modules¶
| File | Role |
|---|---|
_css-variables.scss |
Theming: palette, semantic surfaces, status pills, typography (DM Sans + Source Serif 4 via @fontsource/* in fonts.ts; Chinese: PingFang SC / Microsoft YaHei), spacing, radius, shadows, --overlay-backdrop, z-index, motion, focus rings, print vars. :root + [data-theme='dark']. |
_tokens.scss |
Compile-time mirrors: breakpoints (see below), grid mins, dialog widths, spacing ($space-*), z-index. @use '…/tokens' as ds. |
_mixins.scss |
max-width / min-width, focus-ring-accent, text-truncate, motion-tokens. |
_global.scss |
Reset, body, links, buttons. |
_utilities.scss |
Cross-route helpers + .ds-compact-label, .ds-tap-target. |
_table-wrap.scss |
.ds-table-wrap — scrollable table card (overflow-x: auto). |
_dialog.scss / Dialog.tsx |
Modal shell: Escape / overlay dismiss, focus trap, scroll lock, min(--dialog-w, 100vw - 2rem), max-height: 90dvh. |
_empty-state.scss / EmptyState.tsx |
Zero-results panel (icon / title / description / action). |
_panel-toolbar.scss / PanelToolbar.tsx |
Detail panel header: leading / tabs / actions. |
_metric.scss / Metric.tsx |
Compact metric grid for entity detail headers. |
_checkbox.scss |
Shared checkbox skin (.ds-checkbox) + dense multi-select list (.ds-check-list). |
_check-row.scss / CheckRow.tsx |
Checkbox + title + hint card row (uses .ds-checkbox). |
_query-failure.scss / QueryFailureNotice.tsx |
A failed query, told apart by cause (definition / request / data) with the driver detail folded away. Shared by the Playground and the Data Insights — not a page sheet, because both routes need it. |
_status.scss / StatusChip.tsx |
A short state word (draft / ready / …) as a chip, so list pages stop inventing their own pill. Tones: neutral (default) / accent / ok / warn. |
--report-series-1…8 (_css-variables.scss) |
The Data Insights's series palette — distinguishable hues for charts and rankings, themed (dark mode lifts them) rather than hard-coded in a component. A caller may retheme without touching TS. |
--sql-keyword/string/number/comment/placeholder |
Syntax colours for the SQL rule editor — a code palette, themed like the series, muted so code reads as code rather than as status. --color-selection is translucent on purpose: that selection sits over a highlight layer. |
CheckList.tsx |
Dense membership / filter lists (CheckList / CheckListItem). |
_field.scss / FormField.tsx |
Labeled text field stack (.ds-field) + shared control look (.ds-control). |
_index.scss |
Optional Sass barrel (@forward tokens + mixins). |
knowledge-map/ |
Map-only compile-time sizes. |
_a2ui-platform.scss |
Shared A2UI platform theme: maps suite var(--color-*) → --a2ui-* tokens; COMPOSER-like Card elevation, type hierarchy, Modal/TextField/Button polish. Host surfaces add .a2ui-platform-surface; not app-specific recipes. |
Export surface for TSX: styles/design-system/index.ts (Dialog, EmptyState, PanelToolbar, Metric / MetricGrid, CheckRow, CheckList / CheckListItem, FormField, Pagination, …).
A2UI platform surfaces¶
Google A2UI renders in Light DOM. openKMS hosts (Knowledge Map overview, …) wrap content in .a2ui-platform-surface and call ensureA2uiPlatformStyles() once at startup (src/a2uiPlatform.ts → @a2ui/react/styles injectStyles()). Product Apps are hosted module iframes — they do not use A2UI.
| Piece | Role |
|---|---|
design-system/_a2ui-platform.scss |
Token bridge + primitive polish (Cards, Buttons, Modal/TextField, type hierarchy). |
| Host wrapper | e.g. .km-overview-a2ui.a2ui-platform-surface — surface owns content inset (padding) |
| Catalog components | KmOverviewNodeLink, … — behavior + domain wiring, not visual theme |
Extend _a2ui-platform.scss when a primitive pattern is reusable across A2UI hosts. Do not add feature-named layout rules for a single screen.
Shared UI primitives¶
Use these instead of per-page copies. npm run check:styles blocks regressions (see § Style guardrails).
| Primitive | When to use |
|---|---|
.ds-table-wrap |
Any bordered table/list card that may overflow horizontally on phone. |
<Dialog> |
New modals. Legacy overlay class names may remain as CSS aliases during migration. |
useConfirm() |
Destructive or blocking confirms — not window.confirm. Provider: ConfirmProvider in MainLayout. |
<EmptyState> |
Standalone zero-results blocks. .channel-page-empty* remains the channel-list alias. |
| Doc-inspector drawer | .model-yaml (ontology-manager) — a right-docked full-height panel for reading a long document (folding tree + raw text). A deliberate exception to "use <Dialog>": a dialog caps at 90dvh and wastes height. It uses the --z-modal / --overlay-backdrop tokens and names its backdrop __scrim (not *-modal-overlay) so check:styles stays meaningful. |
PanelToolbar |
Detail split panels and section headers with leading + actions (Document/Article detail, KB sections, eval subsections, knowledge-map tree). A whole entity detail page with several such sections uses EntityViewShell (sidebar identity + navItems) instead — see the dataset and semantic-model pages. Always use it instead of a hand-rolled flex toolbar — a page-local *__toolbar class only ships in its own lazy route chunk, so it silently loses its layout on any other page. |
<Metric> / <MetricGrid> (.ds-metric*) |
Compact label/value metrics on entity detail headers. Prefer over list-index-stat when there is no icon and several metrics share a row. |
.ds-checkbox |
Opt-in checkbox skin (accent fill + focus ring). Prefer over bare native checkboxes. |
<CheckRow> (.ds-check-row) |
Card-style checkbox with title + optional hint (settings / entity toggles). |
<QueryFailureNotice> (.query-failure*) |
A query failure — anywhere a query can fail. Pass the classified cause (kind) so a definition problem never reads like a data one; the label is localized by the caller. |
<StatusChip> (.ds-status) |
A state word in a list or a header. Neutral by default; only reach for ok / warn when the state really is one. |
<EntityViewPanel actions> |
What acts on a section (edit / reorder / remove) belongs in the card head, not above the card — and it reveals on hover/focus-within so a reader is not looking at a button wall. |
<CheckList> / <CheckListItem> (.ds-check-list*) |
Dense multi-select lists (e.g. group ↔ object-type membership). |
<FormField> (.ds-field / .ds-control) |
Label + text input / select / textarea in dialogs and forms. Prefer over bare <input> or page-local console-form-control / account-input for new UI. |
.ds-compact-label |
Hide button/link text ≤768px; keep aria-label. |
--tap-min (40px) |
Minimum hit area for icon buttons on phone (via .ds-tap-target or component rules). |
Confirm example:
const confirm = useConfirm();
if (!(await confirm({ title: t('deleteTitle'), message: t('deleteBody'), danger: true }))) return;
Breakpoint in JS: useIsMobile() (hooks/useIsMobile.ts) mirrors $bp-md-min — do not call matchMedia elsewhere. Detail info panel default: useDetailInfoVisible() from the same module.
Breakpoints¶
Six compile-time tokens in _tokens.scss — use @include max-width(ds.$…), not raw px:
| Token | px | Typical use |
|---|---|---|
$bp-phone-max |
520 | Very narrow field grids |
$bp-xs-max |
640 | Extra-small layouts |
$bp-sm-min |
720 | Small two-column min |
$bp-md-min |
768 | Phone shell — must match MOBILE_BREAKPOINT_PX in useIsMobile.ts |
$km-layout-max |
900 | Settings width cap; narrow column; comments overlay |
$bp-wide-max |
1050 | Wide two-column stacks (ontology playground) |
Spacing rhythm¶
4px grid — var(--space-*) in rules. Common: label↔control --space-2, form gap --space-3, card stack --space-5, actions --space-6, page header→content --space-6. App gutters: --app-page-padding-x/y on .app-content or .app-page-pane only. Half-step: --gap-compact, --padding-compact-*.
App shell layout¶
Goal: one gutter source per scroll column — no stacked padding on shell + page root.
App layers (Application / Ontology / Data)¶
The App Rail and the Home page group suite apps into three layers — application → ontology → data (top to bottom) — declared by layer in frontend/src/config/appModules.ts. The rail inserts a thin .sidebar-layer-divider between groups; the Home grid renders a titled section per layer (.app-catalog-section-<layer> + .app-catalog-section-label). Console stays outside the layers (shared platform). Group with groupModulesByLayer(); do not re-sort in UI code.
The header's app launcher is gone: it listed exactly what the rail lists, and npm run check:app-modules now asserts that — a module marked showInLauncher without showInMainSidebar would be reachable from nowhere.
Mobile shell (≤ $bp-md-min)¶
| Element | Behavior |
|---|---|
| Header | Logo only (brand text via .ds-compact-label); hide ⌘K; Console enter lives in the user menu (admin role or console:access only); Exit Console stays in the header while inside the console shell; Console shell adds PanelLeft drawer toggle for admin nav |
| App / Console sidebar | App rail hidden (--sidebar-width: 0); Console admin sidebar becomes overlay drawer when open (ConsoleMobileNavContext) |
| App Launcher | Full-width under header + --overlay-backdrop |
| Channel apps (Docs / Articles / Media) | No drawer — section index shows channel tree; All channels back link in channel view |
| Ontology second rail | Overlay drawer; Header toggle; OntologyMobileRailContext — not for channel rails |
| Tables / dialogs | .ds-table-wrap or <Dialog> sizing; channel rows → cards via _channel-table-as-cards.scss |
| Comments | Push panel → overlay at ≤900 → bottom sheet at ≤768 (ContentCommentsRail.scss) |
| Channel row actions | Single <TableRowActions> in .channel-item-actions — CSS places desktop vs mobile |
Immersive views (header hidden)¶
Views that take over the viewport add a body class that hides the header — openkms-agents-fullpage (Agents session + Session Review) and openkms-kb-qa-fullpage (KB Q&A full page). Claim it through useImmersiveView(bodyClass, active?) (contexts/ImmersiveShellContext.tsx), which applies the class and flags useImmersiveShell(). Never add the class by hand: the rail reads the flag, so a manual class would hide the header without restoring its navigation.
With the header gone, the left rail is the only chrome: brand → Home at the top (sidebar-header--collapsed, 56 px rail) and the account menu at the bottom (.sidebar-footer + UserMenu placement="up"). The header and the rail share components/Layout/UserMenu, so the menu is defined once.
Gutter decision tree¶
| Route shape | Gutter provider | Page rules |
|---|---|---|
| Default list / settings / console | .app-content |
.page-header + .page-subtitle; no root padding |
| Channel or ontology rail layout | .app-page-pane on *__main |
Channel/ontology SCSS must not pad __main |
| Home | .app-content |
Optional .app-page-shell (max-width only) |
| Full-bleed | Exception row below | SCSS comment app-layout-exception: |
List toolbar (design-system/_console.scss): .console-datasets-toolbar, .console-datasets-search (the search pill, magnifier inside), .console-datasets-filter (a labelled select). Shared by every list — the catalog, the audit log — so one toolbar does not drift into four.
Shell classes (app-page.scss): .app-page-pane, .app-page-shell, .app-page-section*, .page-header, .page-subtitle.
Layout exceptions¶
| Pattern | Reason |
|---|---|
.app-content--search |
Wider search gutters |
.app-content--with-rail (and --with-channel-rail, --with-ontology-*) → padding: 0 |
A page with its own left rail — the gutter moves onto .app-page-pane (the main column) so the rail runs to the window edge. The audit log at /audit-log uses the generic --with-rail; MainLayout adds it per route |
.app-content--function-editor-workspace |
Full-height IDE |
.app-content--apps-run → padding: 0 |
Apps Run gutters on .apps-page--run (not editor workspace) |
.app-content--compact:has(.kb-detail--qa-fullpage) |
KB Q&A full page |
.app-content--compact .wiki-page-editor-outer |
Wiki editor edge-to-edge |
body.openkms-kb-qa-fullpage / openkms-agents-fullpage |
Hide header (rail keeps Home + account — see Immersive views); phone IA (≤ $bp-md-min) is chat-primary + full-width Sessions / Chats panel (Agents also has Files) — not a stacked rail that eats chat height |
.app-content--object-explorer |
Flex fill, token padding |
.app-page-pane:has(> .entity-view) |
Ontology Manager entity detail fills the pane; scroll inside .entity-view__main only |
body:has(.app-page-pane > .entity-view) (entity-view.scss) |
Data-layer entity detail (/datasets/:id, /data-model/:modelId) has no rail marker to opt into the full-height chain, so the shell asks for it; .app-content gives up its padding to the pane |
New exceptions: one-line SCSS comment + row here.
Large page files¶
Reference: DocumentDetail — thin route entry + splits by concern:
| Suffix | Role |
|---|---|
X.tsx |
Route entry — layout wiring |
useX.ts(x) |
Fetch, mutations, derived state |
X.splitPanel.tsx / X.infoPanel.tsx |
Layout regions |
X.modals.tsx / X.dialogs.tsx |
Dialogs |
X.types.ts / X.utils.ts |
Types, pure helpers |
Soft limit 700 lines per page .tsx / hook (enforced by check:styles allowlist — shrink the allowlist, do not grow it).
Shared layout SCSS (frontend/src/styles/)¶
| File | Role |
|---|---|
account-page.scss |
Profile, Settings, Git credentials |
app-page.scss |
Page header, shell sections (loaded from index.scss) |
channel-page.scss |
Docs / Articles / Media browse; imports _channel-toolbar-compact.scss, _channel-table-as-cards.scss |
list-index.scss |
Section landing (stats + quick actions) |
channel-tree.scss |
Channel admin tree |
settings-page.scss |
Settings shell (tabs, fields, actions) |
resource-list.scss |
KB and Wiki card-grid lists |
document-detail.scss |
Shared Document / Article / Media detail chrome |
entity-view.scss |
The detail-page shell (EntityViewShell) — identity rail, nav, panels, and the pane layout exceptions. Shared, because every detail page renders the shell |
report-view.scss |
The Data Insights's report views — metric (a number, or a KPI with a trend), bar_list (a ranking), tracker (a coverage strip), and the chart host. Global because the same components render in "save this query", which the Playground and the Data tab open |
Import from page TSX: import '../../styles/<name>.scss'. Never import another page's colocated SCSS.
Account pages¶
.account-page → header + .account-stack → .account-card (head + content on white surface, no inner gray box). Forms: .account-field, .account-form-grid, .account-form-actions (margin-top: var(--space-6)). Lists: .account-list-item. .account-empty for list-only empty states.
Channel pages¶
.channel-page* (header, toolbar, bulk bar), .channel-table* (wrap uses .ds-table-wrap or transparent mobile wrap), .channel-item* (primary cell + .channel-item-actions). At ≤768, generic rule hides non-primary <td>; card styles live in the channel partials above.
Conventions¶
- Colors —
var(--color-*),var(--status-doc-*); no new raw hex in feature SCSS (charts/print excepted). - Spacing & type —
var(--space-*),var(--text-*);$space-*only in Sasscalc. - Breakpoints & z-index — tokens + mixins;
var(--z-*)for overlays. - TSX styling —
className+ SCSS;style={{}}only for data-driven geometry. - Settings width —
max-width: ds.$km-layout-max, left-aligned. - Reuse — shared SCSS + design-system components before one-off patterns.
- Gutters & exceptions — § App shell layout; run
check:app-layoutwhen touching shell files.
Renaming — or deleting — a class¶
A class edit has two halves (the SCSS and every className), and the missing half is silent:
no type error, no failing test, just something unstyled. Three times, each from a mechanical edit:
className="explorer-chart"survived a rename to.report-chart, so the chart host lost its height;- the report views' styles lived in one page's sheet while another route rendered them (a move's other half);
- tidying a sheet with a line-range slice cut
.dashboard-narrativeand its editor, and the narrative was a bare browser textarea until a reader said so.
So there are two checks, and both are worth running after a class edit:
python3 scripts/check-undefined-classes.py # a class used in JSX exists in some sheet
python3 scripts/check-global-classes.py # a shared class is not page-owned
rm -rf dist && npm run build && rg -l '\.class-name\b' dist/assets/*.css # what shipped
# Clear dist first: chunk names are hashed, so a stale build leaves *older* files beside the new ones
# and a "missing" class may just be a stale file. And grep **all** chunks, never "the newest" — a
# shared sheet lands in whichever chunk imports its component (here: EntityViewShell-*.css).
The first is rule 11 in the list below (83 accepted classes style nothing today — dead markup or a real gap — and the file only shrinks). It is deliberately flat: tokens, not ownership, because SCSS nesting and compound selectors made a cleverer version wrong often enough to be ignored.
Renaming a class¶
A rename is a two-file edit (the SCSS and every className), and the second half fails silently:
className="explorer-chart" survived a rename to .report-chart and the chart host quietly lost its
height — no type error, no test failure. So after renaming in a layout-critical sheet, check the build
output, which is the ground truth about what shipped:
npm run build && rg -l '\.new-class-name\b' dist/assets/*.css # global sheet → index-*.css
rg -n 'old-class-name' src --glob '*.tsx' --glob '*.scss' # no stragglers
(An "undefined class" checker was written and dropped: SCSS nesting and compound selectors
(.a.b) make the static answer wrong often enough — four false positives on the very files it was
meant to protect — that it would have been a checker nobody trusts.)
Style guardrails¶
From frontend/ — one command, the same one CI runs:
npm run check # typecheck → check:app-layout → check:app-modules → check:styles
# → check:lint (eslint ratchet) → vitest → build
| Gate | Covers |
|---|---|
check:app-layout |
single gutter source (.app-content / .app-page-pane) |
check:app-modules |
the Suite app list/order lives in APP_MODULES |
check:styles |
the rules in the table below |
check:lint |
eslint, ratcheted: scripts/eslint-baseline.txt freezes the errors we have — a file that gains one fails. The baseline is 0, and eslint-disable is banned (like # noqa in backend/check.sh): fix the finding instead. History — react-hooks/refs (108) came from view-models that return refs, because React's compiler flags every property read on an object containing a ref, so a hook's refs stay private (expose behaviour) and the view-model is destructured at the call site; react-hooks/set-state-in-effect (16) and react-refresh/only-export-components (19) are gone too (render-time state adjustment, and non-component exports moved to their own modules) |
test |
vitest |
check-shared-styles.sh (npm run check:styles) — allowlists are known debt, not permission:
| # | Blocks | Use instead |
|---|---|---|
| 1 | Page importing another page's SCSS | Move rules to src/styles/ |
| 2 | matchMedia outside useIsMobile.ts |
useIsMobile() |
| 3 | Raw px in @media / @include max-width(Npx) |
ds.$bp-* tokens |
| 4 | MOBILE_BREAKPOINT_PX ≠ $bp-md-min |
Keep TS and Sass in sync |
| 5 | Multiple <TableRowActions> per channel row |
One instance in .channel-item-actions |
| 6 | New *-table-wrap class |
.ds-table-wrap (a bounded grid adds --scroll: the wrap scrolls, headers stick to it, the chrome around it stays put) |
| 7 | New *-modal-overlay / *-dialog-overlay |
<Dialog> |
| 8 | window.confirm( |
useConfirm() |
| 9 | Raw rgba(0,0,0,…) modal backdrop |
var(--overlay-backdrop) |
| 10 | New page .tsx > 700 lines |
Split like DocumentDetail |
| 11 | select / text input with no skin (outside <FormField>, no .ds-control) |
<FormField>, or className="ds-control"; // ds-control-exempt when a bespoke wrapper styles it. scripts/ds-controls-baseline.txt freezes the legacy hits (70) — a file only fails when it adds one |
| 12 | t('key') missing from the locale files (i18next prints the key, so the UI shows dataModel.removeDimension) |
add the key, or fix the reference. Only files whose namespace is knowable are checked; a dynamic useTranslation(ns) falls back to "exists in some namespace" |
| 13 | Icon-only <button> with no accessible name |
aria-label (or title) |
| 14 | console.log / console.debug / debugger |
remove before shipping |
| 15b | A stylesheet in src/components/** that nothing under src imports |
add the import './X.scss' — writing both halves and skipping the import leaves every rule in a file no bundle loads (this is why the built CSS is still the only place some mistakes show) |
| 15 | font-size: <n>px in SCSS |
var(--text-*); the root html { font-size } is exempt |
| 16 | A class used by 2+ routes but defined in one route's SCSS (Vite ships it with that route's chunk only) | move it to src/styles/. Route membership comes from the import graph rooted at the lazily-imported pages in App.tsx, so two sections of one area count as two routes — an earlier area-based version missed exactly that (the report view styles lived in what is now data-insights.scss while the Playground and the Data tab rendered the same components). The check counts only classes a page sheet defines: a class also defined globally, one owned by a component's colocated sheet, and a scoped override (.page--fill .component-class, .a.b) are not ownership. scripts/global-classes-baseline.txt is a ratchet (28 today, all pre-existing): fixing one is fine, adding one fails. The last one to leave was entity-view__readonly — the "nothing here yet" line, used by ~10 pages across several routes and defined in one page's sheet, so every other page rendered it unstyled |
| 19 | .ds-table-wrap without flex-shrink: 0 |
It clips (overflow-y: hidden) and is used as a flex item: a column flex parent shrinks it to the available height and the clip silently eats its last rows — and whatever sits under them. This is how the audit log's pagination went invisible: in the DOM, inside the card, clipped by 16 px. A card that clips must keep its height; the scroll container above it scrolls |
| 18 | A request with body: JSON.stringify(…) and no headers: |
add 'Content-Type': 'application/json' — fetch does not default to JSON, so the body reaches the server as a string and FastAPI answers "Input should be a valid dictionary or object to extract fields from", which names neither the cause nor the file. Checks the shape, not the value: a headers object that is missing the type is not detected (and not claimed to be) |
| 17 | New hand-rolled console-modal-overlay markup (dialog migration debt) |
<Dialog> — every hand-rolled modal is gone (scripts/legacy-modals-baseline.txt is 0, so a new one fails the check). The dead .console-modal-overlay / .console-modal / -header / -body / -actions chrome and the legacy field rules that were scoped to .console-modal-body were deleted with them; the content-level helpers (.console-modal-hint, -section, -fk-*, -index-*) remain |
check:styles is currently clean (all pre-existing findings cleared: the two over-long files were split, the raw px media query / rgba backdrop / window.confirm / A2UI overlay allowlist entry are gone or justified). Keep it that way — a new finding should be fixed, not allowlisted.
Run check:app-layout when editing App.scss, app-page.scss, ChannelSectionLayout*, MainLayout.tsx.
Which control do I use?¶
Reach for a shared primitive before writing markup. The list below is the answer to "how do I build X here?" — each row is what the rest of the app already does.
| Need | Use | Notes |
|---|---|---|
| Labelled field (text / number / select / textarea) | <FormField label hint> from styles/design-system |
supplies ids, label, hint, and the control skin |
| A control with no visible label | className="ds-control" |
same skin, standalone |
| A SQL query field | <SqlEditor> from components/quality |
a textarea with syntax highlighting, zero dependencies (the textarea stays the input, a <pre> behind it paints). The two layers must lay text out identically — join(highlightSql(x)) === x is a test, not a hope. Tab indents |
| Checkbox / switch row | .ds-check-row (or <CheckRow>) |
never a bare <input type=checkbox> |
| Multi-select list (pick several) | <CheckList> + <CheckListItem> |
scrollable, uses the shared checkbox skin |
| Buttons | btn btn-primary / btn-secondary / btn-danger |
btn-danger for destructive actions only |
| Destructive action (delete, purge) | .ontology-danger-zone + btn-danger + useConfirm() |
never window.confirm |
| Confirm / blocking prompt | useConfirm() (ConfirmProvider in MainLayout) |
|
| Modal dialog | <Dialog> (_dialog.scss) |
size="sm|md|lg", footer slot |
| Panel / section header with actions | <PanelToolbar leading tabs actions> |
actions are pushed right |
| Whole detail page with several sections | <EntityViewShell> + navItems (see the dataset and semantic-model pages) |
sidebar identity + routed sections |
| Collapsible section | <EntityViewPanel title defaultCollapsed> |
not a hand-rolled <details> |
| Card surface | .account-card / <EntityViewPanel> |
|
| Tabs | page-local tab row (KB) or EntityViewShell nav |
no ds-tabs primitive exists |
| Pagination | <Pagination> |
server-side when the API supports it, otherwise slice |
| Empty / loading state | <EmptyState> / console-loading |
|
| Search box | .console-datasets-search (list) or ds-control (in a header) |
no primitive; do not invent a third |
| Row actions | <TableRowActions> / .table-row-actions |
|
| Table | .ds-table-wrap (+ .ds-table-wrap--flush) + .console-table |
|
| Overlay backdrop / stacking | var(--overlay-backdrop), var(--z-*) |
No primitive exists yet — build it, but keep it honest:
| Thing | Where the CSS lives |
|---|---|
| Chip / tag (e.g. the Playground's group-by chips) | page-local (.playground__chip) |
| Code / YAML / SQL block | page-local (<pre> + a small class) |
| Right-docked drawer (the Ossie document inspector) | page-local .model-yaml, using --z-modal + --overlay-backdrop; a deliberate <Dialog> exception (see § Style guardrails) |
The trap that keeps biting: a page-local *__toolbar / tab stylesheet ships only in its own
lazy route chunk, so reusing it from another page silently loses the layout (this is how the model
page's Add buttons ended up unstyled). Shared looks belong in src/styles/design-system/ (loaded by
src/index.scss) — see the _tabs.scss note in the git history for that lesson.
New feature stylesheet¶
(Adjust ../ depth for components/ vs pages/.)
Updating this doc¶
When shared SCSS, tokens, guard rules, or cross-route React primitives change, update this file. If design-system/ layout changes, also refresh the frontend section in architecture.md.