Shared UI design system (CAD-Preview ↔ MDPA-Preview ↔ KKSS)
The written, canonical description of "the look" shared by the Kratos preview family: extracted from CAD-Preview (the visual reference) and extended where MDPA-Preview has surfaces CAD-Preview lacks. Materialised as webview/design-system.css — a static, dependency-free stylesheet + token file written so CAD-Preview and KKSS can adopt the identical file verbatim.
Principles
- VS Code theme tokens are the only colour source. Every colour is
var(--vscode-*, <dark-fallback>); hardcoded hex appears only as the fallback, plus the handful of scene-side constants CAD-Preview already hardcodes (orientation-cube faces, axis arrows, quality band colours). - A border, and a light shadow on what floats. Floating surfaces are
editorWidget-background+ a 1 pxeditorWidget-border; the toolbar card, dropdown menus and floating panels add--ui-elev-2(the dock--ui-elev-3). Nothing in the page flow has a shadow. - No decorative motion. Hover/active states switch instantly. Exactly two animations are permitted, both of them status rather than ornament: the indeterminate progress sweep, and the mark on a full-screen loading overlay — allowed because a blocking screen is a state, not chrome, and because when the host cannot report a total there is otherwise nothing on screen saying the extension is alive. Overlay motion must be slow, single-axis and linear (MDPA-Preview turns its logo once every three seconds) so it reads as working rather than as decoration, and must be disabled under
prefers-reduced-motion: reduce— as must any motion added later. font: inheriton every control so buttons/inputs/selects never fall back to the browser default font.- Two active-state idioms, never mixed (documented in CAD-Preview's stylesheet):
- mode-on ("this mode is enabled"):
inputValidation-infoBackground+ 1 pxfocusBorderoutline. Used for toggles: Wireframe, Clip, Grid, Ortho. (The toolbar's own toggles — Pan, Quality, Field, Find, Inspect — now use CAD's filled accent instead, and an open menu trigger the quiet lift; see the Toolbar recipe.) - selected-1-of-N ("this option is the current one"): in a segmented track (see Segmented picker) the choice is the quiet lifted segment; the accent (
button-background) fill is for "the thing that is ON" — the clip axis, the Field panel's mode toggles (several can be on at once), an active.panel-btntoggle.
- mode-on ("this mode is enabled"):
Tokens (design-system.css :root)
| Token | Value | Role |
|---|---|---|
--ds-font | var(--vscode-font-family, sans-serif) | the one font stack |
--ui-fs-micro/label/sm/md/lg/xl (legacy --ds-font-size-xs…xl, one step behind: -md is --ui-fs-sm) | 9 / 10 / 11 / 12 / 13 / 14 px | the whole type scale; weight is default or 600, never 700. style.css carries no literal font-size — pick the token (a 15 px empty-state title became 14) |
--ui-r-xs/sm/md/lg/pill (legacy --ds-radius-sm/-/md/menu/card) | 3 / 5 / 8 / 12 / 999 px | by ROLE: xs chips, swatches, ticks, band bars; sm every button, input, select and segment; md floating cards — toolbar, menus, panels, toast; lg the nav dock; pill badges and the document chip. --ds-radius-sm (2 px) survives only for slider/progress tracks; 50% (dots, thumbs) is the one literal allowed. style.css carries no numeric border-radius |
--ui-s-0…6 | 2 / 4 / 6 / 8 / 12 / 16 / 24 px | spacing scale; use it for gaps, paddings and margins where the value is on the scale (odd one-offs — 3, 5, 10, 14 px — stay literal) |
--ui-h-ctl | 24 px | the height of every control: buttons, inputs, selects, segments (20 px inside the 2 px track), icon buttons |
--ds-fg / --ds-bg | foreground / editor-background | base text/canvas |
--ds-widget-bg / --ds-widget-border | editorWidget-background / editorWidget-border | floating surfaces (nav dock, toast, panels) |
--ds-border | alias of --ds-widget-border | the generic hairline, for rules that just want "a border" (it also silences the theme guard's warning about var(--ds-border, …) fallbacks) |
--ds-sidebar-bg / --ds-sidebar-border / --ds-section-header-fg | sideBar-background / sideBar-border / sideBarSectionHeader-foreground | the sidebar column |
--ds-primary / --ds-primary-fg / --ds-primary-hover | button-background / button-foreground / button-hoverBackground | primary buttons, selected-1-of-N |
--ds-secondary / --ds-secondary-fg / --ds-secondary-hover | button-secondary* | secondary buttons, unselected segments |
--ds-mode-on-bg / --ds-focus | inputValidation-infoBackground / focusBorder | the mode-on idiom |
--ds-input-bg / --ds-input-border / --ds-input-fg | input-* | text/number inputs, selects; focus = border-color swap, no outline ring |
--ds-menu-bg / --ds-menu-border / --ds-menu-fg / --ds-menu-sel-bg / --ds-menu-sel-fg | menu-* | dropdown panels and item hover |
--ds-hover / --ds-toolbar-hover / --ds-list-sel-bg / --ds-list-sel-fg | list-hoverBackground / toolbar-hoverBackground / list-activeSelection* | rows, ghost buttons, tree selection |
--ds-track / --ds-thumb | scrollbarSlider-background / progressBar-background | the slider recipe |
--ds-error / --ds-warning | errorForeground / editorWarning-foreground | inline errors / warnings |
--ds-shadow-menu | 0 2px 8px rgba(0, 0, 0, 0.36) | dropdowns only |
--ds-header-bg / --ds-header-border | editorGroupHeader-tabsBackground / editorWidget-border | the 34 px menubar strip and the 24 px status bar |
Component recipes
- Menubar: in-flow, full-width, 34 px,
--ds-header-bg, 1 px bottom--ds-header-border, 13 px. The File trigger is CAD's bordered pill (glyph("home")· label ·glyph("chevronDown"), 1 px--ds-widget-border, radius 5,padding: 3px 10px). Right-aligned (margin-left: auto, neverspace-between, which would re-centre the pill when the chip appears) is the document chip#doc-chip: an unsaved dot (.ui-dot) · file name · format badge (.ui-badge, the router's extension —mdpa,post.msh) · "N unsaved edits" (mono, muted). It shipshiddenand is filled by thedocumentInfohost→webview message (src/documentInfo.ts), posted by both providers onreadyand whenever the op list or save point moves, de-duplicated by serialisation. Dirty is not VS Code's tab dot: the dot is a one-way latch cleared only by a save or revert, while the chip counts the operations that differ from what the source file holds (unsavedEditCount, by record identity) — undoing back to the save point clears it at once, undoing past it counts what the file has and the view lacks. - Status bar:
#statusbar, the last child of#appafter#main(so the viewport, timeline bar, nav dock and toast all rise by layout, not arithmetic). 24 px (--ui-statusbar-h), 10 px muted text on--ds-header-bg, 1 px top border. Cells left→right:#engine-status(a dot withdata-toneidle/loading/ready + text, exactly as wide as the sidebar via--side-width, whichsidebarResize.tskeeps current),#sb-count-model("12,345 nodes · 6,789 elements · 42 conditions", zero kinds omitted),#sb-count-frame("frame 3 / 12 · step 0.25", only for multi-frame data), and a right-aligned#sb-cursor(the last Inspect pick, "element 45 · Triangle2D3 · node 123 (x, y, z)"; empty while the Inspect panel is closed). Every fact cell shipshiddenand shows only when it has text; wording is pure (src/statusStats.ts, en-US digit grouping, facts never verdicts). - Command line (CAD-Preview's command row):
#commandline, directly above#statusbarand below#mainin#app(same layout-not-arithmetic reasoning as the status bar). 24 px (--ui-statusbar-h),--ds-header-bgwith a 1 px top border that swaps to--ds-focuswhile the input has focus; a muted>prompt, a borderless transparent mono input (--ui-font-mono,--ui-fs-sm) and a--ui-fs-labelecho cell (--ds-description-fg,data-tone="error"→--ds-error) that shows usage while typing and the result after a run.help/listoutput is a--ds-menu-bgcard growing upward from the row. Behaviour: Tab completes, ↑/↓ history, Enter runs, Esc cancels/dismisses/clears/blurs,/focuses it. The command registry issrc/parser/commandSpec.ts. - Engine status (
engineActivity.ts→engineStatusmessage →#engine-status): idle → loading → ready per engine, INFERRED from calls exactly like CAD's kernel status. Real signals only:loadMeshio()(meshio++), the MMG runner call inoperations.ts(remesh / level-set, worker or in-process) and Pyodide's memoised init. "Ready" means "a call has succeeded this session", not "an instance is resident" — meshio++ is a fresh instance per call and each MMG run is its own worker. Engines nothing signals are left out (the Flowgraph editor is a forked process, not WASM), so a plain.mdpaopen reads "Engines idle". - Toolbar:
#toolbaris ONE floating card (CAD's recipe) —--ds-widget-bg+ 1 px--ds-widget-border, radius 8,--ui-elev-2,padding: 2px,gap: 2px, 12 px text,top: 8px; right: 8px, wrapping onto a second row rather than running under the sidebar. Buttons are ghosts: 28 px tall, transparent with a 1 px transparent border, radius 5,padding: 0 8px,gap: 6px, hover--ds-toolbar-hover, a 15 pxuiGlyphsglyph then the label (rotateCcwReset,movePan,activityQuality,paletteField,searchFind,crosshairInspect,eyeView,slidersAdvanced — the TikZ set stays for menu items). The two menu triggers (.tb-menu) end in a 12 pxchevronDownat opacity 0.7, and a.tb-div(1 px × 18 px hairline) separates them from the plain actions. A plain toggle that is on (.active) is the accent fill (--ds-primary); an open or.activemenu trigger is the quiet lift (list-inactiveSelectionBackground) plus a 6 px--ds-focusdot, never the accent. Everybutton[data-action]must stay a direct child of#toolbar— mesh's JS selects#toolbar button[data-action=…]. - Dropdown panel (
.tb-dropdownin CAD;#file-menu-popup,#view-popup,#advanced-popuphere):min-width: 200px, radius 8,--ds-menu-bg+ 1 px--ds-menu-border,--ui-elev-2,padding: 4px, above every floating panel; items radius 3,padding: 6px 12px,gap: 8px, hover/focus--ds-menu-sel-bg/-fg; separators 1 pxmargin: 4px 6px; checkable items reserve a✓column (::before,width: 1em, opacity 0 → 1). One wiring,webview/dropdownMenu.ts: opening a menu closes the others, an outside pointerdown dismisses (and is swallowed, so it never reaches the canvas), Escape closes and returns focus to the trigger, ArrowUp/Down/Home/End move between items, clicks inside don't dismiss, one-shot items close themselves. The toolbar's two popups are siblings of#toolbar, so the module hangs them under its bottom edge, right-aligned with the trigger, on every open. - Sidebar:
--ds-sidebar-bg, right border--ds-sidebar-border; section headers 11 px / 600 / uppercase /letter-spacing: 0.05em/--ds-section-header-fg, hairline separators. Section header = a chevron button (.panel-chevron, onechevronDownglyph rotated offaria-expanded) · a 22 px.panel-icontile ·.panel-title· optional 24 px.panel-icon-btnactions; a collapsed header drops its bottom border. The sections that edit the model stay top-level; read-only ones sit in one collapsed#advanced-groupcard with ann of mbadge. Default width 272 px, resizable. - Tree/list row: 22–24 px tall; hover
--ds-hover; selection--ds-list-sel-bg/-fg; chevron 10 px opacity 0.7; count badge 10 px opacity 0.55; row action buttons are ghost (18×18,line-height: 1, opacity ~0.5 rest → 1 +--ds-toolbar-hoveron hover). - Form row / controls: label column 72 px / 10 px / opacity 0.7; every input, number field and select is 24 px (
--ui-h-ctl), radius 5,padding: 0 6px, 11 px,font: inherit,--ds-input-bg+ 1 px--ds-input-border+--ds-input-fg; focus =border-color: --ds-focus,outline: none(no glow). Checkboxes/radios are left native. Instyle.cssthis is ONE selector list (the control list, above the panel rules), so a new control class is one added line. Numeric inputs are mono + tabular. Inline-editable fields are invisible until hover/focus (transparent border → input border → focus border). - Slider:
appearance: none; track 3 px, radius 2,--ds-track; thumb 12 px round--ds-thumb; disabled thumb falls back to the track colour. (Webkit-only — VS Code webviews and Electron are Chromium.) - Floating panel (MDPA extension — CAD has no floating panels): CAD's card —
--ds-widget-bg+ 1 px--ds-widget-border, radius 8,--ui-elev-2,padding: 10px 12px, 12 px text — written ONCE as an:is(#quality-panel, #field-panel, …)list, so the recipe is a single edit; each panel's own rule keeps only position, size and z-index. Header = CAD's.panel-header: an 11 px / 600 / uppercase title, a hairline under the row and a 24 px ghost.panel-icon-btn-style close (glyph("x"); the Field panel adds achevronUpcollapse). Panels anchored to the bottom edge (Quality, Mesh size/Spheres/Beams/Integrals, Inspect) stop above the nav dock —bottom: calc(var(--nav-rest) + var(--nav-height) + 8px)—--nav-restis the dock's resting line, set beside--nav-bottominsyncNavOffset(); the toast uses--nav-bottom, which also grows with a docked table or chart — instead of painting over its ends. - The bottom-left panel cluster (MDPA extension): the panels that share the viewport's bottom-left corner are laid out by ONE authority, not by each panel's own rule. The cluster is the card column (Selection, 300 px, on the left edge) and the strip column (the Probe chart and the Plot-over-time chart, which run to the Inspect panel's left edge). The strips sit beside the card, and move below it when the viewport is too narrow for both; within a column the panels stack upwards.
src/parser/panelLayout.tsowns the arithmetic (pure, Node-tested) andwebview/panelDock.tsapplies it through--panel-left/--panel-bottom/--panel-width/--panel-heighton adata-dockedelement. Two rules make it hold: the dock only moves the panels, never re-renders their contents (so a form keeps the user's draft), and a panel that does not fit is shortened and scrolls internally rather than covering the canvas or the panel beside it. The panels that dock to the TOP edge (Quality, Mesh size, Field, …) keep their own anchors and are not part of this cluster. - Secondary button (
.panel-btnand the panels' action rows): 24 px, radius 5, no border,--ds-secondary/-hover, 11 px;.active(a toggle that is ON) is the accent fill. One selector list. A row of them is.meshsize-actions. - Segmented picker (
.ui-segments/.ui-seg; the dock's.nav-segments/.nav-segand the panels'.field-modes/.meshsize-modesare the same recipe under historical names — new code usesui-*): CAD's inset track —--ds-input-bg,padding: 2px, radius 5, transparent segments with radius 3, hover--ds-toolbar-hover. The chosen segment is the quiet lift (list-inactiveSelectionBackground, 600);.ui-segments.accentand the Field panel's toggles use the accent fill. Keep the.active/aria-*hooks — the panel and dock code toggle them. Use it for a genuine 1-of-N; a row of plain actions is.meshsize-actions. - Toast (
#message, was the "status pill"): CAD's#status— bottom-centre over the canvas (left: 50%of#vtk-sub), radius 8,--ui-elev-2,--ds-widget-bg+ 1 px border,padding: 6px 12px, 12 px;.errorrecolors to--ds-error. Lifted clear of the nav dock:bottom: var(--nav-bottom) + var(--nav-height) + 8px, where--nav-bottomis the dock's resting offset (syncNavOffset(), which itself clears the 36 px timeline bar) and--nav-heightis the dock's REAL height, published byNavControlsthrough aResizeObserver— so a wrapped second row or a collapse moves the toast with it instead of a hardcoded card height. Inline feedback beats toasts whenever the webview owns the flow. - Progress: indeterminate sweep on a 2–3 px track (
--ds-trackbase,--ds-thumbbar); cancellable runs pair it with a play→stop button swap (MDPA extension). - Timeline bar (
#timeline-bar,webview/timeline.ts): 36 px, the status bar's surface (--ds-header-bg+ a top hairline, no shadow) at the viewport's bottom edge; prev / play-pause / next are 24 px ghost.panel-icon-btns carryinguiGlyphs(skipBack,play⇄pause,skipForward) and anaria-label,#tl-scrubis the standard slider,#tl-labeland the fps field are mono + tabular. The 36 px is load-bearing: the series chart and the dock's bottom offset are computed from it. - Numeric readouts: mono + tabular (
.ui-num) — the stats values, the timeline label, panel value cells, table cells. Applied by class list instyle.cssfor cells built by panel code. - Nav dock (
webview/navControls.ts, JS-built; CAD's#view-controlsrecipe): ONE wrapping row —[reset · fit · zoom out · zoom in]|[Shaded | Wire]|CLIP [Off] [X Y Z Free] ──o── readout|[Persp] [⋯]— with the collapse chevron last byorder: 2. Bottom-centre over#vtk-sub, radius 12,--ds-widget-bg+ 1 px border,--ui-elev-3,padding: 6px 12px,width: max-content; max-width: calc(100% - 32px)with#nav-body { flex-wrap: wrap }— one row while it fits, nevernowrapor a sideways scroll, wraps to a second row when narrow. Nav icons are 26 px ghost buttons with 15 pxuiGlyphs; hairlines.nav-divbetween clusters; the CLIP caption is.nav-label(10 px / 600 / uppercase / 0.06em / opacity 0.7). A 1-of-N choice (Display, Clip axes, rotate step) is ONE inset.nav-segmentstrack (--ds-input-bg, radius 5, 2 px padding) with the selected.nav-seglifted out: a quiet grey lift for a mode (.active), the accent fill only for the clip axis; standalone controls (.nav-pill: Off/On, Persp/Ortho, Flip, Edges) are 24 px bordered ghosts, and the toggled ones use the mode-on treatment. The readout is a.ui-numcell of fixed width, ellipsized (full text in itstitle). The ⋯ popover (#nav-more-popup, wired by the sharedsetupDropdown) is a.tb-dropdown-style card (radius 8,--ui-elev-2) that opens UPWARD from the dock (bottom: calc(100% + 6px), right-aligned,#nav-more-wrapstatic so the dock is its containing block) and holds Rotate (step picker + 24 px D-pad) beside Pan, Clip Flip + the Free-normal inputs, the theme picker + opacity, and Edges + Center on model. Clicks inside it leave it open; an outside click / Escape closes it, and a slider, number field or select inside keeps its own arrow keys. Collapsed = the chevron alone (rotated 180°).
Layout rules
- Orientation cube: top-left, ~96 px, 10 px margin. Uniform blue faces
#2b6cb0, border#1a4a7a, bold white labels RIGHT/LEFT/TOP/BOTTOM/FRONT/BACK; axis arrows X#ff3653, Y#8adb00, Z#2c8fff. - Nav dock: bottom-center. Toolbar: top-right. Toast: bottom-center, just above the nav dock. Command line: full width, directly above the status bar. Status bar: full width, below everything. The 36 px timeline bar sits at the bottom of the viewport, i.e. directly above the status bar.
- Sidebar: left column; viewport fills the rest. The resize sash is invisible (6 px hit area over the sidebar's border) and shows a hover/focus border.
- Plots is an in-flow auxiliary pane, defaulting beside the mesh with an optional below orientation. Its 6 px separator supports pointer and arrow-key resizing; collapse/restore retains its workspace. Flowgraph and Plots expand exclusively, rather than stacking splits. Plots uses the existing header, input, secondary-button, focus and theme tokens; the simple point/region form comes first, with Advanced hidden until requested. Units, sample counts, gaps and partial-result state remain visible. This layout does not add a mesh-edit operation or another editor tab.
- Plot chart picker / actions: a bordered chart-family disclosure reveals an in-flow responsive grid of 24-unit
uiGlyphs, labelled buttons and the mode-on focus-border treatment. Arrow keys/Home/End move within the grid, Enter/Space chooses, Escape closes and restores trigger focus. Keep per-family drawing/bar/aggregation/size controls visible outside Advanced. Toolbar actions are wrapping ghost-button groups separated by hairlines; icon-only actions retain accessible names and tooltips. Curve rows use an explicit series-color swatch (orcharts-*theme token), a wrapping name, and small visibility/removal actions. Interface surfaces stay theme-token-only; explicit scientific series colors are data styling, not chrome colors. No decorative motion, shadows on in-flow cards, emoji or unrelated icon family. - z-ladder (low→high): canvas overlays (5) → floating bars/pills (10–12) → toolbar (15) → menubar (20) → floating panels (20–22) → dropdowns (30) → loading overlay (100).
Interaction vocabulary (glossary)
Canonical verbs, with MDPA's current names mapped:
| Canonical | CAD-Preview | MDPA-Preview today | Notes |
|---|---|---|---|
Fit | Fit | Fit | reframe in place |
Ctr | Ctr | Ctr | re-center focal point |
Clip | Clip | Cut Plane | rename pending Tier-1 approval |
Ortho / Persp | Persp↔Ortho toggle | Parallel Projection menu check | adopt the flip-label toggle |
Export… | Export… | Export as ▸ (inline formats) | MDPA keeps the inline format list (36 formats) |
Save/Load Preprocess… | Save/Load Preprocess… | Save/Load problem… | MDPA keeps "problem" (Kratos domain term, shipped .kratosproblem.zip format) |
BACK | BACK | REAR | cube face; rename pending Tier-1 approval |
Isolate | ⊙ Isolate | — (per-layer checkboxes) | no MDPA equivalent planned |
View snaps 1–6, i | — | 1–6, i | MDPA extension; candidate for upstreaming to CAD |
KKSS adoption notes
KKSS consumes both siblings as unmodified submodules, copies their stylesheets verbatim at build time, and gates the build on tools/check-theme-vars.mjs: every --vscode-* variable referenced must be defined in its app/renderer/theme/vscode-vars.css. Adopting this design system adds these variables to that contract (Dark Modern values in parentheses):
--vscode-sideBar-background (#181818*), --vscode-sideBar-border (#2b2b2b*), --vscode-sideBarSectionHeader-foreground (#cccccc), --vscode-editorWidget-border (#454545), --vscode-editorGroupHeader-tabsBackground (#181818*), --vscode-inputValidation-infoBackground (#063b49), --vscode-scrollbarSlider-background (rgba(121,121,121,0.4)), --vscode-list-activeSelectionBackground (#04395e), --vscode-list-activeSelectionForeground (#ffffff), --vscode-menubar-selectionBackground (rgba(90,93,94,0.31)). * classic Dark+ uses #252526/#3c3c3c — match whichever family vscode-vars.css follows.
KKSS integration steps on the next submodule bump: add mesh/media/design-system.css to the copies array in esbuild.mjs's copyArtifacts(), add one <link> before style.css in the mesh page template (tools/webviewMarkup.ts / gen-webview-html.mjs), extend check-theme-vars.mjs to scan the new file, and add the variables above.
Hoisting proposal (not yet executed)
Today the file lives in this repo and is adopted by verbatim copy — the same convention the three repos already use for the icon pipeline (build-toolbar-icons.mjs is copied verbatim) and that KKSS uses for whole stylesheets. Copying keeps each repo self-contained and has no licence consequence (each copy is licensed under its host repo's terms; CSS tokens/recipes of this kind are far below any threshold of originality concern between projects with the same author).
The alternative is a shared npm package (e.g. @kratos-multiphysics/ui-design-system) that each repo lists as a devDependency and copies into its media/ at build time (KKSS already does exactly this with xterm.css). Licence consequence: the package must be permissively licensed (MIT or Apache-2.0) to be consumable by GPL-2.0-or-later CAD-Preview and AGPL-3.0 MDPA/KKSS — publishing it under either GPL flavour would wall off one consumer. Recommendation: stay with verbatim copy until the file stabilises across both extensions, then publish MIT if drift becomes a maintenance problem.