@umriss-ui/dashboard 0.0.0-stage → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,964 @@
1
+ # @umriss-ui/dashboard 0.40.0
2
+
3
+ > React dashboard for data-dense applications - widgets a reader arranges, packed in order on twelve columns.
4
+
5
+ Install with `npm install @umriss-ui/dashboard @umriss-ui/core`. This text is generated from the package's demo (https://romanhaendler.github.io/umriss-ui/dashboard/): every page with its import line, its examples - the source exactly as it runs, with the package name where the demo imports its own source - its props tables generated from the code, and what it deliberately does not do. The pages index stands in https://romanhaendler.github.io/umriss-ui/dashboard/llms.txt.
6
+
7
+ ## Scenarios
8
+
9
+ Composed, realistic screens built from the package. A numbered mark on the screen is an element with `data-callout`.
10
+
11
+ ### Arrange the start screen
12
+
13
+ A dispatcher at Ferrow Parcel opens the same start screen every day and wants the figure she looks at fifty times a day first. Four cases walk through what the screen does at other widths, while she rearranges it, when something goes wrong and what the application stays in charge of.
14
+
15
+ 1. The case: each one starts the dashboard anew, with the arrangement the application chose for the role.
16
+ 2. What the application does - the window it is opened in, a navigation away, a stored value from an old release, a lock - as buttons. What the reader does stands in the sentence beside them: the real controls are in the dashboard.
17
+ 3. What happened, in a sentence.
18
+ 4. The dashboard itself. It measures its own width, so the stage narrows and widens it without a window.
19
+ 5. What the application was told: once per "Done" that changed something, with the whole arrangement.
20
+
21
+ Built from: Dashboard, Chart, SegmentedControl, Alert, Text.
22
+
23
+ ```tsx
24
+ import { useRef, useState } from "react";
25
+ import type { CSSProperties, ReactNode } from "react";
26
+ import { useChart } from "@umriss-ui/charts";
27
+ import { Alert, Button, SegmentedControl, Stack, Text } from "@umriss-ui/core";
28
+ import { HeadingType, useDashboard } from "@umriss-ui/dashboard";
29
+ import type { DashboardView, HeadingOptions } from "@umriss-ui/dashboard";
30
+
31
+ /* Data from the logistics world, written out here so the example runs on its own. */
32
+
33
+ type Verdict = "ok" | "warn" | "bad";
34
+
35
+ const MARK: Record<Verdict, CSSProperties> = {
36
+ ok: { background: "var(--u-color-success)", borderRadius: "50%" },
37
+ warn: { background: "var(--u-color-warning)", borderRadius: 2 },
38
+ bad: { background: "var(--u-color-danger)", transform: "rotate(45deg)" },
39
+ };
40
+
41
+ const hairline = "1px solid var(--u-hairline)";
42
+
43
+ /** A status mark whose shape says it too: circle, square, diamond. */
44
+ function Dot({ verdict }: { verdict: Verdict }) {
45
+ return <span aria-hidden="true" style={{ flex: "none", width: 8, height: 8, ...MARK[verdict] }} />;
46
+ }
47
+
48
+ /** A figure: a large value with its unit, and a status line under it. */
49
+ function Kz({ value, unit, verdict, note }: { value: string; unit?: string; verdict: Verdict; note: string }) {
50
+ return (
51
+ <div style={{ display: "flex", flexDirection: "column", justifyContent: "center", gap: "var(--u-space-1)", height: "100%", minWidth: 0 }}>
52
+ <Text as="div" size="2xl" weight="semibold" leading="tight" style={{ fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" }}>
53
+ {value}
54
+ {unit && <Text as="span" size="sm" tone="muted" style={{ marginInlineStart: "var(--u-space-1)", fontWeight: "var(--u-weight-regular)" }}>{unit}</Text>}
55
+ </Text>
56
+ <Text as="div" size="xs" tone="secondary" style={{ display: "flex", alignItems: "center", gap: 6, whiteSpace: "nowrap" }}>
57
+ <Dot verdict={verdict} />
58
+ {note}
59
+ </Text>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ /** The trend: this week as a line with a faint area, last week dashed and muted, on one scale; with `previous` the legend too. */
65
+ function TrendChart({ data, previous, labels = ["this week", "last week"] }: { data: readonly number[]; previous?: readonly number[]; labels?: readonly [string, string] }) {
66
+ const rows = data.map((now, i) => ({ i, now, before: previous?.[i] ?? now }));
67
+ const { Chart, XAxis, YAxis, Line, Area } = useChart(rows);
68
+ const swatch = (color: string, dashed = false): CSSProperties => ({ display: "inline-block", width: 14, borderTop: `2px ${dashed ? "dashed" : "solid"} ${color}`, verticalAlign: "middle", marginInlineEnd: 5 });
69
+ return (
70
+ <div style={{ display: "flex", flexDirection: "column", gap: "var(--u-space-2)", height: "100%", minWidth: 0 }}>
71
+ {previous && (
72
+ <Text as="div" size="xs" tone="secondary" style={{ display: "flex", gap: 14 }}>
73
+ <span><i style={swatch("var(--u-color-accent)")} />{labels[0]}</span>
74
+ <span><i style={swatch("var(--u-color-text-muted)", true)} />{labels[1]}</span>
75
+ </Text>
76
+ )}
77
+ <div style={{ flex: "1 1 0", minHeight: 0 }}>
78
+ <Chart ariaLabel={previous ? `${labels[0]} and ${labels[1]}` : labels[0]} padding={4} style={{ minHeight: 0, height: "100%" }}>
79
+ <XAxis value="i" domain="data" grid={false} tickFormat={() => ""} />
80
+ <YAxis grid tickFormat={() => ""} />
81
+ {previous && <Line value="before" name={labels[1]} color="var(--u-color-text-muted)" strokeWidth={1.25} dash={[3, 3]} markers="never" />}
82
+ <Area value="now" name={labels[0]} color="var(--u-color-accent)" fillOpacity={0.09} strokeWidth={1.75} />
83
+ </Chart>
84
+ </div>
85
+ </div>
86
+ );
87
+ }
88
+
89
+ /** Rows under each other, a hairline between them: a mark, a text, what belongs to it at the end. */
90
+ function Rows({ rows }: { rows: ReadonlyArray<{ verdict: Verdict; text: string; meta?: string }> }) {
91
+ return (
92
+ <ul style={{ listStyle: "none", margin: 0, padding: 0 }}>
93
+ {rows.map(({ verdict, text, meta }, i) => (
94
+ <li key={text} style={{ display: "flex", alignItems: "center", gap: "var(--u-space-3)", padding: "6px 0", borderTop: i ? hairline : undefined }}>
95
+ <Dot verdict={verdict} />
96
+ <Text as="span" size="sm" style={{ flex: "1 1 0", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{text}</Text>
97
+ {meta && <Text as="span" size="xs" tone="muted" style={{ whiteSpace: "nowrap" }}>{meta}</Text>}
98
+ </li>
99
+ ))}
100
+ </ul>
101
+ );
102
+ }
103
+
104
+ /** A table with a muted head and a hairline between the rows; `numeric` columns stand right. */
105
+ function DataTable({ head, rows, numeric = [] }: { head: readonly string[]; rows: ReadonlyArray<readonly ReactNode[]>; numeric?: readonly number[] }) {
106
+ const align = (i: number): CSSProperties => ({ textAlign: numeric.includes(i) ? "right" : "left", paddingBlock: 5, paddingInline: "0 var(--u-space-2)", whiteSpace: "nowrap" });
107
+ return (
108
+ <table style={{ width: "100%", borderCollapse: "collapse", fontSize: "var(--u-text-sm)", fontVariantNumeric: "tabular-nums" }}>
109
+ <thead>
110
+ <tr>
111
+ {head.map((name, i) => (
112
+ <th key={name} style={{ ...align(i), color: "var(--u-color-text-muted)", fontSize: "var(--u-text-xs)", fontWeight: "var(--u-weight-medium)" }}>{name}</th>
113
+ ))}
114
+ </tr>
115
+ </thead>
116
+ <tbody>
117
+ {rows.map((cells, r) => (
118
+ <tr key={r}>
119
+ {cells.map((cell, i) => (
120
+ <td key={i} style={{ ...align(i), borderTop: hairline }}>{cell}</td>
121
+ ))}
122
+ </tr>
123
+ ))}
124
+ </tbody>
125
+ </table>
126
+ );
127
+ }
128
+
129
+ const FIGURES = {
130
+ Orders: { value: "1,284", unit: "per day", verdict: "ok", note: "+3.1% on last week" },
131
+ Utilisation: { value: "87.4", unit: "%", verdict: "ok", note: "on target" },
132
+ Complaints: { value: "2.9", unit: "%", verdict: "warn", note: "just above target" },
133
+ Backlog: { value: "38", unit: "", verdict: "bad", note: "target missed" },
134
+ } as const satisfies Record<string, { value: string; unit: string; verdict: Verdict; note: string }>;
135
+ type Figure = keyof typeof FIGURES;
136
+
137
+ const TREND: Record<Figure, { now: readonly number[]; before: readonly number[] }> = {
138
+ Orders: { now: [58, 52, 55, 51, 49, 53, 47, 52, 56, 60, 57, 55, 56, 58, 54, 52, 52, 49, 46, 49, 52, 55, 62, 58, 61, 55, 57, 62], before: [56, 56, 50, 53, 53, 60, 66, 68, 66, 64, 56, 52, 49, 50, 48, 49, 47, 45, 42, 38, 36, 36, 32, 30, 28, 34, 34, 30] },
139
+ Utilisation: { now: [60, 62, 66, 64, 68, 70, 72, 71, 74, 73, 76, 78], before: [58, 59, 57, 60, 61, 60, 63, 62, 64, 63, 65, 66] },
140
+ Complaints: { now: [66, 72, 70, 62, 55, 62, 68, 54, 55, 58, 50, 52, 48, 51, 49, 47, 47, 47, 47, 58, 59, 52, 50, 53, 58, 60], before: [64, 66, 62, 60, 58, 56, 55, 53, 54, 52, 50, 49, 48, 47, 46, 46, 45, 45, 45, 46, 47, 47, 46, 46, 45, 45] },
141
+ Backlog: { now: [22, 25, 31, 29, 34, 36, 38], before: [30, 28, 27, 29, 26, 25, 24] },
142
+ };
143
+
144
+ const ALERTS = [
145
+ { verdict: "bad", text: "Tour 14 is 25 min late", meta: "12 min ago" },
146
+ { verdict: "warn", text: "Dock 3 is out of service", meta: "34 min ago" },
147
+ { verdict: "warn", text: "Call: Oakridge Pharmacy", meta: "1 h ago" },
148
+ ] as const satisfies ReadonlyArray<{ verdict: Verdict; text: string; meta: string }>;
149
+
150
+ const ORDERS = [
151
+ ["A-2041", "Holloway Garden Supplies", "120", "Packed"],
152
+ ["A-2042", "Marlow & Finch Books", "490", "Picking"],
153
+ ["A-2043", "Brixley Cycles", "860", "Late"],
154
+ ];
155
+
156
+ const heading = (id: string, text: string) => ({ id, type: "heading", size: "full" as const, options: { text } satisfies HeadingOptions });
157
+
158
+ const DISPATCH: DashboardView = {
159
+ widgets: [
160
+ heading("h1", "Overview"),
161
+ { id: "f1", type: "figure", size: "small", options: { name: "Orders" } },
162
+ { id: "f2", type: "figure", size: "small", options: { name: "Utilisation" } },
163
+ { id: "f3", type: "figure", size: "small", options: { name: "Complaints" } },
164
+ { id: "f4", type: "figure", size: "small", options: { name: "Backlog" } },
165
+ heading("h2", "Trend"),
166
+ { id: "t1", type: "trend", size: "large", options: { name: "Orders" } },
167
+ { id: "a1", type: "alerts", size: "tall", options: {} },
168
+ heading("h3", "Work in hand"),
169
+ { id: "o1", type: "orders", size: "full", options: {} },
170
+ ],
171
+ };
172
+
173
+ const MANAGEMENT: DashboardView = {
174
+ widgets: [
175
+ heading("h1", "The week"),
176
+ { id: "t1", type: "trend", size: "wide", options: { name: "Utilisation" } },
177
+ { id: "t2", type: "trend", size: "wide", options: { name: "Complaints" } },
178
+ { id: "f1", type: "figure", size: "medium", options: { name: "Orders" } },
179
+ ],
180
+ };
181
+
182
+ const ROLES = { dispatch: DISPATCH, management: MANAGEMENT } as const;
183
+ type Role = keyof typeof ROLES;
184
+
185
+ /** What an older release stored: a widget type that is gone, and a size the trend no longer offers. */
186
+ const OLD = {
187
+ widgets: [
188
+ { id: "f1", type: "figure", size: "small", options: { name: "Orders" } },
189
+ { id: "x1", type: "weather", size: "small", options: {} },
190
+ { id: "t1", type: "trend", size: "huge", options: { name: "Orders" } },
191
+ { id: "t2", type: "trend", size: "wide", options: { name: "Backlog" } },
192
+ ],
193
+ } as unknown as DashboardView;
194
+
195
+ const CASES = [
196
+ { value: "monitors", label: "Other monitors" },
197
+ { value: "rearranging", label: "Rearranging" },
198
+ { value: "wrong", label: "When something goes wrong" },
199
+ { value: "governs", label: "What the application governs" },
200
+ ] as const;
201
+ type Case = (typeof CASES)[number]["value"];
202
+
203
+ const INTRO: Record<Case, string> = {
204
+ monitors:
205
+ "The arrangement of the role has three sections, each with a heading across the width, and four figures in a row at the top. Open it on other screens and see whether every widget stays beside its neighbours.",
206
+ rearranging:
207
+ "Editing is a mode and a draft. A widget's head is the grip, and its menu does everything without a drag. After \"Edit\", try it yourself: drag a head, open a menu, or Tab to a head and press an arrow key. Undo goes back one step.",
208
+ wrong:
209
+ "A drag goes wrong, and a removed widget must not be lost. Undo takes one step back, \"Cancel\" all of them, and the application decides what happens when someone leaves with a draft open.",
210
+ governs:
211
+ "The arrangement is a value the application keeps and hands over. It owes the reader a lenient reading of an old value, a frame that outlives a widget that fails, a way back to its default and the choice to lock editing.",
212
+ };
213
+
214
+ function Stage({ shown }: { shown: Case }) {
215
+ const [said, setSaid] = useState("");
216
+ const [width, setWidth] = useState<number | null>(null);
217
+ const [role, setRole] = useState<Role>("dispatch");
218
+ const [locked, setLocked] = useState(false);
219
+ const [broken, setBroken] = useState(false);
220
+ const [leaving, setLeaving] = useState(false);
221
+ const [open, setOpen] = useState(false);
222
+ const [reports, setReports] = useState<string[]>([]);
223
+ const kept = useRef<Partial<Record<Role, DashboardView>>>({});
224
+
225
+ const parts = useDashboard({
226
+ defaultView: ROLES[role],
227
+ allowEdit: !locked,
228
+ onDraftChange: setOpen,
229
+ onViewChange: (view) => {
230
+ kept.current[role] = view;
231
+ setReports((all) => [...all, `${role === "dispatch" ? "Dispatch" : "Management"}: ${view.widgets.length} widgets`]);
232
+ },
233
+ });
234
+ const { Dashboard, WidgetType } = parts;
235
+ const edit = () => {
236
+ parts.edit();
237
+ setSaid("The frame, the label \"Editing\" and the changed heads show the mode. The widgets' content rests while it lasts.");
238
+ };
239
+
240
+ const buttons: Record<Case, ReactNode> = {
241
+ monitors: [1920, 2560, 820, 390].map((px) => (
242
+ <Button
243
+ key={px}
244
+ onClick={() => {
245
+ setWidth(px);
246
+ setSaid(
247
+ px >= 1920
248
+ ? "The dashboard stands at its maximum width, at the start. The room beside it belongs to the application."
249
+ : px > 400
250
+ ? "The same twelve columns, narrower. Every neighbour stands where it stood."
251
+ : "Below the width a column can be read at, the dashboard is a list, in the same order.",
252
+ );
253
+ }}
254
+ >
255
+ {`Open at ${px} px`}
256
+ </Button>
257
+ )),
258
+ rearranging: [
259
+ <Button key="edit" onClick={edit} disabled={parts.editing}>Edit</Button>,
260
+ <Button key="undo" onClick={() => { parts.undo(); setSaid("One step back, not all of them."); }} disabled={!parts.editing}>Undo</Button>,
261
+ <Button key="catalogue" onClick={() => { parts.catalogue(); setSaid("Every widget type with its name, a sentence and the room it takes. A new widget shows something at once and stands at the end."); }} disabled={!parts.editing}>Add a widget</Button>,
262
+ <Button key="done" onClick={() => { parts.done(); setSaid("Only now, and once, the application is told the whole arrangement."); }} disabled={!parts.editing}>Done</Button>,
263
+ ],
264
+ wrong: [
265
+ <Button key="edit" onClick={edit} disabled={parts.editing}>Edit</Button>,
266
+ <Button key="undo" onClick={() => { parts.undo(); setSaid("One step back; what you did before it stays."); }} disabled={!parts.editing}>Undo</Button>,
267
+ <Button key="cancel" onClick={() => { parts.cancel(); setSaid("Everything stands as before \"Edit\". Nothing was reported."); }} disabled={!parts.editing}>Cancel</Button>,
268
+ <Button
269
+ key="leave"
270
+ onClick={() => {
271
+ if (open) setLeaving(true);
272
+ else setSaid("The application navigates. Nothing was open.");
273
+ }}
274
+ >
275
+ Navigate away
276
+ </Button>,
277
+ ],
278
+ governs: [
279
+ <Button key="old" onClick={() => { parts.cancel(); parts.setView(OLD); setSaid("The stored value named a weather widget that is gone and a trend in a size it no longer offers: the one fell out, the other stands at its starting size, and the room beside it is free. Nothing broke."); }}>Load an old arrangement</Button>,
280
+ <Button key="crash" onClick={() => { setBroken(true); setSaid("A trend threw while drawing. Its frame, head and menu stand; everything else keeps working."); }}>Let a widget fail</Button>,
281
+ <Button key="reset" onClick={() => { parts.edit(); parts.reset(); setSaid("Back to the application's arrangement for this role - one more step of the draft: Undo brings yours back."); }} disabled={locked}>Reset to the default</Button>,
282
+ <Button key="lock" onClick={() => { parts.cancel(); setLocked(!locked); setSaid(locked ? "Editing is allowed." : "Without editing the dashboard is complete as it is: the same arrangement, no button, no mode."); }}>{locked ? "Allow editing" : "Lock editing"}</Button>,
283
+ <Button
284
+ key="role"
285
+ onClick={() => {
286
+ const next: Role = role === "dispatch" ? "management" : "dispatch";
287
+ parts.cancel();
288
+ setRole(next);
289
+ parts.setView(kept.current[next] ?? ROLES[next]);
290
+ setSaid("Another role, another default. Each role keeps its own value.");
291
+ }}
292
+ >
293
+ {role === "dispatch" ? "Sign in as management" : "Sign in as dispatch"}
294
+ </Button>,
295
+ ],
296
+ };
297
+
298
+ return (
299
+ <Stack gap={4}>
300
+ <Stack direction="row" gap={2} wrap data-callout="2">
301
+ {buttons[shown]}
302
+ </Stack>
303
+ <Text data-callout="3" tone="secondary" size="sm" aria-live="polite">{said || INTRO[shown]}</Text>
304
+ {leaving && (
305
+ <Alert
306
+ tone="warning"
307
+ title="Unsaved changes"
308
+ actions={
309
+ <Stack direction="row" gap={2}>
310
+ <Button size="sm" onClick={() => { setLeaving(false); setSaid("The draft is untouched."); }}>Keep editing</Button>
311
+ <Button size="sm" onClick={() => { parts.cancel(); setLeaving(false); setSaid("The draft is gone and the application navigates."); }}>Discard and leave</Button>
312
+ </Stack>
313
+ }
314
+ >
315
+ The library tells the application that a draft is open. What it makes of that is its own business; here it asks.
316
+ </Alert>
317
+ )}
318
+ <div data-callout="4" style={{ overflowX: "auto" }}>
319
+ <div style={{ width: width ?? "100%" }}>
320
+ <Dashboard aria-label="Dispatch, today">
321
+ <HeadingType />
322
+ <WidgetType
323
+ name="figure"
324
+ label="Figure"
325
+ sentence="One number."
326
+ sizes={{ small: { columns: 3, rows: 2 }, medium: { columns: 4, rows: 2 } }}
327
+ start="small"
328
+ defaultOptions={{ name: "Orders" as Figure }}
329
+ title={(options) => options.name}
330
+ form={({ options, change }) => (
331
+ <SegmentedControl
332
+ aria-label="Figure"
333
+ options={(Object.keys(FIGURES) as Figure[]).map((name) => ({ value: name, label: name }))}
334
+ value={options.name}
335
+ onChange={(name) => change({ name })}
336
+ />
337
+ )}
338
+ >
339
+ {({ options }) => {
340
+ return <Kz {...FIGURES[options.name]} />;
341
+ }}
342
+ </WidgetType>
343
+ <WidgetType
344
+ name="trend"
345
+ label="Trend"
346
+ sentence="A figure over the last seven days."
347
+ sizes={{ wide: { columns: 6, rows: 4 }, large: { columns: 8, rows: 4 } }}
348
+ start="wide"
349
+ defaultOptions={{ name: "Orders" as Figure }}
350
+ title={(options) => `${options.name}, last 7 days`}
351
+ >
352
+ {({ options, size }) => {
353
+ if (broken && options.name === "Orders") throw new Error("The trend could not be drawn");
354
+ return <TrendChart data={TREND[options.name].now} previous={size === "large" ? TREND[options.name].before : undefined} />;
355
+ }}
356
+ </WidgetType>
357
+ <WidgetType name="alerts" label="Open alerts" sentence="What needs attention now." sizes={{ tall: { columns: 4, rows: 4 } }} start="tall" defaultOptions={{}}>
358
+ {() => <Rows rows={ALERTS} />}
359
+ </WidgetType>
360
+ <WidgetType name="orders" label="Orders" sentence="The work in hand." sizes={{ full: { columns: 12, rows: 4 } }} start="full" defaultOptions={{}}>
361
+ {() => <DataTable head={["No.", "Customer", "Quantity", "Status"]} numeric={[2]} rows={ORDERS} />}
362
+ </WidgetType>
363
+ </Dashboard>
364
+ </div>
365
+ </div>
366
+ <Text data-callout="5" size="sm" tone="muted">
367
+ {reports.length === 0 ? "Told to the application: nothing yet." : `Told to the application: ${reports.join("; ")}.`}
368
+ </Text>
369
+ </Stack>
370
+ );
371
+ }
372
+
373
+ export default function ArrangeTheStartScreen() {
374
+ const [current, setCurrent] = useState<Case>("monitors");
375
+ return (
376
+ <Stack gap={4}>
377
+ <div data-callout="1">
378
+ <SegmentedControl aria-label="Case" options={CASES.map((one) => ({ value: one.value, label: one.label }))} value={current} onChange={setCurrent} />
379
+ </div>
380
+ <Stage key={current} shown={current} />
381
+ </Stack>
382
+ );
383
+ }
384
+ ```
385
+
386
+ ## Getting started
387
+
388
+ What a dashboard is for, and what an application sets up before the first one.
389
+
390
+ ### Dashboard
391
+
392
+ Shows widgets a developer declares, in the order a view stores them, each at its size, packed on twelve columns: the start screen of a data-dense application, with the figures one role looks at first.
393
+
394
+ ```ts
395
+ import { useDashboard, WidgetType, HeadingType } from "@umriss-ui/dashboard";
396
+ ```
397
+
398
+ ```sh
399
+ npm install @umriss-ui/dashboard @umriss-ui/core
400
+ ```
401
+
402
+ Demo page: https://romanhaendler.github.io/umriss-ui/dashboard/dashboard/
403
+
404
+ The command brings the core package along, a peer dependency. React 19 or newer is a peer as well and stays the application's own. The stylesheet loads itself: the package imports it and marks CSS as a side effect.
405
+
406
+ [`useDashboard`](#/api/useDashboard) takes the arrangement the application gives a reader and, optionally, the one the reader kept (`initialView`, read once). It hands back `Dashboard`, `WidgetType`, the view and `setView`.
407
+
408
+ A widget type is declared as JSX: its name, a sentence, the sizes it offers with the columns and rows each takes, the size to start from, its default options and its content as a function of the size and the options. The options type is inferred from the default options.
409
+
410
+ The widgets are packed in the stored order, each at the first place at or after the one before it where it fits; room left behind stays free. The columns never change in number, under a maximum width. Below the width at which a column would be too narrow to read, the dashboard is a list in the same order. The height follows the size and never the content.
411
+
412
+ #### Examples
413
+
414
+ ##### The start screen of a dispatcher
415
+
416
+ Declare each widget type once, hand over the arrangement for the role: the widgets stand in that order, each at its size.
417
+
418
+ ```tsx
419
+ import { useState } from "react";
420
+ import type { CSSProperties, ReactNode } from "react";
421
+ import { useChart } from "@umriss-ui/charts";
422
+ import { FormField, Select, Text } from "@umriss-ui/core";
423
+ import { HeadingType, useDashboard } from "@umriss-ui/dashboard";
424
+ import type { DashboardView, HeadingOptions } from "@umriss-ui/dashboard";
425
+
426
+ type Verdict = "ok" | "warn" | "bad";
427
+
428
+ const MARK: Record<Verdict, CSSProperties> = {
429
+ ok: { background: "var(--u-color-success)", borderRadius: "50%" },
430
+ warn: { background: "var(--u-color-warning)", borderRadius: 2 },
431
+ bad: { background: "var(--u-color-danger)", transform: "rotate(45deg)" },
432
+ };
433
+
434
+ const hairline = "1px solid var(--u-hairline)";
435
+
436
+ /** A status mark whose shape says it too: circle, square, diamond. */
437
+ function Dot({ verdict }: { verdict: Verdict }) {
438
+ return <span aria-hidden="true" style={{ flex: "none", width: 8, height: 8, ...MARK[verdict] }} />;
439
+ }
440
+
441
+ /** A figure: a large value with its unit, and a status line under it. */
442
+ function Kz({ value, unit, verdict, note }: { value: string; unit?: string; verdict: Verdict; note: string }) {
443
+ return (
444
+ <div style={{ display: "flex", flexDirection: "column", justifyContent: "center", gap: "var(--u-space-1)", height: "100%", minWidth: 0 }}>
445
+ <Text as="div" size="2xl" weight="semibold" leading="tight" style={{ fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" }}>
446
+ {value}
447
+ {unit && <Text as="span" size="sm" tone="muted" style={{ marginInlineStart: "var(--u-space-1)", fontWeight: "var(--u-weight-regular)" }}>{unit}</Text>}
448
+ </Text>
449
+ <Text as="div" size="xs" tone="secondary" style={{ display: "flex", alignItems: "center", gap: 6, whiteSpace: "nowrap" }}>
450
+ <Dot verdict={verdict} />
451
+ {note}
452
+ </Text>
453
+ </div>
454
+ );
455
+ }
456
+
457
+ /** The trend: this week as a line with a faint area, last week dashed and muted, on one scale; with `previous` the legend too. */
458
+ function TrendChart({ data, previous, labels = ["this week", "last week"] }: { data: readonly number[]; previous?: readonly number[]; labels?: readonly [string, string] }) {
459
+ const rows = data.map((now, i) => ({ i, now, before: previous?.[i] ?? now }));
460
+ const { Chart, XAxis, YAxis, Line, Area } = useChart(rows);
461
+ const swatch = (color: string, dashed = false): CSSProperties => ({ display: "inline-block", width: 14, borderTop: `2px ${dashed ? "dashed" : "solid"} ${color}`, verticalAlign: "middle", marginInlineEnd: 5 });
462
+ return (
463
+ <div style={{ display: "flex", flexDirection: "column", gap: "var(--u-space-2)", height: "100%", minWidth: 0 }}>
464
+ {previous && (
465
+ <Text as="div" size="xs" tone="secondary" style={{ display: "flex", gap: 14 }}>
466
+ <span><i style={swatch("var(--u-color-accent)")} />{labels[0]}</span>
467
+ <span><i style={swatch("var(--u-color-text-muted)", true)} />{labels[1]}</span>
468
+ </Text>
469
+ )}
470
+ <div style={{ flex: "1 1 0", minHeight: 0 }}>
471
+ <Chart ariaLabel={previous ? `${labels[0]} and ${labels[1]}` : labels[0]} padding={4} style={{ minHeight: 0, height: "100%" }}>
472
+ <XAxis value="i" domain="data" grid={false} tickFormat={() => ""} />
473
+ <YAxis grid tickFormat={() => ""} />
474
+ {previous && <Line value="before" name={labels[1]} color="var(--u-color-text-muted)" strokeWidth={1.25} dash={[3, 3]} markers="never" />}
475
+ <Area value="now" name={labels[0]} color="var(--u-color-accent)" fillOpacity={0.09} strokeWidth={1.75} />
476
+ </Chart>
477
+ </div>
478
+ </div>
479
+ );
480
+ }
481
+
482
+ /** Rows under each other, a hairline between them: a mark, a text, what belongs to it at the end. */
483
+ function Rows({ rows }: { rows: ReadonlyArray<{ verdict: Verdict; text: string; meta?: string }> }) {
484
+ return (
485
+ <ul style={{ listStyle: "none", margin: 0, padding: 0 }}>
486
+ {rows.map(({ verdict, text, meta }, i) => (
487
+ <li key={text} style={{ display: "flex", alignItems: "center", gap: "var(--u-space-3)", padding: "6px 0", borderTop: i ? hairline : undefined }}>
488
+ <Dot verdict={verdict} />
489
+ <Text as="span" size="sm" style={{ flex: "1 1 0", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{text}</Text>
490
+ {meta && <Text as="span" size="xs" tone="muted" style={{ whiteSpace: "nowrap" }}>{meta}</Text>}
491
+ </li>
492
+ ))}
493
+ </ul>
494
+ );
495
+ }
496
+
497
+ /** A table with a muted head and a hairline between the rows; `numeric` columns stand right. */
498
+ function DataTable({ head, rows, numeric = [] }: { head: readonly string[]; rows: ReadonlyArray<readonly ReactNode[]>; numeric?: readonly number[] }) {
499
+ const align = (i: number): CSSProperties => ({ textAlign: numeric.includes(i) ? "right" : "left", paddingBlock: 5, paddingInline: "0 var(--u-space-2)", whiteSpace: "nowrap" });
500
+ return (
501
+ <table style={{ width: "100%", borderCollapse: "collapse", fontSize: "var(--u-text-sm)", fontVariantNumeric: "tabular-nums" }}>
502
+ <thead>
503
+ <tr>
504
+ {head.map((name, i) => (
505
+ <th key={name} style={{ ...align(i), color: "var(--u-color-text-muted)", fontSize: "var(--u-text-xs)", fontWeight: "var(--u-weight-medium)" }}>{name}</th>
506
+ ))}
507
+ </tr>
508
+ </thead>
509
+ <tbody>
510
+ {rows.map((cells, r) => (
511
+ <tr key={r}>
512
+ {cells.map((cell, i) => (
513
+ <td key={i} style={{ ...align(i), borderTop: hairline }}>{cell}</td>
514
+ ))}
515
+ </tr>
516
+ ))}
517
+ </tbody>
518
+ </table>
519
+ );
520
+ }
521
+
522
+ const FIGURES = {
523
+ Orders: { value: "1,284", unit: "per day", verdict: "ok", note: "+3.1% on last week" },
524
+ Utilisation: { value: "87.4", unit: "%", verdict: "ok", note: "on target" },
525
+ Complaints: { value: "2.9", unit: "%", verdict: "warn", note: "just above target" },
526
+ "On-time delivery": { value: "96.2", unit: "%", verdict: "ok", note: "on target" },
527
+ } as const satisfies Record<string, { value: string; unit: string; verdict: Verdict; note: string }>;
528
+
529
+ const TREND = {
530
+ Orders: { now: [58, 52, 55, 51, 49, 53, 47, 52, 56, 60, 57, 55, 56, 58, 54, 52, 52, 49, 46, 49, 52, 55, 62, 58, 61, 55, 57, 62], before: [56, 56, 50, 53, 53, 60, 66, 68, 66, 64, 56, 52, 49, 50, 48, 49, 47, 45, 42, 38, 36, 36, 32, 30, 28, 34, 34, 30] },
531
+ Complaints: { now: [66, 72, 70, 62, 55, 62, 68, 54, 55, 58, 50, 52, 48, 51, 49, 47, 47, 47, 47, 58, 59, 52, 50, 53, 58, 60], before: [64, 66, 62, 60, 58, 56, 55, 53, 54, 52, 50, 49, 48, 47, 46, 46, 45, 45, 45, 46, 47, 47, 46, 46, 45, 45] },
532
+ } as const;
533
+
534
+ const ALERTS = [
535
+ { verdict: "bad", text: "Tour 14 is 25 min late", meta: "12 min ago" },
536
+ { verdict: "warn", text: "Dock 3 is out of service", meta: "34 min ago" },
537
+ { verdict: "warn", text: "Call: Oakridge Pharmacy", meta: "1 h ago" },
538
+ { verdict: "ok", text: "Maintenance on line 3 finished", meta: "2 h ago" },
539
+ ] as const satisfies ReadonlyArray<{ verdict: Verdict; text: string; meta: string }>;
540
+
541
+ const ORDERS = [
542
+ ["A-2041", "Holloway Garden Supplies", "120", "Oct 14", "Packed"],
543
+ ["A-2042", "Marlow & Finch Books", "490", "Oct 15", "Picking"],
544
+ ["A-2043", "Brixley Cycles", "860", "Oct 16", "Late"],
545
+ ["A-2044", "Ferrow Bakery", "75", "Oct 16", "Planned"],
546
+ ];
547
+
548
+ const heading = (id: string, text: string) => ({ id, type: "heading", size: "full" as const, options: { text } satisfies HeadingOptions });
549
+
550
+ const VIEW: DashboardView = {
551
+ widgets: [
552
+ heading("h1", "Overview"),
553
+ { id: "f1", type: "figure", size: "small", options: { name: "Orders" } },
554
+ { id: "f2", type: "figure", size: "small", options: { name: "Utilisation" } },
555
+ { id: "f3", type: "figure", size: "small", options: { name: "Complaints" } },
556
+ { id: "f4", type: "figure", size: "small", options: { name: "On-time delivery" } },
557
+ heading("h2", "Trend"),
558
+ { id: "t1", type: "trend", size: "large", options: { name: "Orders" } },
559
+ { id: "t2", type: "trend", size: "wide", options: { name: "Complaints" } },
560
+ heading("h3", "Work in hand"),
561
+ { id: "o1", type: "orders", size: "wide", options: {} },
562
+ { id: "a1", type: "alerts", size: "tall", options: {} },
563
+ ],
564
+ };
565
+
566
+ export default function Dispatcher() {
567
+ const [open, setOpen] = useState(false);
568
+ const [kept, setKept] = useState(0);
569
+ const { Dashboard, WidgetType } = useDashboard({
570
+ defaultView: VIEW,
571
+ allowEdit: true,
572
+ onDraftChange: setOpen,
573
+ onViewChange: () => setKept((count) => count + 1),
574
+ });
575
+ return (
576
+ <Dashboard aria-label="Dispatch, today" catalogue={["figure", "trend", "alerts"]} data-draft={open || undefined} data-kept={kept}>
577
+ <HeadingType />
578
+ <WidgetType
579
+ name="figure"
580
+ label="Figure"
581
+ sentence="One number with its verdict."
582
+ sizes={{ small: { columns: 3, rows: 2 }, medium: { columns: 4, rows: 2 } }}
583
+ start="small"
584
+ defaultOptions={{ name: "Orders" as keyof typeof FIGURES }}
585
+ title={(options) => options.name}
586
+ form={({ options, change }) => (
587
+ <FormField label="Figure">
588
+ <Select value={options.name} onChange={(event) => change({ name: event.target.value as keyof typeof FIGURES })}>
589
+ {Object.keys(FIGURES).map((name) => (
590
+ <option key={name}>{name}</option>
591
+ ))}
592
+ </Select>
593
+ </FormField>
594
+ )}
595
+ >
596
+ {({ options }) => <Kz {...FIGURES[options.name]} />}
597
+ </WidgetType>
598
+ <WidgetType
599
+ name="trend"
600
+ label="Trend"
601
+ sentence="A figure over the last weeks."
602
+ sizes={{ wide: { columns: 4, rows: 4 }, large: { columns: 8, rows: 4 } }}
603
+ start="wide"
604
+ defaultOptions={{ name: "Orders" as keyof typeof TREND }}
605
+ title={(options) => `${options.name} trend`}
606
+ >
607
+ {({ options, size }) => <TrendChart data={TREND[options.name].now} previous={size === "large" ? TREND[options.name].before : undefined} />}
608
+ </WidgetType>
609
+ <WidgetType name="alerts" label="Open alerts" sentence="What needs attention now." sizes={{ tall: { columns: 4, rows: 4 } }} start="tall" defaultOptions={{}}>
610
+ {() => <Rows rows={ALERTS} />}
611
+ </WidgetType>
612
+ <WidgetType name="orders" label="Orders" sentence="The work in hand." sizes={{ wide: { columns: 8, rows: 4 }, full: { columns: 12, rows: 4 } }} start="wide" defaultOptions={{}}>
613
+ {() => <DataTable head={["No.", "Customer", "Quantity", "Due", "Status"]} numeric={[2]} rows={ORDERS} />}
614
+ </WidgetType>
615
+ </Dashboard>
616
+ );
617
+ }
618
+ ```
619
+
620
+ ##### Your own component as a widget
621
+
622
+ A widget's content is any React component: a `WidgetType`'s children are a function of the size it stands at and its options, and return what the widget shows. Here the application's own list of tours shows more at a larger size, and its form chooses the depot.
623
+
624
+ ```tsx
625
+ import { FormField, Select, Stack, Text } from "@umriss-ui/core";
626
+ import { useDashboard } from "@umriss-ui/dashboard";
627
+ import type { WidgetSize } from "@umriss-ui/dashboard";
628
+
629
+ /* The application's own component - nothing in it knows the dashboard. */
630
+ const TOURS = {
631
+ North: [
632
+ { tour: "Tour 12", stop: "Holloway Garden Supplies", eta: "10:40" },
633
+ { tour: "Tour 14", stop: "Brixley Cycles", eta: "11:05" },
634
+ { tour: "Tour 17", stop: "Ferrow Bakery", eta: "11:30" },
635
+ { tour: "Tour 19", stop: "Marlow & Finch Books", eta: "12:15" },
636
+ ],
637
+ South: [
638
+ { tour: "Tour 3", stop: "Oakridge Pharmacy", eta: "10:20" },
639
+ { tour: "Tour 5", stop: "Kestrel Hardware", eta: "11:50" },
640
+ ],
641
+ };
642
+ type Depot = keyof typeof TOURS;
643
+
644
+ function ToursOnTheRoad({ depot, rows }: { depot: Depot; rows: number }) {
645
+ const tours = TOURS[depot];
646
+ return (
647
+ <Stack gap={1}>
648
+ <Text size="sm" tone="secondary">{`${tours.length} tours on the road from ${depot}`}</Text>
649
+ {tours.slice(0, rows).map((one) => (
650
+ <Text key={one.tour} size="sm">{`${one.tour} · ${one.stop} · ${one.eta}`}</Text>
651
+ ))}
652
+ </Stack>
653
+ );
654
+ }
655
+
656
+ /* How many lines each size has room for: the content is told its size. */
657
+ const ROWS: Partial<Record<WidgetSize, number>> = { small: 1, wide: 4 };
658
+
659
+ export default function YourOwnWidget() {
660
+ const { Dashboard, WidgetType } = useDashboard({
661
+ defaultView: {
662
+ widgets: [
663
+ { id: "north", type: "tours", size: "wide", options: { depot: "North" } },
664
+ { id: "south", type: "tours", size: "small", options: { depot: "South" } },
665
+ ],
666
+ },
667
+ });
668
+ return (
669
+ <Dashboard aria-label="Tours">
670
+ <WidgetType
671
+ name="tours"
672
+ label="Tours on the road"
673
+ sentence="The tours of one depot and their next stop."
674
+ sizes={{ small: { columns: 4, rows: 2 }, wide: { columns: 8, rows: 3 } }}
675
+ start="small"
676
+ defaultOptions={{ depot: "North" as Depot }}
677
+ title={(options) => `Tours, ${options.depot}`}
678
+ form={({ options, change }) => (
679
+ <FormField label="Depot">
680
+ <Select value={options.depot} onChange={(event) => change({ depot: event.target.value as Depot })}>
681
+ {Object.keys(TOURS).map((depot) => (
682
+ <option key={depot}>{depot}</option>
683
+ ))}
684
+ </Select>
685
+ </FormField>
686
+ )}
687
+ >
688
+ {({ size, options }) => <ToursOnTheRoad depot={options.depot} rows={ROWS[size] ?? 1} />}
689
+ </WidgetType>
690
+ </Dashboard>
691
+ );
692
+ }
693
+ ```
694
+
695
+ ##### Headings that divide the dashboard
696
+
697
+ The heading widget type is built in: one size across the full width, its text as its options, drawn as a title without a card. An application adds it to the dashboard when readers should be able to structure it.
698
+
699
+ ```tsx
700
+ import { Text } from "@umriss-ui/core";
701
+ import { HeadingType, useDashboard } from "@umriss-ui/dashboard";
702
+ import type { DashboardView, HeadingOptions } from "@umriss-ui/dashboard";
703
+
704
+ const VIEW: DashboardView = {
705
+ widgets: [
706
+ { id: "h1", type: "heading", size: "full", options: { text: "Today" } satisfies HeadingOptions },
707
+ { id: "n1", type: "note", size: "wide", options: {} },
708
+ { id: "n2", type: "note", size: "wide", options: {} },
709
+ { id: "h2", type: "heading", size: "full", options: { text: "This week" } satisfies HeadingOptions },
710
+ { id: "n3", type: "note", size: "wide", options: {} },
711
+ ],
712
+ };
713
+
714
+ export default function Sections() {
715
+ const { Dashboard, WidgetType } = useDashboard({ defaultView: VIEW });
716
+ return (
717
+ <Dashboard aria-label="Sections">
718
+ <HeadingType />
719
+ <WidgetType name="note" label="Note" sentence="A line of text." sizes={{ wide: { columns: 6, rows: 3 } }} start="wide" defaultOptions={{}}>
720
+ {() => <Text size="sm">Whatever the application puts in a widget.</Text>}
721
+ </WidgetType>
722
+ </Dashboard>
723
+ );
724
+ }
725
+ ```
726
+
727
+ ##### A dashboard without widgets
728
+
729
+ A dashboard that has no widgets says so, in the language of the surrounding provider.
730
+
731
+ ```tsx
732
+ import { useDashboard } from "@umriss-ui/dashboard";
733
+
734
+ export default function Empty() {
735
+ const { Dashboard } = useDashboard({ defaultView: { widgets: [] } });
736
+ return <Dashboard aria-label="Nothing yet" />;
737
+ }
738
+ ```
739
+
740
+ #### When to use something else
741
+
742
+ - A layout the developer fixes, which the reader does not arrange → `Grid`, `Stack` and `Splitter` from @umriss-ui/core
743
+
744
+ #### Keyboard
745
+
746
+ | Key | Action |
747
+ |---|---|
748
+ | `Tab` | In the editing mode, moves to the next widget's head and its menu button. |
749
+ | `Arrow keys` | On a focused head, move the widget one place earlier or later. |
750
+ | `Home or End` | On a focused head, move the widget to the start or the end. |
751
+
752
+ #### Accessibility
753
+
754
+ The dashboard is a group named by its `aria-label`; every widget is a region named by its title. In the editing mode a widget's head is a tab stop named "<title>, place n of m", and its menu button "Actions for <title>".
755
+
756
+ Every move, size, removal, addition, undo, redo, reset, "Done" and "Cancel" is announced through core's live region: "Orders: moved from place 1 to place 3 of 5". The words are core's wording, German under `@umriss-ui/core/wording/de`.
757
+
758
+ #### API
759
+
760
+ ##### `DashboardOptions`
761
+
762
+ | Prop | Type | Default | Description |
763
+ |---|---|---|---|
764
+ | `defaultView` *required* | [`DashboardView`](#type-DashboardView) | — | The arrangement the application gives a reader who has none of their own.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Headings that divide the dashboard](#/dashboard/sections) and 2 more |
765
+ | `initialView` | [`DashboardView`](#type-DashboardView) | the `defaultView` | The reader's kept arrangement, read once when the hook first runs; a different value on a later render is ignored, as React ignores a changed `defaultValue`. To go to another, call `setView`. |
766
+ | `allowEdit` | `boolean` | `true` | Whether the reader may edit. Without it no bar is drawn and the mode cannot be entered.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Arrange the start screen (Scenarios)](#/scenarios/a-dispatchers-day) |
767
+
768
+ ###### Events
769
+
770
+ | Prop | Type | Default | Description |
771
+ |---|---|---|---|
772
+ | `onViewChange` | `(view: `[`DashboardView`](#type-DashboardView)`) => void` | — | Called once per "Done" that changed something, with the whole arrangement. Not called by `setView`.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Arrange the start screen (Scenarios)](#/scenarios/a-dispatchers-day) |
773
+ | `onDraftChange` | `(open: boolean) => void` | — | Called with `true` when a draft opens and with `false` when it closes - to warn before navigation.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Arrange the start screen (Scenarios)](#/scenarios/a-dispatchers-day) |
774
+
775
+ ##### `DashboardParts`
776
+
777
+ | Prop | Type | Default | Description |
778
+ |---|---|---|---|
779
+ | `Dashboard` *required* | `(props: `[`DashboardProps`](#type-DashboardProps)`) => ReactNode` | — | The dashboard. One per `useDashboard`.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Headings that divide the dashboard](#/dashboard/sections) and 2 more |
780
+ | `WidgetType` *required* | `typeof WidgetType` | — | Declares a widget type inside `Dashboard`.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Headings that divide the dashboard](#/dashboard/sections) and 1 more |
781
+ | `view` *required* | [`DashboardView`](#type-DashboardView) | — | The arrangement in view: the draft while the mode is on. |
782
+ | `setView` *required* | `(view: `[`DashboardView`](#type-DashboardView)`) => void` | — | Goes to a whole arrangement. Not reported.<br>Shown in: [Arrange the start screen (Scenarios)](#/scenarios/a-dispatchers-day) |
783
+ | `editing` *required* | `boolean` | — | Whether the editing mode is on.<br>Shown in: [Arrange the start screen (Scenarios)](#/scenarios/a-dispatchers-day) |
784
+ | `edit` *required* | `() => void` | — | Enters the editing mode. Does nothing where editing is not allowed.<br>Shown in: [Arrange the start screen (Scenarios)](#/scenarios/a-dispatchers-day) |
785
+ | `done` *required* | `() => void` | — | Leaves the mode, keeps the changes and reports them once if there are any.<br>Shown in: [Arrange the start screen (Scenarios)](#/scenarios/a-dispatchers-day) |
786
+ | `catalogue` *required* | `() => void` | — | Opens the catalogue. Does nothing outside the editing mode.<br>Shown in: [Arrange the start screen (Scenarios)](#/scenarios/a-dispatchers-day) |
787
+ | `cancel` *required* | `() => void` | — | Leaves the mode and puts the arrangement back as it was before `edit`.<br>Shown in: [Arrange the start screen (Scenarios)](#/scenarios/a-dispatchers-day) |
788
+ | `undo` *required* | `() => void` | — | Goes one step back in the draft. Does nothing at the first.<br>Shown in: [Arrange the start screen (Scenarios)](#/scenarios/a-dispatchers-day) |
789
+ | `redo` *required* | `() => void` | — | Goes one step forward again. Does nothing at the last. |
790
+ | `reset` *required* | `() => void` | — | Steps to the `defaultView`; `undo` brings the arrangement back.<br>Shown in: [Arrange the start screen (Scenarios)](#/scenarios/a-dispatchers-day) |
791
+
792
+ ##### `DashboardProps`
793
+
794
+ | Prop | Type | Default | Description |
795
+ |---|---|---|---|
796
+ | `children` | `ReactNode` | — | The `WidgetType`s of this dashboard.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Headings that divide the dashboard](#/dashboard/sections) and 1 more |
797
+ | `catalogue` | `ReadonlyArray<string>` | every declared type | The `name`s of the widget types the catalogue lists.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher) |
798
+
799
+ Also takes every attribute of `<div>`.
800
+
801
+ ##### `DashboardView`
802
+
803
+ | Prop | Type | Default | Description |
804
+ |---|---|---|---|
805
+ | `widgets` *required* | `ReadonlyArray<`[`DashboardWidget`](#type-DashboardWidget)`>` | — | The widgets, in the order they are drawn and read.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Headings that divide the dashboard](#/dashboard/sections) and 2 more |
806
+
807
+ ##### `DashboardWidget`
808
+
809
+ | Prop | Type | Default | Description |
810
+ |---|---|---|---|
811
+ | `id` *required* | `string` | — | Unique within the view.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Headings that divide the dashboard](#/dashboard/sections) and 1 more |
812
+ | `type` *required* | `string` | — | The `name` of the widget type it is an instance of.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Headings that divide the dashboard](#/dashboard/sections) and 1 more |
813
+ | `size` *required* | [`WidgetSize`](#type-WidgetSize)<br>`"small" \| "medium" \| "wide" \| "tall" \| "large" \| "full"` | — | One of the sizes its type offers.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Headings that divide the dashboard](#/dashboard/sections) and 1 more |
814
+ | `options` *required* | `unknown` | — | The type's own options, typed through the hook.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Headings that divide the dashboard](#/dashboard/sections) and 1 more |
815
+
816
+ ##### `WidgetSize`
817
+
818
+ Declares no props of its own.
819
+
820
+ ##### `SizeSpan`
821
+
822
+ | Prop | Type | Default | Description |
823
+ |---|---|---|---|
824
+ | `columns` *required* | `number` | — | Columns of twelve.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Headings that divide the dashboard](#/dashboard/sections) and 1 more |
825
+ | `rows` *required* | `number` | — | Rows of the fixed row unit.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Headings that divide the dashboard](#/dashboard/sections) and 1 more |
826
+
827
+ ##### `WidgetTypeProps<Options>`
828
+
829
+ | Prop | Type | Default | Description |
830
+ |---|---|---|---|
831
+ | `name` *required* | `string` | — | The name a view refers to this type by. Unique within the dashboard.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Headings that divide the dashboard](#/dashboard/sections) and 1 more |
832
+ | `label` *required* | `string` | — | The type's name for a reader, and the title of its frame unless `title` says otherwise.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Headings that divide the dashboard](#/dashboard/sections) and 1 more |
833
+ | `sentence` *required* | `string` | — | One sentence on what the type shows.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Headings that divide the dashboard](#/dashboard/sections) and 1 more |
834
+ | `sizes` *required* | `Partial<Record<`[`WidgetSize`](#type-WidgetSize)`, `[`SizeSpan`](#type-SizeSpan)`>>` | — | The sizes the type offers, each with the columns (of twelve) and rows it takes.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Headings that divide the dashboard](#/dashboard/sections) and 1 more |
835
+ | `start` *required* | [`WidgetSize`](#type-WidgetSize)<br>`"small" \| "medium" \| "wide" \| "tall" \| "large" \| "full"` | — | The size a new widget of this type starts at. One of `sizes`.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Headings that divide the dashboard](#/dashboard/sections) and 1 more |
836
+ | `defaultOptions` *required* | `Options` | — | The options a new widget starts with. Their type is the options type of `title` and of the content.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Headings that divide the dashboard](#/dashboard/sections) and 1 more |
837
+ | `title` | `(options: NoInfer<Options>) => string` | — | The title of a frame as a function of its options; the `label` without it.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Arrange the start screen (Scenarios)](#/scenarios/a-dispatchers-day) |
838
+ | `form` | `(props: `[`WidgetFormProps`](#type-WidgetFormProps)`<NoInfer<Options>>) => ReactNode` | — | The form for the options, opened from the widget's menu. Without it the menu has no options entry.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Arrange the start screen (Scenarios)](#/scenarios/a-dispatchers-day) |
839
+ | `children` *required* | `(content: `[`WidgetContent`](#type-WidgetContent)`<NoInfer<Options>>) => ReactNode` | — | The content, as a function of the size and the options.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Headings that divide the dashboard](#/dashboard/sections) and 1 more |
840
+
841
+ ##### `WidgetContent<Options>`
842
+
843
+ | Prop | Type | Default | Description |
844
+ |---|---|---|---|
845
+ | `size` *required* | [`WidgetSize`](#type-WidgetSize)<br>`"small" \| "medium" \| "wide" \| "tall" \| "large" \| "full"` | — | The size the widget stands at.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Arrange the start screen (Scenarios)](#/scenarios/a-dispatchers-day) |
846
+ | `options` *required* | `Options` | — | The widget's options.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Arrange the start screen (Scenarios)](#/scenarios/a-dispatchers-day) |
847
+
848
+ ##### `WidgetFormProps<Options>`
849
+
850
+ | Prop | Type | Default | Description |
851
+ |---|---|---|---|
852
+ | `options` *required* | `Options` | — | The widget's current options.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Arrange the start screen (Scenarios)](#/scenarios/a-dispatchers-day) |
853
+ | `change` *required* | `(options: Options) => void` | — | Replaces the options; one step of the draft.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Your own component as a widget](#/dashboard/your-own-widget), [Arrange the start screen (Scenarios)](#/scenarios/a-dispatchers-day) |
854
+
855
+ ##### `HeadingOptions`
856
+
857
+ | Prop | Type | Default | Description |
858
+ |---|---|---|---|
859
+ | `text` *required* | `string` | — | The title across the full width.<br>Shown in: [The start screen of a dispatcher](#/dashboard/dispatcher), [Headings that divide the dashboard](#/dashboard/sections), [Arrange the start screen (Scenarios)](#/scenarios/a-dispatchers-day) |
860
+
861
+ #### Known limits
862
+
863
+ - A widget's height follows its size, never its content: what does not fit is cut, not grown.
864
+ - Not yet: sections that wrap as a whole, dragging from the catalogue to a chosen place, a height that follows the content in the list form, hiding a widget that has nothing to show, locking one widget ([ADR-0064](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0064-a-dashboard-a-user-arranges-is-a-sequence-on-fixed-columns.md)).
865
+ - Never: stacks of widgets that share one place, coordinates and free placement, a layout per breakpoint, dense packing ([ADR-0064](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0064-a-dashboard-a-user-arranges-is-a-sequence-on-fixed-columns.md)).
866
+ - Where an arrangement is stored, which default a role gets, several dashboards and what stands in a widget are the application's.
867
+
868
+ What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/).
869
+
870
+ ## API index
871
+
872
+ Every name the package exports, in one place.
873
+
874
+ ### API index
875
+
876
+ Everything `@umriss-ui/dashboard` exports, with its signature. A component's props stand on its own page.
877
+
878
+ Demo page: https://romanhaendler.github.io/umriss-ui/dashboard/api/
879
+
880
+ #### Components
881
+
882
+ ##### `HeadingType`
883
+
884
+ The built-in heading widget type, to put inside `Dashboard` when the
885
+ application wants readers to divide their dashboard: one size, `full`, one
886
+ row, its text as its options, drawn as a title without a card.
887
+
888
+ ```ts
889
+ function HeadingType(): null;
890
+ ```
891
+
892
+ Used on [Scenarios](#/) and [Dashboard](#/dashboard).
893
+
894
+ ##### `WidgetType`
895
+
896
+ [Dashboard](#/dashboard) – Declares one widget type inside a `Dashboard`. It draws nothing itself: the
897
+ dashboard reads its props. The options type is inferred from `defaultOptions`.
898
+
899
+ Used on [Scenarios](#/) and [Dashboard](#/dashboard).
900
+
901
+ #### Hooks
902
+
903
+ ##### `useDashboard`
904
+
905
+ Declares a dashboard: hands back `Dashboard`, `WidgetType`, the view in
906
+ view, `setView` and the editing actions.
907
+
908
+ ```ts
909
+ function useDashboard({ defaultView, initialView, allowEdit, onViewChange, onDraftChange }: DashboardOptions): DashboardParts;
910
+ ```
911
+
912
+ Used on [Scenarios](#/) and [Dashboard](#/dashboard).
913
+
914
+ #### Types
915
+
916
+ ##### `DashboardOptions`
917
+
918
+ Its table stands on [Dashboard](#/dashboard/type-DashboardOptions).
919
+
920
+ ##### `DashboardParts`
921
+
922
+ Its table stands on [Dashboard](#/dashboard/type-DashboardParts).
923
+
924
+ ##### `DashboardProps`
925
+
926
+ Its table stands on [Dashboard](#/dashboard/type-DashboardProps).
927
+
928
+ ##### `DashboardView`
929
+
930
+ Its table stands on [Dashboard](#/dashboard/type-DashboardView).
931
+
932
+ Used on [Scenarios](#/) and [Dashboard](#/dashboard).
933
+
934
+ ##### `DashboardWidget`
935
+
936
+ Its table stands on [Dashboard](#/dashboard/type-DashboardWidget).
937
+
938
+ ##### `HeadingOptions`
939
+
940
+ Its table stands on [Dashboard](#/dashboard/type-HeadingOptions).
941
+
942
+ Used on [Scenarios](#/) and [Dashboard](#/dashboard).
943
+
944
+ ##### `SizeSpan`
945
+
946
+ Its table stands on [Dashboard](#/dashboard/type-SizeSpan).
947
+
948
+ ##### `WidgetContent`
949
+
950
+ Its table stands on [Dashboard](#/dashboard/type-WidgetContent).
951
+
952
+ ##### `WidgetFormProps`
953
+
954
+ Its table stands on [Dashboard](#/dashboard/type-WidgetFormProps).
955
+
956
+ ##### `WidgetSize`
957
+
958
+ Its table stands on [Dashboard](#/dashboard/type-WidgetSize).
959
+
960
+ Used on [Dashboard](#/dashboard).
961
+
962
+ ##### `WidgetTypeProps`
963
+
964
+ Its table stands on [Dashboard](#/dashboard/type-WidgetTypeProps).