X · Composites
Complete interaction patterns
Composites assemble components into a whole interaction and may be domain-aware. The static layer owns their geometry, regions and states; data and behavior arrive with the framework binding. The app shell that hosts them is shown first.
One grid of five children — brand, utility, nav, app bar and content — sharing a single placement map.
It is the only arrangement whose reading order matches DOM order, so focus never jumps back up the page.
Row one is the legacy masthead (logo on white, the navy .topMenu hugging the right corner).
The nav-toggle button collapses the nav, whose rows have no icons, so the rail leaves the grid and the
app bar and content take its column; on mobile it becomes a drawer. AppBar tabs are added dynamically by
pages.
The full-screen page is shell.html, styled by
css/classic/composites/shell.css. The specimen below is copied from shell.html by
bun scripts/design-pages/build-composite-pages.mjs — edit that file, not this one;
--check fails when they drift. The same run copies its shell around every other composite's
full-screen page.
| Axis | Values |
|---|---|
.qf-shell[data-nav] |
expanded · collapsed · open (mobile drawer) |
.qf-shell__brand › .qf-shell__logo | .qf-shell__logo-placeholder |
configurable SVG logo, or the dashed “Add logo here” slot when none is set |
.qf-shell__utility › .qf-shell__links · .qf-shell__identity |
secondary destinations and the one signed-in identity; rows are .qf-nav__item, each with an icon in a .qf-tooltip-host. When the labels no longer fit on one line (a narrow window or zoom), data-compact shows the icons and moves each label into a bottom-end tooltip. At 30rem or less the links fold into a .qf-shell__more (vertical ellipsis) menu, keeping the masthead one row. The identity opens a menu: the signed-in time, Profile, the Dark theme switch (menuitemcheckbox, the menu stays open), and Sign out. The utility strip carries no switches of its own. |
.qf-nav__item[aria-current=page] · __badge · __section
|
navigation parts: the rail is
SideNavigation (C-50), placed by .qf-shell > .qf-nav
|
.qf-nav__row › .qf-nav__twisty · .qf-nav__item ·
.qf-nav__action
|
a row for what the link cannot contain: a group disclosure (.qf-nav__subitems) or a row action such as “new document”. Plain destinations stay a bare .qf-nav__item. |
.qf-appbar__tab[aria-selected] · __search · .qf-bell
|
app bar parts |
.qf-page-header · .qf-breadcrumbs |
content header: the title, subtitle and actions, with the path above them as a Breadcrumb (C-49) |
Dialogs behave like windows in an operating system. A [data-window-area] is the view area
they live in; each dialog.qf-overlay[data-window] inside it drags by its header, snaps to
the edges and corners of that area, resizes from eight grips, raises to the front when it is clicked or
focused, and minimizes to a dock. js/windows.js writes the geometry; the nine
data-snap rects, the grips, the dock and the drop-zone ghost are CSS.
In an app the area is the whole .qf-shell, not its content: windows stack at
--qf-z-window, above the nav and the app bar, and snap over them; minimized windows become
tabs in a dock along the foot of the screen, at --qf-z-dock, and snapped windows stop above
it. Menus, popovers, toasts and the narrow nav drawer stay above both.
The full-screen page is
overlay-system.html, inside
the app shell. The specimen below is copied from overlay-system.html by
bun scripts/design-pages/build-composite-pages.mjs — edit that file, not this one;
--check fails when they drift.
| Axis | Values |
|---|---|
[data-window-area] |
flag — the bounding view area windows are positioned inside; the app's .qf-shell
|
.qf-overlay[data-window] |
flag — make the dialog an OS-style window (drag · snap · resize · raise) |
[data-snap] |
left · right · top · bottom · top-left · top-right · bottom-left · bottom-right · maximized — absent is floating |
[data-state] |
active (front window) · minimized (docked; wins over active) |
[data-window-action] |
minimize · maximize · close — on a button in .qf-window-controls |
[data-ds-open-window] · [data-ds-close-window] |
opens that window id (non-modal show()) · closes the window it is in |
[data-window-title] · [data-window-icon] |
label and sprite id for the dock chip; default to the window's own title and icon |
.qf-overlay__resize[data-edge] |
n · e · s · w · ne · nw · se · sw — resize grips, injected by js/windows.js |
.qf-snap-preview[data-snap] · .qf-dialog-dock |
drop-zone ghost and the dock of minimized windows |
[data-window-minimizable] |
false — a window that must be answered (a confirmation) does not minimize |
qf-window-snap · -minimize · -restore ·
-close
|
bubbling events on the window, so a framework that renders it can follow its state |
--qf-window-dock |
the dock's height on its area, written by js/windows.js; snap rects stop above it
|
The render-props search host, and the parent of every result view. It owns the toolbar (query, filters,
view switch, saved views), the optional filter rail, the results region and the footer with pagination;
data-view picks which of its five child views renders inside __results — list,
table, gallery, kanban or two-pane. The chrome never changes with the view. DataGrid (X-03), Kanban
(X-04) and Inbox (X-12) are the same idioms used standalone, and are documented separately.
Every result resolves to an object. In list, table, gallery and kanban a result carries
data-ds-open-window and opens that object as a window (the non-modal
dialog.qf-overlay[data-window] of X-01, bounded by the shell's
.qf-content[data-window-area]); the row's primary cell, and each card's title, is a
focusable __open control so the keyboard reaches it. In two-pane a result does not open a
window: it takes aria-current="true" and the object swaps into the right pane.
The full-screen page is search.html, inside
the app shell. The toolbar follows the app's: the query (Clear, then Search on Enter), an icon-only Add
filter that takes the input fields' fill while filters apply ([data-filtered]), a sort
menu, then the layout switch; there is no filter rail, no result count and no saved views (they live in
the navigation). Each filter is a .qf-filter-row in
.qf-search__filter-rows under the toolbar, in the same bordered band: field · value · Clear
· Remove, and the last row ends with Add filter. The table view has it so far; the other views still
show the rail. The specimens below are copied from search.html by
bun scripts/design-pages/build-composite-pages.mjs — edit that file, not this one;
--check fails when they drift.
| Axis | Values |
|---|---|
.qf-search[data-view] |
list · table · gallery · kanban · two-pane (the five child views; mirrors the segmented view
switcher and the __more menu)
|
.qf-search[data-filters] |
open · closed (rail visibility; closed hides __filters) |
__toolbar · __query · __filters ·
__results · __footer · __active-filters ·
__count
|
regions |
__gallery · __list · __amount |
gallery is a responsive card grid, list is full-width stacked cards with columns aligned across
rows (owner hides below 40rem), __amount is the right-aligned tabular figure
|
.qf-kanban inside __results |
the kanban child view: the X-04 board, grouped by status, with the search toolbar, filter rail and footer unchanged around it |
__pane-list · __row · __row-icon ·
__row-title · __row-subtitle · __row-amount ·
__row-status · __detail · __detail-body ·
__heading
|
the two-pane child view: results left, the selected object right.
__row[aria-current='true'] is the selection (tinted, primary rule, link-coloured
title); __detail is a .qf-object with its own scrolling body
|
__open · [data-ds-open-window] |
every result returns an object: __open is the focusable control in a row's primary
cell or a card's title, and the result surface carries the window opener
|
.qf-saved-views |
scrollable chip row of saved views |
__more · __filters-label |
narrow toolbar (container query): count and Filters label hide below 54rem, saved views move
into the __more menu below 46rem, the five-button view switcher below 30rem — the
__more menu lists all five views, so none is ever unreachable
|
| Select | Actions | |||||
|---|---|---|---|---|---|---|
| 2h ago | jdJane Doe | Approved | £4,250.00 | |||
| Yesterday | akAlex Kim | In review | £980.00 | |||
| 3 days ago | mrMaya Rossi | Open | £12,080.10 | |||
| Last week | tsTom Sato | Rejected | £2,100.00 | |||
| Last week | jdJane Doe | Approved | £640.00 | |||
| 2 weeks ago | akAlex Kim | Open | £7,315.40 | |||
| 3 weeks ago | mrMaya Rossi | In review | £1,560.00 | |||
| Last month | tsTom Sato | Approved | £3,020.75 |
Table plus column persistence, a column picker and infinite load. Adds a toolbar, a sticky-header scroll body, the picker popover and the load-more sentinel.
The specimen below is copied from
datagrid.html, which shows it inside the
app shell, by bun scripts/design-pages/build-composite-pages.mjs — edit that file, not this
one; --check fails when they drift.
| Axis | Values |
|---|---|
.qf-grid · __body · __sentinel ·
__drop-indicator
|
regions and reorder feedback |
.qf-column-picker |
List of toggles with drag handles |
| Approve invoice | INV-2026-0412 | jdJane Doe | Tomorrow | High |
| Index batch | Batch #2041 | akAlex Kim | Today | Medium |
| Review contract | MSA-2026 | mrMaya Rossi | 3 days | Low |
| Assess claim | CS-2041 | Unassigned | Next week | High |
| Archive statements | Finance / 2025 | tsTom Sato | Next month | Low |
| Approve invoice | INV-2026-0398 | jdJane Doe | Next month | Medium |
| Verify identity | Person 10442 | akAlex Kim | Next month | Low |
Columns derived from a list-of-values property; cards are compact Cards wrapped in a DragSource. Cross-column drag moves the item and updates the property.
The specimen below is copied from
kanban.html, which shows it inside the app
shell, by bun scripts/design-pages/build-composite-pages.mjs — edit that file, not this
one; --check fails when they drift.
| Axis | Values |
|---|---|
.qf-kanban__column[data-state=over] |
drop target highlight |
.qf-kanban__card[data-state] |
dragging · overlay |
__header · __swatch · __count · __cards ·
__placeholder · __footer
|
parts |
Dispatches a FieldType to the right control. Visually it adds only the property-type icon
in the label; filter mode lays Fields out in a compact wrapping row, and click-to-edit mode uses Field's
rest state.
The specimens below are copied from
mapper.html, which shows them inside the app
shell, by bun scripts/design-pages/build-composite-pages.mjs — edit that file, not this
one; --check fails when they drift.
| Axis | Values |
|---|---|
FieldType |
String · Date · DateTime · Boolean · Numeric · Currency · User · UserMultiple · Classification · Hierarchy · ListOfValues · LinkedObject · RichText · File … |
.qf-field__type-icon · .qf-filter-row |
label icon / filter-mode layout |
Lexical rich-text chrome: a Field-like frame with a formatting Toolbar, a content surface with typographic defaults and a status footer.
The specimen below is copied from
editor.html, which shows it inside the app
shell, by bun scripts/design-pages/build-composite-pages.mjs — edit that file, not this
one; --check fails when they drift.
Documents that are not plain rich text open in qf-office, the QEditor host wrapper, as a
standalone dialog.qf-overlay[data-window] window rather than in this surface. The editor
runs in an iframe (qf-office__embed) and draws its own ribbon, canvas and status bar (office mockups); the window header is its title bar, with the app glyph, the file name and the save state. See
DocumentViewer for the full qf-office axis
table and OverlaySystem for the window
contract (data-window, data-snap, data-ds-open-window).
| Axis | Values |
|---|---|
.qf-editor · __toolbar · __surface ·
__placeholder · __footer
|
regions |
[data-mention] |
inline mention token |
.qf-office[data-kind] · [data-mode] · [data-state] |
the QEditor host wrapper — documented under DocumentViewer |
Assessment notes
Visited the property with @Alex Kim on 14 Aug. The kitchen ceiling shows water staining consistent with the tenant's report; see photo set 3.
- Source: upstairs bathroom, failed seal
- Estimated repair:
£4,250
Recommend approval subject to a second quote.
Dispatches office files, PDF, image and email to one frame: viewer toolbar, thumbnail rail, canvas and a properties rail. Email swaps the canvas for a header/body/attachments layout.
The specimens below are copied from
viewer.html, which shows them inside the app
shell, by bun scripts/design-pages/build-composite-pages.mjs — edit that file, not this
one; --check fails when they drift.
qf-office is the host wrapper QAction opens office files in. The QEditor editor runs in an
iframe (__embed) and draws its own ribbon, canvas and status bar — the
office mockups (X-13 OfficeApp). The window
header is the editor's title bar (owner decision Q1): the app glyph and name, the file name, the save
state and the window controls. A file therefore opens as a standalone window rather than in place.
The window is an OverlaySystem window: a
non-modal dialog.qf-overlay[data-window] driven by js/windows.js — dragged,
resized, raised and snapped (data-snap) inside the nearest [data-window-area],
which on these pages is the shell's .qf-content. Each file row carries
data-ds-open-window="viewer-office-window", so clicking it opens or raises the window. The
title bar carries the file name, the kind chip, a save indicator and the standard
.qf-window-controls.
| Axis | Values |
|---|---|
.qf-viewer[data-kind] |
pdf · image · office · email |
[data-thumbs] · [data-props] |
hidden (collapse a rail) |
__toolbar · __thumbs · __thumb[aria-current] ·
__canvas · __sheet · __props · __page
|
regions |
.qf-office[data-kind] |
document · spreadsheet · presentation · pdf — picks the sprite icon and title-bar tint |
.qf-office[data-mode] |
edit · review · view (the editor drops its ribbon panel) |
.qf-office[data-state] |
ready (default) · loading · error |
.qf-office__embed · __placeholder · __fallback |
the editor iframe · boot skeleton · unsupported state with a Download action |
__app · __glyph · __title · __filename
|
the title bar in the window header: app glyph and name, the file name button (file flyout) |
__brand · __meta · __save[data-state] |
kind icon, kind chip, saved · saving · unsaved · offline · error |
Three levels wrap an EmptyState[variant=error]. Dialogs get one automatically. Component-level keeps the rest of the page alive.
The specimen below is copied from
error-boundary.html, which
shows it inside the app shell, by bun scripts/design-pages/build-composite-pages.mjs — edit
that file, not this one; --check fails when they drift.
| Axis | Values |
|---|---|
.qf-error-boundary[data-level] |
app · page · component |
__details · __stack |
collapsible technical detail |
e7f2-91ab has been reported.
Technical details
TypeError: Cannot read properties of undefined (reading 'label')
at ObjectProperties (object-properties.tsx:112)
at renderWithHooks (react-dom.development.js:16305)
Bell with unread badge; a Popover holding a grouped, sectioned List. The bell in the app shell above opens the same popover.
The specimen below is copied from
notifications.html, which shows
it inside the app shell, by bun scripts/design-pages/build-composite-pages.mjs — edit that
file, not this one; --check fails when they drift.
| Axis | Values |
|---|---|
.qf-bell · __badge |
trigger and unread count |
.qf-notifications · .qf-list__item[data-unread] · __time
|
panel parts |
- Today
- jdJane Doe assigned you a taskApprove invoice INV-2026-0412 · due tomorrow2h
- Batch #2041 finished indexing14 pages · 0 errors4h
- Yesterday
- 3 tasks are overdueIn the Finance queue1d
- Alex Kim mentioned you"…can @Jane confirm the amount?"1d
- jdJane Doe assigned you a taskApprove invoice INV-2026-04122h
- Batch #2041 finished indexing14 pages · 0 errors4h
Form shell: sections with titles, a 12-column row grid, an error summary and a sticky action bar. Field binding and validation come from react-hook-form in the binding.
The specimen below is copied from
form.html, which shows it inside the app shell,
by bun scripts/design-pages/build-composite-pages.mjs — edit that file, not this one;
--check fails when they drift.
| Axis | Values |
|---|---|
.qf-form · __section · __section-title ·
__section-hint · __row · __actions[data-sticky] ·
__errors
|
regions |
[data-span] |
3 · 4 · 6 · 8 · (12) columns; collapses to full width on mobile |
Display-definition parsing, tabbed property groups and click-to-edit values. The header shows icon, label, type and actions; each tab is a grid of properties whose values become Fields on click. LabeledObject and LabeledUser chips resolve labels automatically.
Every result resolves to an object. Clicking a result in any view of
Search opens this block; only the presentation changes,
and data-presentation is the axis that says which. A two-pane view (the
Inbox, and Search’s own two-pane view) shows it in
place in the right pane, data-presentation="pane", beside the list it was selected from.
Every other view — table, cards, list, tree — opens it as an OS-style window:
dialog.qf-overlay[data-window] inside the view’s [data-window-area],
launched by [data-ds-open-window] and dragged, snapped and docked by the
OverlaySystem window layer. In that presentation
the window frame carries the object’s icon and title, so the block drops its own and keeps a
compact meta strip; the secondary header actions collapse into an overflow menu and the body scrolls on
its own inside the frame.
The specimen below is copied from
object.html, which shows it inside the app
shell, by bun scripts/design-pages/build-composite-pages.mjs — edit that file, not this
one; --check fails when they drift.
| Axis | Values |
|---|---|
.qf-object · __header · __icon · __label ·
__meta · __actions
|
header |
.qf-properties · .qf-property[data-span=full] · __label ·
__value[data-empty]
|
property grid; hover reveals the edit pencil |
.qf-labeled |
LabeledObject / LabeledUser chip |
data-presentation |
pane — framed, in place, filling the right pane of a two-pane view ·
window — inside dialog.qf-overlay[data-window], which
carries the icon and title instead. Absent: the bare block, laid out by whatever holds it.
|
__heading · __body · __footer ·
__overflow
|
title column (label + meta), the scrolling region, a pinned action bar, and the menu button the icon-only header actions collapse into in the window presentation |
A Search (X-02) with its own treatment: every result set is a canned search, and there is no filter
rail. The app bar tabs (Active · Completed · Delegated) are the canned searches. Saved searches reuse
the same two panes: a task list on the left and the selected task on the right. Pagination or Load more
sits under the list. Saved searches are a menu button in the toolbar; the
.qf-saved-views chip row stays in the markup but is hidden.
The right pane has two presentations. Which one shows is a user setting, not a control on the page:
Open inbox tasks as in the identity menu, beside the theme, sets
.qf-inbox[data-view]. Task view is task-centric. Its header is the task
(process, step, status and the object it is on) on a tinted band, with the task’s facts under it:
Due, Received, Assigned to, Working user and Last action. The v2 API edits none of those facts, so they
are read-only; the header’s buttons are the state changes it does allow, Put on hold and Put back.
The body groups the rest into cards: Instructions, the object (its fields and initiating document) and
the object’s notes. The footer completes the task with an outcome, the default one filled and one
with a note in More, or offers Work task while the task waits in a queue. Object view
is how the product works today: the object is the header, then its fields and its activity tabs (Active
Task · Documents · Notes · Timeline · Workflow · Related) opened on Active Task, where the task is
worked through Instructions, Completion Options, Note and Submit.
Everything a task shows comes from the v2 task (status, dueDate and daysOverUnder, startDate, assigneeId, workingUserId, processName, lastAction, instructions, initiatingDocumentVSId, availableCompleteTaskActions) or from a lookup it names: user and group labels, the object’s label and fields, and the object’s notes, which have a subject and a date but no author. The API has no priority, sender, unread state, step position or delegation, so the Inbox shows none. List rows carry only list data: the task name, process and object type, due state and status.
The Inbox is the two-pane case of the rule that
every result resolves to an object: selecting a task in the
left list shows that task’s .qf-object[data-presentation="pane"] on the right, and
the row it came from carries aria-selected="true" (aria-current="true" is
styled the same way, for two-pane views whose list is not a listbox). The right pane is not a bespoke
reader: it is the ObjectDisplay block with the Inbox’s own sections composed into its
__body. A view that is not two-pane opens the same object as a window instead — see
ObjectDisplay.
The full-screen page is inbox.html, inside the
app shell and styled by css/classic/composites/inbox.css. The specimens below are copied
from inbox.html by bun scripts/design-pages/build-composite-pages.mjs — edit that file, not
this one; --check fails when they drift.
| Axis | Values |
|---|---|
.qf-search.qf-inbox |
two-pane Search: toolbar across the top, task list and footer on the left, detail on the right; no filter rail |
__tasks · __task |
scrolling list pane (.qf-list[data-divided][data-interactive], listbox) and one
task row; aria-selected (or aria-current) marks the task whose object
the right pane is showing
|
__due[data-state] |
overdue · due-soon (none: muted) |
__saved · __saved-label · __saved-chevron |
saved-searches menu button, soft with the search name while one is applied: labelled (icon only below a 36rem toolbar) |
[data-view] |
task · object: which detail pane shows, from the user’s Open inbox tasks as setting ([data-inbox-view]
items in the identity menu)
|
__detail[data-view] |
the right pane, one per view: .qf-object[data-presentation="pane"] placed in the
grid, holding .qf-object__header, __heading, __body and
__footer; __eyebrow above the label names what the pane is (Task ·
process, or the object type)
|
__facts · __fact |
Task view: the task’s read-only facts in a band under the header; the header’s own buttons are the state changes (Put on hold · Put back), the footer the outcomes or Work task |
__group[data-tone] · __document · __notes |
Task view: a card in the sunken body (info: the instructions); the initiating-document row inside the object card; the object’s notes, subject and date |
__activities · __active-task |
Object view: the object’s activity tabs, and the Active Task panel
(__active-head, __task-name, __ask,
__active-actions)
|
- Approve invoiceInvoice approval · Invoice2d overdueIn progress
- Review contractContract review · ContractDue todayNot started
- Verify bank detailsVendor onboarding · Vendor5d overdueReturned
- Approve expense claimExpense approval · Expense claim18 SepNot started
- Sign NDALegal signature · Agreement19 SepNot started
- Correct index fieldsScan indexing · DocumentTomorrowReturned
- Approve purchase orderPurchase approval · Purchase order22 SepOn hold
- Review claim assessmentClaims · Claim24 SepPut back
- Confirm delivery receiptGoods receipt · Goods receiptNo due dateNot started
Approve invoice
In progress- Due
- 13 Sep · 2d overdue
- Received
- 11 Sep 09:14
- Assigned to
- Accounts payable
- Working user
- You
- Last action
- Started 14 Sep
Instructions
INV-2026-0412
- Supplier
- Acme Supplies Ltd
- Amount
- £4,250.00
- Invoice date
- 09 Sep 2026
- Purchase order
- PO-7731
Notes
on this invoice-
Freight line
-
Scanned
INV-2026-0412
Approve invoice
Overdue +213 Sep 2026Accounts payable
Instructions
Note
- Approve invoiceInvoice approval · Invoice2d overdueIn progress
- Review contractContract review · ContractDue todayNot started
- Verify bank detailsVendor onboarding · Vendor5d overdueReturned
- Approve expense claimExpense approval · Expense claim18 SepNot started
- Sign NDALegal signature · Agreement19 SepNot started
- Correct index fieldsScan indexing · DocumentTomorrowReturned
- Approve purchase orderPurchase approval · Purchase order22 SepOn hold
- Review claim assessmentClaims · Claim24 SepPut back
- Confirm delivery receiptGoods receipt · Goods receiptNo due dateNot started
Approve invoice
In progress- Due
- 13 Sep · 2d overdue
- Received
- 11 Sep 09:14
- Assigned to
- Accounts payable
- Working user
- You
- Last action
- Started 14 Sep
Instructions
INV-2026-0412
- Supplier
- Acme Supplies Ltd
- Amount
- £4,250.00
- Invoice date
- 09 Sep 2026
- Purchase order
- PO-7731
Notes
on this invoice-
Freight line
-
Scanned
INV-2026-0412
Approve invoice
Overdue +213 Sep 2026Accounts payable
Instructions
Note
- tsApprove purchase orderPurchase approval · Purchase order17 SepIn progress
- Approve invoiceInvoice approval · InvoiceDue todayIn progress
- Approve capital requestCapex approval · Capital request21 SepNot started
- akReview contract valueContract review · Contract1d overdueReturned
- jdApprove payment runPayment run · Payment run25 SepOn hold
- Approve credit noteCredit note approval · Credit note26 SepNot started
- Sign off accrualsMonth-end close · Journal30 SepPut back
Approve capital request
Not started- Due
- 21 Sep
- Received
- 17 Sep 16:02
- Assigned to
- Finance team
- Working user
- Nobody yet
- Last action
- Received 17 Sep
Instructions
CAP-114 Forklift
- Requested by
- Ben Okafor
- Amount
- £42,000.00
- Budget line
- CAP-2026-OPS
- Quotes
- 2 attached
Notes
on this capital requestNo notes yet.
CAP-114 Forklift
Approve capital request
Due in 4 days21 Sep 2026Finance team
Instructions
The QEditor editors as full-screen pages in a maximized qaction window: OfficeApp (X-13), Backstage (X-14), DocumentCanvas (X-15), Workbook (X-16), Deck (X-17), PdfDocument (X-19). Each opens on its own page with a review strip for theme, ribbon mode and width.