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).
  • Engine status (engineActivity.tsengineStatus 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.
  • 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, playpause, 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. 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.
  • 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, i1–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.