# CAD app — UI specification

Reference implementation: `cad-unified-topbar.html` — one file, no build, opens in a browser.
This document is the source of truth. Where it and the mockup disagree, this wins.

Platforms: macOS, Windows, Linux, iPadOS, Web. **One layout on all five.** Differences are
additive and confined to the shell; the canvas and its interactions are identical everywhere.
Minimising cross-platform divergence is a primary goal — the ~20 tutorials get recorded once,
and every divergence multiplies that library.

Superseded: `cad-shell-mockup.html` (docked bands), `cad-tablet-layout.html` (tablet-only),
`cad-unified-layout.html` (floating pills), and `cad-chrome-concepts.html` (A/B/C comparison that
led to the top bar). They remain only as history.

---

## 1. Core model

**Tools have no parameters.** A tool creates or places something. Everything after that happens
by selecting the result and acting on it.

**Selection is the command surface.** Selecting an object raises an overlay of commands
applicable to that object's type, positioned near it. This is where the majority of the
application's functionality lives. The tool cluster is a small launcher for the few things you
do with nothing selected.

Consequence: no parameter band, no properties panel, no ribbon. The canvas is the interface and
all chrome floats over it.

---

## 2. Frame

Three persistent regions, and only three: a docked top bar, a docked activity bar down the left
edge with the one panel it opens, and a floating toolbar at the bottom. Everything that is not
editing has one address in the activity bar; everything that is editing floats over the diagram.

| Region | Treatment | Contents |
|---|---|---|
| Top bar | docked, flat, translucent, no border | breadcrumb left · mode centre · undo, redo, fit right |
| Activity bar | docked, flat, left edge under the top bar | Diagrams, Parts, History; Settings at its foot |
| Sidebar panel | floating over the diagram beside the activity bar; never reflows the canvas | the one section the activity bar has open |
| Toolbar | floating, shadowed, bottom centre | Select and the tools, in use order |

**One elevation rule:** docked chrome is flat with no shadow and square corners; floating chrome
has a shadow and large radii. The activity bar is docked, so it is flat; the panel floats over
the diagram, so it is shadowed — which is also why opening it never moves the toolbar or the
diagram.

### 2.0 Top bar grammar

54px. Left is navigation, right is actions, centre is mode. Never tools, never status, never
icon labels. Six or seven icons on the right at most.

- **Left:** the save-state dot, then the edit-context breadcrumb with the document as its root
  segment. Pressing the root segment opens the Diagrams section. The breadcrumb collapses from
  the middle — root and current level are never hidden — and segments truncate with ellipsis at
  ~200px.
- **Centre:** empty unless a mode is active. While viewing an earlier rev, the bar tints amber and
  the centre shows the rev, a stepper, and Return. No new object appears; the bar you already look
  at changes state.
- **Right:** undo, redo (hidden when empty), go up a level, fit. Nothing else: documents, parts,
  history and settings live in the activity bar.

**The bar is the title bar on native builds.** Frameless window; macOS traffic lights inset at
the left, Windows/Linux caption buttons at the right, the bar acts as the drag region. This is
the single biggest factor in the app passing as both a desktop app and a web app — Figma, Linear,
and Arc all do it.

### 2.0.1 Status line

Plain muted text, bottom right, no container — the corner nothing else claims, so a sidebar panel
open on the left never covers it. Ambient information is not a control and must not
look like one. One line, four states, always showing the most relevant thing:

| State | Shows |
|---|---|
| idle | units · zoom · snap |
| selection | selection path (ephemeral — this is why it cannot share the top-left slot) |
| tool active | the tool's instruction, persisting as long as the tool is active |
| after an action | confirmation with rev number, ~2s, then reverts |

Things about mode come from the top; things about feedback come from the bottom.

### 2.0.2 Toast

Reserved for events that need attention: a rebuild failure, a collaborator's change arriving, a
lost connection. Carries an action button where one applies. Keeping it rare is what keeps it
noticeable — routine confirmations go in the status line, never here.

### 2.1 Safe insets

Floating chrome means fit-to-view **must** compute an inset from the measured bounding boxes of
the visible chrome, not from the raw viewport. Same function clamps overlay placement. Insets
change when the sheet opens or closes and must be recomputed, not cached.

This is small but load-bearing: without it, models land under the tool cluster after every fit
and the whole layout reads as sloppy.

The open sidebar panel and the bottom toolbar are measured bands like any other floating chrome:
fit-to-view centres in what they leave, and the selection overlay is clamped out of them. The
panel's band is taken off the safe region only while it is open.

### 2.2 Color

The palette is **base16**: eight grayscale slots (base00–07) and eight accents (base08–0F). It is
sufficient, with three additions layered on top. Components never reference a base slot
directly; they reference a role.

**Roles.** The complete list a component may use:

| Role | Slot | Used for |
|---|---|---|
| `ink`, `ink-2`, `ink-3` | 05, 04, 03 | primary, secondary, tertiary text and icons |
| `accent` | 0D | selection, active tool, primary action |
| `live` | 0B | saved state |
| `warn` | 0A | viewing mode, flags |
| `danger` | 08 | destructive commands |
| `sketch` | 09 | sketch geometry in the viewport |
| `surface` | 00 (light) / 01 (dark) | chrome and panels |
| `canvas` | derived | the viewport background |

Unused accents (0C, 0E, 0F) stay unused. A theme swaps hues; the app owns the mapping.

**Derived tints, never new hex.** Soft fills, hover, dividers, and text-on-tint are all
`color-mix()` of a role toward the surface or the ink: `accent-soft` = accent 16% over surface,
`hover` = ink 6% over transparent, `warn-ink` = warn 55% toward ink. This is what makes the eight
accents enough — every variant is a function of one slot, so it re-derives when the theme
changes. In egui this is a lerp, not a mix; same rule.

**Elevation is lightness, in both modes.** Surfaces sit above the canvas. In a light scheme the
chrome is base00 and the canvas is mixed slightly toward base01. In a dark scheme base00 is the
darkest slot, so the chrome steps *up* to base01 and the canvas is base00. Shadows carry
elevation on light schemes; on dark schemes they nearly vanish, so raised elements also gain a
one-pixel `line` ring. This rule is what a raw base16 palette lacks and what makes light and
dark schemes interchangeable without per-theme fixes.

**Two things base16 does not guarantee, and the spec must:**

1. Accent contrast. Nothing stops a scheme's yellow from failing on its own background. Accents
   used as *text* (warn-ink, danger labels) are mixed toward ink until they pass; accents used as
   *fills* carry base00 text on light schemes and base06 on dark. Do not put raw accent text on a
   surface.
2. Viewport colors are a separate decision. Edge and face colors default to base04/03 so the
   model follows the theme, but users may want the model to hold still while the chrome changes.
   Treat `geo-*` roles as overridable independently of the theme.

OS-owned colors — macOS traffic lights, the Windows close-button red — are not themed.

Base24 adds eight brighter accent variants. Not needed: derived tints cover the cases those
would serve.

### 2.3 Metrics

44px minimum interactive target **on every platform**, including desktop. Slightly generous with
a mouse, but the chrome is sparse enough that density isn't a virtue, and it removes a scale
variant from the CSS, the docs, and the tutorials.

Radii 13–20px. Icon strokes 1.75–1.8px. No hairline rules anywhere — separate with space, tone,
and elevation. Body 14px.

Motion: one spring curve (`cubic-bezier(.32,1.5,.5,1)`) for panels, pills, and press states.
Honour `prefers-reduced-motion`.

---

## 3. Selection overlay

The most important component in the application. It is also the most input-agnostic thing in the
UI — identical with mouse, trackpad, touch, and pen — which is why so much rests on it.

### 3.1 Commands are per object type

Each type (body, face, edge, sketch, …) declares its command list. Order within a type is fixed
and never reorders by frequency or recency: **predictable position beats shorter average travel**,
especially with training cost as an objective.

### 3.2 Multi-select

Show the **intersection** of the applicable commands, prefixed with a selection count. When the
intersection is empty, say so plainly rather than showing an empty bar.

### 3.3 Placement

- Centred above the selection bounding box, 14px clear.
- Flips below when it would land under the top chrome.
- Clamps horizontally into the safe insets.
- **Never covers the selection itself.**

### 3.4 Camera

The overlay tracks the object, but hides during active camera manipulation and re-places on
release. Tracking through an orbit is a jittering distraction; parking it at a fixed position
breaks the connection to what's selected.

### 3.5 Overflow

Beyond 5 inline commands, the remainder go behind one overflow button that opens a menu. Order
stays stable regardless of which commands overflowed.

### 3.6 Right-click

On pointer platforms, right-click opens a menu with **exactly** the overlay's commands. It never
extends the set — iPadOS has no equivalent, and any command reachable only by right-click is
invisible there.

---

## 4. Platform differences

The complete list. Anything not here is identical across all five.

| Difference | Handling |
|---|---|
| macOS menu bar | Mirror the Diagrams section's actions into it. Nothing lives only there. |
| ⌘ vs Ctrl | One binding table, platform modifier token, rendered per platform |
| Right-click | Duplicates the overlay (see §3.6). Absent on iPadOS. |
| Hover | Tooltips only, showing name and shortcut. **No command may be hover-revealed.** |
| Additive select | `⌘`/`Ctrl`-click on pointer platforms; long-press on touch |
| Camera input | Wheel vs pinch; same operations |

### 4.1 One document per window

The app is single-document. There are no tabs and no internal window management.

Multi-document cases are **delegated outward**, not reimplemented: a second document opens a
second OS window (desktop), a second browser tab (web), or Split View (iPadOS). This is the
pattern behind Figma and Excalidraw — not that multi-document went away, but that apps stopped
rebuilding MDI inside themselves.

Three cases this pushes onto you, all real:

1. **Cross-document copy/paste** must work between windows and instances. With no shared
   in-process state, the clipboard payload has to be self-contained — geometry plus enough
   provenance to resolve or re-link references on paste.
2. **Opening a linked or referenced document** (an external part, a vendor model, a
   subassembly defined elsewhere) opens a *new* window rather than replacing the current one.
   Replacing it silently loses the user's place with no back affordance.
3. **Switching documents** happens in the Diagrams section of the sidebar: search, New, Import,
   and every document this installation holds. Selecting one replaces the document in this
   window — the section says so, since with no tabs the user has no visual cue that the current
   document is going away.

Removing tabs is what makes the shell literally identical on all five platforms. Treat adding
them back as a decision that reopens §4 as a whole.

### 4.2 Filesystem

iPadOS and Web have no user-visible paths; the desktop three do. **Go document-centric on all
five** — a picker with recents and search, no paths in the UI anywhere. Splitting this is the
one difference that would fragment the model rather than decorate it.

This is a product decision, not a layout one. Make it deliberately.

---

## 5. Tools

8 parameterless tools, one floating bar at the bottom centre of the diagram, most used first:
Select, New block, Add block pin, Route, Add image, Add sheet port, Add text, Comment. Select
sits first, separated by a rule.

**Select is visible on every platform, including desktop**, where `Escape` also returns to it.
It costs one button and buys an identical bar in every screenshot, tutorial, and support
conversation.

Digit keys 1–8 map to the bar in order. Shown in tooltips on pointer platforms.

Tapping the active tool returns to select.

---

## 6. Document state and history

### 6.1 Model

- **No save action.** Every edit is recorded continuously.
- One rev = one edit action. Flat log, monotonic numbering.
- **Append-only.** Nothing is ever removed, including by undo.
- Descriptions are generated. Prefer deltas: `Base plate 78 → 84 mm`, not `Edit base plate`.
- Coalesce continuous manipulation into one rev on release, consistently across all commands.

### 6.2 Viewing is not checkout

Selecting a rev shows the document as it was. A lens, not a location. No current-branch or
current-commit identity appears anywhere in the chrome.

While viewing: the viewing pill appears, the canvas desaturates, the tool cluster dims and goes
inert, selection is cleared and disabled, and `Escape` returns to current. Three redundant
signals, deliberately — modelling against a state that isn't live is severe enough to justify it.

Stepping between adjacent revs is the primary interaction, not row selection. Stepping past the
newest returns to current, closing the loop.

Viewing state does not survive a reload.

### 6.3 Restore and flags

Restore authors a **new rev at the head**. Never rewinds or truncates. Say so in the confirmation
— users from version-control tools assume otherwise and will avoid the feature.

Any rev can be flagged and labelled at any time, including retroactively. Flags replace a
separate "named version" tier.

---

## 7. Undo and redo

### 7.1 Two kinds of entry, one stack

| Kind | Undoing it | Touches the log |
|---|---|---|
| `doc` — a document edit | authors a new inverse rev at the head | yes |
| `view` — camera state | restores prior camera state | no |

Because undo authors rather than retracts, it always succeeds, including on top of another
user's work.

### 7.2 Requirements

- Undo lives in the top-right action cluster. Always visible, disables when empty.
- Redo is hidden entirely when its stack is empty.
- **The undo tooltip names the target and its kind.** `Undo zoom to fit — view only, no rev` vs
  `Undo fillet front-left edge — authors a rev`. Same key, materially different consequence, and
  in a shared document the consequential one is public. Not optional.
- Consecutive same-type view entries within ~1.5s coalesce. Without this, active navigation
  buries document edits and undo stops reaching them.
- Inverse revs render distinctly in the log — muted, `Undo — <original>`.
- While viewing an earlier rev, undo remains available for `view` entries only.

---

## 8. Sidebar

One activity bar, four sections: Diagrams, Parts, History, and Settings at its foot. Pressing a
section opens its panel beside the bar; pressing the open one closes it. The panel floats over
the diagram at every size — it never docks, never reflows the canvas, and has no scrim, because
the diagram is *output* while a pick is being made.

**Parts and History mutate nothing.** History changes what you are viewing; Parts changes what is
selected or which subassembly is in context. They keep the navigator's rules:

- **Every pick completes a hand-off but leaves the panel open.** A rev opens viewing mode; a part
  becomes a canvas selection with its overlay raised; a subassembly becomes the edit context and
  the breadcrumb updates. Walking through several revs or parts is one open, not several. A row
  that only highlights and leaves the user hunting for a second action breaks the model.
- **Only working dismisses it.** A click on the canvas or a tool pick is unambiguous intent to
  edit, so it closes; a canvas click can dismiss and select in one gesture. `Escape` closes it
  before doing anything else. Nothing inside the panel ever closes it.
- The Parts filter resets on dismiss; it is a transient search, not a persistent view.

**Diagrams and Settings act.** Diagrams is where documents are made, opened, imported, exported,
renamed and deleted — the one place for all of them, with this document's own actions at its
top. Settings holds the preferences: mode, diagram colours, lettering, and the name changes are
attributed to. They follow the same open and dismiss rules.

Wanted if it comes up in testing, not built: a pin to hold the panel open while editing, to watch
revs accumulate.

### 8.1 History

Each rev carries six fields: number, author, timestamp, description, scope (a path into the
model), and zero or more free-text tags. The list is **rows, not cards** — a log of hundreds of
homogeneous items must stay dense enough to scan. The rev being viewed expands in place into a
card that shows every field in full and hosts the tag editor.

Row layout, in order of what people scan for:

| Position | Field | Treatment |
|---|---|---|
| leading | author | initials avatar, colour derived from an accent slot by name hash; full name on hover |
| line 1 | description | primary text, wraps |
| line 2 | scope | muted, truncated from the *left* so the leaf stays visible |
| line 3 | tags | chips, only rendered when present; clicking one filters the list to that tag |
| trailing | time, then #rev | small, right-aligned; full date on hover; the day is in the group header |

Rev number is an identifier, not something scanned for, so it is the smallest element. Author
is compact because in a small team the *change* matters more than who made it; the avatar keeps
it visible without spending a text line.

**The expanded card** (viewing rev): author name, full timestamp, #rev on one line; description
at 15px; the full scope path; then tags as removable chips with an inline add field. Adding a tag
offers the document's existing tags as suggestions so the vocabulary stays consistent. Tags are
annotations on history, not document edits — editing them does not author a rev.

**Search.** A sticky field at the top of the list. Free text matches description, scope, tags,
author, and rev number. Prefixes narrow to one field: `tag:`, `by:`, `in:` (scope), `#` (rev).
The count reads "N of M" while a query is active. Clicking a tag chip anywhere in the list sets
`tag:<name>`. Search resets when the panel is dismissed.

Text search is viable here — unlike the earlier design where descriptions were the only
searchable field — because scope and tags are things people remember. Keep descriptions as
deltas (`78 → 84 mm`) so that numbers are searchable too.

### 8.2 Parts

Nested assembly tree, five or more levels in practice. Three mechanisms make depth workable:

1. **Focus re-roots.** Past level three, assemblies expose a focus affordance that redraws the
   tree with that node as root plus a breadcrumb. Indentation resets. Conventional indentation
   runs out of width around level four and CAD names are long enough that truncation removes the
   distinguishing part.
2. **Filtering flattens** to matches with the ancestor path beneath each, truncated from the
   left. Filter crosses the focus boundary and searches the whole document.
3. **Reveal from canvas** expands ancestors, selects, and scrolls into view. This is the dominant
   navigation path in a deep tree.

Four independent signals, none overloaded:

| Signal | Answers |
|---|---|
| Twisty | does this expand |
| Icon | what type is it |
| Focus affordance | can I enter it |
| Badge | does its content live elsewhere |

A part's children are its features; an assembly's are components. Both expand — only one is
enterable. Users must be able to predict what double-click does before trying it.

Also required: leaf counts on collapsed nodes, instance counts (`×4`) rather than repeated rows,
resizable panel.

Wanted, not built: sticky ancestor headers; expanding an instance group when one instance is
individually mated or suppressed.

### 8.3 Learn

> **Not built — struck by shell-playbook R56 (2026-09-03).** The Learn
> segment, the walkthrough library and the player were built and then
> deleted; the navigator has two segments. Invariant 13 below is void with
> them.


~20 narrated walkthroughs.

- Library browses; the player floats over the canvas so users can follow along.
- **Chapters, not the scrubber**, are the primary affordance — ~65 addressable answers instead of
  20 videos. Search matches chapter titles.
- Contextual entry drives most plays: when a selection overlay command has a tutorial, surface it
  from the overlay's overflow menu with its duration shown.
- Track watched state; offer resume.

---

## 9. Search

| Job | Location | Scope | Behaviour |
|---|---|---|---|
| Filter | sheet header | that segment | stateful, survives Escape |
| Facets | History segment | history | author, date, type chips |
| Search | ⌘K / Ctrl-K palette | everything | transient, dismisses on selection |

Filter and search behave oppositely on Escape and on selection, so they cannot share a control.

The palette returns typed results grouped by source: tools, commands, parts, revs, tutorials.
Everything in the Learn library must also be reachable from the palette.

No persistent search field in the chrome.

---

## 10. ECAD (if in scope)

ECAD inverts the MCAD assumption: the hierarchical node is a **drawn object on the sheet**, not a
tree row. A sheet symbol must be visually unmistakable from a component symbol.

Three things MCAD has no equivalent of:

1. **The interface is a contract.** Sheet entries must match the child's hierarchical ports. A
   mismatch is a surfaced error with a reconcile action — the drawing looks correct while being
   wrong.
2. **Reuse means shared editing.** One source, many instances. Editing through any instance
   changes all. The chrome must say which instance you descended through and that edits are
   global.
3. **Navigation is by path, not node.** Breadcrumbs carry instance indices (`Top / Amplifier[2] /
   Filter`). Back/forward remembers paths.

Keep explicit descend/ascend commands with bindings alongside double-click.

Two decisions this forces: whether nets cross sheet boundaries implicitly (global labels vs
hierarchical ports — both is standard, but which-is-which must be obvious); and whether sheets
are one document or many. **Sheets embedded in one document keeps the rev model intact.** Given
how much rests on the single continuous log, treat this as close to a constraint.

---

## 11. Drafting and the title block (if in scope)

A second workspace sharing the frame, with a different tool cluster and a Sheets segment.

The title block is **content, not chrome** — bottom-right of the sheet, inside the border, per
ISO 7200 / ASME Y14.1. It scrolls and zooms with the sheet because it prints.

| Thing | Home | Scope |
|---|---|---|
| Template (border, logo, field layout) | preferences, org level | company standard |
| Field values (part number, material) | document properties | the model, all sheets |
| Rendered instance (sheet number, scale) | the sheet | per sheet |

Editing is direct manipulation on the sheet first. Derived fields (material, mass, date, author,
rev) render **visibly differently** from typed ones. Rev derives from flagged revs — a typed
revision field drifts within a week and then the drawing lies.

---

## 12. Open decisions

Do not invent answers. Flag them.

1. **Undo/redo debounce.** Rapid thrash authors a rev per press. Deferring until the sequence
   settles keeps the log clean at the cost of a window where log and model disagree.
2. **Inverse revs that cannot rebuild.** Undoing a feature a later one depends on has no valid
   result. Refuse with explanation, or author and surface downstream failures?
3. **Document-centric filesystem** (§4.2) — product decision, blocks nothing but fragments the
   model if deferred too long.
3a. **Clipboard payload format** for cross-window copy (§4.1) — how much provenance travels with
   copied geometry, and what happens on paste when a reference can't resolve.
4. **Overlay command counts.** If any type exceeds ~8 commands, the overflow menu becomes the
   primary surface and the design needs revisiting.
5. **Numeric search** (`all 6.6 mm holes`) — queries over parameters, not text. Does not retrofit
   into a fuzzy-match palette.
6. **Portrait sidebar.** The toolbar already sits at the bottom; on a narrow portrait screen the
   floating panel may cover most of the diagram. It narrows to ~320px and keeps its form until
   testing says otherwise.
7. **ECAD scope** and the two decisions in §10.

---

## 13. Invariants

Violations are bugs, not preferences.

1. One layout on all five platforms. Differences are limited to the §4 table.
2. Single document per window. No tabs, no internal window management.
3. No command is reachable only via hover, right-click, or a platform menu bar.
4. Persistent chrome never covers the selection; fit-to-view respects measured insets.
5. The selection overlay never covers what is selected.
6. Command order within an object type is fixed and never reorders by usage.
7. The history log is append-only. Nothing is removed, including by undo.
8. Any control whose consequence varies by context names that consequence in its tooltip.
9. Read-only mode is signalled at least three ways, and `Escape` always exits it.
10. Restore and undo both author; neither truncates.
11. 44px minimum target on every platform.
12. There is no save action, and no UI implies one.
13. ~~Every tutorial reachable from Learn is also reachable from the palette.~~
    *(Void — R56 struck the library this quantified over.)*
