Demo application layout — options and decision
Written 2026-07-25. Requirement: obot.roadmap#113. Supersedes the v0 tab-strip layout shipped in PR #3.
The demo page is not a page about the application. It is the application. This document picks the shell it lives in.
1. What changed and why
The first Demo surface put the four views in a tab strip below a page title and a paragraph explaining what you were about to see:
┌────────────────────────────────────────────────────────┐
│ open.csr Home Demo Quality Design & Research │
├────────────────────────────────────────────────────────┤
│ DEMO │
│ CDISCPILOT01 — a Clinical Study Report you can open │ ← 340px of
│ One report, four ways in. Read the document, inspect… │ preamble
│ │
│ ┌────────┬────────┬──────┬───────────┐ │
│ │ Reader │ Tables │ Text │ Templates │ │ ← the app,
│ └────────┴────────┴──────┴───────────┘ │ below the fold
│ …content… │
└────────────────────────────────────────────────────────┘
That is a brochure wrapper around a tool. Three things were wrong with it:
- The explanation outranked the thing. A visitor read a paragraph about the four views before seeing any of them. If the app is legible, it does not need a paragraph; if it needs a paragraph, the paragraph will not save it.
- The views were not navigation. A tab strip inside the content area reads as a filter on the page you are already on. Reader / Tables / Text / Templates are not filters — they are the four places you can be.
- Nothing told you where you were. Select the AE table, scroll into a 3,000-row ARD, and the page no longer says which display, which iteration, or which ARD you are looking at. In a regulated document that is the first question, not a detail.
2. What this application actually is
A CSR is a document where everything has a number and a provenance. Section 12.2.1. Table 14.3.1.3. adae, cut-off 2014-07-01. Iteration v002. ARD sha256:1a2b…. The vernacular of the subject is identification: every artifact states what it is, what produced it, and when.
So the design principle for the shell:
Identity and provenance live in the chrome, not in a dialog.
Every other tool in this category puts provenance behind a properties panel or an audit export. open.csr's entire claim is that the chain from dataset to sentence is always available — the shell should behave that way too. That is the one place this design spends its boldness; everything else stays quiet.
3. The three options
Option 1 — Two-tier header ("document toolbar")
A site strip over an application strip. The four views are the application's primary navigation; the right-hand side of the app strip is a live context readout.
┌──────────────────────────────────────────────────────────────────────┐
│ ▪ open.csr Home Quality Design ◐ GitHub │ site strip
├──────────────────────────────────────────────────────────────────────┤
│ Reader Tables Text Templates CDISCPILOT01 · 14.3.1.3 · v001 │ app strip
├──────────────────────────────────────────────────────────────────────┤
│ │
│ content, full width │
│ │
└──────────────────────────────────────────────────────────────────────┘
- For: full horizontal width, which is the scarcest resource in a TFL viewer — the AE table by SOC and preferred term is six columns of hierarchy and already scrolls. Simplest of the three. Each view stays a real URL, so it degrades to working navigation with no JavaScript.
- Against: two horizontal bands cost roughly 100px of vertical space. The context readout has to collapse on narrow screens.
Option 2 — Left rail ("workbench")
A persistent vertical rail carries the four views; one top bar carries context.
┌────┬─────────────────────────────────────────────────────────────────┐
│ ▪ │ CDISCPILOT01 · Table 14.3.1.3 · v001 Quality Design ◐ │
├────┼─────────────────────────────────────────────────────────────────┤
│ 📖 │ │
│ ▦ │ content, ~200px narrower │
│ ¶ │ │
│ ⧉ │ │
└────┴─────────────────────────────────────────────────────────────────┘
- For: view switching is always visible without competing for the same row as context. Scales past four views.
- Against: it spends 200px of horizontal width to save 50px of vertical — the wrong trade for wide tables. Icon rails also need labels to be legible to an occasional visitor, at which point the rail is 200px, not 56px.
Option 3 — Header nav + persistent inspector ("editor")
Option 1's header, plus a right-hand inspector that replaces today's pop-over trace panel. Provenance, the matched ARD row, the spec, and the iteration ledger stay open beside whatever is selected.
┌──────────────────────────────────────────────────────────────────────┐
│ ▪ open.csr Home Quality Design ◐ GitHub │
├──────────────────────────────────────────────────────────────────────┤
│ Reader Tables Text Templates CDISCPILOT01 · 14.3.1.3 · v001 │
├────────────────────────────────────────┬─────────────────────────────┤
│ │ TRACE │
│ document / table │ adae sha256:7d42… │
│ │ analysis.yaml v001 │
│ │ ARD row: any_ae / n / … │
│ │ ───────────────────── │
│ │ [ edit spec ] [ diff ] │
└────────────────────────────────────────┴─────────────────────────────┘
- For: this is what the application becomes once spec editing lands (#113 increment B). The inspector is the natural home for an edit and its regenerated ARD diff, and it makes the trace permanent rather than modal.
- Against: costs the same horizontal width as the rail, and it is premature — there is nothing to edit yet, and a pop-over trace panel is adequate for a reader. Building it now would design the editor before we know what the editor needs.
4. Decision
Ship Option 1. Treat Option 3 as the target once the spec editor exists.
Horizontal width is the constraint that decides it. A CSR's widest artifacts — the SOC/PT adverse-event table, the serious-AE listing, a 13-column ARD — are the things a statistician came to look at, and both Option 2 and Option 3 take width away from them to buy chrome that is not yet earning its place. Option 1 gives the content everything and still fixes all three faults in §1.
Option 3 is not rejected, only sequenced: when an edit produces a diff that needs somewhere to live, the inspector earns its width, and Option 1's header survives into it unchanged. That is why the recommendation is Option 1 and Option 3 rather than Option 1 or Option 3 — they are the same header with one panel added.
Option 2 is rejected outright.
5. What Option 1 specifies
The site strip keeps the brand, the documentation surfaces (Home, Quality, Design & Research), the theme toggle and the source link. It is site furniture and it does not change between pages.
The app strip is new and appears only on the demo:
- Views, left. Reader · Tables · Text · Templates. Each one is a real link to its standalone page (
../reader/index.html,../gallery/index.html,../text/index.html,../templates/index.html), upgraded by the client into an in-place pane switch through the same link-interception rule the panes already use. With JavaScript off they are four working links to four working pages. The current view is marked witharia-currentand a rule under the label, not a filled tab — this is navigation, not a control. - Context, right. A monospace readout of the current selection, updated live: study, then the assigned display number and slug, then the iteration and the short ARD hash when a display is selected. Monospace because these are identifiers, not prose. It collapses to the study alone below 900px.
No page title, no lede. The content starts at the top of the pane. The Reader pane opens on the assembled document; the Tables pane opens on a display; there is nothing to explain first.
Density. The app strip is 44px, the site strip 56px. Content gets everything below.
6. Revision: the explorer (2026-07-25, same day)
@jwildfire reviewed §4 and overrode it: the application gets a persistent left sidebar listing the contents of the study, file-explorer style. That is closer to Option 2 than to Option 1, and the recommendation in §4 was wrong about one thing — it weighed width against a nav rail, when what the product actually needs on the left is a content tree, which is a different and far more valuable use of the same pixels. A rail that only switches four views does not earn 200px. A tree that lists every document, display and text block does, because it replaces navigation and the in-pane display picker, and it is the surface a second study or a second document would extend.
The shape, from his direction:
┌──────────────────────────────────────────────────────────────────────┐
│ ▪ open.csr Home Quality Design ◐ GitHub │
├──────────────────────────────────────────────────────────────────────┤
│ CDISCPILOT01 · 14.3.1.3 · t-ae-common · v002 Templates │Read│Edit│
├──────────────────┬───────────────────────────────────────────────────┤
│ CDISCPILOT01 │ │
│ cut-off 2014-… │ │
│ ▾ DOCUMENTS 2 │ content │
│ Clinical Stu… │ │
│ Statistical… │ │
│ PLANNED │ │
│ ▸ DISPLAYS 6 │ │
│ ▸ TEXT 14 │ │
└──────────────────┴───────────────────────────────────────────────────┘
Four decisions this encodes:
The study is the root. Everything under it belongs to it. A second study becomes a second root, not a second application.
Documents is a collection, not a page. The CSR is one document; the ICH E3 Annex I synopsis is another; a SAP would be a third. The registry is
library/templates/itself (#32): every template object the assembler has built becomes a document here, with its own reader page, its own document-model page and its own entry in this tree, without being named anywhere in the site build.site/config.json→documentscarries editorial metadata — a nicer title, an abbreviation, a blurb — and the documents that are planned and so have no template object to discover. It is a merge, never a gate: a template object the config has never heard of still publishes, titled from its own model, with a build warning naming it. Planned documents are listed and marked rather than hidden, because the point is to show the shape.The app renders one document at a time. The document on screen keeps its section tree and its in-app selection; every other document is an ordinary link to its own reader page, so it works with JavaScript off and never offers a selection that would resolve to nothing.
Displays sit beside documents, not inside one. The same display can be referenced by more than one document — the AE overview belongs in the CSR and would belong in an ISS. So a display records which documents use it instead of living under one of them.
Templates stays in the header. A template describes what a report of that kind is, across every study, so it does not hang off one study in the tree.
Vocabulary: Reader → Documents, Tables → Displays (tables,
figures and listings). The emitted directories keep their v0 names — /reader/
and /gallery/ are what the evidence pages and the trace panel already link to,
and renaming them would break every one of those links to change a label.
Read / Edit sits at the right of the application strip. Edit is genuinely disabled, not merely styled that way: there is nothing to edit until the spec editor lands (#113 increment B), and a control that looks live but does nothing is worse than one that says so.
The width objection from §4 still stands and is handled, not dismissed: the explorer collapses below 950px, where wide tables need the room more than a permanently visible tree does, and the app page drops the 74rem measure the documentation pages read at.
The document's own contents moved into the tree too. The reader carried a separate table of contents beside the explorer — two navigation columns asking the same question on different axes. A document's top-level sections are now the level below the document, so there is one place to navigate from. A section that E3 models but this report does not fill is still listed and still navigable: the heading really is in the document, saying so. A top-level section counts as populated when anything beneath it is, because E3 puts the content in subsections — 12.2.1 carries the AE summary, not section 12.
One thing this exposed: jumping between sections of a ~35,000px document with
scrollIntoView({behavior: 'smooth'}) stalls in Chrome — the page never
arrives. Distance now decides the behaviour, so a nearby target animates and a
jump across the document lands immediately, which reads better anyway.
Option 3's inspector is unaffected by this revision — it remains the target for when spec editing produces a diff that needs somewhere to live.
7. What this does not change
The panes themselves, the shared selection, the link-interception rule, and the standalone permalinks are all unchanged — this is a shell replacement, not a rewrite. /gallery/, /reader/, /text/ and now /templates/ remain the addressable permalinks and the destinations the evidence pages and trace panels link to.
8. Revision: one state, not two (2026-08-26)
§6 gave every explorer node with children a disclosure button, on the reasoning that expanding a node and selecting it are different intentions. In the tree that shipped they are not, and holding them apart is what made the sidebar wrong.
The Documents view has exactly one document open at a time — that is the pane's rule, not the tree's — so "is this document's contents showing?" already had an answer. The disclosure button gave it a second one, remembered per node for the browsing session and never reconciled with the first. The stylesheet only ever revealed the section list next to the current document, so the two answers produced two visible faults: the arrow on a document that was not open moved and revealed nothing, and the arrow on the document that was open hid its contents. @jwildfire, on the live demo: "Sidebar logic is slightly wonky. you can toggle without showing the controls."
The fix is a subtraction rather than a cleverer arrow. The open document shows
its table of contents; every other document shows its title alone; selecting a
different document moves the table of contents with it. What is left is one rule,
.nav-item[data-current] + .nav-sections, and no second control able to disagree
with it. The button, its is-collapsed class, its per-node sessionStorage key
and its stylesheet rules all left with it.
Three things deliberately did not change:
- Group-level collapse stays. Which collection you are looking at — DOCUMENTS, DISPLAYS, TEXT, VALUES — is a genuinely separate question from which document is open, it has its own caret, and it is remembered for the browsing session. That state was never wonky.
- The unpopulated-section greying stays. A section a template declares but does not populate renders dimmed, navigable and tooltipped. That is the framework telling the truth about the difference between declaring and filling, and this revision does not touch it.
- Deep links still open the document they name.
#tab=documents&doc=…&focus=…resolves the document, swaps the panel and moves the tree's contents with it before the scroll is attempted — the order inrender()is what makes a link into a document you are not reading work at all.
Removing four buttons from the tab order costs a keyboard user nothing: the document links were always focusable, and activating one is now the whole of opening its contents.
9. Revision: the sidebar stops knowing about the CSR (2026-08-26)
The same pass turned up two more places where the shared surfaces still assumed one document, both of them the assumption §6 was written under and #36 removed from the shell.
A display listed the documents that use it, and could only ever list one.
usedIn was an array — someone anticipated more than one document — built from
the primary document's displayIndex alone, so with four template objects in the
library three of them were invisible to it. It is now built by displayUsage()
over the whole library, and it carries what each document CALLS the display, not
just that it uses it. That index feeds three surfaces at once, so the gallery, a
display's own page and the explorer cannot disagree about where a display
appears.
The explorer stated a display number, and four assemblies disagree about it.
The framework's own contract is that a display is identified by its slug and the
number belongs to the assembly: sections.yaml is the document model,
assembly.yaml is what a given report puts in it, and that split is what makes
the numbering claim possible at all. The AE overview is Table 14.3.1.2 in the
report and Table 13.4 in the synopsis. So a number on the explorer — a surface
that serves every document at once — is one report's fact printed on a
document-agnostic place, and removing it is the framework being applied rather
than a matter of taste.
Numbers stay wherever the assembly is unambiguous:
- Inside a document, where the assembly that assigned the number is the one you are reading.
- On the display's own store page, where every document is listed and each
number is stated beside the document that assigned it. That is what makes
14.3.1.2and13.4on the same page read as two documents' names for one display rather than as a contradiction — and it is the only reason a number is safe on a shared surface at all.
A display in a document now links to its store entry, mirroring the text
block's link to the Text Library exactly: a trailing reference line under the
object, naming it, linking to its own library page. The href is written in the
form resolveAppLink parses, so inside the demo it is absorbed into a pane
switch and on the standalone reader page it navigates — one link, both
behaviours. The point of the mirroring is that a reader learns one gesture rather
than two.
This document was drafted by Claude Code using Opus 5 and not yet reviewed by @jwildfire.