Shared shell and components
Scientist workflow entry and export choices
The Bench header's Import data opens and focuses Your data inside the existing Quick start control, including from Library and other Bench views. The separate disclosure is Study recovery → Restore a Study. Existing app import review, units, diagnostics and retention controls remain app owned. Fresh task launch links acknowledge pending work before clearing the previous Study/Experiment/task context. Superseded or unmounted intents cannot navigate later or clear a newly chosen destination; failed saves provide feedback.
Study export choices retain their location and are ordered PDF report, Reopenable Study file, Complete Study ZIP. PDF help states that it cannot restore a Study. The Study file uses the shared guarded recovery operation; the ZIP and all other report paths retain their existing closure. The existing storage line links to Study recovery where a real Study exists. Memory-mode failures remain visible after routine information is dismissed.
Bench execution surfaces
Study Work includes a separately labelled Bench sessions list, alongside saved tasks and planned Experiments. Sessions are not scientific result snapshots. Each execution Object opens the intent-loaded session controller and panel; its frozen plan, all preparation/dispense rows and captured qualifications remain readable without the original Calc task. The list includes marked, skipped and unmarked counts and the retained source label/revision. Material lists and the Materials progress milestone exclude execution in their presentation only; actual owning membership remains intact. The session shows its current owning Study/Experiment and a separate source link when available. A changed source revision is disclosed without replacing or recalculating the frozen plan.
The phone panel uses 48 px action controls, stacked labelled quantities and explicit Done/Skip/Note/What differed actions. Next only navigates. Skip and removing a mark require reasons; amounts begin empty and require a chosen unit. Corrections append to the retained history. Finish requires a closing note for unmarked steps and changes neither individual marks nor Study/Experiment state. Draft guards preserve unfinished text across refused navigation and failed saves. An uncertain save exposes Check saved entry, using the same operation.
Reminders are researcher-selected foreground timers. The local controller records only explicitly confirmed monotonic segments. Visibility loss, page exit, clock discontinuity or a substantial callback gap invalidates its token without adding a saved entry. Reopening requires explicit takeover and review of remaining time. Reaching zero does not mark a step or silently write history. No notifications or background timing service is introduced.
The saved-session report uses retained formatters and the existing print kit. All rows, entered quantities, correction history and qualifications travel with the source/method/hash receipts; exact saved JSON is also downloadable. It includes the retained plate map and source/role key, current entered status and mark counts, retained closing notes, and a report-generation timestamp separate from the plan's capture time. A 384-well map uses four labelled quadrants; the full dispense table remains available. The screen map uses the same retained source/role codes and a visible text key; 384-well maps use four twelve-column quadrants at phone width. Session history and reminder targets resolve saved row/field identities to their retained human-readable labels, while exports retain the original identifiers. The five new lazy-module budgets have provisional CTO ceilings pending measured production sizes; existing ceilings are unchanged. Browser and visual acceptance are recorded separately in the Bench/Patent progress record.
Everything an app builds on lives in src/components/suite/: the app definition contract, the shell that frames every workspace, the primitives and class recipes, the theme, tokens and accents, the export writers and the accessibility, motion and keyboard helpers. This page describes each part as the code implements it, names the design-system section it implements and the tests that guard it, and ends with a walkthrough for adding a panel or view to an app.
Storage (the project store, Objects, Studies, the #s= fragment) is described in Storage and state; the network layer and the policy-navigation boundary in Security and network; the registry in Registry and content. This page links to them rather than repeating them.
Design-system references below are to docs/design/suite-design-system.md (written "DS §n"). That document is normative but carries dated overrides at its top (D-0028, D-0029, D-0035, D-0036, D-0038 to D-0040), and docs/design/research-workspace-principles.md supersedes its older composition rules. Where the code follows a later decision rather than the body of the design system, this page says so.
What lives in src/components/suite/
The folder holds 427 files (about 61,500 lines including tests, counted on 10 October 2026).
| Folder or file | What it holds | Design system |
|---|---|---|
app-definition.ts |
The AppDefinition type and the rail, inspector, command, storage, share and object-tab types. Types only. |
§11.3 |
host.tsx |
ToolHost, the handful of page services a tool tree uses (useHost()). |
§11.3 |
app-entries.tsx |
AppEntriesProvider and useAppEntry(slug): full registry entries for the part of the page that needs them. |
registry split memo |
shell/ |
AppShell and everything it renders: top bar, Research sidebar, Tools & sections, Inspector, status bar, menu sheet, notices, About, jobs, commands, layout state, the no-flash theme script. |
§2, §6.1 to §6.6 |
recipes.ts |
Whole-literal Tailwind class recipes (buttons, inputs, chips, panels, type, stage controls, links). | §6.0 |
form/ |
Field, Input, NumberField, Select, Combobox, SuggestionInput, Segmented, Slider, Switch, Checkbox and radio, Textarea, FileDrop, number and date formatting. | §6.20, rule 18 |
overlay/ |
Dialog, Drawer and Sheet, Popover, Menu, Tooltip, Toast, CommandPalette, ShortcutsOverlay, the layer stack and Esc ladder, Floating UI wrapper. | §2.4, §6.7 to §6.14, §6.24 |
layout/ |
Panel, SubjectHeader, DiscoveryHeader and DiscoveryStart, Tabs and ViewTabs, SplitPane, Stage, media-query hooks. | §6.11, §6.22, §6.32, §8 |
feedback/ |
Banner, EmptyState, ErrorBox, Skeleton, Progress and the loading ladder, InlineStatus, Kbd, LimitsBanner and drawer, ToolBoundary, ToolLoading, ViewerGate, optional sound. | §6.15 to §6.21, §6.31, §6.32, §7.5 |
a11y/ |
announce() and the one live region, focus helpers, skip links, the shortcut registry. |
§9 |
motion/ |
animate() on the Web Animations API, usePresence, useReducedMotion; effects.ts, the interaction effects and their switches. |
§7 |
theme/ |
The shared theme preference and store, ThemeScope, Settings model and panel, chart and stage colour readers. |
§3, §6.28, §6.29 |
tokens.css, tokens.ts, accents.css, brand.css, print.css |
Semantic tokens, their JavaScript mirror, generated per-app accents, the brand layer, print rules. | §3.2, §4.1, §5, §6.26 |
chart-palette.ts, data/ |
Chart palette and geometry, DataTable, KpiTile, PercentileBar, ProvenanceChip and the provenance model, ExportMenu and the CSV, ZIP, JSON, SVG, PNG and print writers. | §3.4, §6.12, §6.18, §6.26, §6.30 |
icons.tsx, mark.tsx, wordmark*.tsx, suite-mark.tsx, education-mark.tsx |
Shell glyphs, app marks and lockups. | §4.2, §4.3, §4.5 |
objects/ |
Object store, refs, verbs (Pin, Send to, Add to study), object and study pages, Bench library, SuiteFrame. |
build-spec-objects |
workspace/ |
Bench home panels, Save to Bench and Record result controls, Study panels, recorded-result views. | D-0038 to D-0040 |
onboarding/, walk/, use-walk-action.ts |
WelcomeCard, CoachMarks, the guided walk (J3) bar and actions. | §6.23, D-0022 |
navigation/ |
PolicyNavigationBoundary and usePolicyRouter. See Security and network. |
|
store/, net/ |
Covered in Storage and state and Security and network. The store's UI components are listed below. | §6.25, §6.27 |
engines/rdkit/ |
The shared RDKit worker and its client. See Engines. | |
literature/ |
The Literature desk components. See Literature. | |
marketing/ |
Server-rendered public-page blocks (*.marketing.tsx). |
§10 |
build-info.ts, format-date.ts |
Build hash and time; UK date formatting. |
Bench and report loading boundaries
Bench imports Study limits from objects/study-limits.ts and read-only
saved-view/app helpers from objects/study-display.ts; the existing model
and Study modules preserve their public exports. Those views no longer
load Study validation or bundle-building code for presentation helpers.
Quick start's existing Examples panel is workspace/bench-quick-examples.tsx
and loads only when Examples is selected. Start and Import retain their
existing rows. The Library fragment-search disclosure reads its title from
fragment-search-entry.ts, with the form and result copy kept behind the
existing search intent. The examples catalogue and walk copy arrive with that panel;
loading, failure and retry use the existing optional-control feedback.
content/apps/example-study-entry.ts holds the unchanged visible entry and
load-feedback copy, while the scientific example story stays with the
existing requested example loader. The full content object retains its
original fields and values.
The PDF section-selection dialog imports its own unchanged copy from
content/apps/study-export-dialog.ts. Report-only text lives in
content/apps/study-export-report.ts; the original study-export-builder.ts
content module retains the complete public content object. Report selections, defaults and scientific source
qualifications are unchanged. Report generation also imports only its
unchanged brief/status/paper-origin labels from workspace-story-report.ts,
workspace-experiments-report.ts and study-workflow-report.ts; the original
content objects compose those same single-source values.
The populated example's existing science module prepares its unchanged scientific story, source inputs and paper-evidence fields before any writes. The orchestrator retains the original scope checks, actual note creation, recorded-at time and seed-receipt sequence. Citation capture delegates to the original strict function through that loaded module only after the note has been persisted, so its reference and revision are the saved note's. Both existing example chunk budgets and the complete action closure are measured.
How a workspace is assembled
A request for /<segment>/app is served by src/app/(workspace)/[app]/app/page.tsx, which renders WorkspaceFrame (workspace-route.tsx in the same folder). The chain from there:
WorkspaceFrameemitsThemeScript(the inline no-flash script), aThemeScopeon<html>, a<noscript>message andSuiteWorkspace.SuiteWorkspace(src/apps/definitions.ts) holds onenext/dynamicloader per app, keyed by slug and mode, created once and kept, so the same component renders before and after the chunk arrives and the workspace is never remounted by a load. Each loader importssrc/apps/<slug>/entry.tsx; the loader table andpreloadWorkspacelive insrc/apps/preload.ts, which the shell's navigation can import without importingAppShell.preloadWorkspace(slug)only fetches that chunk into the bundler's cache: the product page's live preview awaits it, and the sidebar calls it when a person points at or focuses one app's row. Nothing preloads every app. While the chunk loads,ShellSkeleton(built from the light client index) stands in where no server HTML exists. The skeleton is the shell's own frame (the sidebar column at--railfromlg, the bar, the status band) with the workspace loader centred in the work area (feedback/workspace-loader.tsx; the CEO, 14 October 2026: an app should not arrive in parts): three bars in the app's accent rising in turn above "Loading <app>…", still under reduced motion, with ToolLoading's slow rung (after 15 s it says so and offers Reload).AppShellthen keeps the same loader over its grid of main and the inspector (WorkspaceCurtain,pointer-events: none, its loader sticky in the window's work area, the whole curtainaria-hiddenand its line no status region,announce={false}, so the app's one status line stays the only one) untiluseSettledreports the work area settled: fonts ready, then no[data-skeleton](everySkeletoncarries it),[data-tool-loading]or[aria-busy="true"]inside main for six frames running, or 4 s at most so it never holds the app back. It fades over 200 ms and does not return for the life of the page. Not in a preview.- The entry module is a client component that calls
render(definition, app): the app'sAppDefinitionand its full registry entry (content/apps/<slug>.entry.ts). Each entry setsdisplayName = "drybench-entry:<slug>", the markerscripts/size-check.mjsuses to find the chunk. SuiteWorkspace's render prop createsAppShellwith{ app, definition, mode, page, initial }.AppShell(shell/app-shell.tsx) reads the Workspace accessscopeEpoch(useWorkspaceAccess). When the account scope changes in full mode it clears the address withhistory.replaceState, showsToolLoadingand remountsAppShellFrameunder a new key, so the previous account's in-memory science is never shown.AppShellFramebuilds the layout state, the host, the shell context, the command and export registries and the overlays, then rendersdefinition.Maininside the grid withpreviewandinitialprops. If the route names a page (/<segment>/app/<page>), it rendersAboutContentforaboutordefinition.pages[page]instead, under a back link.
The shared workspace route therefore carries the shell and the light index but no app's definition, entry or copy; each app's code downloads only on its own workspace. See Architecture.
Full mode and preview mode
AppShell has two modes. The product page's live preview renders SuiteWorkspace with mode="preview".
| Concern | Full (workspace page) | Preview (product page) |
|---|---|---|
| Landmarks | <main id="main"> |
A role="region" labelled with the app name; main is a div with role="group" |
| Ids | As given | Suffixed with a useId() value (ids(name)), so the product page has no duplicate ids |
| Keys | Shell keys registered globally (useShortcuts("shell", …)) |
Only Mod+k, ?, / and ], handled on the frame's onKeyDown; setShortcutRoot(root) limits app keys to the frame |
| Theme | ThemeScope on <html> from the route |
ThemeScope targeting the shell root |
| Remembered layout | localStorage keys (below) |
In memory only |
| Address | #s= fragment through writeUrlState(…, { mode: "fragment" }) |
memoryUrl(); the page address is never written |
| Object verbs | createObjectVerbs(slug) |
previewVerbs(slug): upsert and link do nothing |
| Saving, Export, Share, Settings, app switcher, walk | Available | Absent |
| Scroller | The window | The shell root (overflow-y-auto) |
| Navigation | Research sidebar (ResearchNavigation) |
The app's own Rail |
The suite's own pages use SuiteFrame
Bench (/o), object pages, Study pages, Literature and the education tools render inside SuiteFrame (objects/suite-frame.tsx) rather than AppShell. It draws the same ShellBar, Research sidebar (RailColumn plus ResearchNavigation), menu sheet and status bar (D-0028; UX audit SHELL-2), with a breadcrumb trail instead of an app lockup, the command palette with object, study and identifier results, toasts, the layers element and a suite host whose verbs carry "suite". An optional accent prop gives the page the accent of the app that owns the object type.
The app definition contract
What an app folder holds
Every launch app has a folder src/apps/<slug>/. The files below are the conventions found across the ten apps; not every app has every file.
| File | Role |
|---|---|
definition.ts |
export const definition: AppDefinition. Kept light: it is in the app's entry chunk. Main should itself load the workspace with next/dynamic. |
entry.tsx |
Client component: render(definition, app), with displayName = "drybench-entry:<slug>". |
workspace.tsx |
The Main component: usually a ToolBoundary around a next/dynamic import of the real workspace with a skeleton fallback. |
views/ |
The workspace's views. |
store.ts |
A tiny external store (publish…, read…, use…) through which the workspace exposes its state and actions to the parts the shell renders outside the workspace tree (commands, shortcuts, share link). |
page-ids.ts |
PAGE_IDS: the app's own static pages under /<segment>/app/<id>. Empty for all ten apps today; about is the shell's. |
objects.ts |
Pure module: TAB_TYPES, objectInbound (how the app opens a type through Send to) and the object data shapes. |
object-tabs.ts, object-tab.tsx |
objectTabs: one { load } per type the app shows a tab for on an object page; the tab components. |
inspector.tsx |
Static inspector content, loaded with next/dynamic from the definition. |
exports.ts, provenance.ts, report.ts |
Export items, provenance blocks and print-report models. |
tasks.marketing.tsx, task-islands.tsx, islands/ |
Task pages and their client islands. See Registry and content. |
palettes.ts |
Optional light and dark colour pairs; tokens.test.ts imports every such file and checks the dark set (below). Present for Pipeline, Molecule and Protein. |
Shared maps over every app sit at the top of src/apps/: definitions.ts (the entry loaders and SuiteWorkspace), page-ids.ts (pageIdsFor), objects.ts (objectInbound, objectTabTypes), object-tabs.tsx (tab loaders for object pages), send-to-rules.ts, tasks.ts and explainers.ts. Each is written once so no app edits it, and each is a Record<LaunchAppSlug, …>, so a new slug fails type checking until it is added.
AppDefinition
Everything is optional except Main. The registry entry carries what pages and the CSP read; the definition carries what only code can (DS §11.3; conflict C09 keeps storage kinds, export capabilities and fragment keys in code).
| Field | Type | What the shell does with it | Apps that set it today |
|---|---|---|---|
Main |
ComponentType<WorkspaceMainProps> |
Renders it in main with preview and initial |
All |
rail |
RailSection[] |
The first-paint local sections, shown in Tools & sections (full mode) or the app rail (preview). useRail() or host.setRail() replaces them while mounted. |
All (Bioreactor and Calc pass [] and set sections from the workspace) |
inspector |
{ title, kicker?, Content } or "selection" |
Static content, or content published with inspector.open(node, opts) |
All but Bioreactor; Protein and Pipeline use "selection"; Cell uses the shared SourcesInspector |
inspectorLayout |
{ defaultState?, startHidden?, dockMinMain? } |
Hidden by default; explicit docked or docked-from-xl opt-in; startHidden suppresses the column until the workbench publishes its start/work state |
Calc and Sequence opt into methods for active inputs (D-0060); Pipeline and Cell keep their dockMinMain |
LimitsNotice |
ComponentType<LimitsBannerProps> |
Optional contextual-summary renderer in the single notice position below the work | Assay suppresses only its C50 summary outside dose-response, keeping the full drawer |
commands |
Command[] |
Added to the palette after the shell's own | All but Cell |
shortcuts |
Shortcut[] |
Registered under owner app:<slug> while mounted; may not use a reserved key |
All |
widgetKeys |
WidgetKey[] |
Listed in the ShortcutsOverlay, never registered | Protein |
sources |
() => SourceRef[] |
The status bar's freshness chip shows the oldest | Protein, Molecule, Pipeline |
statusChips |
() => ReactNode |
Extra status-bar chips | Molecule (engine chip) |
exports |
() => ExportItem[] |
Export menu items beside those registered with useExports |
Pipeline |
search |
{ label, focus() } |
/ focuses it; otherwise / opens the palette in search mode |
Protein, Molecule, Pipeline, Calc, Sequence |
share |
ShareSpec |
Copy link, the share dialog and the "Not in this link" fallback | All but Cell |
usesUrlState |
boolean |
Settings offers the address-bar preference | All but Cell |
aboutSections, settings |
ExtraSection[] |
Appended to About and licences, or to Settings | aboutSections: Protein only |
methods |
{ group(): string | null } |
The id of the calculator, analysis, tool, view or step open now, read from the app's published store; the methods and checks dialog opens at the group that answers to it (D-0085) | All ten apps |
objectTabs |
Partial<Record<ObjectType, ObjectTab>> |
Object-page tabs (also registered in src/apps/object-tabs.tsx) |
All but Cell |
storage, session, onSave |
StorageSpec, SessionSpec, function |
The shell's local project saving: autosave after shell.storage.changed(), ProjectChooser, SaveStatus, ConflictDialog, ⌘S |
None of the ten apps; only the dev kit's demonstration workspace |
pages, TopBarActions, paperMain, palettes |
Static pages, extra top-bar actions, a light paper main in dark theme, palette pairs | None today |
The "apps that set it" column was read from the nine definition.ts files on 10 October 2026. The important consequence is that the shell's own project-saving path (contracts §4; DS §6.25) is not used by any shipped app: apps save through the Workspace controls (WorkCaptureControls, below) instead. The path is still exercised by the dev kit and by e2e/smoke/shell-a11y.spec.ts ("saving flows (dev kit demonstration workspace)").
src/apps/definitions.test.ts checks one rule of the contract: an app whose definition gives share.notInLink must also give fileKind and onShareFile.
The rail model
RailSection has an id, an optional title, ordered (render as <ol>), collapsed (what the 56 px rail shows: "icons", "rule", "hide" or "popover"), an optional icon, items and an empty line. Items are one of four kinds (app-definition.ts):
| Kind | Element | Behaviour |
|---|---|---|
section |
<button> |
Scrolls to the panel and focuses its heading through scrollToPanel (shell/scroll-to-panel.ts), never writing location.hash. Scroll-spied with aria-current="true"; aria-disabled with an explanation while the panel is not on the page. |
view |
<button> |
aria-current="page" on the open view; runs onSelect. |
link |
<a href> |
A real link (works in a new tab); onNavigate may call preventDefault() to handle a plain click in place. Optional numeral index and badge. |
action |
<button> |
Runs onSelect; pressed gives aria-pressed. |
The shell turns every non-action item into a "Go to" palette command (shellCommands in app-shell.tsx).
The host
useHost() (host.tsx) returns the ToolHost the shell provides, or standaloneHost outside a shell.
| Member | Suite host | Standalone host |
|---|---|---|
kind |
"suite" |
"site" |
announce(text) |
announce() into the one live region |
No-op |
links |
linksFor(slug) (src/lib/handoff/links.ts): cross-app links carrying via=<slug> |
linksFor() without via |
assetBase |
"" |
"" |
scrollContainer() |
null (window), or the shell root in preview |
null |
id(name) |
Suffixed in preview | Identity |
url |
Fragment read and write, or memory in preview | Query string |
objects |
Object verbs carrying the app's slug | Verbs carrying "suite" |
setSubject, openInspector, setRail |
Present | Absent |
registerExport |
Full mode only | Absent |
walk |
Full mode only (walkHost) |
Absent |
Calc and Sequence pass preserveExplicit: true when their blank/work defaults change: explicit choices on this visit win, while docked choices remain separate from floating-open state on resize. Other apps retain their existing transient-reset behaviour.
Apps should build every element id through host.id() so the same view works in a preview.
The shell context
useShell() throws outside a shell; useShellOptional() returns null (shell/shell-context.tsx). The value holds app, definition, mode, page, root, ids, rail (state, sections, toggle, setTransient("collapsed" | null), set), inspector (available, state, width, setWidth, open, close, toggle, setTransient("hidden" | null, { preserveExplicit?: boolean })), announce, density, the resolved theme, jobs, subject and setSubject, open (palette, shortcuts, settings, about, limits, coachMarks, menu), reportInbound, arrivedObject and storage (showConflict, changed). useRail(sections) shows a memoised section list while the calling component is mounted and restores the definition's on unmount.
The shell frame
Regions and geometry
The frame is the UI redesign of 13 October 2026 (D-0082, docs/board/decisions/), on the desktop scale of D-0016 and the Research sidebar of D-0028: the shell root is a row; the sidebar ([data-shell-region="rail"], RailColumn, a sticky top-0 column the full height of the window) stands at the left from lg, and the body column ([data-shell-body]) beside it stacks the top bar, the notices, the grid of main and the docked inspector, and the status bar.
Constant (SHELL in tokens.ts) |
Value (px at the 16 px root) |
|---|---|
strip / top / status |
0 / 56 / 26 |
rail / railCollapsed |
216 / 76 |
inspector (xl) / inspectorLg |
360 / 320 |
inspectorMin / inspectorMax |
280 / 520 |
On desktop (min-width: 70.4rem) tokens.css sets html { font-size: 110% } (D-0016), so every rem length is a tenth larger; code that measures the shell converts with designPx() and rootScale() (shell/use-shell-layout.ts). The breakpoints are lg = 70.4rem, xl = 88rem, 2xl = 105.6rem (BREAKPOINTS); md and sm keep Tailwind's defaults. Media queries in rem resolve against the browser's initial 16 px, so lg starts at 1,126.4 CSS px and xl at 1,408 CSS px.
The shell root carries data-app, data-mode, data-rail, data-inspector, data-mounted and data-rail-motion. data-rail says what is drawn: expanded only for the labelled column at xl, collapsed for the tiles at lg and for the collapsed choice at xl (the rows read it to switch to tiles; 13 October 2026). The sidebar's width is var(--rail-collapsed) at lg and var(--rail) at xl, and .suite-shell[data-rail="collapsed"] sets --rail to the collapsed width; its width transition, and the rail's label, brand and padding transitions, run only under data-rail-motion, which useRailMotionReady (shell/use-shell-layout.ts) sets on the AppShell and SuiteFrame roots from the second frame after mount, so the remembered rail state that arrives with hydration is applied without sliding and only a person's toggle animates; the width transition fires suite:layout (LAYOUT_EVENT) at its end. The grid (data-shell-grid, and data-research-grid in full mode) holds main alone, plus var(--inspector) while docked (tokens.css); its column transition is disabled while resizing and under reduced motion and fires the same event. The window scrolls; the sidebar and the inspector column are sticky with their own overflow (DS §2.3).
Research sidebar and Bench links
In full mode the rail column renders ResearchNavigation (shell/research-navigation.tsx, DS §6.3, D-0028), the same component the suite pages and the phone menu use. Its order is:
StudyContext: the tab's working Study selector (see Storage and state for the research context).- The Bench group, coloured with
--color-suite-accentregardless of the active app: Bench (/o, named byBENCH_NAME), then Literature (/literature). The Studies and Objects rows that sat indented under Bench (UI-03 of 11 October 2026) were removed at the CEO's word of 14 October 2026: the Bench's own count cards (BenchStats, each a link over the card to#studies,#pinned,#activityand#objects), its Recent studies and its Pinned objects reach every Study and every saved object. These are plain<a>elements, so a click loads a new document at the top of the page (Bench and Literature use a different policy from the apps); the policy boundary turns a cross-boundary click into a document load.suitePlacereads the place from the path: the Bench link is the current page (aria-current="page") on/oand every view of it, and current (aria-current="true") on a Study page (/study/…) or an object page (/o/<type>/<id>); Literature is the current page on/literature. Nothing in the group is current inside an app or an education tool. - Literature (
/literature), in its own hairline box, followed in the same list by the apps whoseworkspaceGroupis "literature" (Patent, D-0079; a tile beside Literature's, not indented, with no group heading). - App groups from
APP_INDEXbyworkspaceGroup: Discovery, Bench & process, and Learn. Learn listsEDUCATION_CATEGORIESfromcontent/education-tools.tsas<details>groups, opening the group of the current tool; Cell sits inside it. - A footer line, "On this device / Work is kept in this browser" (
content/apps/workspace-navigation.ts).
App links use the last safe address recorded for that app in this browser (useAppAddresses, shell/last-address.ts) and fall back to appHref(slug). They are ordinary next/link navigations (a new page starts at its top); pointing at or focusing one starts preloadWorkspace for that app alone. Clicking the current app on its own workspace does nothing, so its state is kept.
The compact rail is one Tab stop (UI-04): useRovingRail gives the current destination, else the first row, tabindex="0" and the rest -1; ArrowUp and ArrowDown move between the rows (wrapping), Home and End jump, and the item last focused keeps the stop, also through a change of address or hash while it holds focus (REL-36). The skip link before the rail stays first. From lg the column draws ResearchNavigation compact, the sidebar of the UI redesign of 13 October 2026 (D-0082): while the sidebar is expanded (xl, unless collapsed), each destination is a 32 px row, its mark in a 26 px badge and its short name beside it (the label is aria-hidden; the link's name is the app's full name), the current app's row washed in its glow with a 3 px bar at its left; while collapsed (data-rail="collapsed" on the shell root, and always at lg) the same rows draw as 36 px icon keys, the mark alone (16 October 2026, e60bed8): the label fades and is clipped to zero width, and the link keeps the app's full name (aria-label) and a tooltip. This replaces the 64 × 40 px labelled tiles of the 11 October fix round (UI-01) that D-0082 describes as "the 76 px tiles"; the column is still 76 px wide. D-0093 (17 October 2026) keeps the icon-only collapsed rail; the CPO recommended restoring the tiles, citing touch screens at lg, which show no tooltips. Groups are split by hairlines. Studies and Objects have no rows of their own (the CEO, 14 October 2026): they are Bench views, reached from the Bench, and the Bench row is aria-current="true" on them. The Bench group is sticky inside the scrolling list (useRailScroller, below), so Bench never leaves view and the sole roving tab stop always belongs to that list (REL-18); at 1366 × 768 the whole list, Cell included, fits between the brand and the footer without scrolling. The working Study is the header's chip rather than a rail entry. Every app keeps its own row except Cell, which is listed under Learn (D-0034, REL-200): the collapsed tiles show one Learn tile, which widens the column (below), and the expanded column lists Learn's subjects as <details> groups in place of the tile, so the expanded column has no Learn tile (the browser helper openLearnNavigation presses it only while the column is collapsed, REL-235). The Learn tile is the current destination on an education tool's page. The non-compact ResearchNavigation (the subjects and the Study selector) is the Menu sheet's list.
The column is a stage surface (data-surface="stage", class suite-rail): tokens.css draws it on the rail tokens (--color-rail, -raised, -edge, -fg, -fg-muted: the concept's #102832 family in light; the violet-graphite chrome in the workspace dark), and the accent inside it resolves to each app's glow (accents.css), so the rows, keys and focus rings read as on dark chrome in both themes. RailBrand (rail-column.tsx) sits above the list: the rail's toggle at its left in both states, then the serif initial of SUITE_NAME, the name and "Research workspace" (faded out and clipped while collapsed) (workspaceNavigation.label, which wraps to two lines in the 168 px column rather than truncating), decorative beside the Bench row.
RailColumn (shell/rail-column.tsx) gives the list the room between the brand and the footer; its useRailScroller hook fades whichever end of a scroll box has more beyond it (edgeMask from layout/tab-overflow.ts) and scrolls the current destination into view, on a new address and again when the column widens (a data-rail change on the shell root shows rows the tiles hid, such as the current Learn subject). The column scrolls the list itself for the Menu sheet and a preview's Rail; the compact rail passes scroller={false} and uses the hook on its own list containing the sticky Bench group. That group suppresses the top fade, and the compact rail draws its bottom fade as RailFade, a zero-height sticky anchor at the list's foot with a 24 px gradient into the rail's own surface above it (pointer-events: none, aria-hidden, not drawn in forced colours), rather than as a mask on the box (useRailScroller(list, { fade: "overlay" }), whose fadeEnd says when): a masked box is a stacking context to axe, which then read the rows scrolled under the opaque Bench group as text on the page's paper (REL-236; CPO memo of 17 October 2026). The Menu sheet's and a preview's lists keep the mask. focusin reveals an arrow-key destination below the sticky group with room for its focus ring and above the bottom fade. The scrolling container adds no second tab stop. RailFooter pins the sidebar's foot under it: the project chooser trigger for apps that save, Settings (onSettings), About and licences with the version under it (v0.2.0 · Preview form), and the device line ("On this device / Work is kept in this browser", the suite mark in a 20 px circle). Since 15 October 2026 (the CEO) the footer's rows are text-ui-sm and About and licences text-ui-xs over its version line (which wraps rather than truncates), the project name under the chooser text-ui-xs, and the device line text-ui-xs over a text-ui-2xs detail that wraps rather than truncates; while the sidebar is collapsed these are 32 px icon keys that wrap in the 76 px column on fine pointers (44 px keys in a column on coarse pointers), and the version and device line are not shown. The research sidebar's toggle is in the brand row in both states (RailBrand collapse), so its footer draws none (collapseInRow={false}); the app preview's footer keeps its own. From lg the toggle reads "Collapse navigation" on the expanded column and "Expand navigation" on the collapsed one, and writes the shared suite:suite:rail choice (collapsed or expanded); the Learn tile expands it. Esc and Collapse close the overlay and return focus to the column's toggle; a press outside or a choice also closes it.
Rail state (useRailLayout):
| Width | Full workspace | Preview |
|---|---|---|
below lg |
sheet: the Menu button opens AppMenuSheet |
sheet |
lg to xl |
collapsed (the tiles) unless the person expanded it; the toggle, [ and the Learn tile widen it to the labelled list beside main |
as full, in memory |
xl and up |
expanded (the labelled sidebar) unless the person collapsed it; the toggle and [ switch them |
expanded or collapsed, in memory |
An app may collapse the rail for one view with setTransient("collapsed"), which never overwrites the stored choice.
From lg the sidebar is always a column beside the page, never an overlay (the CEO, 14 October 2026). useRailLayout keeps one choice for every width in suite:suite:rail (collapsed or expanded; without one, the list from xl and the tiles below; the first-paint script in theme-script.tsx applies the same rule). The column's width follows --rail (lg:w-(--rail)) and transitions over --motion-move (300 ms, ease-sine-in-out), so widening or narrowing pushes the page rather than covering it. The expanded width is --rail-open (10.5 rem, 168 px at the 16 px root; 13.5 rem until the CEO asked for it 20 to 30 % slimmer on 15 October 2026). The list (.suite-rail) fills the column, which clips it (overflow-x: clip, which keeps the column's sticky list sticky); since 16 October 2026 data-rail follows the state at once and the labels fade and narrow (max-width, opacity) while the column's width transition runs, rather than the open list staying drawn until the tiles replace it. collapse still sets closing for the transition's length (data-rail-closing on the root, which sets --rail to the collapsed width); reduced motion swaps at once. The former lg overlay and its rail-widen animation were removed.
The current destination is drawn by its row's tint alone (bg-accent/12, /15 in the compact rail) with the semibold label; the row has no accent bar on its curved edge (the CEO, 14 October 2026).
Tools & sections and Find on this page
In full mode the app's RailSections are not shown as a second rail. AppSectionMenu (shell/app-section-menu.tsx) renders the app's page tools, a nav labelled "<app>: page tools" at every width, with:
PageFind(shell/page-find.tsx,page-find-model.ts): literal text search over the rendered main region, at mostPAGE_FIND_LIMIT(500) matches, highlighting with the CSS Custom Highlight API where available and an outline otherwise. It never changes app state or makes a request. Enter and Shift+Enter move between matches; the first Esc clears the query.- "Tools & sections": a Popover containing
Rail variant="sheet", shown only when the app has sections. - The Inspector toggle, shown only when the app has an inspector, with
aria-expandedandaria-controlsnaming the docked inspector or the drawer.
Below lg (and in the server render) they are a sticky row at the top of main, Find leading it (SHELL-15). The row measures its height into --app-subnav-height; app-section-menu.css adds it to scroll-padding-top and starts the inspector drawer beneath it. From lg, once mounted, they are portalled into the bar's [data-header-tools] bank as flat icon keys, which ShellTools places after Export and an app's extras and before the workspace search (D-0082). Find is the last key, so it sits in one place in every app whichever tools an app adds (SHELL-15); it opens its field in a Popover, which stays open while it has a query. Both forms carry app-subnav, so a focused stage (html[data-stage-focus]) hides them.
Inspector
shell/inspector.tsx (DS §6.4) renders DockedInspector (a sticky column with a SplitPane-style resize gutter: 280 to 520 px, arrows 16 px, Shift 64 px, Home and End, Enter or double click resets) and OverlayInspector (a non-modal right Drawer from md, a bottom sheet below). Content comes from a static inspector.Content or from inspector.open(node, { title, kicker, focus }); with nothing published it shows InspectorEmpty. A content change slides in 8 px over 200 ms unless motion is reduced; focus moves to the title only when focus: true (an explicit Inspect).
Docking follows one rule for every app (shell/inspector-dock.ts, DISC-8 of the 10 October 2026 audit): the inspector docks only from lg and only while viewport − railWidth − width ≥ dockMinMain, where dockMinMain defaults to DEFAULT_DOCK_MIN_MAIN = "40rem". The rail's width is an input to the check and is never changed by it. useInspectorLayout repeats the comparison inline (the code comment explains the React Compiler only memoises the hook without the call); inspector-dock.test.ts tests the pure function. Where it cannot dock, the inspector opens only on an explicit request and changes form on resize without closing. The person's choice is stored in suite:<slug>:inspector-open ("1"/"0") and the width in suite:<slug>:inspector. Before hydration gridMode starts without an inspector column for the hidden default and for startHidden opt-ins; a conditional method inspector opens only after its workbench publishes active inputs (D-0060).
Top bar, status bar and menu sheet
TopBar(shell/top-bar.tsx, D-0082) supplies the app's content toShellBar(shell/shell-bar.tsx), whichSuiteFramealso draws: a 56 px white bar under a hairline, inside the body column. Left: the Menu pill belowlg, then the breadcrumb (described next) and after it the workspace search as a fixed-width field fromlg(16rem, 20rem fromxl, the ⌘K hint beside its label; an icon at the right belowlg; the CEO's feedback of 14 October 2026). The breadcrumb (nav"Breadcrumb"): Bench (a link to/o, fromsm), the app's mark and short name (apwithidapp-titlethat labels main and carries the full name as its accessible text; not a heading since D-0084, so the subject in the body is the page's one H1) and the subject set bysetSubject, each after anaria-hiddenslash that sits outside the crumb so the crumb holds the plain name; the status slot (SaveStatus) fromsm.SuiteFramedraws the same breadcrumb with its own trail. The working Study chip shows at every width on every research workspace page (the sidebar's column, expanded or collapsed, has no Study entry; the non-compact list in thelgoverlay and the Menu sheet carries its own selector beside it), naming "Without a Study" when none is chosen; it opens the chooser as a menu under it (StudyContext, below). ThenShellTools(Export and app extras frommd; the page tools fromlg, above) besideShellActionsfromsm: the More circle, with no separate Sources key (the CEO, 14 October 2026; About and licences, with the sources, is in More, the Help items and the sidebar footer) (PageActionsMenu, the suite mark on ink: Copy link, Settings, shortcuts, the theme choice as a radio group, the Help items, and the apps' Start fresh and All apps, which load a new document as the former switcher's links did). A 2 px sweep runs along the bottom edge while a long job runs. A running guided walk'sWalkBarrenders after the sticky top wrapper (walkslot), as a fixed card (below, "Onboarding and guided walks").StatusBar(shell/status-bar.tsx, DS §6.5, §9.2) is 26 px and sticky at the foot of the body column, on the canvas under a hairline (the redesign's quiet footer line). Left fromsm: the freshness chip (FreshnessChipslot), app chips and the network indicator for apps whose registry lists it; the bar shows state only, the "Not medical advice." statement having moved to the head of the limits line (UI-07, below). Centre:AnnounceRegion, the app's one polite live region, as visible text. Right fromsm: Jobs (while a job of 10 s or more is listed), Limits, and the?and ⌘K keys as buttons. A 2 px sweep shows while requests are in flight throughsuite/net.AppMenuSheet(shell/menu-sheet.tsx) is the left drawer (md) or bottom sheet belowlg: the Research sidebar, and whatever the bars no longer show at that width (save status belowsm, Copy link, Export and extras belowmd, theme, Help and apps belowsm).- The Apps switcher (
shell/app-switcher.tsx, DS §6.6) was removed with D-0082: the sidebar names every app, the Learn row opens the education tools' full list, and More carries Start fresh and All apps.
The notepad (D-0087)
Notepad (workspace/notepad.tsx) is mounted by AppShell (not in a preview, with the app's short name and subject as the entry's place) and by SuiteFrame (with its crumbs as the place), beside the Toaster and outside main, so Find on this page and figure exports never read it. It is a 48 px round key filled with the page's accent, the notepad glyph (a ruled sheet with a pencil at its foot, 24 px; the CEO, 14 October 2026, in place of the note sheet) in the accent's on-colour (white in light, the dark ink on dark's mint), inside a 2 px surface ring with the float shadow so it stands out (the CEO's feedback of 14 October 2026; a neutral 44 px key before), fixed at the bottom right above the status bar, at z-(--z-chrome), hidden in print and in stage focus. While a draft is held it shows a surface dot ringed in the accent and is named "Notepad, draft kept". tokens.css sets --notepad-clear (3.5 rem) on html:has([data-notepad-key]), which lifts the toast stack (overlay/toast.tsx) above the key, and moves the key left of a docked inspector ([data-shell-grid][data-inspector="docked"] from lg, docked-xl from xl). The palette's "Open notepad" (shell.notepad, suite.notepad) calls openNotepad, a window event the key listens for; there is no single-key shortcut.
The panel (workspace/notepad-panel.tsx) loads on the first hover, focus or open. From sm it is a non-modal card above the key (title, a visible close key, Esc closes and returns focus to the key); below sm it is the non-modal bottom Sheet. It holds an optional Title (Input, 120 characters; the CEO's request of 14 October 2026) and a plain Textarea (20,000 characters, the field's own count), which are one draft per account (notepad-model.ts: localStorage under suite:notepad:<scope> as { text, title, at }, a draft kept before titles reading with none, held in memory when refused, "Kept in this browser" or the refusal said in the pad; another tab's typing arrives by the storage event). "Save to" offers:
- Notes of <working Study>, when a Study is chosen and the person can write:
studyStore().updateappends### <title> · Notepad, <date and time> · <place>(the title only when given) and the text as its own section toStudyData.description(appendEntry), each line escaped withescapeNoteText, since the notes are Markdown. The update reads the Study as it is now, so it appends and never overwrites; past 20,000 characters it is refused with the count. The Study page's notes editor and the Bench note pick the entry up, or report the change through their own revision check when they hold an edit. The choice names its Study and falls back to a note, never to another Study, when the working Study changes. - New note in Objects:
createMine("note", { label: <title, else first line>, data: { text } }, { by: "suite" }), not pinned; the confirmation links to the note and offers Pin.
A save clears the pad and confirms through InlineStatus (the status bar's live region) with Open Study or Open note; a failure keeps the text and offers Try again. Without write access the pad says so and offers Copy and Download. The ⋯ holds Copy text, Download as text (drybench-notepad.txt, the title, a blank line and the text exactly as typed; Copy text the same) and Clear, which offers Undo. Settings › Data lists the notepad with Clear notepad. Copy is in content/apps/notepad.ts; e2e/smoke/notepad.spec.ts covers the key, the kept draft, both destinations and the phone sheet.
Notices and arrival
Under the top bar, in order (shell/notices.tsx): the arrival strip ("Came from {app}: {context}", plus one line per ignored hand-off key), the offline banner, and storage notices for apps that save. On mount the shell reads via, obj and the hand-off keys this app does not read (readArrival), then removes via, obj and ignored query keys from the address. A #s=handoff=<id> address makes the shell read the hand-off record and write its parameters into the fragment before Main mounts (waitingForHandoff renders ToolLoading meanwhile). An app adds its own dropped values with shell.reportInbound. See Storage and state.
Commands, palette and keys
The palette lists, in group order: Search {app}, Go to, Actions, Recent, Apps, Help, then any app groups (paletteOrder). An object or identifier result is one row, its "Open" command, with its "Open in <app>" and "Add to study" commands (Command.parent, set in objects/palette.ts) nested under it: the row says "N more", Right (or the pointer affordance) lists them indented beneath it and Left folds them back (UI design review of 11 October 2026, UI-05). Every command remains a flat listbox option, without a nested button; the pointer affordance is hidden from assistive technology. The parent option's accessible description names its expanded/collapsed state and the Right/Left keyboard route. Both routes keep focus in the combobox. The shell contributes: search (when definition.search exists), Go to for every rail item, Copy link and Share this view, Sessions or Projects (apps that save), Save (Mod+S), Toggle rail ([), Toggle inspector (]), Stage focus (f, for apps with a stage), Theme: Light / Dark / System (apps that allow dark), Settings (Mod+,), every other app, All apps, Your objects, Keyboard shortcuts (?), About, Limits and "Show me around" (only while a coach target is present). Apps add definition.commands and may register more while mounted with useCommands(list) (shell/commands.ts); each AppShell owns its own registry. Recent command ids are kept in suite:<slug>:palette-recent (at most five). When saved objects and Studies cannot be read, the object results add a "could not be searched" row (Your objects; the other results stand) rather than leaving them silently missing, so a failure never reads as no matches.
CommandPalette (overlay/command-palette.tsx, DS §6.7) is a modal <dialog> with a combobox. Matching (overlay/palette-match.ts) scores a whole-label prefix highest, then a word prefix, a substring, then letters in order; keywords score 5 lower and group names 40 lower. When open with a non-empty query, it also lazy-loads sniffScientificIntent (src/lib/omnibox/scientific-sniffer.ts, copy in content/apps/omnibox.ts), which recognises inline calculations (C1V1, mass from molarity, oligo Tm), public accessions (PDB, UniProt, DNA sequences) and 14 cross-app bench-task intents (kind: "task-intent", opening the target app's #s=task=…, #s=op=… or #s=view=… address). The result count is announced after 500 ms of no typing and retracted on close. With objects, typed text also finds the person's objects, Studies and full public identifiers (objects/palette.ts). useObjectCommands returns the commands with the outside lookups' state (LookupState: UniProt and PubChem pending or failed, from useUniprotSuggestionState and usePubchemSuggestions); a status line above the key hints says "Looking up UniProt and PubChem…" while they answer, or that a source could not be reached and the list comes from the person's records and the local index, so a short list never reads as all there is ([data-palette-lookup]; the design audit of 14 October 2026). Answers to an older query are dropped (each result keeps its query).
Keys go through the registry in a11y/shortcuts/registry.ts (DS §9.3, contracts §7):
- Reserved keys:
?,/,Escape,[,],f,Mod+k,Mod+s,Mod+,(RESERVED_KEYS). Only owner"shell"may bind them. A conflict or reserved key throws in development; in production the later registration is ignored. - Single keys match
event.key, ignore Shift for printable keys, never fire with Ctrl, Meta or Alt, never in a typing target (isTypingTarget: inputs, textareas, selects, contentEditable, anything inside.msp-pluginor[data-shortcuts="off"]), and never while a modal layer is open exceptEscapeand?. - The Settings preference
singleKeysis"on","off"or"alt"(fire only with Alt, matched onevent.code);/,?and Escape stay available in every mode. - A shortcut with
scopefires only inside[data-shortcut-scope~=scope]. useShortcutanduseShortcuts(use-shortcut.ts) install one bubble-phasekeydownlistener onwindow, so a widget handler runs first and can claim a key withpreventDefault().
ShortcutsOverlay (DS §6.24) is generated from the registry plus widgetKeys and the shell's display-only rows (Escape, f), grouped Everywhere, Navigation, This view, Viewer, Tables, Editing. It hides the Save row when the app has nothing to save.
Settings, About, Limits and Help
The palette, Settings, About, shortcuts and coach marks are next/dynamic imports mounted on first open (the "dialog group", DS §7.3) and then kept for their exit animation.
SettingsPanel(theme/settings-panel.tsx, DS §6.29): Appearance (theme where the app allows dark, density, reduce motion, larger text), Numbers (extra digits; significant figures only for apps that declareunits), Viewer, Keyboard, Data (the StorageSummary slot for apps that save, the hosts the app contacts, the address-bar preference whenusesUrlState, clearing this app's preferences), About, thendefinition.settings.AboutDrawerandAboutContent(shell/about.tsx): Sources and licences, Limits, Regulatory position, Privacy (with the host table), Version, thendefinition.aboutSections. The same content is the static page/<segment>/app/about.LimitsBannerandLimitsDrawer(feedback/limits-banner.tsx, DS rule 6, §6.31; instrument design system §2 rule 3 and §4.4; UI design review of 11 October 2026, UI-07): the shell renders the banner once, below every workspace state in main (D-0060; moved from above the work by the CEO's feedback of 14 October 2026) (not in previews, the About or the Terms page), so apps do not place it. It is one notice line that leads with the app's standing statement (lead, which the shell passes asadviceLine(app): "Not medical advice." or "Not medical or investment advice."; "Limits." for Bioreactor and Cell, which have none), then on first open the first full sentence oflimits[0](limitLineinfeedback/limit-line.ts: never a sentence cut mid-way, and a sentence that restates the lead is skipped; the CEO's feedback of 14 October 2026) (up to two lines at every width; the whole limit is the line'stitle) and "Read all N limits". Reading the limits or dismissing them collapses the line to the statement and the link, which stay on the page; the collapse lasts 30 days insuite:<app>:limits-seen(or the session withremember="session").data-limits-noticeis"open"or"collapsed". Assay setsshowSummaryfrom its published dose-response state; when false the notice retains its standing statement and link, without the C50 summary or its title. The drawer lists every limit, the shared statements (statementsFor,shell/about-statements.ts) and the sources.HelpMenuandPageActionsMenuhold Keyboard shortcuts, Show me around, "What this app does not do", Report an error (About at the terms), Report a bug, Give feedback and Request a feature (each amailto:fromcontactMailtoincontent/apps/contact.ts: its own subject tag,[drybench/<slug>] Bug reportand so on, and the app, build and page without its fragment; the Menu sheet lists the same three), About and Settings.MethodsDialog(shell/methods-dialog.tsx, DS §6.35, D-0085): each app's methods and checks, a §6.9 Dialog at size lg (bottom-anchored and at most 85 dvh on phones), mounted on first open like the rest of the dialog group.shell.open.methods(target?)opens it; with no target it opens atdefinition.methods.group(), read as the dialog renders, so a press before the workspace has published still lands;{ method }opens at the group holding that method (groupForMethod, the open group first). The ways in: the limits line's "Methods and checks" at every width (LimitsBanner'smethodsprop, which Assay's own notice passes through); the Menu sheet's Help row; the palette'sshell.methodscommand; a chip's "How this number is computed" in the app's own full workspace (ProvenanceDetails, throughuseShellOptional; elsewhere it stays the docs link); andMethodsLink(shell/methods-link.tsx) in Calc's and Sequence's method inspectors. The dialog's content is the app's sheet (content/apps/<slug>.methods-sheet.ts), loaded on first open throughsrc/apps/methods-sheets.tsand cached for the tab; see Registry and content. Its links end with "All of this on one page" (/methods/<segment>at the open group's heading, D-0085;headingIdfromsrc/lib/technical-docs/heading-id.ts, so the dialog does not load the Markdown renderer) and a "Copy methods and reference checks" button ([data-methods-copy],formatMethodsAndReferenceChecksincontent/apps/methods-sheet.ts, matching the label inNotebookCopyControl,content/apps/workspace-recorded-methods.ts) that copies a plain-text summary of the open group, its methods and versions, citations, oracle verification sets, the scope note on what a reference check does and does not show, and the current#s=workspace address when withinLINK_MAX_LENGTH. Its sections, in order: what the three levels mean and what a check does not show; the standing statements,limits[0]and "Read all N limits"; the open group, expanded; every other group as a native<details>with its lowest level in the summary; the sheet's app-wide checks;registry.oraclesandregistry.sources; and About (no docs page link: the CEO's instruction of 14 October 2026; if the sheet fails to load, the message links to/methods/<segment>). Limits and About close the dialog first and open afterMS.base + 40. No count, percentage or score for the app is shown anywhere (the CSO's ruling R3).
Slots for the store and data UI
shell/slots.ts lists seven slots the shell renders when filled; shell/slot-adapters.tsx fills them with thin adapters, each loading its component with next/dynamic on first use or after first paint.
| Slot | Component | Where it appears |
|---|---|---|
SaveStatus |
store/save-status.tsx |
Top bar status slot from sm; first line of the Menu sheet |
FreshnessChip |
shell/freshness-chip.tsx (a ProvenanceChip line) |
Status bar left, for the oldest of definition.sources() |
ShareLinkDialog |
store/share-link-dialog.tsx |
"What is in this link", "Share this view" |
ExportMenu |
data/export-menu.tsx |
Top bar from md; Menu sheet below, where its onDone (after a finished export, never after a failure or a cancelled figure dialog) closes the sheet as Copy link does (CPO ruling, 12 October 2026; DS §6.26). A failure's title names the item ("Export failed: {item}", failureTitle; itemName takes the label up to its first ": ", trimmed, one trailing "." or "…" removed, the CPO's ruling of 21 October 2026, docs/board/memos/2026-10-21-cpo-export-failed-item-name.md; export-failure-names.test.ts holds every app's export labels to 40 characters or fewer), and its reason is the error's own words with the first letter upper-cased (reasonOf), both in data/export-failure-text.ts (the CPO's ruling of 21 October 2026, docs/board/memos/2026-10-21-cpo-rel378-export-retry-words.md, 3 and 4). In the sheet (trigger="row") a failure is said under the Export row, an ErrorBox inside a role="alert" region with Try again, not a toast; if the sheet closes with it unretried, the danger toast takes it over with quiet (CPO ruling 2 of 12 October 2026; REL-378, the phone export retry). Retries go through module-scope handles every mounted menu registers (data/export-retry.ts, registerRetry and retryExport: the menu the failure came from first; only if it has gone, another live menu in the same retryScope, which ExportMenuSlot sets to the app's slug so the TopBar's menu, mounted but hidden below md, takes a retry from a closed sheet; a menu with no scope, a table's, gets no fallback; each handle resolves the id against its latest items under its own lock and absorbs a retry while busy), and the held failure (sheetFailure) is module state owned by the sheet's menu, so closing the sheet removes neither (the CTO's memo of 21 October 2026, docs/board/memos/2026-10-21-cto-rel378-export-retry-mechanism.md). The TopBar menu's staying mounted below md (shell-bar.tsx, hidden md:flex) is what the phone toast's retry relies on; menu-sheet-export.spec.ts guards it |
ProjectChooser |
store/project-chooser.tsx |
Rail footer and palette, when definition.storage exists |
StorageSummary |
store/storage-summary.tsx |
Settings › Data, when definition.storage exists |
ConflictDialog |
store/conflict-dialog.tsx |
shell.storage.showConflict(payload) |
Copy link (shell/copy-link.tsx) copies share.link() (default: the current address), swaps its label to "Copied" for 1.5 s and shows one info toast with "What is in this link". When share.notInLink() returns a line, it opens the share dialog instead and copies nothing.
Jobs
shell/jobs.ts is the job registry (DS rule 19, §6.21, §7.5): running, done, failed or cancelled, with a progress fraction or null, a detail line and a Cancel that says what it does ("Stop now", or "Stop after this page" for a job registered with stopsAfterStep). The Jobs popover (jobs-popover.tsx) lists jobs that ran for 10 s or more until dismissed. Escape never cancels a job. jobs.test.ts covers the rules.
First paint and remembered layout
ThemeScript (shell/theme-script.tsx, DS §3.3) runs before the shell markup, from the server's HTML only: a copy React creates on a client navigation would never run, so the client draws it as an inert type="text/plain" block (React does not warn about data blocks), with suppressHydrationWarning for that one attribute. Besides the theme (below) it sets data-rail="collapsed" from suite:suite:rail and --inspector-saved from suite:<slug>:inspector when the stored width is between 280 and 572 (520 at the desktop scale). AppShell removes both hints after hydration, when its own state takes over. All localStorage reads go through guarded helpers (shell/local-value.ts: useLocalValue, writeLocal), which return null on the server and during hydration and notify other tabs through the storage event.
Navigation between policy areas
PolicyNavigationBoundary is mounted in the root layout, and the shell imports usePolicyRouter as its router, so palette commands, rail links and app switching load a new document when they cross a CSP boundary. This is described in Security and network.
Primitives and recipes
Recipes
recipes.ts (DS §6.0) exports whole-literal class strings so Tailwind finds every class in the file. Each is built from semantic tokens only; the site's original string is quoted in a comment beside it.
| Group | Recipes |
|---|---|
| Focus | focusRing, regionRing (scrollable regions), groupRing (radio chip groups) |
| Inputs | inputBase (a flat 36 px channel, 16 px text on phones, 14 px from sm; inside a Field the label sits above a 40 px channel), inputProse, textareaBase |
| Buttons | buttonPrimary and buttonAccent (filled with the accent), buttonQuiet (white with a hairline), buttonDanger, buttonIcon (control height; 44 px on coarse pointers), buttonSmall, buttonText; all flat, 3 px corners (--radius-control; the polish pass of 14 October 2026, which superseded D-0082's 7 px) |
| Chips and segments | chipBase, chipOn, chipOff, chip(on) (pills since D-0082: a tinted accent wash when on), segmentGroup, segment(on, size), segmentThumb |
| Panels and type | panel (the card radius, --radius-card 3 px, and the resting shadow --shadow-rest, 0 2px 6px rgb(30 25 15 / 0.1), the CEO's feedback of 14 October 2026; the Bench, Study and library cards take the same two tokens, dashed placeholders the radius only; the recipe carries .ui-card, so the shadow drops on a card inside a card, a dialog, a drawer, a popover or the inspector and is none in dark, D-0086; 16 px padding on phones, 20 px from sm), panelHeading, subjectHeading (Source Serif 4, 500; 34 px phone, 43 px from sm, 20% above D-0037's 28 and 36 px at the CEO's request of 14 October 2026), subjectPlaceholder, headlineAnswer (the serif face for one headline number), panelHeadingDense (17 px, Inter), panelKicker (the small bold accent eyebrow, sentence case), monoLabel, metaLine, metaText, disclosureSummary, note (a left-ruled aside, capped at max-w-3xl for a readable measure, REL-222; a site that must span a wider element would add max-w-none with a comment, and none does today), callout (a note box on the canvas), pill, kickerTag (a tinted pill label), featureTag, inlineFlag |
| Stage controls | stageButton, stageCluster, stagePlate, stageHint, stageToast |
| Links and inline | link, linkAccent, idLink, arrowLink, smiles, spinnerDot, spinnerDotMark |
| Site strings | site* exports, re-exported unchanged under their old names by src/components/tools/{chips,form-styles,panel-styles,stage-controls}.ts for the carried-over tool trees. Suite code never uses them. |
Rules stated in the file: do not combine two recipes that both set a background or text colour on one element; three recipes change shape with density (inputBase, the buttons, chipBase). The ui-action, ui-nav-item, ui-field, ui-link and ui-hover: names are utilities defined for the hover and press states.
Form primitives
All live in form/ and implement DS §6.20 and rule 18 (values stored at full precision, displayed rounded, never coerced silently).
| Component | Notes |
|---|---|
Field |
Label above the control, help and error ids wired into aria-describedby; the error lives in an always-present role="alert" element (empty:hidden) so a message inserted on blur is read once. kind="group" labels a group with role="group". Controls read the ids with useField(). |
Input |
Optional leading icon, clear button (Esc clears), busy state; type="search" gets role="searchbox". |
NumberField |
A text input, never type=number, so the mouse wheel never changes a value; the phone keyboard comes from keypad() in field-logic.ts (the full keyboard for a field that takes negatives or allowExponent, the digit pad for a non-negative integer, else the decimal pad). Parses 1e-6, 1,000, 0.5, ~5 (field-logic.ts); shows sigFigs out of focus and the full value in focus; validates on blur and Enter; live passes each parsable keystroke through. Inside EntryValidity it reports whether its text is readable so a calculator withdraws its answer. |
Select |
Native <select> styled with selectRecipe. |
Combobox |
Static list or async source; the source is called 280 ms after the last keystroke, from two characters, with an AbortSignal; ARIA combobox pattern; suggestion count announced after 500 ms of quiet. |
SuggestionInput, SearchInput, SearchPopup, SearchResult |
Synchronous suggestions whose matching the parent owns; the popup is shown in the top layer and follows its anchor. Enter on a highlighted suggestion picks it; Enter without one submits the typed text and closes the list, so it never covers what the search answers under the field; typing opens it again. Focus opens the list unless openOnFocus is false, which Pipeline sets while its not-found message or an error stands under the field. |
ChoiceGroup |
A <fieldset> with a mono <legend> for checkboxes and radios. |
Segmented |
At most four options that set one value; role="group" with aria-pressed; sliding thumb after mount. |
Slider, Switch, Checkbox, Radio, Textarea, FileDrop |
Native inputs where possible; Switch is role="switch" with an optional On/Off word; Textarea grows to 8 rows and counts characters; FileDrop checks type, emptiness and size before handing files over. |
format.ts |
formatSignificant, formatQuantity (per-quantity decimals plus extraDigits), formatExportNumber (full precision, . decimal, no grouping), isoUtc, UK dates and local times with offset. Pure; format.test.ts. |
Overlays
| Component | Behaviour (from the module headers) | DS |
|---|---|---|
Dialog |
Native <dialog> with showModal(); enters through @starting-style (200 ms), exits with data-state="closing" (150 ms) before closing; Esc through the ladder; a busy dialog refuses to close and says why; Tab and Shift+Tab are wrapped by hand inside the modal; focus returns to the opener or the nearest panel heading; page scroll locked. |
§6.9 |
Drawer / Sheet |
Same base with side right (420 px), left (280 px) or bottom. modal={false} (inspector drawer) uses show(), no scrim, no trap, and sits between the top and status bars so the chrome stays usable. The sheet's grab bar closes past 40 % of its height or faster than 0.5 px/ms. Drawers and sheets enter in --motion-drawer (220 ms) and leave in --motion-base (200 ms). |
§6.10 |
Popover |
Rendered after its trigger in the DOM and shown in the top layer (popover="manual"), positioned by Floating UI (offset 6, flip, shift 8 px, height capped at min(60vh, 420px)); within="workspace" keeps it between the bars and inside main. Its sticky header row holds a visible Close and, where the body has no heading of its own, the visible title (the label by default), which also names the dialog, so no popover opens with a row that holds only a close button (UI-08); the Save to Bench and Task details panel, Find on this page and Tools & sections pass one. After Escape or a choice it returns focus to its trigger; if the choice removed the trigger, to the first control that survives where the trigger sat. |
§6.14 |
Menu |
Roving focus and type-ahead (500 ms buffer); menuitem, menuitemcheckbox, menuitemradio; disabled items stay focusable. The panel establishes left alignment and normal whitespace independently of its trigger's table cell. Multiline Send to descriptions determine their row height; Library's compact sizing applies only to the direct row controls. The opaque pinned Close row reaches the panel's bottom border without a padding gap exposing scrolled text beneath it. Choosing an item returns focus to the trigger, closes, then runs the action, so a dialog the action opens restores focus correctly. No submenus. |
§6.14 |
Tooltip |
Text only, at most 240 px; 300 ms hover intent; on :focus-visible; never on touch; aria-hidden when the trigger's label already says the same. |
§6.13 |
toast() / Toaster |
At most three; success and info 6 s, warning 10 s, Undo 10 s, danger and any other action toast until dismissed (toast-store.ts). Success and info are announced once through announce(); warning and danger are role="alert" and not also announced; a toast marked quiet (its words already read once, as the phone Menu sheet's export failure is) is neither (isAlert). Toasts never take focus. |
§6.8 |
OverlayCloseButton |
The visible close control every persistent popup carries (principle 6). |
Supporting modules: layers.tsx (SuiteLayers, the #suite-layers portal element; useEscapeLayer; lockScroll), layer-stack.ts (the Esc ladder: the most recent reachable layer, then stage focus, then nothing; Esc never navigates or cancels a job), floating.ts (imports exactly computePosition, autoUpdate, offset, flip, shift, size from @floating-ui/dom), menu-keys.ts (rovingIndex, type-ahead). Each pure module has a test.
Popovers and menus position with floating.ts (position, follow); useFloatingPanel in popover.tsx passes stayInView: true, so shift() works on the cross axis too and a panel whose trigger scrolls away stays inside its bounds (DS §6.14, 13 October 2026; shiftOptions is unit-tested in floating.test.ts, the behaviour in e2e/smoke/popover-in-view.spec.ts at 1408 and 375 px). Tooltips keep main-axis shift only.
Layout primitives
| Component | Notes | DS |
|---|---|---|
Panel, PanelTitle |
<section aria-labelledby> with the panel recipe (the card radius and resting shadow), an optional kicker drawn as the small bold accent eyebrow (indexLabel, sentence case; passed only where it carries a source name since D-0084), a serif 20 px h2 (panelHeadingDense; 22 px under the 110 % root) that takes programmatic focus (tabIndex={-1}), an aside slot and ticks (accent corner marks for the one specimen panel); a level-3 title is sans 15 px (panelHeadingSub), so each panel carries one serif line (D-0084). Quiet by default: no mandatory accent rail or divider (principle 5). Server component. |
§6.32, §8.1 |
ViewToolbar |
The row under a page's section tabs (D-0084): a view's own selector at the left, a small muted <label for> beside the select (toolbarLabel), and the page's action keys at the right (actions, in R1 order: the split Save to Bench first). A selector scoped within the selected tab (Calc's calculator, Sequence's operation, Assay's analysis, the library's view) lives here. No app has a Layout menu: an app's views are its tabs (the CEO, 14 October 2026). Wraps on a narrow container. Server component. |
atlas |
DisclosureList, DisclosureRow |
A panel's optional sections as native <details> rows separated by hairlines (divide-y), each summary carrying the row's name and a decorative chevron that turns when the row opens; the open state may be controlled (open, onToggle). Callers keep the content mounted so Find on this page reaches it, and never put an error, a changed-input warning or a limitation behind a row (D-0084). |
atlas |
CopyButton (feedback/copy-button.tsx) |
The suite's one copy key (DS §7.4; the design audit of 14 October 2026), which Protein, Molecule, Assay and Sequence use: the label names what is copied; success shows a tick and "Copied" for 1.5 s, announced once; failure shows "Copy failed" with the remedy in its title and announcement until the next press; a press while a copy is in flight is ignored; the key holds one width whichever word shows. variant small, quiet or icon (the state in the accessible name). |
§7.4 |
ReadoutStrip |
A subject's headline numbers as a row of stat cards (<dl>, white cards with the card radius and resting shadow; the tinted tiles take the radius only, as they sit inside a panel): a small bold muted label, the value in tabular figures (the one keyValue in the accent), an optional unit and a detail line for the qualifier or provenance (D-0082); icon and tint draw the Bench's glyph tiles (D-0083); tone="tint" draws the atlas's soft accent-tinted tiles with a short accent dash and no hairline (.ui-readouts[data-tone=tint], D-0084). A withheld value shows its reason, never a dash. |
instrument DS §4.4 |
SubjectHeader |
The subject block of every bench tool: a 4 px accent bar beside the app's H1 (H2 in a preview), identifier lines beneath, and optional search and mode rows. Work-action keys now sit at the right end of the tab region rather than beside the title (CEO, 23 October 2026). | principles, R1 and R2 |
DiscoveryStart, DiscoveryHeader |
The shared start and loaded header of Protein, Molecule and Pipeline, and Assay's start. The start retains its optional aside preview/chooser; the loaded header keeps compact search above the subject and its source details. Action keys follow in the tab region, leaving the H1 and identifiers their own width. |
principles |
Tabs |
kind="line" (underline tabs on a hairline that spans the content width (tabList; tabListFit fits the row to its tabs, D-0084) with a sliding 3 px accent indicator, the selected tab in the accent deep at the same weight as the rest, so choosing a tab never widens it or moves its neighbours (the design audit of 14 October 2026); automatic activation, sideways scroll with edge fades; D-0082), "segment" (the recessed track), "steps". |
§6.11 |
ViewTabs, viewTabId |
Line tabs for an app-owned panel without remounting the subject; clears the last announcement on a tab change. The actions slot places Save work and the other keys at the right end of the tab region, on a right-aligned line beneath the tabs below a 48 rem container. Tabs alone scroll sideways with edge fades; the selected tab carries the 3 px accent underline. WorkCaptureControls keeps routine Study ownership and membership inside Work details; status and warnings appear as small unboxed text above the keys, outside the row's height on wide screens and on a compact line at narrow widths. AppShell's data-inspector state hides the tab-region keys while the Inspector is open, including its docked, drawer and sheet forms. Views without tabs use an equivalent right-aligned action line. |
§6.11 |
SplitPane |
Resizable gutter with keyboard steps of 16 and 64 px (split-math.ts), persisted per app, suite:layout on release. |
§6.22 |
Stage |
Frame for 3D and canvas viewers: data-surface="stage" or "paper", §8.2 heights (320 px phone, 380 px from sm, clamp(380px, 56vh, 720px) from lg), the control cluster, wheel and touch arming rules, stage focus mode. Charts and live 3D views use mode="theme" and follow the suite theme (principle 8). |
rule 1, §3.4, §8.2, §8.3 |
WorkflowGuide |
A native <details> for optional guidance. |
|
useMediaQuery, useInserted |
SM_UP, COARSE_POINTER; whether a component was inserted after load (entry motion only then). |
Feedback
Banner (tones info, success, warning, danger; a glyph so colour is not the only cue; role="alert" only when inserted after an action), EmptyState (dashed for empty, solid for no match or offline; the cards' 3 px corners; the action placeholder of the never-empty rule, D-0084: one sentence and one real verb), ErrorBox (not live; focusOnRetry), Skeleton and LoadingStatus (hidden for 300 ms, then a 1.6 s pulse, static under reduced motion), Progress (the loading ladder), InlineStatus and PendingIndicator, PendingLabel and useRunGuard (pending-label.tsx: a key's word turns to its pending word, "Fitting…" or "Calculating…" from content/apps/interaction-feedback.ts, reserving the wider of the two, while a second press during the run is ignored; the Assay fits, the Bioreactor measured review), Acknowledgement, Kbd, ToolBoundary (the one role="alert" crash surface), ToolLoading (says so and offers a reload after 15 s), ViewerGate (holds back a heavy viewer until clicked or visible) and the opt-in sound.ts (local synthesis only after an explicit action; off by default).
Since D-0086 the same feedback/copy-button.tsx also exports useCopyAction (the behaviour for a control drawn elsewhere) and takes write (a caller's writer, for a table copied as text and HTML; the tick waits for it), silent with onCopied (the caller announces, so the live region hears it once), dataAttributes, and variant="button" as an alias of quiet; the icon variant shows its word as a tag beside the key. Beyond the four apps above, the Calc answers, Plasmid's sequence, the data grid, the Assay results table, design notes and method paragraphs, and the label dialog use it.
The loading ladder (feedback/ladder.ts, DS rule 19 and §7.5): nothing for 300 ms; a skeleton or bar to 2 s; words and one announcement at 2 s; the slow sentence with Keep waiting at 6 to 8 s (default slowMs 6,000); the recovery verb and one announcement at 15 s; a stalled flag after 20 s without progress; alternatives at 40 s without stopping the job. InlineStatus sends its message to the live region and retracts it when it stops holding (status-retraction.ts).
Objects UI
objects/ implements build-spec-objects (docs/design/build-spec-objects.md). The data side (store, refs, links, Studies, research context) is in Storage and state. The UI parts:
| Component | Role |
|---|---|
FigureSpec.physical |
A figure with a physical size: the export menu opens PhysicalFigureDialog (formats SVG, PNG, TIFF, PDF; backgrounds per format; the size in mm; resolution in dpi with pixel size for PNG and TIFF, greyed past the device's canvas limits; the TIFF size and alpha lines, the PDF note and its substitutions; the app's notes) and writes through the app's write, never the record (the CPO's §D.24; Assay from A-0 commits 4 and 5). |
WorkCaptureControls sessionUnsaved |
An app's dirty flag for unnamed work: "Unsaved changes" in the control's status slot with the app's project download beside it, quiet, never for a named task (the unit-of-work design §3.9; Assay from A-0 2d). |
ObjectActions, MoreActions, SendToLink |
The object verbs on every row, card and panel: variant="bar" (More (⋯) holding Pin, then Send to, Add to Study or "Add molecule to Study", and Open page; D-0102) or "row" (one More menu: lead items, Send to, then Pin first in Keep, Add to Study and Open page; pinInMenu defaults to true and pinInMenu={false} exposes Pin as its own key; a dense table may shorten the row keys from its own class on the group, as Pipeline's trials do with [&_.ui-key]:[--key-h:1.5rem], while the key's coarse-pointer rule keeps 44 px on touch, UI-27). The object is upserted when a verb is used. A row may lead its More menu with the work's own acts under their own label (leadItems: { label, items }, Assay's "This assay": Open a file…, Download project file, Repeat): a labelled MenuGroup of ordinary items first, then Send to and Keep as before, then moreItems with Delete last after its separator; one arrow-key order; nothing rendered when empty; the label from content. Only acts on the current work itself go there (open, download, repeat); anything else is the CPO's first (the CPO's ruling of 12 October 2026). |
ObjectPage |
/o/<type>/<id>: identity header, Overview (cached facts per source with licence and date, the person's numbers), one tab per app that declares one (loaded on open, inside ObjectTabFrame with the app's own host, accent and error boundary; the tab's address is memory only), Mine, Used in. The open tab is kept as tab= in the fragment. |
MinePanel, Numbers, Trail |
Notes, numbers, tags, files (up to 2 MB each) and versions kept on an object; where a number came from. |
StudyPage, StudyPicker, StudyContext, StudyChoices |
/study/<id>, the "Add to study" picker, the working-Study chip and its chooser: a Menu under the chip (objects/study-context-menu.tsx, loaded with the menu) with the Studies as a radio group, "Without a Study" first, archived ones behind a checkbox, "Open Study" and a way to a first Study; a choice applies at once with the D-0046 rules (pending work saved first, the Study re-read, a changed or archived destination refused with a toast and a status line) and the menu closes (UI design review of 11 October 2026, UI-02). |
ObjectList, ObjectLibrary |
Bench at /o with its hash views (#studies, #work, #pinned, #activity, #objects) and Library (List, the default, or Cards, suite:library:layout; D-0084, D-0120). |
HomeParts, HomeExample, ExampleParts, RemoveExample |
Bench panels and the guided walk's example card and removal dialog. |
CandidateList (objects/candidate-list.tsx, copy content/apps/candidate-list.ts) |
The records a typed name matches, none chosen (the CPO's ruling 7 of 17 October 2026; built with E10, reused by E9's compound identity). Source-agnostic: props query, candidates ({ id, name, identifier, distinguishing, matched: { on, text } }), source ({ name, release, accessionPrefix }), onUse(row), optional onKeepTyped(). The heading "{N} records match “{query}”. None has been chosen.", announced once per result set through one polite status region; each row with the name, the identifier in mono, the line that tells it apart, "Matched on: primary name" or "Matched on: synonym “…”" and a quiet "Use this" whose name includes the record; no row preselected and no key handler, so Enter in the caller's field chooses nothing; at most CANDIDATE_LIST_MAX_ROWS (10) rows, then "Show all N" for that result set; with no candidates, the no-match sentence naming the source, its release and its accession prefix; "Keep as typed text" when offered. Rows full width and 44 px targets below sm and on coarse pointers. content/apps/candidate-list.test.ts pins the words and that the component names no source. |
ObjectPickerPill, ObjectPickerDialog (objects/object-picker-*.tsx, copy content/apps/object-picker.ts) |
The @Object picker that fills one field from a saved object (Calc's molarity, A280 and C1V1). The cell-line target (E10) carries no number: its pill reads "Choose the cell line", and its dialog body, objects/cell-line-picker.tsx (loaded when it opens), lists the saved cell lines and looks a name or accession up in the Cellosaurus snapshot with resolveCellLine, showing the outcome in a CandidateList (even a single match is a candidate to choose); a chosen record is stored by value with its passport, unless this browser already holds that line with one; "Keep as typed text" stores a mine:cell-line record. The selection carries the record's rev and its body as payload. Used by Assay's plate details (one per plate). |
CellLineCard, CellLinePassport (objects/cell-line-passport.tsx, marker drybench-island:cell-line-passport, code row cell-line-passport-chunk) |
A cell line's card details ("Cell line · CVCL_… · species", the disease, the caution-tone "Cellosaurus annotation" quoting the comment) from RecordDetails, and its page's Overview sections (Identity, Origin, Annotations, STR profile, Sources and limits) from ObjectPage, both loaded on demand. The quote is Cellosaurus release {N} ({date}), record {CVCL_…}: “{comment}” with a link to the entry. A page opened before the record exists reads the snapshot and carries the record with its passport. Copy in content/apps/cell-lines.ts. |
useObjects hooks, useLearn, loadRuntime |
Load the store on first use (import("./runtime")); useLearn refreshes facts of records the person already has and never creates one (D-0045). |
The verbs module (verbs.ts) is what every host carries; its implementation (verbs-impl.ts), the store and Send to's planning load on the first call.
The discovery start reads discoveryStartText and discoveryGuidanceText from the
pure content/apps/discovery-guidance.ts leaf. The public workflowGuidanceText
object still composes the same summary, discovery instructions and Bench guidance,
and workflow-guidance.ts re-exports discoveryStartText. This keeps the Bench
copy out of discovery-only import owners without changing a word or interaction.
Workspace UI
workspace/ holds the D-0038 to D-0040 controls for saved work and Studies. The records they write are described in Storage and state; access modes in Server, accounts and billing.
WorkCaptureControlsis the shared Save to Bench and Record result control that bench tools pass toSubjectHeaderasactions. Props:app,label,objectRef,payload(JSON),schema, optionalsubjectRefs,captureRecorded(work)(the app's closure that builds theResultSnapshotandInputSnapshots for this render),onSaved,onNamed,beforeSave,disabled,saveHelp,freeLine(optional: without save access, the app's own sentence of fact and its free actions shown in place of the suite's paid link; Assay passes its free/paid line, roadmap-v0-depth Stage A, exit criterion 7; apps that leave it out keep the paid link),detailsandchildren(the app's own actions, which stay usable without save access). Naming, the actual destination and duplicate creation sit in a details panel opened on demand (D-0040).save-focus.tsdecides where focus goes when a save settles.WorkItemsPanel,WorkOutcome,SavedWork,RecordedResult,ResultHistory,ResultComparison(descriptive only, never pools or computes statistics),InputPicker,WorkNoteEditor,WorkOrganisationDialog,WorkDestination.- Study panels:
StudyBrief(the callout),StudyProgress,StudyStatusMenu,StudyTaskKey,StudyContextPanel,StudyResultsTable,useStudyWorkspace,StudyNextSteps,StudySummary,ExperimentsPanel,StudyRecoveryControls(the.drybench-study.jsonrecovery file),MeetingView,NotebookCopyControl,SourceImpact,ScientificChanges. - Bench:
BenchHomeparts (bench-home.tsx: the greetingBenchGreeting, the counts stripBenchStats, the placeholderBenchPlaceholder, the tools grid with its quote card, the section heads, the view links and the relative timeAgo),bench-figure.tsxandbench-figures.ts(the generated figures),bench-studies.tsx(the Study cards with their covers and ⋯ menu),bench-pins.tsx(the record rowsRecordTable, the pinned table and its type chips),bench-quick-start.tsx(the Templates, Example data and Import tabs),BenchNote(editsStudyData.descriptionwithbaseRev),bench-priority.ts(presentation-only ordering). - Access:
access.tsx(useWorkspaceAccess),account.tsx,backups.tsx,entry-banner.tsx,ManagementDisclosure,use-research-draft-guard.ts.
lazy-result-views.tsx loads the recorded-result and comparison views on first use and retries a failed download on request.
Onboarding and guided walks
WelcomeCard(DS §6.23; decision C22): a first-open frame with one to three actions, optional steps, "Show me around" for apps with coach marks and "Don't show this again" stored understorageKey(slug, "welcome"). A small inline guard hides a dismissed card before first paint.CoachMarks: an opt-in tour of at most three[data-coach="<id>"]targets, never started or advanced automatically. The palette offers it only while a target exists (useCoachTargets). The targets come from the registry entry'scoachMarks.- Guided walks (D-0022, J3):
walk/use-walk-shell.tsreads awalkkey from the fragment on arrival (before the app reads the address) and removes it;walk/run.tskeeps the run insessionStorage["suite:walk:run"];walk/walk-bar.tsxloads withnext/dynamiconly while a walk runs or a key arrived;walk/definitions.tsholds the step definitions;walk/slots.tsthe filled copy. Apps register step actions withuseWalkAction(id, action), which goes throughhost.walkand does nothing in a preview or without a run. After each step action the bar waits until no action has been registered or removed for 60 ms (at least 60 ms, at most 2 s), so a component the action replaced has mounted and registered its own before the next action runs (REL-27). Next never stays on "Working on step n…" (REL-426): every path that does not leave the page clears it, and a watchdog (STEP_WATCHDOG_MS, 45 s) stops a step still waiting after that long. It bumps the bar's revision, so every closure the step captured is stale and a late answer cannot move the run under a retry, logsWalk step n stopped after 45 s, waiting on: …with the stage it was on (saving open work, an action id, the example study, or the hand-off that should open the next page) to the console, and shows the walk's failed line on the bar as well as announcing it. Before this, Next returned without clearing its busy state whenever the run stopped being current mid-step (another tab's run, a change of workspace scope), and a hand-off that never left the page kept the label for good. In the changed state the step text stays open and its Show/Hide toggle is hidden, so Next's explanation is on the page; on the phone that state's controls may wrap to three rows within the 300 px bar (the CPO's amendment of 13 October 2026).objects/write-context.tsmarks what the store writes during a run so "Remove the example" can find it by run id. - The bar's placement (D-0105, updated 24 October 2026). The bar is a fixed floating card (
.walk-bar-shell,tokens.css) beside the step's target over a visual-only scrim:html[data-walk-spot] .suite-shell::after, a box at the complete target rectangle whose 9999 px spread shadow dims the rest of the shell. This implements the CEO's floating-overlay decision rather than the earlier sticky-top bar.useWalkTarget(walk/walk-bar.tsx) syncs placement on scroll, resize, DOM changes and focus, without a polling interval, and writes--spot-*anddata-walk-diron<html>. At desktop widths the card chooses above/below while protecting focus, section navigation and the Inspector; when neither side fits it uses the existing compact edge dock. The edge dock's target clearance uses the actual sticky chrome and available space instead of an assumed 88 px arrival position. Below 640 px it remains a bottom dock whose bottom meets the rendered StatusBar's top, or the visual viewport's bottom when the keyboard is open. Phone controls use an explicit Back/Next row (with Restore retained when changed) and a three-column secondary grid for Go, the text disclosure and Leave; changed-input state uses two columns while the disclosure is absent. Complete secondary labels wrap at their existing font size; every key remains at least 44 px tall and Leave at least 44 px wide. Phone block padding and the title/control gap are 4 px. This prevents the longer “Show the step text” label from pushing Leave onto a third row when the guide compacts. Each visible header and section-row rectangle contributes its actual bottom edge, including the browser’s sticky placement. Together with final card height and StatusBar bounds these define the usable target band; there is no fixed 116 px phone target position.geometry.tscomputes the scroll needed to keep a fitting target wholly in that band. On explicit “Go to the number”, an expanded phone step uses its existing text disclosure to compact when the scientific target needs the space, then focuses and scrolls once using the final card height. The text remains available through the same disclosure; changed-input guidance remains expanded. A target too tall even for the compact band keeps its useful start and complete content in ordinary page flow, with its lower content reachable by scrolling; this does not claim simultaneous whole-panel clearance. During a phone walk, Assay’s existing “First imported readings” scrolling region (the direct region under[data-detected]) has a maximum height of the smaller of 24 rem and the measured viewport minus header, section row, StatusBar, guide and two 8 px clearances. Its name, focusability and all eight preview rows remain; internal scrolling makes the final row reachable without covering the focused region. The outer recognised-import target and its complete content remain the walk target.ResizeObserverreconciles target, card, body and chrome growth until the person scrolls or interacts, so late layout does not silently move a fitting target under the dock, and later intentional scrolling is respected. “Go to the number” explicitly restores placement. After a real focus move, one animation-frame check protects the same still-connected active element if native focus scrolling leaves it under the card; the correction is instant so the global smooth-scroll rule cannot replace focus protection. New focus, user interaction and step cleanup cancel stale work. Explicit Go retains its smooth motion unless reduced motion is requested. Observers, animation-frame work and listeners are removed with the step; one per-stepAbortControllerowns the window, document and visual-viewport listeners and preserves their capture/passive options. A press or focus outside the target/card lifts the scrim, which returns on a step change or Go. Phone scroll padding and the toast stack retain the measured card height (--spot-bh/--walk-clear). The scrim remains at--z-coach, the card one level above, and the target's content-sourced “Step n” tag below chrome; no accent focus ring is added. Reduced-motion preferences still disable movement transitions and smooth scrolling. The unchanged hard target predicates in the plate/J3 journeys ande2e/support/walk.tsremain separate from the softly reported focus assertion under D-0080; no oversized-target exemption is added.walk/geometry.test.tscovers measured fitting phone/desktop targets, late card growth, stationary clear targets and the honest oversized-panel start.e2e/objects/j3-focus-clearance.spec.tsseparately models one late browser scroll after synchronous focus handling and uses the unchanged focus-clear predicate; its controlled scheduling evidence is distinct from the unchanged J3 journey. See Known issues for browser outcomes and remaining oversized-target limits; the existing 10 KB walk budget is unchanged.
Motion and reduced motion
motion/animate.ts (DS §7.3) wraps the Web Animations API with the suite easings (EASE, from tokens.ts: expo-out cubic-bezier(0.16, 1, 0.3, 1), expo-in-out, sine-in-out, sine-out, cubic-in, standard) and durations (MS: instant 100, fast 150, base 200, move 300, slow 500, draw 600 ms). It exports animate, stagger (60 ms steps, capped at 6), flip, countUp, drawPath, keyframes fadeUp, fadeUpSm, pop and prefersReducedMotion. No motion library is a dependency. The ANIMATIONS tokens (tokens.ts, mirrored as --animate-* in tokens.css) loop only for the existing busy indicators (indeterminate, pulse-soft, dot-pulse); shimmer is one 1.2 s sweep (DS §3.6, "Nothing loops"). The glow-pulse keyframe added on 16 October 2026 was removed: DS §1 rules out glows.
Reduced motion has three sources, honoured identically (DS rule 11, §9.5): the OS media query, the Settings switch (html[data-motion="reduce"], with MOTION_EVENT = "suite:motion"), and for opt-in decoration (mark draw, skeleton pulse) the browser's data saver. Under any of them, and when the tab is hidden, animate() applies the final keyframe and resolves at once. tokens.css repeats the site's 0.01 ms collapse under html[data-motion="reduce"] and under prefers-reduced-motion, leaving out ::backdrop; html[data-theme-instant] disables transitions while a theme swaps under reduced motion. Nothing inside a closed <details> (below its summary) transitions: Chromium never advances an animation in that hidden content, so a transition started there (a key enabled while its disclosure is shut) never finished and the page never settled. useReducedMotion() is false on the server and during hydration; usePresence(open, exitMs) keeps an element mounted through its exit. Entry motion never runs on first paint (useInserted, shellMounts in AppShell).
Interaction effects (the motion pass of 14 October 2026)
motion/effects.ts holds the suite's acknowledgements and the expressive effects, with their switches. Every effect is presentation only: it starts after the operation it acknowledges has succeeded, never delays it, never changes a result, a record or the address, and leaves the final state meaningful without it. Visual proxies are aria-hidden and removed when they finish (or after 1.2 s).
- Switches (internal, no setting).
EFFECTS.expressiveturns every expressive effect off at once; each has its own flag (travel,expand,compareAssembly,inkCurve,recordedStamp, and the opt-inlauncherLightandlauncherTilt, off). For review,html[data-effects="core"]turns the expressive effects off without a rebuild andhtml[data-effects="all"]turns every one on.expressive(effect)is false under either reduced-motion source and in a hidden tab;effectEnabled(effect)ignores motion, for an effect that also carries information (the recorded stamp's time stays, still, under reduced motion);motionAllowed()gates the core tints;motionAllowed({ saveData: true })also honours the data saver, for a surface that treats save-data like the other two sources (the Bioreactor visual layer, BR11). - Core feedback (not under the switch).
trace(el, { tone })tints an element's own background 14 % with the accent, success or danger colour and lets it settle over 700 ms (TRACE_MS), withfill: "none"so nothing inline is left behind. On an SVG element it tints the element's own fill instead of a background: the one shared trace for HTML and SVG (suite DS §7.2, BR11.6);accentpasses the app's accent explicitly where the element does not resolve it inside[data-app], andsaveDatahonours the data saver. Users:InlineStatuson a new successeventKey(opt out withacknowledge={false}),NumberFieldafter a unit conversion (with anaria-hidden"Converted to …" receipt for 1.6 s; the conversion itself is announced), Molecule's comparison columns added later, the Assay plate's wells after an exclusion decision (danger) or inclusion (accent), the arrival section (below) and the return highlight. - Keys.
.ui-key(instrument.css) answers hover in 130 ms and press in 80 ms with a sunken face and an inset shadow, never a translate or scale (the old.ui-actionpress scale inglobals.cssis gone)..ui-pendingholds a pending ring back for 150 ms so a fast run never flashes it.Acknowledgement's persistent tick draws its path in 140 ms. - Panels and lists.
ViewTabsslides one indicator (data-tab-indicator,data-liveafter the first placement) under the selected tab;DisclosureList's chevron rotates; an open<details>body fades in over 160 ms (closed within that time, Chromium leaves the fade unfinished in the closed body until it opens again, so the browser helpersettle()does not wait on animations inside a closed disclosure, REL-206); the search popup settles 3 px over 160 ms and its active option carries a 2 px accent edge; a windowedDataTable's sticky head takes a shadow only once scrolled (.ui-head-shadow-host, a scroll-driven animation where supported). - Arrival (A5, with B1).
arrive({ section, label, via })runs after a destination has decoded and reported a hand-off: a name card travels from the arrival strip ([data-arrival-strip],shell/notices.tsx) into the section that holds what arrived, from the sender's accent (accentOf(slug)) to the receiver's, and the section is then tinted once; when the travel cannot run (effect off, either end off screen) the section is only tinted. Calc's compound card uses it. - Return (A6).
rememberReturn(ref)keeps the record a list opened insessionStorage(suite:return, one key, 30 minutes; opening a record is a full page load, so memory would not survive it);highlightReturn(root)brings that row back into view (block: "nearest", focus not moved) and tints it once. The Objects library uses it. A record that no longer exists is not found and nothing is selected instead. - B1, travel.
travel(from, to, label): a 420 ms quint-out name card, no overshoot. Add to Study (object-actions.tsx) flies the record's name to the bar's Study control only when that control shows the Study it was added to ([data-study-context="header"][data-study-id]); the toast receipt always stands. - B2, expand. The Stage's Focus FLIP (
layout/stage.tsx) runs 320 ms expo-out whenexpressive("expand"), else it is instant. Focus is offered wherever the shared Stage is used (focusModedefaults to true): Protein's structure, Molecule's map and shape in the default layout, Cell's tour. Molecule's Compare layout draws its own map, without a Stage. - B5, recorded stamp.
workspace/recorded-stamp.tsx: after Record result, an outlined accent "Recorded · time" stamp beside the receipt (content/apps/workspace-work.ts,capture.recordedStamp), settling from 102 % with a short trace; the receipt's own tint is turned off for that event so only one acknowledgement plays. Not shown for a save, a memory-only record or a failure. - B6, launchers.
workspace/launcher-light.tsxlights the Bench's tool cards under a fine pointer (data-lit,--lx, a shadow offset) and tilts them at most 1° (data-tilt); both off by default.
The Assay curve draw (B4) and Molecule's comparison assembly (B3) are described on their app pages.
Accessibility helpers
- One live region.
announce(text, { kind })(a11y/announce.ts, DS rule 9, §9.2) writes to the singleAnnounceRegionthe status bar renders. It coalesces for 400 ms (COALESCE_MS), drops identical text within 1 s (DEDUPE_MS), writes progress at most every 5 s (PROGRESS_MS), queues at most 3 messages 1.5 s apart (QUEUE_MAX,QUEUE_GAP_MS), and drops messages when no region is mounted.clearAnnouncement(text)normalises whitespace, removes matching pending/queued text and empties the region if it shows that text. An explicit nonempty target matching the last emission also releases only that emission's dedupe identity, even if its text is already absent; a later explicit identical request follows normal coalescing and queue spacing (REL-518). Clearing unrelated or blank text leaves dedupe intact. No-argument clear drops all waiting text and empties the region but deliberately retains dedupe history. Targeted withdrawal does not reset progress cadence or the active queue-gap timer and never republishes a message by itself. A pre-existing deliberate-repeat callback can still restore withdrawn text if cleared between its blanking write and next animation frame (REL-519, open); the bounded dedupe amendment does not change that scheduler.announceOnArrival(text)carries a message throughsessionStorage["suite:announce:arrival"]to the next page.answer-text.tsbuilds a calculator's first-answer sentence from[data-answer]cards. - Focus.
a11y/focus.ts(DS rule 13, §9.1):focusAfterRender(target, { fallback }),focusHeading(target, { scroll }),nearestHeading,focusableIn,restoreFocus(invoker, fallback), andtrackStableFocus()/lastStableFocus(), which the shell starts on mount so a layer opened from a toast or a menu item can return focus to the control the person used. Busy controls usearia-disabled, notdisabled. - Skip links. The root layout's "Skip to content" targets
#main;InspectorSkipLinkadds "Skip to inspector" while it is docked.useSkipLinkFocusfocuses the target without writing the fragment, so#s=state is never lost. - Target sizes. Recipes add
[@media(pointer:coarse)]:min-h-11orsize-11(44 px) to controls.
Theme
One preference covers every page (D-0029). The mechanism is summarised in Architecture and the keys in Storage and state; the modules are:
| Module | Role |
|---|---|
theme/preference.ts |
THEME_STORAGE_KEY = "suite:theme", values light, dark, system, DEFAULT_PREFERENCE = "light" (D-0035). createThemePreferenceStore keeps the choice in memory when storage is blocked and migrates a valid suite:<slug>:theme or, for "suite", drybench:landing-tone once. |
theme/store.ts |
Scopes (a target element per ThemeScope), the resolved theme, THEME_EVENT = "suite:theme" on every change, the storage event and the prefers-color-scheme query; an app whose themes lacks "dark" resolves to light. Updates theme-color metas from CANVAS_LIGHT and CANVAS_DARK. |
theme/theme-scope.tsx |
Applies theme and appearance settings to <html> (or the preview's shell root) while mounted; removes workspace hints on unmount. |
shell/theme-script.tsx |
The inline first-paint script: data-theme, data-motion, data-density, data-text, and the layout hints. |
theme/appearance.ts, theme/settings.ts |
suite:settings JSON: sounds, reducedMotion, density, largerText, singleKeys, significantFigures (2 to 8, default 4), extraDigits (0 to 2), units, viewer.defaultStage, viewer.labelScale, addressBar (live or static). sanitizeSettings validates every field on read. SETTINGS_EVENT = "suite:settings". |
theme/theme-toggle.tsx, settings-button.tsx, settings-panel.tsx |
changeTheme (the 300 ms crossfade and one announcement), used by the More menu's theme group, Settings and the palette; the ThemeToggle button is no longer drawn in the bar (D-0082); the Settings button; the panel. |
theme/chart-theme.ts |
chartTheme(root?) reads the --chart-* tokens with getComputedStyle from the nearest themed element and returns literal colours, cached per element until THEME_EVENT; useChartTheme() re-renders on change and returns the light set on the server. |
theme/stage-theme.ts, theme/viewer-surface.ts |
Literal stage colour tables (obsidian, paper) for MVS scenes, 3Dmol, canvases and RDKit drawing; syncMolstarSurface resets a Mol* canvas background. |
DS §3.4's rule applies: SVG attributes and canvas fills take literal colours read at draw time, never var(), so exported SVGs keep real colours.
Tokens, accents and print
tokens.css(imported at the end ofsrc/app/globals.css, afterbrand.cssand beforeaccents.css) declares the semantic colours in@theme static(light), the dark and stage overrides under[data-theme="dark"], [data-surface="stage"], the paper surface re-declaring the light column ([data-surface="paper"], R42), motion durations, the z ladder (--z-base0 to--z-toast70,--z-showreel100; DS §2.4), chart tokens, density variables (html[data-density="compact"]), shell variables on.suite-shell, the custom variantsdark:andstage:, keyframes, the D-0016 desktop scale and the reduced-motion collapses.- The light semantic colours are the redesign's (D-0082): canvas
#f6f8f7(workspace pages override it with paper beige#f3f0e8,CANVAS_WORKSPACE_LIGHT, underhtml[data-theme="light"][data-suite-shell]and[data-suite], the CEO's feedback of 14 October 2026; thehtmlandbodytake it too, so it runs to the window's foot; public pages keep the cool canvas; since D-0086 the same block gives workspaces the warm neutrals beside it,COLOR_WORKSPACE_LIGHT: hairline#e0dacf, sunken and skeleton#eeeae2, soft line#ebe7de, key and field line#d8d2c6, input edge#857e71), hairline#dfe6e5, ink#18313b, muted#4a5b63(kept at the 7:1 rule), flat white keys and fields (--color-key-topand--color-keyagree;--color-key-edgeis the key line), and the sidebar's--color-rail,-raised,-edge,-fgand-fg-muted(the workspace dark block overrides them with its chrome)..suite-rail[data-surface="stage"]draws the sidebar on those tokens. - Cards and corners (D-0086, the polish pass of 14 October 2026):
--radius-control(3 px, beside the CEO's--radius-cardof the same day) gives keys, fields, selectors and the section track the cards' corners (rounded-control); chips and status pills stay pills, and round wells and avatars stay round..ui-card(ininstrument.css; thepanelandpanelGridrecipes and the discovery start carry it) adds--shadow-rest, and drops it on a card inside a card, a dialog, a drawer, a popover or the inspector, so shadows never nest; both dark sets set--shadow-resttonone. A navigable card (.ui-card-link, the Bench tool and Study cards) answers a pointer with a stronger border and a slightly deeper shadow and never moves. Line tabs keep one weight in every state, so selecting a tab never shifts its neighbours. tokens.tsmirrors every value for JavaScript:EASINGS,D,Z,SHELL,COLOR_LIGHT,COLOR_DARK,COLOR_DARK_RESOLVED,TEXT_UI(thetext-ui-*scale from 11 px2xsto 32 pxbig; below the 14 px body only the three steps 11, 12 and 13 px, the CPO's ruling of 17 October 2026, guarded bytype-steps.test.ts),BREAKPOINTS,SPACING_UI,DENSITY, shadows,CHART_LIGHT,CHART_DARK,CHART_GEOMETRY,STAGE_OBSIDIAN,STAGE_PAPER, and the gate references (GATE_SURFACES,AMBER,ALERT). It is a plain module safe for tests and server code.accents.cssis generated byscripts/accents.mjs(npm run accents) fromACCENTSandAPP_IDENTITYin the registry: per slug, the base, deep and glow colours and hue on[data-app="<slug>"], the swap to the glow on dark and stage surfaces, and the swap back on a paper surface.--color-suite-accentis separate and does not change with the app (D-0036).node scripts/accents.mjs --checkexits 1 when the file is stale.brand.cssis the maker's brand layer (brand colours and fonts, brand utilities, base rules):--font-sans(Inter),--font-display(Bricolage Grotesque, the public pages),--font-serif(Source Serif 4, the workspace subject face since D-0082) and--font-mono(IBM Plex Mono). Suite code may not use its colours (below).print.css(imported by the(workspace)layout) prints A4 or Letter with 16 mm margins, sets every dark-overridden token back to its light value, hides the shell chrome and anything withdata-print="hide", and expandsdata-print-expandregions.
Icons and marks
icons.tsx(DS §4.5): 55 glyphs (ICONS) on a 24 × 24 grid, 2 px stroke incurrentColor, square caps, mitred joins, no fill;aria-hiddenunlesstitleis given. No icon library is imported; a new glyph is added here in the same grammar.mark.tsx(DS §4.2):MARKS, one per registryMarkKey, at most three paths with exactly one specimen point (a zero-length pathM x y h.01whose square cap draws a 2 × 2 px square) standing clear of the strokes.Markcan draw itself once per page load (draw), skipped under reduced motion.wordmark-layout.tsx,wordmark.tsx(DS §4.3): the lockup "{SUITE} | mark Short." inbar,heroandcardsizes; the root carriesdata-app.suite-mark.tsx: the filled Bench mark used for the suite (D-0035).education-mark.tsx: marks for the education tools.
The flask glyph also identifies the methods and checks command in the palette (D-0085).
marks.test.ts checks that every registry mark exists and is used, the path-count and specimen-point grammar, the spacing of the point, that mark.tsx and icons.tsx draw with the shared attributes, and the exact icon set.
Data display, charts and the chart palette
chart-palette.ts:SLOTS(eight categorical colours, assigned in order),NEUTRAL,COMPARISON, their dark counterparts, andchartSeries(theme). The header records that the light slots were checked with the dataviz palette validator (worst adjacent deutan ΔE 10.4, normal-vision ΔE 19.8) and that slots 2 and 4 are below 3:1 on white, so charts using them need a legend, direct labels or a data table. That check is recorded in the comment only; it is not re-run by a test. The dark set keeps each slot's OKLCH hue and chroma with lightness 0.74.tokens.test.tschecks both sets equalCHART_LIGHTandCHART_DARK. Today only Molecule's map view (src/components/tools/molecule-workbench/map-view.ts) imports this module directly; the other charts read the same values throughchartTheme().data/chart-geometry.ts:linearScale,linearTicks(steps of 1, 2, 2.5 or 5 × 10ⁿ),logTicks(whole log10 units, halves when fewer than three) and path helpers. Pure; used by Bioreactor's run chart and Assay's chart.data/data-table/(DS §6.12):DataTablepages at 25 rows and windows the body when every column isnowrapand there are more than 200 rows (ten rows of overscan);pagerAtHead(default false, so no other table moves) repeats the pager above the table, labelled "{name} pages, above the table" (Pipeline's trials, UI-27); roving row focus;<dl>cards belowsm;aria-sort,aria-selected,aria-rowcount.model.tsholds sorting (stable, missing last), filtering, paging, windowing and selection, withmodel.test.ts. Phone cards also accept opt-in column presentation:cardFullWidthgives long values reading width, andcardPairWithplaces two labelled short facts beside each other. Pairing uses only fields rendered in that row, never drops visible fields, and leaves desktop order, visibility controls and exports unchanged. Columns without these options keep their original cards.- The data grid's model,
src/lib/grid/(DS §6.34; A-0's fourth design deliverable): pure, no React. ADataGrid(v: 1) is one table of numbers in one of three shapes,columns(one column per group;pairedwhen rows are matched subjects),grouped(a row factor and column groups ofsubColumnsreplicates) andxy(an X column and Y groups with replicates). A cell is a finite number ornull;nullis blank, never zero, and never counted. Text that is not a number ("n.d.") is kept intextand reads as missing; a comma inside digits is read by evidence (classifyNumber,commaEvidence,readCellWith; the CSO's addendum of 13 October 2026, item 3): "1,234.5" and "1,234,567" are thousands, "1,5" a decimal comma, and the ambiguous "1,234" is thousands or a decimal comma only when the same paste holds an unambiguous cell of that kind (or a semicolon delimiter, or the explicit decimal-comma setting), else text ("comma is ambiguous"); conflicting evidence keeps every comma cell as text; spaces are stripped only between groups of three digits; a typed "1,234" is text without the setting; the paste statement counts each reading (readings); an excluded cell keeps its value, is listed inexcludedand is never read. Edits (setCell,clearCells,toggleExcluded,ensureRows,ensureGroups,removeRow,removeGroup, the label setters) return a new grid and never mutate. Engines read throughgroupValues,columnsOf,pairedValues(a row counts only when both cells hold values; the rest counted asdropped) andxyPoints, each returning the values with their addresses and the blank, excluded and text counts.toLongandfromLongconvert between wide and long (values beyond one per subject in a column table counted, never averaged).readGridis the storage reader (refuses a malformed grid); it andGRID_LIMITS,X_GROUP,cellKeyandparseCellKeylive inread.ts, so a record reader imports them without the rest of the model (model.tsre-exports them).paste.ts:readPastedTablereads a whole pasted table (a first row with no number as the header, and a second header row only under a merged first row, so a text first data row such as "n.d." is data; a merged header as sub-columns; row labels only from a first column with no number at all, a mostly-text first column with numbers being read as data with its text counted and the statement saying so (labelsAsData); a one-column paste of decimal commas kept as one column; long tables with a group and optional subject column, the decimal comma by the delimiter) and returns aPasteStatementof what was read;pasteBlockpastes a tab-separated block at a cell as a spreadsheet does (growing rows and groups, counting what it cannot place);copyBlockcopies a rectangle out;gridToTextwrites the whole grid back as tab-separated text thatreadPastedTablereads as the same grid (header words passed in by the caller; an optionalrowTitlekeeps the row labels' heading). Limits: 10,000 rows and 200 data columns.pzfx.tsreads the .pzfx project file's column, grouped and XY tables into grids (see the Assay page, Import). Tests:grid.test.ts,pzfx.test.ts. data/data-grid/(DS §6.34; the CPO's memo of 13 October 2026):DataGrid, the editable grid oversrc/lib/grid/, loaded on demand by its consumer. Fromsm, arole="grid"table (aria-rowcount,aria-colcount,aria-multiselectable), one tab stop with a roving active cell (data-grid-active), sticky group and sub-column headers and row labels, fixed column widths (table-fixed, so an edit never widens a column), in-place editing (Enter, F2 or a printable key; Enter, Tab and their Shift forms save and move; Escape cancels), blocks with Shift, Delete to blank,Xto exclude or include (when the consumer allows exclusions), and the nativecopyandpasteevents for tab-separated blocks (copyBlock,pasteBlock). Belowsm, one column at a time: a column select with previous and next keys and a list of labelledinputMode="decimal"inputs, each with an Exclude toggle. Each cell state is a visible marker word and a state word in the cell'saria-label("1,234, text, comma is ambiguous, read as missing"; "blank"); the grid writes toannounce()only for what an act did. Rows beyondRENDER_ROWS(500) are not rendered and the statement says so.keys.tsis the keyboard without React (keyAction,clampPos,blockOf,afterEdit), tested bykeys.test.ts(including thatxandXnever start an edit).gridTextwrites a grid as text with the grid's own header words. Words:content/apps/data-grid-ui.ts(anddata-grid-view.tsfor a consumer's Text and Table switch).pairing={false}hides the columns shape's "Rows are matched" for a consumer with no pairing (Counts and the false discovery rate; the CPO's design-check ruling, 14 October 2026).KpiTileandkpiGrid(DS §6.18): loading, ready, stale and n/a states; count-up and flash only for a person's own change.PercentileBar(DS §6.20 D-04): a staticrole="img"picture with a hidden<dl>, never a range input.ProvenanceChip(kindchip,lineormethod) andProvenanceMark(DS rule 5, §6.30): sources resolve through the app's registry entry viauseAppEntry, so an unknown source id throws.ScrollBox(sideways scroll with edge fades and a focusable region),ExampleChip(published, synthetic or run examples, info tone),freshness.ts(oldestSource),clipboard.ts(copyTextwith a fallback that keeps focus and selection).
Export writers
All writers are in data/ and implement contracts §5 (docs/design/shared-contracts.md) and DS §6.26. The Study bundle and recovery file are described in Storage and state.
Protein's recorded-image history loads its app-owned reader lazily through
recorded-result.tsx. Capture uses insertPngText before freezing and hashing
the retained PNG, embedding capture-time provenance. History exports those exact
verified PNG bytes and a separate input/result JSON receipt through the existing download writer. Its ordinary
live-view PNG export continues to use insertPngText; experimental provenance
now names the successful coordinate provider and consumed metadata sources,
including RCSB when used. A retained PNG is not re-encoded or relabelled with
current live provenance. Text-only Study projections explicitly state that
the image is omitted and its bytes are unverified in that view.
| Writer | Format and rules | Test |
|---|---|---|
csv.ts |
RFC 4180, CRLF, UTF-8 with BOM always, quoting when a field holds the separator, ", CR or LF; numbers at full precision with . and no grouping; dates ISO 8601 UTC; units in header parentheses; optional ASCII headers; OWASP formula escaping (a text cell starting with =, +, -, @, tab or CR gets a leading '; numbers never escaped). TSV too. |
csv.test.ts |
csv-bundle.ts |
buildTabularExport (decision C07): a ZIP with the CSV, <base>.provenance.json, <base>.README.txt and, for share-alike rows, <base>.CHEMBL-LICENCE.txt is primary when a row carries a notice that must travel; otherwise the plain CSV with per-row provenance columns (source, source_version, retrieved_at_utc, app_version, attribution). The other form is always offered. When the provenance block carries methods, the README has "Methods and parameters": "Method: {name}, version {version}" per method, never a file path, then "Software: {appName} {version}, build {buildHash}" (readmeTexts; the CPO's memo of 12 October 2026 on the method line, ruling 5); nothing precedes the CSV header. Figure exports carry the same methods and software in the provenance block they embed. Pure, Worker-safe. |
csv-bundle.test.ts |
zip.ts |
Store-only ZIP (no compression), UTF-8 names with flag bit 11, CRC-32, MS-DOS times; refuses ZIP64 sizes (65,535 entries, 4 GiB) and climbing names; readZip for tests and checks. |
zip.test.ts |
json-export.ts |
Data export envelope { $schema, format: "mbio-suite-export", formatVersion: 1, kind, schemaVersion, app, generatedAt, provenance, data } and project file { $schema, format: "mbio-suite-project", formatVersion: 1, suite, app, exportedAt, project, records, provenance? } with local-only key and storage fields removed. toJson writes non-finite numbers as null, -0 as 0, Maps and Sets as arrays, Dates as ISO UTC. |
export-files.test.ts |
svg.ts |
Clones the chart SVG and inlines computed presentation values as literal #rrggbb attributes with separate opacity; named font stacks (fonts not embedded); drops data-export="exclude" and <foreignObject>; provenance JSON in <metadata id="mbio-provenance">; optional caption; width capped at 16,384 px; result checked with DOMParser. |
export-files.test.ts |
png.ts |
From an SVG rasterised in an idle callback or from a canvas; scales 1, 2, 4 capped by MAX_TEXTURE_SIZE/MAX_RENDERBUFFER_SIZE and the browser's canvas area; provenance as iTXt chunks (Title, Source, Creation Time, Provenance) after IHDR. |
export-files.test.ts |
print-data.ts, print-report.tsx, print-styles.ts |
The print report (decision C01: no print route). Fixed order: title, key figures, figures, tables, notes, then an appendix that cannot be left out (methods, limits in full, regulatory position and statements, sources with version, build, browser and time). PrintReport renders it on a paper surface; while mounted it adds an @page running footer with "page n of N" (the header notes Chrome and Edge print the page numbers). printNow() waits for fonts and images, then opens the browser's print dialog. |
print-styles.test.ts |
download.ts |
exportFilename: {slug}-{subject}-{kind}-{YYYYMMDD}-{HHMM}.{ext} with the subject slugified to 40 characters and -2, -3 for repeats in the same minute; project files {slug}-{kind}-{name}-{YYYYMMDD}.{kind}.json; downloadBlob, and saveBlob using the File System Access API where present. |
|
provenance.ts |
ProvenanceBlock (schema version 1) built by buildProvenanceBlock; LicenceId; attributionLines in a fixed order (uniprot, alphafold, chembl, ctgov, openfda, fda); citationText, sourcesTable, snapshotDifferences. licenceState is always "preview" and no tier line is written while pricing is indicative. |
provenance.test.ts; chipState (a chip's state: stale past its age, "date unknown" when no source carries a date, else the first source's; a method's provenance with no source and the method's name and version is "computed", never "date unknown", the Assay closing walk's W4 of 14 October 2026; DS §6.30 "States"); chipProvenanceProblem (null when a chip's provenance names a source, or a method with its name and version; a guard test runs it over every Assay method record), re-exported by provenance-chip.tsx |
The figure core (src/lib/figure/, Stage A). A second SVG path, built for figures at a physical size rather than copied from the page: layoutFigure lays a FigureDocument out as a scene in millimetres and sceneToSvg writes it as a string with no DOM (width="85mm", viewBox in millimetres, <metadata id="mbio-provenance">, one <g id="series-<id>"> per series with points, bars, curve, interval and marker groups, real <text> in the FONT_STACKS.sans stack, literal colours resolved from roles by the caller, no CSS or <foreignObject>, a white rectangle first when the background is white). Group names that are not XML names are escaped (svgId). Tested in src/lib/figure/figure.test.ts. No Export menu uses it yet; svg.ts stays for the other apps. tiff.ts writes baseline TIFF 6.0 (little-endian, uncompressed, RGB composed on white or RGBA with ExtraSamples 2 when transparent, the resolution in dots per inch, the provenance JSON in ImageDescription with non-ASCII escaped) from RGBA strips the caller draws (at most 1 Mpx each), returned as parts for a Blob, refused at 100 MB (TiffTooLarge); pixelSize gives px = round(mm × dpi / 25.4). Tested against pipeline/assay-analyser/fixtures/figure/tiff-expected.json in src/lib/figure/tiff.test.ts. pdf.ts writes one page of PDF 1.4 from a scene (sceneToPdf): the MediaBox in points, one content stream deflated by CompressionStream("deflate") (zlib format; uncompressed where the API is absent), Helvetica and Helvetica-Bold in WinAnsiEncoding and Symbol for Greek, none embedded, any other character written as a documented ASCII form (PDF_SUBSTITUTES, else "?") and returned in substitutions, Info (Title, Subject, Creator, Producer, CreationDate, the source line as /Source and the provenance block in full as /Provenance, as PNG carries them; the closing walk's W1) and the provenance as an embedded provenance.json. helveticaMeasure measures text with the AFM widths in afm-widths.ts so a figure laid out for PDF anchors its text exactly. Tested against pdf-expected.json in src/lib/figure/pdf.test.ts.
The app print report uses the browser's "Save as PDF" under print.css.
Study reports additionally have a direct PDF download in Manage Study:
workspace/study-report-pdf.ts lays out the same immutable report document as
the readable HTML action, and figure/pdf.ts provides scenesToPdf alongside
the compatible one-page sceneToPdf. Pages share the standard fonts and
embedded provenance receipt, with individual content streams and one page tree.
AFM-measured line wrapping splits long identifiers and URLs, table rows can
continue onto another page, figures stay together, and every page has a number.
The renderer refuses more than 1,000 pages rather than truncating content.
Unsupported visible characters are listed in a final section and announced after
the download. The content-owned formatter in content/apps/workspace-study-report.ts
names the source and every supplied replacement code point in order, retaining repeats:
U+2032 (prime) printed as U+0027 (apostrophe). Its bounded printable-ASCII
names cover the existing encoder substitutions and their output characters; unknown
names retain the bare code point, including U+003F (question mark) for the
encoder's fallback. It describes the existing receipt without changing the encoder,
fonts, original document or collected substitution order (REL-576). Whitespace
consumed by wrapping is still not listed. The HTML action keeps the original Unicode. Both modules are lazy, with their own budgets in
scripts/size-budgets/study-report.json. No PDF dependency or server is added.
The menu: useExports(items) (export-registry.tsx) registers a view's items with the shell's ExportProvider while mounted; host.registerExport and definition.exports() add more. ExportMenu (export-menu.tsx) orders object groups first, then Data, Figure, Report, Project, Share (orderItems), shows at most ten items (nine and "More formats…" beyond that, MENU_MAX), keeps unavailable items as aria-disabled with the reason, and opens the figure dialog for figure items (SVG or PNG, the allowed backgrounds, scales with a size estimate, Copy PNG where supported). csvExportItems() (data/csv-export-items.ts, re-exported by export-menu.tsx) gives the two CSV items of the C07 rule; an app's export list imports it from the leaf so its workspace chunk does not carry the menu (Protein's exports.ts; the CTO's memo of 21 October 2026 on REL-378, 4).
Schema versions. Every JSON export and project file carries $schema set to <suite origin>/schemas/<slug>/<kind>-v<n>.json (schemaUrl in json-export.ts, schemaPath in content/apps/routes.ts). store/schema.ts has toJsonSchema to produce such files from the validator definitions, and its header says they are published from a prebuild step. As of 10 October 2026 no prebuild step calls toJsonSchema (it is called only from schema.test.ts), there is no public/schemas/ directory and no route serves /schemas/…, so the $schema URLs in exported files do not resolve. The header also names a different path (public/apps/<slug>/schemas/…) from the one schemaPath builds.
Engine wrappers
engines/rdkit/ holds the shared RDKit.js worker used by Molecule and Assay: protocol.ts (request and response messages, error codes invalid, too-large, network, engine, crashed), client.ts (request ids, timeouts, restart after a crash, cancel or timeout, and an engine status for the status chip) and rdkit.worker.ts (loads vendor.rdkitJs and the WebAssembly on the first request; closes itself after an RDKit failure). The chemistry the worker runs is imported from Molecule's tool tree (src/components/tools/molecule-workbench/chem.ts, engine.ts, library.ts). Mol* and 3Dmol.js have no wrapper here; they are loaded by their app trees through src/lib/load-script.ts. See Engines.
Other shared folders
store/UI:ProjectChooser(DS §6.25),ShareLinkDialog(§6.27; updated 19 October 2026 for Milestone B: the canonical#s=link, "This link contains" fromdescribeState(schema, state), explicit statements that anyone with the link can open it without an account, that opening it recreates editable inputs and settings under current methods rather than a frozen historical result while local saved work/notes stay in this browser, when a portable file is needed instead (inputs over 2,000 characters, plate readings or saved records;content/apps/store-ui.ts), a secondaryDownload {fileKind} fileaction when available, Copy citation, the "Not in this link" warning banner when local data is omitted, and the long-link fallback over 2,000 characters),UnnamedDraftBanner(workspace/unnamed-draft-banner.tsx: local unnamed draft recovery and discard with undo),SaveStatus,ConflictDialog,StorageSummary, andNotesMarkdown/NotesPlain(an allowlist parser inmarkdown-allowlist.ts: paragraphs, emphasis, lists, inline code andhttp(s)links only; neverdangerouslySetInnerHTML). The data layer is in Storage and state.literature/: the Literature desk, search, paper reader, evidence table and review notebook. See Literature.marketing/: the*.marketing.tsxblocks for the landing, product, docs, regulatory and verification pages, the suite header and footer (SuiteChrome,MarketingScope) and social images. They render on the server and are exempt from the brand-colour lint rule.
The developer kit
/dev-kit and /dev-kit/workspace (src/app/(workspace)/dev-kit/) exist only in builds made with SUITE_KIT=1; otherwise both call notFound(). They are not linked, not in the sitemap and not named in robots.txt (see Deployment and environment).
/dev-kitis the primitive gallery (KitFramewith a theme switch): sections for tokens (with WCAG contrast ratios fromkit-contrast.ts), identity, motion, shell (a preview-modeAppShell), the workspace demonstration, layout, overlay, feedback, data (chips and KPIs, exports, tables, store UI), form, store and marketing. Each section is insections/<area>.tsx./dev-kit/workspacerendersDemoWorkspace: a demonstration app in a full-modeAppShellborrowing Pipeline's registry entry, with astoragekind that has versions, asessionserialiser, exports from the definition,useExportsandhost.registerExport, a share spec with a URL schema, a consent-gated request and an in-place print report. Its projects are kept under the namespacedev-kit-demo. This is the only place the shell's local project-saving path runs.
The design system's §11.6 step 3 still describes a /apps/_kit route guarded by NODE_ENV === "production"; the code moved to /dev-kit and the SUITE_KIT guard.
Rules enforced by tests and lint
| Rule | Where it is enforced |
|---|---|
Suite code uses semantic colour tokens, never brand colour utilities (bg-white, text-ink, border-hairline, …) or raw hex |
ESLint suite/no-brand-colour in eslint.config.mjs (matcher in scripts/lib/brand-colour.mjs) for src/components/suite/** and src/apps/**; exempt: *.marketing.tsx, tokens.ts, chart-palette.ts, tests, and in recipes.ts the site* declarations and the two buttonDanger hover hexes #8a2d19 and #ffb3b3. recipes.test.ts applies the same check to the recipes. |
| Every recipe is a whole literal; no recipe transforms case or uses wide tracking meant for capitals | recipes.test.ts |
The site* strings equal the tools' pre-move strings and the four tools modules re-export them under their old names |
recipes.test.ts |
No uppercase in any class string in src (table headers included) and no text-transform: uppercase in stylesheets outside the showreel |
no-uppercase-labels.test.ts (principle 7: transforming µL reads "ML" and mangles formulae and gene names) |
tokens.css and tokens.ts agree (@theme static, :root, density, shell variables, dark and stage block, paper block, reduced-motion twin) |
tokens.test.ts |
| Contrast gates: §3.2 semantic pairs in both themes, primary actions and input boundaries, skeleton bars, §5.7 status colours, §3.4 chart and viewer colours, §4.1 accent triples, hue separation and sharing, the chart focus ring's two tones at 3:1 | tokens.test.ts |
accents.css is up to date with the registry |
tokens.test.ts (and node scripts/accents.mjs --check) |
Every app palettes.ts dark colour reaches 4.5:1 on the dark panel |
tokens.test.ts |
print.css resets every dark-overridden token to its light value |
tokens.test.ts |
The Tailwind build emits what the token layer relies on (variable-reading utilities, @theme static, dark: and stage: variants, arbitrary-variable utilities, opacity-modifier shape, both reduced-motion collapses, the brand layer) |
npm run tailwind-check (scripts/tailwind-check.mjs), tested under npm test by scripts/tailwind-check.test.mjs |
| Mark and icon grammar | marks.test.ts |
| Inspector docking rule | shell/inspector-dock.test.ts |
| Shortcut grammar, reserved keys, matching and firing | a11y/shortcuts/registry.test.ts |
| Announcer coalescing, dedupe, progress cadence, queue and carry-over | a11y/announce.test.ts |
| Pure rules of overlays, layout, feedback, forms, jobs, last address, page find, theme and settings | overlay/*.test.ts, layout/*.test.ts, feedback/*.test.ts, form/*.test.ts, shell/*.test.ts, theme/*.test.ts |
| Export formats and provenance | data/*.test.ts |
Product names only in content/app-names.ts; copy rules |
content/apps.test.ts, see Registry and content |
| Client code does not import the full registry | ESLint no-restricted-imports, see Registry and content |
Browser checks of the shell are in e2e/smoke/ (for example shell-a11y.spec.ts: focus return from menus and dialogs, Tab containment in the share dialog, saving flows in the dev kit, reduced motion across the shell's layers; suite-theme.spec.ts, settings.spec.ts, popup-close.spec.ts, page-find.spec.ts, app-navigation.spec.ts). See Build, test and CI. The five guard tests above (tokens, recipes, no-uppercase-labels, marks, inspector-dock) passed, 40 of 40, when run with node --test on 10 October 2026.
Size budgets: scripts/size-budgets/suite.json budgets the Bench and object-page routes (225 KB each), the walk chunk (10 KB) and public routes; each app's file budgets its definition and workspace chunks on the shared /[app]/app route. The separate shell-core, dialog-group, DataTable, motion and Floating UI budgets named in DS §7.3 and §11.7 have no entries of their own.
Building a new panel or app view
The steps below follow the Bioreactor app (src/apps/bioreactor-simulator/), which uses most of the shell's surfaces without an inspector, with notes from Calc (src/apps/lab-calculators/) for an inspector. Product names and all words go in content/; nothing below is typed in a component.
1. Put the words in content
Add the view's copy to the app's content module (Bioreactor: content/apps/bioreactor-simulator.workspace.ts, exporting workspaceCopy and VIEW_IDS). Headings, button labels, announcements, empty states and command labels all come from there.
2. Give the view an address
If the view or its inputs should survive a reload or travel in a link, add keys to the app's UrlSchema (Bioreactor: bioreactorSchema in src/components/tools/bioreactor-simulator/address.ts, with view: { label, check: checks.oneOf(VIEW_IDS), default }). Read and write through useUrlState(schema), which returns { state, set, ready, … }; set pushes history for a subject change and replaces otherwise. Never put private inputs in the query string; see Storage and state. A key that a hand-off may carry must also be in HANDOFF_PARAMS (content/apps/params.ts).
3. Compose the view inside the workspace
Bioreactor's BioreactorWorkspace (views/bioreactor-workspace.tsx) shows the pattern:
const host = useHost(); // ids, announce, setSubject, objects
const url = useUrlState(bioreactorSchema); // address state
<SubjectHeader heading={<h1 className={subjectHeading}>…</h1>}
actions={<WorkCaptureControls …>{<ObjectActions …/> or <MoreActions …/>}</WorkCaptureControls>}
modes={<Field><Select …/></Field> …} />
The shell renders `<LimitsBanner>` at the bottom of main from the registry entry (`app-shell.tsx`); apps do not render it.
<ViewTabs id={host.id("bioreactor-tabs")} panelId={host.id("bioreactor-view-panel")} value={view} items={…} onSelect={setView} />
<div id={host.id("bioreactor-view-panel")} role="tabpanel" aria-labelledby={viewTabId(…)} tabIndex={0}>
<Panel titleId={host.id("run-heading")} kicker={…} title={copy.run.heading}> … </Panel>
</div>
Rules this follows:
- One subject heading (
h1, orh2whenpreviewis true) withsubjectHeading; the shell's subject is set separately withhost.setSubject?.(…)in an effect. - Every id goes through
host.id(), so the view works inside a product page's preview. - Panels use
Panelwith atitleId; its heading takes focus, sofocusHeading(host.id("run-heading"), { scroll: true })can move focus after a view change. - Primary views are
ViewTabsin the workspace (principle 3); Tools & sections must not repeat them. Bioreactor and Calc therefore passrail: []in the definition and set their few secondary sections from the workspace withuseRail(memoisedSections); Bioreactor's are its presets. - Wrap anything that can crash (a viewer, a worker-backed chart) so the page survives:
MainisToolBoundaryaround anext/dynamicimport with aSkeletonfallback (src/apps/bioreactor-simulator/workspace.tsx). - Use the primitives and recipes (
Field,NumberField,Select,Switch,Banner,EmptyState,buttonPrimary,buttonSmall,metaLine) rather than new class strings, and only semantic tokens. Charts readuseChartTheme()orchartTheme()at draw time and usechart-geometry.ts.
4. Announce, do not add live regions
Send state changes through host.announce(text) (or announce()), once per completed result, not per keystroke. The announcer (src/components/suite/a11y/announce.ts) coalesces within 400 ms, but a progress message never displaces a waiting status: the status is queued at once, ahead of the count (DS §9.2, REL-246). Bioreactor announces once per completed run, guarded by a ref holding the last announced result key. Visible status lines use InlineStatus or LoadingStatus, which are not live regions themselves.
5. Register exports and provenance
Build the view's ExportItem[] in a useMemo and pass it to useExports(items) (an empty list in preview). Bioreactor's exportItems(…) in src/apps/bioreactor-simulator/exports.ts produces the run and pump-sheet CSVs, the chart as SVG or PNG, the printed bench sheet, the method paragraph and (from the call site) the scenario JSON; each carries a ProvenanceBlock from the app's provenance.ts, and an item the view cannot provide sets disabledReason. A print report is a PrintReport rendered in place and printNow().
6. Expose actions to the shell
Commands and shortcuts are defined in definition.ts, outside the workspace tree. Bioreactor publishes its current view and actions to src/apps/bioreactor-simulator/store.ts (publishBioreactor in an effect, null on unmount), and the definition's commands call readBioreactor()?.actions.setView(id, { focus: true }) with when: () => readBioreactor() !== null. Shortcuts use keys 1 to 5 in group "Navigation"; never a reserved key. Commands registered only while a component is mounted can use useCommands(list) instead.
7. Add an inspector if the app needs one
For fixed content, set inspector: { title, kicker, Content } in the definition with Content loaded through next/dynamic (Calc: MethodInspector from ./inspector), and leave the inspector hidden by default when it should wait for an explicit pick (D-0060). Calc and Sequence explicitly opt into { defaultState: "docked", startHidden: true } for method content when the selected workbench has inputs; their blank starts and floating layouts remain hidden until explicitly opened. For "what is picked", set inspector: "selection" and call shell.inspector.open(node, { title, focus: true }) or host.openInspector on an explicit Inspect. Give dockMinMain when the main view needs more than 40rem.
8. Saving, sharing and objects
- Saved work and recorded results go through
WorkCaptureControlswith the app's payload schema and acaptureRecordedclosure; see Storage and state. - Share: set
share.schemaso the share dialog lists exactly what the link carries, andshare.link/contentswhere the link differs from the address. If the view holds something a link cannot carry, givenotInLink,fileKindandonShareFiletogether. - Objects: use
ObjectActionsfor an object on screen,useLearnto refresh facts, and add a type toobjectTabsandsrc/apps/<slug>/objects.tsTAB_TYPES(and the registry'sobjects.tabs) for an object-page tab.src/apps/objects.test.tschecks the three agree.
9. Check it
Run the commands in CLAUDE.md: npm run lint (brand colours, registry imports), npm test (tokens, recipes, uppercase, registry and the app's own tests), npx tsc --noEmit, npm run build and npm run test:e2e. Check the view at 375 px, with keyboard only, in both themes and with reduced motion (DS §11.8), and in the dev kit if you added a primitive.
Known gaps and divergences
These are recorded for the Known issues page.
- Exported
$schemaURLs do not resolve.json-export.tswrites/schemas/<slug>/<kind>-v<n>.jsonURLs, but no build step generates the files andpublic/schemas/does not exist;store/schema.ts(lines 112 to 115) describes a prebuild that does not exist and a different path. - The shell's local project saving is unused by the apps. No app definition sets
storageorsession; the autosave, ProjectChooser, SaveStatus, ConflictDialog and ⌘S save paths are exercised only by the dev kit.pages,TopBarActions,paperMain,palettesandonSaveare also unused, and every app'sPAGE_IDSis empty. - Shared code depends on app trees.
engines/rdkit/rdkit.worker.ts(lines 23 to 25) imports Molecule'schem.ts,engine.tsandlibrary.ts, andengines/rdkit/protocol.tsandclient.tsimport Molecule's result types;objects/study-export.ts(line 32) imports@/apps/assay-analyser/example-export. A change in those app modules can break the shared engine or Study export. - Stale breakpoint comments.
shell/use-shell-layout.ts(lines 11 and 14) andtokens.ts(line 84) describexlas 1,280 px andlgas 1,024 to 1,279 px; the code uses70.4remand88rem(1,126.4 and 1,408 CSS px). The same header (line 12) says the rail choice is kept insuite:<slug>:rail; in practice only the preview uses per-app layout and it persists nothing, while full workspaces usesuite:suite:rail. - Stale route paths in comments.
src/apps/page-ids.ts(line 3) namessrc/app/(apps)/apps/[slug]/app/[page]/page.tsx, which no longer exists; the route issrc/app/(workspace)/[app]/app/[page]/. - Copy in definitions.
src/apps/lab-calculators/definition.ts(line 78) andsrc/apps/sequence-toolkit/definition.ts(line 89) typeshare.contentssentences inline, against the rule that copy lives incontent/. - Design system body out of date in places. DS §6.5's status-bar recipe and several §6 recipes still contain
uppercaseand wide tracking, which the code andno-uppercase-labels.test.tsforbid (principle 7). DS §11.6 and §11.7 still describe/apps/_kitwith aNODE_ENVguard, and §11.7 lists atiers.test.tsthat does not exist in the repository. D-0036's "panel headings use an accent rule and divider" is superseded by principle 5;Panelfollows the principle. - No separate shell size budgets. The shell-core, dialog-group, DataTable, motion and Floating UI budgets of DS §7.3 and §11.7 are not entries in
scripts/size-budgets/. - Education marks are outside the mark grammar test.
education-mark.tsxdraws withstrokeWidth={1.8}, and some of its marks have no specimen point or two points in one path;marks.test.tschecks onlymark.tsxandicons.tsx. - The chart palette's validation is recorded, not repeated. The colour-vision figures in
chart-palette.tscome from an external validator run; no test recomputes them.
Bench and Study dashboard composition (11 October 2026)
The CEO authorised a bounded dashboard pass using existing storage. Bench leads with a compact full-width selected Study summary, followed by a full-width three-item, Study-prioritised resume queue. Studies read as research questions; pinned records occupy a separate shelf. Study cards show actual updated dates rather than object/view counts that omit owned work. The guarded Bench note stays mounted below those sections; workflow guidance, full library and tool shelf remain reachable. Empty areas use short explanations without seeding records or repeating starter cards. Study context precedes Resume and notes at every width. Studies and pinned records each use the full width; comparable entries share responsive card grids. Two pins appear initially with an explicit expansion control. This corrects the unequal paired sections that left large empty columns throughout the page.
Bench rows show concise saved times and short Resume/Results actions with full accessible names. Work details retains mounted note editors and secondary facts. Recorded outcomes have their status and label in the disclosure summary, with numbers, original timestamps and qualifications together inside. Changed-input, loading and unavailable warnings stay visible. Dirty or saving notes reveal their enclosing disclosures when an action is blocked; filtering or collapsing the library first flushes pending work, and live reordering retains edited rows.
Study keeps its five sections and mounted editors. Overview distinguishes the research brief, planning checklist and recent saved inputs. Management is a compact disclosure with feedback visible when closed. Materials explains linked records; Evidence explains literature findings and links to Work for explicitly recorded results. Resume still uses existing newer-input consent and pending-save barriers; result history and qualifications remain separate from editable inputs.
The object library labels saved-input owners separately from explicit record membership. Both layouts and Study/name filters use the same read-only projection; all manifests per input reference are retained. Unresolved owner IDs are not unassigned, archived owners remain resolvable, and unavailable/failed reads cannot establish negative ownership. The compatibility Resume map chooses the most recent updated manifest with a deterministic ID tie-break. Multiple manifests have a visible explanation of that Resume choice. No scientific engine is loaded to compute dashboard summaries, and no storage records are created or moved by these presentation relations. See the 11 October coordinator CPO §6 and CTO continuity memos for the acceptance contract and scope.
The Bench (13 October 2026, D-0083 and D-0084)
The CEO's Bench screenshot of 13 October 2026 is applied on D-0082's shell (object-list.tsx, workspace/bench-*.tsx, bench.module.css; D-0084 item 4, which decides where the morning's mock-up, D-0083, and the screenshot differ). The page (as changed by the CEO's feedback of 14 October 2026): the serif greeting behind the accent bar ("Good morning", "Good afternoon" or "Good evening" by the browser's hour, before noon, before six and after, with the name, benchText.greetingName, a placeholder "Drybench scientist" until accounts carry one; BenchGreeting renders the standing title "Your research, connected." on the server and swaps in the greeting once the page runs, so the hour never mismatches hydration) with the lede "Explore. Analyse. Record. All in one dry-lab workspace."; at the right, Import (the Study recovery controls at #recovery) and New study (the page's primary key). The hero card and the working Study's brief beside the headline are gone: Recent studies, most recent first, carry the Studies. Then the notices; then two columns from a 58 rem main container (the left minmax(0, 1fr), a column container for its own grids, the right 21 rem), with the guide and Recovery and imports full width below; one column under that width and on phones. There is no section tab row: the full lists are views of the same page named by the hash (below). Section and panel titles are the serif face at 1.25rem (panelHeadingDense).
- Counts (
BenchStats, heading the left column with Recent activity beside them at the top of the right, the Bench's own<dl>markup rather thanReadoutStrip, whose tile sits at the top right with the label above the number; each card here holds the glyph in a tinted tile at the left and the number above its label): Studies (not archived; "n archived" as the detail), Pinned objects (the number in the working Study as the detail), Saved results (useResultCount; the newest result's app and time as the detail) and Saved work (every live manifest fromuseSavedWorkState; "n with results" fromlatestResultId). A count the store has not answered yet says "Counting…", never a dash (D-0084 item 5); when all four are a confirmed zero the strip is not drawn (D-0086). Each card's tile is coloured by an accent key throughdata-accent. - Recent studies lead the left column (the CEO, 14 October 2026). The saved-work queue, Resume saved work (
WorkItemsPanel compact bench, the task cards in a row, quiet Resume keys and Search all saved work), is no longer on the overview: it heads the#objectsview above the library, hidden there once the manifests are known to be none. Recent saved work stays in Recent activity. - Recent studies (
StudyCards; four, then "View all →" opens#studies): each card carries a cover across its top (RecordCover,objects/record-preview.tsx), drawn from the Study's own records and never fetched: the 2D depiction of its first compound member with a saved structure (the RDKit drawing the record preview makes, through the Bench's one lazy worker lease, started when the first such cover mounts), else the plate grid of its first assay member with a saved dose-response layout (PlateGrid, roles and exclusions only), else a generated figure in the owning app's accent seeded by the Study's id (studyCoverRecordchooses;Figuredraws): a protein ribbon-like curve for a target, a strand for a sequence, a circle for a construct, a drawn molecule otherwise. The figure stands until a depiction arrives and when drawing fails. A "Pinned" chip sits on the cover of a pinned Study (StudyData.pinned, set bysetStudyPinned). Beneath: the Study's number (studyNumbers: its place by start date among the account's Studies, display only), the ⋯ menu (Open Study, Work in this Study, Pin to the top or Unpin, Open notes, Export study, Archive), the name, "n objects · n saved work", a "Current Study" chip and the person's status chip (StudyStatusChip), the contributing apps' marks and "Edited x ago". Most recently opened or changed first (prioritiseBenchStudieswithout a working Study: archived ones after the rest, examples after real Studies), the CEO's feedback of 14 October 2026. Four to a row where the column is 37 rem or wider, fewer beneath; the Start a new study tile ends the row. Empty: D-0039's starter (BenchStarter) with the walk offer, never a seeded record (left-aligned, the walk's link on the primary key's row and its source line beneath in small text, the CEO's feedback of 14 October 2026); the quiet "Load example Study" key stays in the section's head. - Your tools: the bench apps as cards, then Literature (to
/literature, the text glyph, "Papers & evidence") in Cell's place (the CEO, 14 October 2026; Cell stays under Learn), with the mark in a tinted tile, the short name, one line and an arrow, five to a row where the column is 47 rem or wider (four from 37 rem), then a quiet quote card on a tinted ground filling the last row ("A faster path from data to discovery." with the suite's name, frombenchText); "Browse tools" goes to the landing page's directory. - Pinned objects (
PinFilter,PinnedTable,PinnedMenu; between Recent studies and Your tools since the CEO's feedback of 14 October 2026): a panel whose head holds the title, type chips with counts, "View all →" (to#pinned) and a ⋯ to the library; the table is the sharedRecordTable(bench-pins.tsx): the mark and name, the type word with its mark in the app's colour (saved work as "Saved inputs · App", D-0045), the Studies (the owning Study and the Studies that include it, each a link whose visible text is the Study's name and whose accessible name is the relation sentence, "Work saved in X" or "Included as a record in X"; the unknown-read words as the library's), the record's own note or its tags (muted, truncated), when it changed, and the row's keys (Resume for saved work;ObjectActions variant="row": Pin and the ⋯ holding Send to, Add to Study and Open its page, as the record's page offers them). Five rows; the table scrolls inside its own labelled region. Empty: the placeholder sentence with "Choose a tool" (BenchPlaceholder, dashed), never a header band without rows. - Right column: Recent activity first, then Quick start (
Tabs kind="segment": Templates opens the New study dialog or an app; Example data lists the example Study, the guided walk and each app's labelled example fromcontent/apps/app-examples.generated.ts; Import points at#recovery, whichManagementDisclosureopens by itsid, and at the apps that read files); Recent activity isActivityPanel( results, numbers, saved work, objects added to a Study, versions saved and Studies changed, each as a verb line with the subject and "x ago" under a tinted tile joined by a hairline; five rows, "See all →" in the head and "View all activity" beneath open#activity; the full sentence stays the link's name for screen readers and the specs; empty, the sentence with "Open a tool"), the example card, the Bench note (BenchNote compact, the panel's card chrome and serif title; without a working Study, a "Bench note" panel holding the sentence and "Create a Study for your notes", which opens the New study dialog) and Continue where you left off. - Views of the page (
viewOf):#studieslists every Study as cards (with Include archived),#pinnedthe pinned records in the library's shape,#activitythe whole timeline and#objects(or an object type,#target) the saved-work queue and the saved-object library; each replaces the overview under a "Back to the Bench" link (BenchBack, to#bench, the page's root) and keeps the headline and the keys; the hero and the counts belong to the overview. A view opened from a link comes into view with focus on its heading. The library loads on demand and moves focus to its own heading when it mounts only if focus has not moved since the view was asked for, or has fallen to the page (focusOnShow, a guard the Bench passes; REL-237): a key pressed in the rail while the module loads stays in the rail. The "Find DNA fragment" search sits with the library in the#objectsview. The count cards open#studies,#pinned,#activityand#objects(the sidebar's rows for them were removed on 14 October 2026); without read access the overview shows the library gate under#studiesand#objectswhatever the hash, and Recover browser work opens#objects. - Relative times (
relativeParts,ago) say "x minutes/hours/days ago" for a week and the date beyond it, with the absolute time in the<time>title; they render after the stores load, never on the server.
23 October Bench hierarchy refinement. The CEO removed the overview's counts strip and kept the suite's 110% desktop scale while tightening Bench content. When a browser-local recent location exists, Continue heads the main column: one compact featured destination, two smaller visible destinations and a disclosure for the rest. Its app and relative time stay visible, and the absolute time is in the <time> title. Recent studies follows; one pinned Study spans the main column and the other Cover cards use two columns from a 34 rem column width. The #studies view uses the same two-column card grid. Cards keep their real objective and object/saved-work counts, with an explicit Open Study key in the footer. No result count is inferred from a work manifest's latestResultId.
Study cards share cover and body tracks within each grid row, so a wrapping title aligns the neighbouring body without clipping either title. All titles use the same treatment, including the pinned card. Small cards reserve one objective line when none has been written; status/app marks and the footer form one bottom-aligned group, retaining the compact status-row height even without badges. The desktop minimum remains 19 rem and the pinned card spans the width. Include archived sits below the cards in both Recent studies and All Studies, using the existing checkbox and filter state.
Recent activity keeps five chronological events in the overview and now displays each event's full verb-and-subject sentence; the quieter second line gives Study context where it is known, app and relative time. #activity groups the same events by local day. Quick start keeps Start, Example data and Import as separate modes: Start shows three task rows plus Browse tools; Example data shows its Study and walk entries, two app examples and a disclosure for the remaining app examples. These changes alter presentation only, without changing saved data, destinations or scientific values. The CDO, CPO and CSO rulings are filed in the 23 October Bench hierarchy memos.
The generated figures (workspace/bench-figures.ts, pure, with bench-figures.test.ts; drawn by bench-figure.tsx): a drawn molecule (benzene-like or five-membered rings at one bond length, joined from their facing atoms by short zig-zag chains, one outward substituent each, so no bond crosses a ring; the CEO's feedback of 14 October 2026), a protein ribbon with a helical stretch, a double strand with rungs, and a plasmid circle with feature arcs, each from a seeded generator (FNV-1a over the seed, then mulberry32) so the same seed gives the same picture everywhere. Decoration only: aria-hidden, in the current accent, never a fetched image and never a depiction of a real record.
The saved-object library (objects/object-library.tsx, the redesign atlas's frames 174 and 175): a view of the Bench (#objects; #pinned for the pinned records) with the serif title, a labelled List/Cards segmented control with icons and words (List is the default; suite:library:layout remembers a choice) and the filters (Search, Type, Study, Sort) beside it; List is RecordTable's rows, Cards the rich RecordCard; 24 per page with Show more, which moves focus to the first new row's name link (or card's name). The library states stand: Study filters wait for membership information, unknown membership is not "unassigned", a read failure has Retry, no matches has Clear filters (solid), and no objects at all has the sentence with "Choose a tool" (dashed).
The saved-object record page (objects/object-page.tsx, the atlas's frame 176): the header (the type or owner eyebrow, the serif name, the ref and tags) with the record's cover at its right where the preview model has one (the compound's depiction through its own RecordPreviewProvider, the plate grid, the protein figure); then two columns from 58 rem: the record's tabs and content at the left (the type's tab registry: Overview, the apps' tabs, Literature and Evidence where the type has them, Mine with notes, tags, numbers and files, Used in), and "Object actions" in a panel at the right: Resume saved work (saved inputs), Pin, Send to and Add to Study (ObjectActions variant="bar", each only where the type supports it), and the Studies the record is in (its in-study links) or the sentence that it is in none.
The example Study (workspace/example-study.ts, copy in content/apps/example-study.ts): the "Load example Study" key in Recent studies' head and the Quick start's Example data row create a populated Study through the store's ordinary verbs under a run's write context (objects/write-context.ts, walk example-study@1), so the Study, its two records (EGFR, UniProt P00533; erlotinib, ChEMBL CHEMBL553, both pinned and carrying no facts until the apps retrieve them), their links and its Experiment all carry the run's mark and "Remove the example" takes them away in one action. The Study gets a worked objective, next action, notes, two next steps with due dates, the status "In progress" and the pin; no number, result or fact is written. Its banner is its own (ExampleStudyBanner chooses by walk). Never auto-loaded; a quiet key, never the page's primary. "Remove the example", from a record's or the Study's page, leaves for the library view (/o#objects), where what is left is listed.
The Study dashboard (13 and 14 October 2026, D-0083 and D-0084)
The CEO's Study mock-up (D-0083) and then the CEO's Study screenshot (D-0084, the redesign atlas round) are applied in objects/study-page.tsx with workspace/study-*.tsx, objects/study-material-card.tsx, objects/study-material-figure.tsx, objects/study-depiction.tsx, objects/study-figure.ts and study.module.css. The page: the serif name behind the accent bar with one meta line in one text node (the Study's number, "On this device" behind a lock glyph, when it started behind a clock glyph, the counts of saved tasks and recorded results, and the save state as "Saved x ago", divided by hairlines that read as " · "); at the right, in one row as drawn, the working-Study toggle, the status chip (StudyStatusMenu: the person's own word from StudyData.status, chosen from a menu, "Set status" until chosen), the primary key (StudyTaskKey: the linked next task's label, resumed with the consent for changed inputs; "Choose your next task" opens the brief's editor at its task select; Retry when the tasks could not be read) then a cog key, Manage Study (the CEO's feedback of 14 October 2026: ManagementDisclosure headless, so the operations have no summary row of their own; the cog toggles them as a titled card under the notices, aria-expanded, and its heading takes the focus), the primary key and a ⋯ holding Manage Study (opens the same card), Rename, Save version, Export study, Archive and Delete. The notices and the example banner follow, then the Manage Study card while it is open.
Under them a grid with an 18 rem right rail from 58 rem (21 rem under D-0083; narrowed so three material cards sit in a row as drawn): the main column holds the research brief as a callout (StudyBrief: a target tile, the "Study objective" eyebrow, the objective in serifLead, the next action and the linked task beneath, Edit at the right opening the same editor; the region keeps its name and the editor its labels), the progress line (StudyProgress: Question, Materials, Methods, Results and Conclusion, each done when the Study holds an objective, an object, saved work, a recorded result or a conclusion, the first not done current; each line the fact behind it, never a claimed stage) and the five section tabs.
- Overview shows Linked materials (three materials; the action placeholder with "Link a record" when there are none), Evidence beside Notes, and Recent saved work.
- Cards or List (the CEO, 14 October 2026): a
SegmentedView control, Cards or List, sits in the head of Linked materials and of the Materials tab's Linked records panel whenever the Study has materials. Cards (StudyMaterialCard, below) is the default; List (StudyMaterialRow,study.module.css.materialRows) shows each material as one ruled row: the app's tile, the name linking to the record, the type word and the samematerialFactson one line, when and from where it was added, the member's note, up to two app tabs from 640 px and the ⋯ with Remove. The choice is a presentation preference for this browser, kept inlocalStorageundersuite:study:materials-layout(objects/library-view.ts, the same store shape as the library'ssuite:library:layout), shared by both places and across tabs, held in memory when the write is refused, and never part of a research export. - A material card: the app's mark in a tinted tile, the type word and a ⋯ with Remove; the record's name in the serif face; the record's thumbnail at the left beside its facts in two columns; Open record and up to two app tabs beneath. The facts (
materialFacts) are the record's own, at most four in the drawing's order: up to three saved facts (libraryFacts,formatFactValue, each still carrying its source), then the record's public identifier unless a fact holds the same value, then a saved plate layout's format, plates and dataset and a file's name; when fewer than two remain, when and from where the record was added fills the list; never a dash. The thumbnail (StudyMaterialFigure) is the record's own picture where one is real and a design where none is: a compound's RDKit depiction drawn from its saved SMILES when the card comes into view, through one client for the page (StudyDepictionProvider, the same lazyrecord-depiction-clientchunk and budget as the Bench's previews; restarted when the page or the account scope goes); an assay's saved plate layout as a grid of well roles (roles only, never readings); for a protein, a sequence or a plasmid a flat figure in the app's accent seeded by the record's identifier (study-figure.ts: a cartoon-like backbone with a helix and a strand arrow over faint surface blobs, two strands with rungs, a circle with feature arcs; the same record always draws the same figure),aria-hiddenand titled as a design, never a fetched image; the type's glyph or the app's mark otherwise. - Evidence (a panel): "+ Add data" is a menu of the paths that already add data to a Study (each app that records results, to open it, and "Link a record") and a ⋯ with View all results and Literature evidence; line tabs Results and Literature with their counts (the recorded results; the Study's evidence notes and its papers without one, the evidence table's rows); the Results tab is
StudyResultsTable(the first seven ofuseStudyWorkspace's results with their app, saved task, time, reporting status and whether the conclusion cites them, "Not cited" in words, each opening the recorded result; a header band and hairline rows), or the action placeholder with the same Add data menu when there are none, or the failed read with Retry; Literature is the existing evidence table. - Notes (
StudyNotesPanel): tabs Research notes and Meeting notes. Research notes is the editor (StudyNotesEditor): an editing surface (role="textbox",aria-multiline,aria-readonlywithout write access) showing the notes as rich text and storing them as Markdown inStudyData.description(workspace/study-notes-markdown.ts: the notes allowlist's dialect plus##/###headings, underline stored as_emphasis_since the allowlist admits no underline, drawn underlined in the editor and read as emphasis elsewhere, and links to the suite's own/o/…and/study/…pages beside http(s) addresses), so the Bench note, exports, the Study report packet and search keep reading the field as plain text. The surface is built from the parsed blocks with DOM calls and serialised back on every change; pasted content arrives as plain text, so no image or foreign markup is stored; typed markers are escaped so a plain paragraph is stored exactly as typed. The toolbar: a Text style select (Normal text, Heading, Subheading), Bold, Italic, Underline, bullet and numbered lists and Link (an http(s) address), each labelled witharia-pressedfor the states, arrow keys moving along it; an image key is left out because a note has no real image to hold. The footer: the real saved time ("Saved x ago", "Saving…", "Not saved yet" or the failure) with the character count from 90 % of the 20,000-character limit (an insert past it is refused, a paste cut to fit), "@ Mention" (a menu of the Study's records that inserts a link to the chosen record's page) and Save (flushes the autosave now). Autosave, draft protection, the revision check and the "changed in another tab" states are the page's, unchanged. Meeting notes lists the Study's frozen meeting outputs; Prepare meeting view stays in the rail, at its foot, so it is on every tab and the rail's title row stays as drawn. One editor instance moves between Overview and the Notes tab, where it is full width. - Work, Materials and Evidence keep their contents (Materials: all cards by type, then Saved app views; Evidence: the literature table, the Assay panels across the Study's assays and the Study report).
- The rail (
StudyContextPanel): Provenance (the Study's start, objects added, work saved, results recorded, versions and the change history's edits, newest first, six then Show all; "View graph" opensStudyGraphDialog, the same relations drawn as a small SVG with the Study at the centre and its records, saved views, saved work and results around it, each joined to what it belongs to, inside a scrolling region at its own width, with the relations as a list beneath for keyboards and screen readers); Linked items (the members, each with the mark of the app that owns its type in that app's tinted 30 px tile, as Provenance draws its rows, and a pin for a type no app owns; the generated thumbnails read as broken glyphs at that size, the CEO's feedback of 14 October 2026; and the saved views, as rows to their pages; "+ Link" opensStudyLinkDialog, the person's saved records not yet in the Study with a Link key each, written through the store'saddToStudy); Related studies (the Studies sharing an object, with their number, name and status; "View all" opens the Bench's Studies tab; hidden when none); and Outcomes (StudyNextSteps panels="rail": the to-dos as a checklist with their due dates and "+ Add" in its head; the editor gained an optional Due date; completing one still records no scientific result).
useStudyWorkspace reads the Study's saved work, results, Experiments and change history once for the page (the Experiments panel's effect, plus studyStore().history). StudyData gained the optional status, pinned and todos[].due under D-0083 (studySchema admits them; no schema version change); D-0084 adds no field.
The 11 October section above is superseded where it describes the Bench's full-width single column, "Your studies" and "Pinned records", and the Study page's header, overview and panels. bench.spec.ts, bench-dashboard.spec.ts and rich-records.spec.ts were amended citing D-0083; the Study specs (workspace-study, study-dashboard, bench-note, bench, objects) citing D-0084 for the editing surface and the Outcomes section.
Rich saved-record cards (11 October 2026 candidate)
Library Cards use objects/object-library.tsx’s RecordCard; the pinned table and the library's List share RecordTable (workspace/bench-pins.tsx), and the Study cards and the record page draw RecordCover (D-0084, above).
record-preview-model.ts projects existing saved data: UniProt identity, exact
compound or validated Molecule-workspace SMILES, and supported dose-response
Assay input layouts. Cached facts retain their source and date; RDKit facts are
saved calculations with their cache timestamp, separate from the record’s updated
date. Protein records do not carry structure images. The plate thumbnail shows
categorical well roles and first-plate exclusions, never readings or fitted
responses; it preserves synthetic/published/edited qualifications. Missing or
unsupported payloads retain the card’s ordinary facts or fallback copy.
RecordPreviewProvider spans pinned and library cards without remounting editors.
Show structure dynamically imports record-depiction-client.ts, which statically
imports the existing shared RDKit client. One lazily created client serves the
whole Bench. No worker starts on a card's render, focus or scrolling; a Study
cover with a compound member (D-0084) asks the same lease when it mounts, so one
Bench visit starts at most one worker and only when such a Study is shown. A pending
import is shared; scope and caller-generation checks prevent obsolete requests
from starting a depiction. Already submitted work can finish when one card is
removed, but its result is ignored; individual cards never terminate a worker
another card needs. Scope change or provider unmount invalidates the lease and
calls restart() to terminate the shared worker. Returned light/dark SVG strings
are rendered as image data URLs; a theme change selects the matching string
without recomputation. The shared depiction operation may also initialise engine
patterns and compute an InChIKey; the card does not use that key or call descriptor
analysis. No preview writes a record or fetches a drug library.
The explicit client download has its own budget in
scripts/size-budgets/record-previews.json under the CTO’s 11 October ruling.
Worker and vendored WASM downloads are separate from that client budget. The
225 KB /o initial-route ceiling remains unchanged. Browser checks cover the
no-start-before-click boundary, one worker across pinned/library cards, failure
and keyboard retry, theme, scope cleanup, plate captions, List mode and layout.
October 16 usability changes
Numeric controls reserve six characters plus padding even beside a unit
selector. Assay explicitly opts into stacked phone example buttons on
DiscoveryStart; other starts retain their existing example layout.
Calc's own amber accent is generated through the registry, preserving
Bioreactor indigo, Pipeline violet and all chart/status tokens. PubChem
palette results use the registered singular Molecule name.
The guided walk initially collapses step descriptions after step 1 below 768 px, with a disclosure to reopen them. Active changed-input warnings remain expanded and the existing paused/run state and Leave flow remain.
Stock editing and explicit-draft navigation (16 October 2026)
SuiteFrame provides one on-demand stock creation dialog above its keyed library
rows, so changes of scope hide rather than discard a draft. Saved compound and
private construct Object actions can preselect their exact saved revision; no
material fields populate physical quantities. The stock overview keeps visited
tabs mounted and asks the explicit-draft barrier before changing tabs.
research-draft-navigation.ts registers the same save/discard guard used by
useResearchDraftGuard for imperative same-policy routes such as palette results.
It does not introduce stock autosave or change named-work autosave timing.
Explicit unsaved research forms now also register a synchronous history guard in
research-draft-navigation.ts. The workspace layout installs history tracking synchronously through
ResearchDraftHistoryBoundary before editors are available. Public pages retain
only the global cross-policy interceptor. Cancelable same-document Navigation API traversals
are refused before commit; otherwise a capture-phase popstate listener returns
by the actual native entry index (or a known, href-stamped legacy ordinal) before
Next restores another tree. Compensation preserves Forward history, Next's state
fields and scroll, and never calls go(0). Native hash push/replace events update
the accepted entry separately from traversals. The legacy fallback cannot infer a
delta after an unknown writer removes an entry's marker: it retains the draft and
suppresses routing without pretending that the address was restored.
Bench recovery and Study management mount their optional operations on first opening. Once opened, controls stay mounted while closed, retaining active work, conflict/recovery state and the existing visible feedback. Readable report readers and PDF fonts load only after an explicit export request.
Dismissing the phone Menu for a presentation
The shell context exposes optional closeMenu() (CPO ruling, 16 October
2026). It idempotently closes only AppMenuSheet; it does not change
scientific inputs, save work, flush drafts or navigate. Calc’s plate-print
action calls it after synchronously capturing the current inputs, so the
parent modal cannot cover the new in-place preview. Other export actions
and the drawer’s existing close/focus behavior are unchanged. The phone
plate-print browser test verifies that no dialog blocks the preview and
that Back preserves the source inputs.
Methods from a selected recorded result (16 October 2026)
NotebookCopyControl adds a Methods text section before the existing notebook
iframe. Prepare loads recorded-methods.ts and only the selected app's historical
reader. A visible paragraph, all qualifications and a compact historical receipt
precede the explicit copy/download controls ("Copy methods text": it copies the
methods text alone, which holds no reference checks; the methods dialog's "Copy
methods and reference checks" is the control that carries them, audit of 20
October 2026, M4); expanded evidence includes the text
template, exact hashes, both revision labels and both method-pin layers. Existing
notebook HTML/text output is unchanged. Unsupported coverage has an explanation
and no methods copy action; hash/integrity and lazy-load errors remain distinct.
The session is keyed by scope, access epoch, result ID and result hash. Layout
cleanup invalidates unmounted sessions; a dialog generation invalidates closed
preparations. All async operations check ownership around awaits and immediately
before clipboard/download. Closing a pending methods preparation is allowed;
clipboard operations keep the existing busy-dialog behaviour. One existing
InlineStatus announces errors/success. Long ordinary text wraps at 375 px.
SendOptions.isCurrent is an optional caller-owned lifetime/input check around
asynchronous Send to operations, including the lazy loader, record reads and
handoff creation. The Molecule palette and walk use it with their result identity
and workspace epoch; existing callers retain their prior behaviour.
Public reference workflows (16 October 2026)
ReferenceWorkspace composes the normal shell with a compact dataset/reset bar above the tool and a server article below it. Reference tools use H2 headings under one main landmark; the article owns the sole H1. Passive object learning is suppressed for public examples. See Public reference workflows for the shared route, state, source and export contracts.
Repeat setup in the Study's Experiments panel (contract 06, 17 October 2026)
experiments-panel.tsx puts one quiet Repeat setup button beside Add experiment, for a writable, unarchived Study, even when no pair is eligible. repeat-setup-action.tsx downloads repeat-setup-dialog.tsx (and through it the Calc and Assay adapters and src/lib/workspace/repeat-setup.ts) only when the button is pressed; a failed download says so. The dialog is one Dialog (size lg, one scrollable body, stacked fields below sm) with three stages, Saved pair → Inputs → Review, then the outcome in place:
- Saved pair: two selects that start empty, listing eligible saved tasks with their revision and Experiment; every ineligible Calc or Assay task in the Study is listed in a disclosure with its specific reason and Open source task. Each chosen task gets a stacked summary card (app, label, saved revision, Experiment). A selection pair is read fresh each time; a refusal names its reason with an Open action for each task it involves and Choose another pair; the list of unusable tasks opens by itself when no pair is eligible. When this tab holds a named editor for a selected task (
hasNamedWorkspaceEditor), Next waits for Use saved revision or Open task to save changes; nothing is saved or flushed by the dialog. - Inputs: replacement material (saved compounds only), stock concentration, unit and basis, carrier and percentage (never prefilled from the source; the source values are shown beside them), the dose design prefilled from the source with "Not recorded" for a missing value and a required "I have reviewed the dose design", the optional plate sheet (intermediate and stock on hand cleared; with no source sheet, "Add a plate sheet" starts empty and its plate and direction are chosen), the analysis settings, the analysis-block choice and explicit keep/clear for context and free text. Problems from preparation are shown at their fields.
- Review: editable names and the saved sources, always shown (a name edit marks the preview stale and re-prepares the command after 400 ms; a failed read stops and offers Try again), the saved sources, What changes, then Changed, Kept after review, Cleared and Still needed with the CSO's planning and control-role qualifications, the normalisation's needs by mode, each unentered allowance or limit by name, and the engine's flags with the thresholds applied; Create planned Experiment.
- Outcome: Planned Experiment created (Open Calc, Open Assay, Done; the new Experiment becomes the tab's working Experiment), This setup was already created (listing what survives, "Task unavailable here" for the rest), Saved inputs changed (Review updated saved inputs keeps the replacement entries), Not created with Try again, and Creation not yet confirmed with Check creation (the same prepared command; no edit or second create meanwhile). An account change discards every entry and the preview, leaving only its message and Close.
The Experiment editor shows a schema-2 Experiment's receipt as organisational lineage (RepeatReceipt): the source Study, the source tasks' historical labels and revisions, and the two new tasks, each with Open Calc or Open Assay (through the ordinary resume) when it is among this Study's saved work, else marked "Task unavailable here". Copy is in content/apps/workspace-repeat.ts.
Public-page motion (18 October 2026)
MarketingMotion is a null client leaf enhancing complete server-rendered public HTML. The landing and About routes mount it once. An IntersectionObserver plays each marked scene once, with native SVG/WAAPI via the existing motion helper. Paths draw, decorative nodes arrive, and rings rotate slightly into their final position. Text and controls stay available; graphics use registry app marks and semantic palette tokens. Initially visible scenes settle immediately and the controller does not own the first-visit showreel.
OS/settings reduced motion, data saver, hidden documents, showreel state, width changes and interaction settle the owned animations. Pointer cancellation applies to interactive elements; focus settles the containing scene. Initial hash navigation and hashchange cancel running animation, safely decode and locate the destination, settle its containing section’s scenes plus visible/above-viewport scenes, and leave later offscreen scenes available for their first scroll entry. A malformed fragment has no destination and cannot throw. popstate, pagehide and persisted page restoration retain whole-page settlement. The controller restores only its owned inline properties. No animation dependency, scientific engine, tracker or state store is added.
The landing sections now use illustrated editorial compositions. About routes use a split subject hero, real preview, full-width editorial capabilities (two columns on desktop, one on phones) and reference starters before compact source disclosures. ProductVisual is server-rendered abstract identity geometry, not calculated evidence. SourcesBlock and SourceList accept an opt-in compact/disclosure presentation on About; docs retain their full presentation. The source directory starts closed, with individual source disclosures inside; each keeps its complete registry facts, credit and notice links. Limitations remain in the existing app documentation, with the concise research-use statement and full-policy link retained on About (CEO direction, 18 October 2026). Shared themes, source data, preview gates and workspace behaviour are unchanged.
The landing page's guided example (landing-workflow.marketing.tsx, server only; LandingWorkflow in landing-blocks.marketing.tsx puts it under the section's heading, above its one link, "Try the guided example", and the walk's source note) is three static steps in an ordered list: Molecule, Calc and Assay, in a row on desktop and stacked on a phone. It was rebuilt this way at the CEO's request of 20 October 2026, on Codex's approved design of 18 October (D-0103), replacing the Gemini scene of 18 to 20 October (a stage rail, an app-screen panel, a spotlight overlay and a one-pass player). Each step is laid out like the trust section's columns: a hairline rule, the app's mark and short name in its accent (data-app on the step), the step's heading and sentences (content/apps/landing.ts, workflow.steps), then one card of key figures, its note held at the card's foot under a rule. On desktop a CSS subgrid lines the three cards up, at equal height. Molecule's card shows the salt's and the parent's formula weights and the shortfall if the parent's weight were used (the walk's {shortfall} slot, in the CSO's wording of 17 October 2026). Calc's, labelled "Illustrative preparation plan · not measured results", shows the mass to weigh for the stock and the dilution series with its eight planned concentrations, plus the solvent line "In your plan, …". Assay's shows a small static plot of the 23 finite published readings (raw MFI against log dose, no normalisation, fit or curve), the missing well named from its own fixture row, the CSO's source line and "The Broad made these plates; this example did not.", and a closed native table of every series row with plate and well (the missing reading stays "Missing"), with a link to the dataset. Every word is content/apps/landing-workflow.ts's and every figure is filled in by landing-workflow-data.ts: the walk's values from j3Slots(), the atomic-weight table from formula-mass.json, the dilution ladder from dilution.json, and the readings and missing well from the published PRISM fixture (public/verification/assay-analyser/0.2.0/prism-erlotinib-hcc827.json). The same module exports the plot's frame (PLOT, doseX, mfiY), which the browser spec uses to recompute each point. The structure drawing is gone; the CSO allowed removing it.
Since D-0112 (21 October 2026) the section comes first under the hero, with the CSO's heading and lede (REL-441, closed) and his step fixes. The code is in three parts. landing-workflow.marketing.tsx is the scene: it renders whichever WorkflowExample it is given (its steps, one WorkflowCard per step, its walk link, action, label and source note) and holds no example's words or figures. landing-workflow-j3.marketing.tsx is this example's cards and entry, and landing-examples.marketing.ts lists the examples (one today), so a second or third example (D-0114 adds them, with a switcher, in a later change) is one more module and one more entry. At desktop width (lg) the ol is one grid of seven rows and one column per step (two or three; an example of two steps takes two columns, D-0114); each step is a subgrid spanning them (heading, sentences, the card's four rows, the hand-off line) and each card a nested subgrid of four: caption, key figures, figure and foot, so captions, key figures, figures and foot rules sit level and the figure row takes up the difference. A card with no key figures (Assay's) puts its figure in their row and the figure's own <figcaption> in the figure row, through a third subgrid level; Assay's notes (the missing well, the source line, "The Broad made these plates; this example did not.") are that caption, visible. Every step but the last ends with its handoff line from content/apps/landing.ts, static, with an arrow across at three columns and down when stacked. Stacked (below lg), the cards keep their natural height. Chart tick labels are HTML (data-chart-label, text-xs), not SVG text, so they never scale with the drawing; the plot and the ladder are capped at max-w-md. A key value and its unit stay on one line (whitespace-nowrap, one type step down at lg), and in the words every number is joined to its unit with a no-break space where the landing fills its slots (keepUnits, content/apps/landing.ts, applied by landing-workflow-data.ts and the step texts; the walk's own values keep plain spaces). Nested subgrid needs Chrome or Edge 117, Firefox 71 or Safari 16; an older browser shows the cards in a row at their natural heights.
The section has no controls, no timer and no motion of its own, and no client code: no element in it animates, with or without reduced motion (DS §7.1 rule 3). The shared MarketingMotion owns none of it. landing-workflow-data.test.ts checks the words and figures against the fixtures, refuses "fit", "curve", "IC50", "potency" and "result" in the heading and absolutes in the lede, checks the CSO's six step fixes, and refuses typed copy, a motion hook, a timer or a control in the scene, the example's cards and the list. e2e/marketing/landing-workflow.spec.ts checks the figures, the 23 points' coordinates, the table, the hand-off lines, the notes inside the figure's caption, the row on desktop with its cards and their rows aligned, the stack at 375 px at natural heights, chart text at 11 px or more and each value on one line at seven widths from 1,440 to 375 px, no number parted from its unit at a line end at five widths, and that nothing animates over a minute of Playwright's clock.
Since D-0133 (24 October 2026) the landing's other sections changed in one pass after the board's single sitting on all seven (docs/board/memos/2026-10-24-*-landing-sections.md): the hero's trust line says "Runs in your browser" and its secondary key "See the worked examples"; each example's walk key names its work and the app it starts in, so no two keys on the page share a label with different destinations; the hero tiles (family.marketing.tsx) open the About pages, labelled "About each research tool"; the methods band (LandingTrust) carries three claims in one row at lg, the browser claim third in the CSO's wording with a link to the security page, and the research-use statement full width beneath; the AI section (landing-ai.marketing.tsx) is a band with its benefit rows exported but not rendered (its three-step line was horizontal at lg until the figure below replaced it); Education is tinted with its chart on a surface plate; and the Bench section's figure is landing-bench.marketing.tsx, a server-rendered Study record (four rows: the compound as Work in Molecule, the plan and the readings as recorded Results in Calc and Assay, a Materials & sources row) whose every value is a workflowSlots() slot from the J3 example, with the example's hand-off lines on a traced spine, rows that rise once through MarketingMotion, and no IC50 (that value is in no slot). Its words are content/apps/landing-bench.ts, page-local and not an export field. The band's desktop ceiling in suite-pages.spec.ts is 420 px for the third claim.
Since the AI figure (24 October 2026, later the same day; the CEO: a graphic or animation with the correct text, not a picture and not a tool list) landing-ai.marketing.tsx renders its status, heading, lede and availabilityNote in one column and, beside them, one figure of three examples under tabs, one per app (the CEO, that night: "another two examples with tabs for different apps"): the Calc dilution plan from the J3 fixtures, a Sequence melting temperature from the oligo Tm reference set's CMV forward primer case (public/verification/sequence-toolkit/0.1.0/oligo-tm.json, Biopython's Tm_NN as the oracle, with its ΔH, ΔS and salt-correction intermediates on the engine lines) and an Assay four-parameter fit of the MTT worked example's Compound A (public/verification/assay-analyser/0.2.0/drc-reference.json, R nls and drc as the oracles; synthetic readings, marked so on the card). The Sequence and Assay slots are landing-ai-data.ts (tmSlots(), fitSlots(), AI_FIGURE_CASES naming the cases; landing-ai-data.test.ts fills every example's copy from its slots and refuses a typed digit). The tabs are the worked examples' own :target panels (landing-examples.module.css: the first panel by default, each panel carrying the nav with its own aria-current, so they work without JavaScript); each panel is its own data-marketing-scene and plays once when shown. Each example is three labelled stages on one centred spine ("You ask", "The tool runs", "Your assistant replies"; the CEO, later that evening: the single arrow was out of line and the output was missing): the assistant's question as a bubble, a traced connector, the Calc dilution tool's answer as a result card carrying data-app for Calc's accent, with the mass and the series as its readouts and an engine block beneath them (conversation.engineHeader and engineRows: Calc's registry version with the tool id from mcp-source for the series and the fixture case's mass-for-molarity method id (massMethod, a workflowSlots() slot) for the mass, then the mass as c × V × M(salt) with the fixture's formula weight, the series as top ÷ fold each step, and the references and reference checks in one line; the caption, in the CSO's words, says the series is the tool's and the stock mass is Calc's own method (SCI-650: no MCP tool returns a mass); the CEO, 24 October 2026: the middle stage is the engine the question is sent to), a second connector, and the assistant's reply in prose (conversation.reply, the same slots). Every value is a workflowSlots() slot or a registry value, filled through fill(); nothing is typed and no engine runs. The question and card rise and the connector draws once through MarketingMotion (data-marketing-node, data-marketing-trace); without JavaScript or under reduced motion it is the finished figure. The three-step line, the methods sentence and the tools list left the band; the developer disclosure stays. The Bench figure's rows carry data-app, so each row takes its app's accent rather than the suite's. The Bench section's heading is "The whole experiment, in one place." (the CEO, 24 October 2026, after "Your work stays connected." earlier that evening).
The CEO removed the bespoke homepage concentration explorer on 18 October 2026 because it represented a calculator interface absent from the actual app. Its component, copy and fixture adapter are removed. The connected workflow remains an explicitly labelled illustration of the existing guided example, not an embedded app interface.
Homepage planned assistant connections and local MCP preview (18 October 2026)
marketing/landing-ai.marketing.tsx renders LandingAi after Education and before the closing band (D-0112 item 8, 21 October 2026; from 18 October it sat after the guided workflow and before the research-tool directory), its "Planned" status line visible without opening anything. It leads with planned connections for assistants such as ChatGPT and Claude, explicitly intended future targets rather than tested or available integrations. Its server-rendered explanatory diagram shows Your question → Scientific tools → Results with context, with the engine boundary labelled Hosted MCP connection · Planned and a caption stating those connections are not available yet. Copy comes from content/apps/landing-ai.ts; responseFields supplies human-facing outcome labels, not a protocol schema. The section mounts no assistant, scientific engine, request transport, connection control or state store.
A closed native disclosure, For developers: local MCP preview, contains the implemented four-tool inventory, local-preview status, prerequisites and setup action. The action is a plain link styled with buttonLinkClass, so it does not prefetch the dynamic technical documentation. This local inventory is not a commitment to the future hosted launch inventory. The export preserves the distinction through aiIntegration.localPreview.
The diagram's connector paths reuse MarketingMotion through data-marketing-scene and data-marketing-trace. They remain complete static paths without JavaScript or with motion disabled; prose, controls and numerical content do not depend on animation. The section uses existing semantic tokens and shared marketing components.
marketing/mcp-source.ts extracts identifiers at build time from the existing tool source files without importing the MCP server or engines. The local tool list is checked against the server's registered inventory in its Node test. Export version 4 added aiIntegration and its section-order entry (the export is version 5 since D-0112); the complete contract is documented in Registry and content. The homepage setup link to /technical/mcp-server is the narrow exception authorized by the CEO's 18 October MCP marketing request; noindex and technical security headers remain. This marketing section does not close Q-17 or activate hosted MCP access.
Homepage catalogue selection and motion (18 October 2026)
marketing/landing-catalogue.marketing.tsx supplies LandingTools and landingExport() with one server-side catalogue selection: the registry's catalogueApps (content/apps.ts), the landing apps and any app whose entry sets landingCatalogue: true beside landing: false (Patent, D-0103; the CPO's ruling of 20 October 2026, §7), in registry order. The hero continues using landingApps. An app listed through landingCatalogue reads its About hero lede through APP_PAGES as its description, so the catalogue shows the integrated preview qualification instead of duplicating or broadening the app's claims. No slug is typed in the component or the specs.
The catalogue groups are Discovery, Bench & process, then Papers & patents last (landingCopy.tools.groups; the CEO's words of 21 October 2026, "put literature and patent at the bottom", D-0112 item 6). Patent is in Papers & patents by its registry workspaceGroup, so no registry field changed. Literature’s identity and destination come from SHARED_WORKSPACES; it renders before Patent in the last group. Its icon and promotional description, the last group label and the Patent subtitle are landing content. Since the same decision the cards are compact ("make app card tiles a bit smaller"), and since 24 October 2026 ("make the choose your tools cards a bit smaller", after the CDO's memo of that day asked for compact rows on the phone) more so: the 2.5 rem mark ring sits beside the words (p-4, a base name, an xs description, the Patent subtitle inline after the name, xs links with a 2.25 rem target), so a desktop card is under 11 rem tall where it was about 19 (landing-catalogue.spec.ts pins the ring and the height); before that p-5, a 3.5 rem ring above the name, an xl name and 1 rem gaps. Open carries → (an in-site link; ↗ stays for links that leave the site, DS §9.7). Literature has only an Open action; registered apps retain Open and About. Workspace links disable prefetch. The catalogue is server-rendered and does not mount workspace code.
Cards use up to three columns on desktop, two in Papers & patents, and stack on phones. Each desktop row is a data-marketing-scene with data-marketing-sequence; the controller detects stacked children and observes each separately. data-marketing-catalogue animates a card once from opacity 0.65 and 24 px below its resting position to its complete final state over MS.move (300 ms), with a 60 ms row stagger. Existing SVG trace/node/orbit hooks decorate its mark. Default HTML remains complete, and the normal interaction, focus and reduced-motion policies apply. The catalogue-specific selector lives in the shared controller, so both landing and About JavaScript budgets require measurement even though About markup does not use that selector. No budget ceiling is changed.
The export keeps registered records in apps.items and shared workspaces in apps.sharedTools, with a group on each shared record. See Registry and content for the full export contract (version 6 since D-0114), including the catalogue copy fields.
The product About hero’s AvailabilityLine renders the three productPage.availability benefits: Free to use, Runs in your browser, Transparent methods (CEO instruction, 18 October 2026). It takes no app/advice arguments; the marketing route and internal developer-kit example use the same component. Version/download-size/advice are no longer appended to this row. Detailed app guidance and the lower research-use/policy context remain in their existing surfaces.
About hero subject scenes (18 October 2026)
marketing/product-visual.marketing.tsx selects a distinct decorative SVG scene by registry slug. It is server-only, with no engine, result data, new dependency or client scene bundle. content/apps/suite.ts owns the visible illustration caption. The actual workspace previews remain separate. SVGs use semantic colours and existing data-marketing-node/data-marketing-trace hooks. data-marketing-hero opts into first-visit choreography only when entry is unblocked, at scroll position zero, without a hash, and is not history restoration. Later policy changes retain the normal settle behavior. The unused reveal and label sequences were removed from MarketingMotion; orbit remains used by the landing catalogue. No export-contract change. e2e/marketing/product-hero.spec.ts checks each app's scene in light and dark, one test per app and theme (REL-462), and writes each scene's capture to that test's output folder.
Unified save and Study destinations (18 October 2026)
This amendment supersedes the Save to Bench / visible Pin action descriptions above. Object-backed work uses one primary Save work action. First save opens naming and a Study destination (existing, new, explicitly selected archived, or Without a Study). Repeat save remains visible; Work details is secondary. Record result stays a distinct secondary action, preserving immutable evidence. Actual ownership and View in Study are available inside Work details; the latter reveals the exact saved work.
The destination picker handles failed reads with Retry, blocks saving until the destination is valid, and rechecks account scope after loading its runtime. The picker loads when Work details first opens, so the details' "Save work" (and Save as new's key) is disabled until it has loaded and validated the Studies; a disabled button takes no focus, so keyboard focus reaches it once it is enabled, and e2e/assay-analyser/keyboard-handoffs.spec.ts waits for that before focusing it (REL-461). Adding a material does not change the default Study for future work. Existing work keeps its owner; Save as new uses the chosen destination. Pin is in More, including on object pages.
On a first save into a Study with a primary subject, the sheet's description is workspaceWorkText.capture.primarySubjectSaved, keyed by the first subject's object type (the CPO's ruling of 20 October 2026): compound (Molecule) says the analysis is saved in the chosen Study and its molecule added as a material, target (Protein) says the same of its protein. A subject of any other type, or none, keeps the control's ordinary description.
Study Overview now starts with Study contents: materials and saved work together, labelled by type, newest first, searchable by display names and scientific identifiers. Add material remains available in an empty Study. Attachment receipts open Materials with the exact canonical member highlighted; saved-work receipts open Overview with the exact work highlighted. The fragment carries the tab and member/work identifier. A revealed work remains visible alongside search matches with an explanatory line. The separate Materials and Work tabs remain available. Molecule and Protein show material membership inside Work details.
Optional WorkCaptureControls subjects supplies explicit primary scientific objects; subjectRefs alone may be consumed input bindings and never implies Study material membership. See Storage and state for transaction and legacy behaviour.
The destination chooser and material membership readout load as separate controls. Download failures show Retry; the first-save confirmation stays disabled until the chooser has loaded and validated its destination. The ownership line appears when Work details opens. Destination-only copy lives in content/apps/save-destination.ts. These delivery boundaries do not alter scientific state or save semantics.
The secondary WorkNoteEditor also loads when Work details renders it (CTO review, 18 October). Its lazy component is stable at module scope; existing mounted draft state, navigation guard and close semantics stay with the editor. Import failure has Retry and never implies a saved note. New lazy chunks have explicit measured budgets; this is a delivery optimisation, not a claim that total feature code is free.
Work details opts into retaining opened Popover/Sheet content while closed, so unfinished next-step notes survive close/reopen. Other overlays retain their existing mount policy. Retained closed popovers explicitly hide even without the native Popover API; note navigation guards reopen Work details before exposing the pending draft. Account/work keys still isolate note state. Resizing across the Popover/Sheet breakpoint changes ancestry and is not covered by this retention guarantee.
UI audit refinement — 22 October 2026
The Bench overview uses a unified metric surface, a study starter with an adjacent source-information band, ten peer tool launchers and a separate footer. Bench note and scoped recent locations sit below tools; activity, quick starts and examples stay in the supporting rail. Container queries stack metrics and support panels before their text is cramped. Empty-note and quick-start paths reuse the existing Study starter. Full-list hash views carry their own H1; overview keeps the greeting.
ReadoutContents is the common label, value, unit and qualification presentation
used by ReadoutStrip, KPI tiles, Pipeline metrics and Patent dates. All use the
semantic surface treatment; long values and qualifications wrap. Calc's status
extraction reads an explicit data-answer-unit alongside the existing displayed
value, without numerical recomputation. StatusMessage routes visible ordinary
Assay feedback through the shared polite announcer; hidden panels do not announce,
and urgent alerts retain their separate semantics.
Validation and the user-supplied audit scope are recorded in
docs/progress/2026-10-22-ui-audit-refinement.md.
23 October Study cards, populated example and initial loading
Bench waits behind the shared three-bar WorkspaceLoader until its initial scoped object, Study, work and result reads and Continue validation settle. The hidden subtree remains mounted for Continue's completion callback. Read failures reveal existing Retry states; later background refreshes keep the dashboard visible. The /o and individual Study route fallbacks use the existing lightweight ShellSkeleton with their own loading labels, preserving the same bars without downloading a second interactive SuiteFrame before the page. The full frame and initial scoped-data gate belong to the mounted page.
The shell reads only its three Methods-pane labels from
content/apps/methods-pane-labels.ts; the full methods/verification copy remains
with the lazy pane. The original methods content object reuses those same labels.
Study headers hold their title over a 22% registry accent tint with faint decorative record art. The one pinned card spans the main column without extra height. A small pin replaces any prior pin; current Study state uses its chip, with no green card edge. Study Manage offers the actual app accent groups. Export offers a PDF overview or full recovery ZIP, through the existing report and bundle paths.
Overview separates Saved work and views from Materials and references. Every manifest has its own card, even if two tasks share one editable input. Only an exact stateRef identity removes a repeated input-only material card; subject references and labels never merge records. Experiment containers stay in Work. Resume and Open record share a footer line. Materials retain their own record actions and membership.
material-summary.ts dispatches to eight lazy, validated app readers. Cards and rows show at most four retained input facts, specific to the saved operation: stock versus dilution versus serial or everyday preparation, sequence operation and alphabet, Assay mode/model/unit, and reference views. Missing units and fields are omitted; no current defaults, cached outputs, engine calculations or network facts are promoted into saved input details. Public Patent product facts use a narrow allowlist and preserve zero counts. The Objects page has no separate saved-work queue; its list stays unchanged and Cards uses these same summaries alongside ordinary record previews. Tasks remain available in their Study and through a real manifest's Resume action on its object.
The explicit Load example Study command now loads Erlotinib response in HCC827 cells (example-study@4), superseding the earlier T790M draft. It is a completed reanalysis of the unchanged published PRISM subset, not a performed confirmation experiment. Four ordinary reference materials plus a paper and its reviewed method finding, seven tasks across Protein, Molecule, Sequence, Pipeline, Calc and Assay, three actual captures, source notes, objective, conclusion with the captured Assay result ID, task notes, due dates, colour, pin and two Experiments populate the page. New explicit loads initialise the Study itself as In progress (SCI-250), while the reanalysis Experiment is complete and independent confirmation remains planned. The six unfinished next steps and all captured science remain unchanged. This seed-only correction keeps example-study@4; opening old examples or Studies does not migrate or reset their chosen status. A successfully written example still has receipt.complete: true and its own canonical graph baseline; seed completion is distinct from research completion. Patent is a future lookup, with no invented patent record. The parent identity and illustrative hydrochloride material remain separate; canonical EGFR is not a cell-line genotype claim. No unrelated Plasmid/Bioreactor filler is added.
example-study.sources.ts retains excerpts tested against repository UniProt, PubChem formula-weight and dated NSCLC captures. example-study-science.ts uses ordinary import, current fit, recipe and capture paths. Its Assay record carries the row decisions the app's own import writes with every accepted source (tableDecisions: { v: 1, roles: {}, reasons: {}, history: [] }, read through readTableDecisions), which the strict reader requires once sourceId is set; without them, before 24 October 2026, the example's Assay work opened as "No assay open" and its Repeat always refused (CW-01, REL-560). The field changes no fit input, curve, qualification or verification (example-study-science.test.ts, the CSO's checks), and src/lib/workspace/suite-assay-writers.test.ts holds every Assay object the suite writes itself (the example's, its repeat, Repeat setup's) to readAssayDataStrict. The current unresolved confidence bound and serial transfer warning remain. No authored numerical fit or fabricated outcome fills a missing field.
Demo writes carry explicit bounded per-write marks, never a tab-global write context. The internal scoped example-study-receipt.ts records exact seed identities and revisions before granting cleanup authority; the final Study update and vacant-pin write are revision guarded. Completion rejects unexpected personal tasks or edits. Preview and removal use one complete-graph transaction; embedded dependencies in retained inputs, versions, objects, work and results preserve the whole demo. Only last-opened recency is ignored. Untouched demo rows can be removed together. Edits, reuse or incomplete seeding adopt all evidence as personal work, advance object/link revisions and Study history, strip removal marks, and keep immutable snapshots unchanged. Receipts are scoped internal metadata, never exported deletion authority. Missing authority fails closed even when the Study was removed elsewhere.
Objects rows, Study task cards and selectable reports — 23 October 2026
The Objects List now uses surface-backed compact rows. Public identifiers sit under the record name; private UUIDs stay off the face. A small type chip, actual Study links and a quiet update time replace the empty Notes column. Notes/tags have a disclosure under identity. Resume and More stay together; records with multiple manifests open an explicit task chooser. This presentation is scoped to #objects; Bench's pinned rows retain their existing form. At narrow widths the same semantic record row wraps inside its container without page overflow. List/Cards, filters, sort, paging, return highlighting and ownership failures remain shared.
The Study Work tab, as well as Overview, has one card per manifest. Work cards use a neutral border and a 12 px gap, app mark, task title, saved date, one recorded outcome, researcher note and quiet footer actions. A lazy input reader supplies the operation only when the editable record revision equals the manifest's checkpoint. Newer editable inputs never describe an older saved checkpoint. Exact result history, newer-input consent, pending-note guards and organisation commands are unchanged.
Export Study → PDF opens an intent-loaded builder with eight section checkboxes and Recommended overview / All sections presets. Objective, Materials, Methods, Results and Conclusion are selected initially; Work, Notes and Expanded appendix are optional. An empty selection cannot generate. Selection only changes PDF report parts from one consistent bundle and never changes stored records, complete HTML or the full ZIP. Title/date, historical/example labels, recovery notice and compact captured source credits are mandatory. Selected results retain units, reporting state, qualifications, exact input context, omissions, source credit and figure captions. If Methods is unchecked, retained historical prose still accompanies its result. A conclusion without the Results section keeps exact cited identities and qualifications and says supporting details were excluded. Empty selected sections disclose missing content. Export guards recheck account epoch through capture, rendering and download.
study-report-overview.ts builds the readable PDF separately from the complete document. Repeated import issues are grouped only by exact captured input, recognised diagnostic reason and rejected/unresolved disposition, retaining counts and example coordinates. Other facts, exclusions and totals remain. Detailed diagnostic coordinates and method receipts remain in complete HTML/ZIP with a PDF notice. Identical provenance blocks attached to a result and its exact input print once in the appendix with every capture identity retained. Scientific projectors and the vector engine are unchanged: A4 tables have rules/repeated headers, headings stay with a first row, figures/captions remain together, and running heads name the section. Authored Markdown headings become headings; IDs/hashes move to an optional appendix.
Future example loads capture PRISM provenance through exportExample, blockSources and buildProvenanceBlock, using the declared source release, retrieval date, licence, modifications, example statement and tested-form qualification. This credit belongs only to the published Assay reanalysis, not the illustrative Calc plans. Old snapshots are never backfilled from the current registry; missing supported provenance remains an explicit warning.
Bench, Study and Save simplification — 23 October 2026
The CEO-approved workflow uses Bench, Studies and Library, and the four Study sections Overview / Work / Results / Materials & sources. Bench Continue working reads actual saved WorkManifests; All work includes standalone activities and current saved analysis batches. Browser last-address links move to Recently viewed. App backing records are carried/private by default, while declared primary Protein/Molecule subjects are kept as Library items by explicit Save, including outside a Study. Add to Study links the reusable item without a second saved-view checkbox. The current example is example-study@3, with Sequence schema v2 and no legacy views.
Continue working orders saved activities by most recently saved across all Studies, including work without a Study. The selected Study does not change this ordering; each activity names its own Study or Without a Study. The former selected-Study-first sentence was removed at the CEO's request on 23 October.
On Bench, Continue working and Recent activity use BenchSectionHead above their content so their desktop headings share a baseline. Neither removed helper sentence is rendered. Recent activity retains its white body but uses 32 px tiles, 44 px minimum link targets and reduced vertical row padding; wrapped descriptions and qualifications determine the height naturally. The full Activity page retains its original panel and timeline spacing.
The action cluster stays Save · Send to · More. The first Save opens name/destination choices. For existing work it saves directly; More contains Name & Study and Save a copy through WorkActionContext, scoped only to that cluster. Save calls the captured render's result adapter when eligible. Reference/incomplete work saves no result; reason-reviewed analyses keep their review; partial failures and limited storage remain visible. An explicitly allowed identical Calc/Sequence calculation may reuse its original checkpoint. Save another result deliberately appends. These are explicit user acts, never effects or draft timers.
Study Results headlines read the existing demand-loaded immutable-result projections and their exact input snapshots. Multiple curves show shown/withheld counts; a single endpoint retains its label, unit and qualifications. Refused values are withheld. The synchronous report/Node entry delegates to the same presentation core. Overview shows selected results and the conclusion rather than duplicating the entire Work/Materials list. Experiments are optional grouping under Work, and next steps are editable plans on Overview.
The PDF builder offers Business and Science reports, section checkboxes and a prominent Export action. The report projection omits private IDs/checksums and places the methods, source credit, licences and retained-input detail in the appendix, preserving interpretation-critical qualifications beside each finding. Complete ZIP remains a separate recovery/transfer option. The former content-digest/RO-Crate UI is removed; View graph is at the foot of Provenance. Saved-work cards have increased padding and plate previews use their card's full width.
Library retains the former Objects four-panel icon (LIBRARY_ICON, overview) consistently in the sidebar, command palette and Library menu links, as requested by the CEO on 23 October.
Study workflow follow-ups and Bench archive placement — 23 October 2026
Study Work adds Add existing work and a lazy selection dialog. It names the source and destination and lets the scientist choose exact dated results. Included cards say From [Study] or Started without a Study, and Continue original opens the source activity through existing consent/draft guards. All work's Study filter uses actual memberships. A Study outcome/history shows its included snapshots rather than the activity's global latest pointer.
The Study checklist has Make next / Clear next, with one Next marker. Header Choose next task reveals the existing checklist; it does not create a second editor or another task source. Header, brief and featured Bench card read the same selection. Completing that item clears it.
Materials & sources keeps Open and Use in an app together. Available destinations come from the existing registry rules, with each app named. Starting from a material chooses the current Study with no inherited Work or Experiment. Pending-draft and owner-epoch guards apply before navigation. Unsupported materials show no fabricated destination.
Results and saved Literature findings offer Use in conclusion. This reveals the existing Overview editor, preserves written draft text and checks/adds the chosen citation; an explicit Save is still required. Paper findings are separately labelled From a paper with limitations and source-status qualifications. Frozen passage/citation details expand inline; source licences and methods stay in report supporting sections. There are no additional top-level tabs or floating context boxes.
The residual Include archived control on Continue working now sits after its cards and feedback, matching the controls below Recent studies and All Studies. Both Bench column headings continue to use BenchSectionHead without offsets. Populated-state browser verification covers archived-work filtering, aligned desktop headings and wrapping at 375 px in both themes.
Landing finish, 23 October 2026
Integration retains the approved stronger shared Light fgMuted: #3d4e57
in CSS, TypeScript and print. The following dated landing checkpoint
records its original #425159 value; that value is superseded on this branch.
The two worked examples use plain in-page links styled as line tabs, CSS :target selection, and a fixed J3 default. Each focusable, labelled target panel carries its own navigation with aria-current="location" on its selected link, so fragment navigation retains the switcher below the header. There is no ARIA tablist, selection script or animation. At 768 px through the lg boundary each step has text/card columns (2fr/3fr) with natural card heights; phones stack and desktop restores the existing aligned subgrid. Catalogue cards and the Education disclosure use rounded-card. Public Dark uses --color-sunken: #142a21; Light secondary text uses #425159, including matching paper/print tokens. The violet workspace Dark override remains separate. The AI response list includes refusals with reasons and continues to identify hosted connections as planned.
Calibration-derived bench-sheet export infrastructure (22 October 2026)
src/lib/export/calibration-bench-sheet.ts provides exportCalibrationBenchSheet,
a standalone, script-free HTML writer for the existing normalisation method.
It accepts an existing strict source block and an explicit full config, resolves
selected source IDs, uses calculateNormalizationBatch unchanged, then detaches
and freezes source metadata, intervals, qualifications, withheld counts, omitted
IDs, settings and results together. Refused rows remain visible without transfer
volumes. Unknown configuration fields are refused. No manual concentration can
be associated with a source merely by supplying its name.
Calc’s normalisation results now offer Capture, Refresh after edits and Download
captured sheet. The export module itself defines no stored/importable schema;
editable inputs use the separate np v1 contract described in app-calc.md.
The caller resolves explicit source origins from the current non-example plan. It does
not authenticate a payload or extend the existing method with reducing agent,
overage or well-capacity calculations. Copy lives in
content/apps/calibration-bench-sheet.ts and reuses the existing normalisation
copy. Tests in src/lib/export/calibration-bench-sheet.test.ts cover the existing
independent volume fixture, fit-to-document composition, deep capture, omissions,
refusals and escaping. The finishing plan is
docs/progress/calibration-to-bench-sheet-acceptance.md; the actual browser
journey now tests edits, stale captures, download failure/retry, reload and named
work reopening. Real-file observation and remote queue acceptance remain open.
The chosen unfinished Study checklist item labels only its matching work card as
“Next for [Study] · [task]”. Bench keeps its three globally recent cards; an
outside-three task adds no card. Handover notes remain independent annotations.
Eligible Assay cards expose Repeat with new readings beside Resume/results. The
review names the invocation Study destination and source revision/date. Once
Create has written the run (repeat-assay-action.tsx), the dialog drops its
prepared source, so Create stays disabled and one press makes one run; it closes
before the address moves to the new run's own #s=mine= address, which Assay
opens in place when the repeat started there (CW-02, REL-561). Existing
inclusion review is available as Review other results; later source results are
never included automatically.
The Selected results panel also shows one latest explicitly associated uncited result, sorted by recorded time then stable ID, without preferring reportable outcomes. Its shared preview reads exact immutable inputs against the Study revision, association and scope epoch. Broken chosen inputs show unavailable and Retry, never an older fallback. Captured scientific context and qualifications are visible beside the finding. Use in conclusion prepares an unsaved draft, retaining dirty prose and selections. Every Study-contained result inspector passes its invocation Study explicitly to historical Repeat.
Material menus describe registry purpose and accepted Opens with parameters. Opening reads only; activation flushes and rechecks material revision, Study revision/membership, scope epoch and accepted parameters before transfer writes and navigation. It starts new work in the current Study without an Experiment.
The existing PDF section builder now presents factual Question, Finding, Limits, Planned next step, included result/paper/citation counts and omissions from the actual prepared report document. It captures after notes and registered work flush. Missing prose is advisory. Relevant receipt drift invalidates generation and offers Refresh captured Study while retaining audience/section choices. The final PDF renders that exact prepared document and checks the receipt again after rendering. Recovery remains the complete format-6 closure.
Fresh Bench launches carry their scope/lifetime/intent through the document policy's final save wait; old saving context is cleared immediately before the actual navigation only. Managed launch links still obey document/CSP policy. Material activation has a separate monotonic intent, so a later choice cancels a delayed older transfer/navigation without cancelling merely on menu close.
Workflow context and prepared export follow-up — 24 October 2026
Study findings keep typed captured facts until rendering. Declared preparation value/unit pairs share a wrapping definition group, with explicit missing units; scientific facts, qualifications and biological identities remain visible. No arbitrary fact limit or live material enrichment applies. Latest uncited metadata uses the existing local date/time formatter and registered short app name. The next-task key wraps its label inside the available header width while retaining its normal type and target size. Export preflight precedes section choices and is still the inventory of the exact prepared document.
Prepared PDF selection loads studyOverview.prepare directly. Full HTML remains
on its existing document entry. Unchanged task copy, number formatting, refit
version checks, parsed-source identities and strict notebook preparation have
narrow leaves with existing re-exports; no method, scientific value or cap is
changed by those import boundaries.
The final UI disclosure allowlist moves only recognised dose-response control assignment rows intact to More captured context, showing the recorded role labels and disclosure direction beside the finding. Missing reference fields disclose only when absent in that exact capture. Recorded references, qPCR context, critical missingness and qualifications remain visible. Report facts are unchanged. The implementation lives in the Study-only presentation helper, not the report selector. Notebook preparation and inert writers retain their public compatibility exports; their implementation bodies are separate so a formatter does not import strict readers through a writer.
The HTML recovery control loads only complete-report orchestration. Study PDF preflight/generation has a separate active-capture owner, sharing only the historical-method helper. The overview formatter and complete HTML formatter keep their existing markers; complete HTML does not load overview as a nested branch.
Both export owners load study-report-evidence.ts on export intent, passing the
same validated bundle and current-interaction guard. The stage prepares the exact
ordered input snapshots, notebook block, recorded projection, essential context,
captured provenance with originating identities and historical Methods receipts.
Its detached, deeply frozen data names the bundle hash and Study identity/revision;
it has no database access, recapture or cache beyond the current export interaction.
Section changes prepare again from that same capture. The overview sorts a copied
entry array and assigns display numbers; neither formatter mutates the evidence.
All narrative, grouping, section selection, omissions, inventory and HTML/SVG/PDF
layout remain in the existing formatters. The final PDF still renders the prepared
document and rechecks the original capture receipt and interaction guard.
SCI-252's captured preparation presentation uses one content-owned four-key map
in content/apps/essential-context.ts: fw is Formula weight (g/mol), lw is
Weight from the vial (g/mol), pur is Purity (%) and dmso is Solvent in stock
(%). Only the recognised stock/dilution/serial recipe v1 selections apply these
labels. The selector retains its exact String(value)/missing-value handling;
zero stays zero and absent values gain no defaults. A nonempty explicitly
captured unit on these fields is retained literally as Captured unit declaration: {unit} in the existing fact detail, even when it equals or
contradicts the grammar unit. Trimming tests only presence. No declaration is
inferred, converted, normalised or used to override calculation replay.
The unchanged Study compactor, complete report and both PDF overview styles consume the corrected facts. The complete report's separate exact recipe-input rows reuse their labels/details and retain the literal captured value; other rows keep their existing value/unit handling. Component purity remains a fraction, component formula-weight basis stays separate, and per-dose solvent fraction is unchanged. Rendering does not rewrite input/result snapshots, content hashes, source revisions, historical outputs or prior exported files. SCI-253's separate missing/withheld/inapplicable-state policy remains open.
Save controls and the existing More menu import a dependency-free save-wording
leaf, content/apps/bench-workflow-save.ts, instead of Bench/list wording. The
original benchWorkflowText object spreads those same nine strings and retains
its public shape. Component bodies, save/record guards and chart rendering are
unchanged; this is a static import boundary, with no deferred code or new island.