Summary#
Dock arranges a page around what it has SELECTED: the list or table stays in the middle, the thing a person opened
docks on the right without covering it, and an ambient stream — a log, a console, a timeline — runs along the bottom.
Both drawers are opened by the page (it is what knows what is selected), closed by their own ×, and resized by
dragging their edge or with the arrow keys; the size is remembered.
class Job { public string Ref; public string Title; public string State; }
List<Job> Jobs() {
return [
new Job { Ref = "J-104", Title = "Rebuild the search index", State = "Running" },
new Job { Ref = "J-103", Title = "Rotate the signing keys", State = "Queued" },
];
}
[Route("/jobs")] [Render(CSR)] [AllowAnonymous]
component JobsPage() {
string open = "";
bool log = true;
action Open(Job j) { open = j.Ref; }
action Close() { open = ""; }
action CloseLog() { log = false; }
render {
Box(h: Length.Screen) {
Dock(right: open != "", rightTitle: "Job " + open, onCloseRight: Close,
bottom: log, bottomTitle: "Log", onCloseBottom: CloseLog) {
DataGrid(label: "Jobs", rows: Jobs(), rowSelected: Open, columns: [
new GridColumn<Job> { Name = "Ref", Label = "Job", Value = j => j.Ref, Width = 90 },
new GridColumn<Job> { Name = "Title", Label = "Title", Value = j => j.Title, Fill = true },
new GridColumn<Job> { Name = "State", Label = "State", Value = j => j.State, Width = 110 },
]);
slot right { Stack(gap: 2, p: 4) { Text(Jobs().First(j => j.Ref == open).Title, fontWeight: FontWeight.Semibold); } }
slot bottom { Text("10:24:07 index shard 3 of 8 rebuilt", fontFamily: Font.Mono, fontSize: FontSize.Code); }
}
}
}
}Signature#
Dock(bool right = false, bool bottom = false, string rightTitle = "Details", string bottomTitle = "Panel",
Action onCloseRight = null, Action onCloseBottom = null) { <the page>; slot right { … } slot bottom { … } }Description#
Who opens a drawer?#
The page. right: and bottom: are plain booleans the page computes — open != null, a toggle in its toolbar — and
onCloseRight / onCloseBottom are what the drawers' × buttons run. That keeps the decision where the knowledge is:
the page knows which row is selected; the dock only knows how to show it.
How big is it, and does it stay that size?#
A right drawer starts 380px wide and a bottom one 240px tall. Each edge is a handle: drag it, or focus it and use the
arrow keys (Escape puts it back where the drag started). The size is [Remembered], so it survives a reload and the
next visit, per browser.
What happens on a phone?#
Below Laptop the two drawers part ways. The right one is what the person OPENED, so it takes the screen as a sheet
(its own close, and Escape) and closes back to the list. The bottom one is ambient, so it follows the page in the
flow under its title instead of burying the page the moment it opens.
Where does the page scroll?#
Give Dock the page's height — WorkbenchShell does, as does any shell with Panes = true, or a Box(h: Length.Screen) — and the middle scrolls on its own while the drawers stay put. Each drawer scrolls inside itself.
How do I drive it in a test?#
The drawers are regions named by their titles: Assert.Visible("Rebuild the search index", within: "Job J-104"),
and their close is Ui.Click("Close Job J-104"). Give each drawer a title that differs from anything in the table —
"Job J-104" rather than "J-104" — or a scope naming it matches the row too.
See also#
- WorkbenchShell — the shell built for a page of drawers
- Save, discard, undo — three ways to show a page's unit of work —
SaveActions, the save that belongs in a drawer's head - Arrangements — five ways to build a whole app, and which look wears each — the record desk and the workbench, both built on
Dock - drag — the gesture the edges use