Skip to content

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 ​

  1. 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).
  2. A border, and a light shadow on what floats. Floating surfaces are editorWidget-background + a 1 px editorWidget-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.
  3. 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.
  4. font: inherit on every control so buttons/inputs/selects never fall back to the browser default font.
  5. Two active-state idioms, never mixed (documented in CAD-Preview's stylesheet):
    • mode-on ("this mode is enabled"): inputValidation-infoBackground + 1 px focusBorder outline. 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-btn toggle.

Tokens (design-system.css :root) ​

TokenValueRole
--ds-fontvar(--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 pxthe 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 pxby 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…62 / 4 / 6 / 8 / 12 / 16 / 24 pxspacing 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-ctl24 pxthe height of every control: buttons, inputs, selects, segments (20 px inside the 2 px track), icon buttons
--ds-fg / --ds-bgforeground / editor-backgroundbase text/canvas
--ds-widget-bg / --ds-widget-bordereditorWidget-background / editorWidget-borderfloating surfaces (nav dock, toast, panels)
--ds-borderalias of --ds-widget-borderthe 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-fgsideBar-background / sideBar-border / sideBarSectionHeader-foregroundthe sidebar column
--ds-primary / --ds-primary-fg / --ds-primary-hoverbutton-background / button-foreground / button-hoverBackgroundprimary buttons, selected-1-of-N
--ds-secondary / --ds-secondary-fg / --ds-secondary-hoverbutton-secondary*secondary buttons, unselected segments
--ds-mode-on-bg / --ds-focusinputValidation-infoBackground / focusBorderthe mode-on idiom
--ds-input-bg / --ds-input-border / --ds-input-fginput-*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-fgmenu-*dropdown panels and item hover
--ds-hover / --ds-toolbar-hover / --ds-list-sel-bg / --ds-list-sel-fglist-hoverBackground / toolbar-hoverBackground / list-activeSelection*rows, ghost buttons, tree selection
--ds-track / --ds-thumbscrollbarSlider-background / progressBar-backgroundthe slider recipe
--ds-error / --ds-warningerrorForeground / editorWarning-foregroundinline errors / warnings
--ds-shadow-menu0 2px 8px rgba(0, 0, 0, 0.36)dropdowns only
--ds-header-bg / --ds-header-bordereditorGroupHeader-tabsBackground / editorWidget-borderthe 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, never space-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 ships hidden and is filled by the documentInfo host→webview message (src/documentInfo.ts), posted by both providers on ready and 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 #app after #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 with data-tone idle/loading/ready + text, exactly as wide as the sidebar via --side-width, which sidebarResize.ts keeps 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 ships hidden and 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 #statusbar and below #main in #app (same layout-not-arithmetic reasoning as the status bar). 24 px (--ui-statusbar-h), --ds-header-bg with a 1 px top border that swaps to --ds-focus while the input has focus; a muted > prompt, a borderless transparent mono input (--ui-font-mono, --ui-fs-sm) and a --ui-fs-label echo cell (--ds-description-fg, data-tone="error" → --ds-error) that shows usage while typing and the result after a run. help/list output is a --ds-menu-bg card growing upward from the row. Behaviour: Tab completes, ↑/↓ history, Enter runs, Esc cancels/dismisses/clears/blurs, / focuses it. The command registry is src/parser/commandSpec.ts.
  • Engine status (engineActivity.ts → engineStatus message → #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 in operations.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 .mdpa open reads "Engines idle".
  • Toolbar: #toolbar is 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 px uiGlyphs glyph then the label (rotateCcw Reset, move Pan, activity Quality, palette Field, search Find, crosshair Inspect, eye View, sliders Advanced — the TikZ set stays for menu items). The two menu triggers (.tb-menu) end in a 12 px chevronDown at 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 .active menu trigger is the quiet lift (list-inactiveSelectionBackground) plus a 6 px --ds-focus dot, never the accent. Every button[data-action] must stay a direct child of #toolbar — mesh's JS selects #toolbar button[data-action=…].
  • Dropdown panel (.tb-dropdown in CAD; #file-menu-popup, #view-popup, #advanced-popup here): 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 px margin: 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, one chevronDown glyph rotated off aria-expanded) · a 22 px .panel-icon tile · .panel-title · optional 24 px .panel-icon-btn actions; a collapsed header drops its bottom border. The sections that edit the model stay top-level; read-only ones sit in one collapsed #advanced-group card with an n of m badge. 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-hover on 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. In style.css this 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 a chevronUp collapse). 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-rest is the dock's resting line, set beside --nav-bottom in syncNavOffset(); 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.ts owns the arithmetic (pure, Node-tested) and webview/panelDock.ts applies it through --panel-left / --panel-bottom / --panel-width / --panel-height on a data-docked element. 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-btn and 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-seg and the panels' .field-modes / .meshsize-modes are the same recipe under historical names — new code uses ui-*): 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.accent and 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; .error recolors to --ds-error. Lifted clear of the nav dock: bottom: var(--nav-bottom) + var(--nav-height) + 8px, where --nav-bottom is the dock's resting offset (syncNavOffset(), which itself clears the 36 px timeline bar) and --nav-height is the dock's REAL height, published by NavControls through a ResizeObserver — 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-track base, --ds-thumb bar); 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 carrying uiGlyphs (skipBack, play ⇄ pause, skipForward) and an aria-label, #tl-scrub is the standard slider, #tl-label and 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 in style.css for cells built by panel code.
  • Nav dock (webview/navControls.ts, JS-built; CAD's #view-controls recipe): 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 by order: 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, never nowrap or a sideways scroll, wraps to a second row when narrow. Nav icons are 26 px ghost buttons with 15 px uiGlyphs; hairlines .nav-div between 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-segments track (--ds-input-bg, radius 5, 2 px padding) with the selected .nav-seg lifted 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-num cell of fixed width, ellipsized (full text in its title). The ⋯ popover (#nav-more-popup, wired by the shared setupDropdown) 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-wrap static 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 (or charts-* 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:

CanonicalCAD-PreviewMDPA-Preview todayNotes
FitFitFitreframe in place
CtrCtrCtrre-center focal point
ClipClipCut Planerename pending Tier-1 approval
Ortho / PerspPersp↔Ortho toggleParallel Projection menu checkadopt 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)
BACKBACKREARcube face; rename pending Tier-1 approval
Isolate⊙ Isolate— (per-layer checkboxes)no MDPA equivalent planned
View snaps 1–6, i—1–6, iMDPA 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.

Released under the GNU AGPL-3.0-or-later License.