|
1 | 1 | /** |
2 | 2 | * MetricsLayout — a compound layout for metric / dashboard pages. |
3 | 3 | * |
4 | | - * PHILOSOPHY — slots own their chrome. This family exists so that non-designers (and AI agents) |
5 | | - * can compose a beautiful, consistent metrics page purely by picking slots and setting closed, |
6 | | - * semantic props. There is deliberately NO freeform `className` on any slot: a page cannot restyle |
7 | | - * the spacing, borders or gutters of the layout, because that is exactly how metric pages drifted |
8 | | - * apart before. The slots bake ONE visual rhythm (a 12px grid gap, a 12px page gutter, a doubled |
9 | | - * separation before the content, a pinned 40px filter bar). If you find yourself wanting a class on |
10 | | - * a slot, you probably need a new semantic variant instead — talk to design and add a closed prop |
11 | | - * (like `kind` or `inset`) rather than reopening `className`. |
| 4 | + * Slots bake all the chrome; there is no `className` on any slot, so pages can't drift apart on |
| 5 | + * spacing. Need a variant? Add a closed prop (`kind`, `inset`), don't reopen `className`. |
12 | 6 | * |
13 | | - * Every metrics page reads top-to-bottom as the same slots: |
| 7 | + * Slots, top to bottom: |
| 8 | + * - `Filters` — pinned 40px bar under the NavBar. Left/right clusters are child divs. |
| 9 | + * - `Grid` — tiles; columns derived from tile count unless `columns` is set. `kind="charts"` |
| 10 | + * bakes the fixed chart-row height. |
| 11 | + * - `Content` — table / tabs below the tiles. Full-bleed by default; `inset` for a padded column. |
14 | 12 | * |
15 | | - * 1. `MetricsLayout.Filters` — the pinned bar under the NavBar (search, TimeFilter, pagination…). |
16 | | - * It is hoisted out of the scroll container so it stays put while the page scrolls. Baked |
17 | | - * chrome: a 40px bar with a bottom border. Pages express left/right clusters as child divs |
18 | | - * (e.g. left = search + TimeFilter, right = pagination); `justify-between` spreads them. |
19 | | - * 2. `MetricsLayout.Grid` — one or more grids of tiles (stat BigNumbers, chart cards). The grid |
20 | | - * adapts its column count to the number of tiles unless you pass an explicit `columns` spec, |
21 | | - * and bakes the page gutter + grid gap. `kind="charts"` bakes the fixed chart-row height. |
22 | | - * 3. `MetricsLayout.Content` — the tabs / table / list below the tiles. Full-bleed by default |
23 | | - * (a list table spans edge to edge with its own top border); pass `inset` for a padded column. |
24 | | - * Content always bakes a doubled separation (24px) above it, so the blocks read as a distinct |
25 | | - * band from the content below. |
| 13 | + * Optional: |
| 14 | + * - `Sidebar` — a persistent right-hand panel; fixed `width` or `resizable`. Present ⇒ Root |
| 15 | + * switches to `[main | sidebar]`; absent ⇒ single column. |
| 16 | + * - `scroll` on Root — `"page"` (default): the whole page scrolls as one. `"regions"`: Root owns |
| 17 | + * no scroll, the page composes its own scrolling areas. |
26 | 18 | * |
27 | | - * Two optional structural capabilities extend the basic top-to-bottom column: |
| 19 | + * Purely presentational. Lives inside a `PageContainer` after the `NavBar`. |
28 | 20 | * |
29 | | - * - `MetricsLayout.Sidebar` — a persistent side panel rendered to the RIGHT of the main column |
30 | | - * (main content left, sidebar right, full height). Drop a single `<MetricsLayout.Sidebar>` |
31 | | - * anywhere among Root's children and Root switches to a `[main | sidebar]` horizontal layout; |
32 | | - * omit it and nothing changes. The sidebar is fixed-width by default (`width`), or set |
33 | | - * `resizable` to make the split draggable via the shared Resizable primitives. |
34 | | - * |
35 | | - * - `scroll` on Root — chooses who owns the vertical scroll: |
36 | | - * - `"page"` (default): Root owns a single `overflow-y-auto` and lays the slots out as a |
37 | | - * `flex` column with the standard vertical rhythm; the WHOLE page (filters aside) scrolls |
38 | | - * as one. This is what every current metrics page wants. |
39 | | - * - `"regions"`: Root does NOT create a scroll container and does NOT impose the column |
40 | | - * rhythm — it only bounds the height as a bare `flex` column. The page composes its own |
41 | | - * independently-scrolling areas inside the slots (e.g. a fixed toolbar over a scrolling |
42 | | - * table, or a vertical resizable split). Use this when a single page-level scroll would be |
43 | | - * wrong. |
44 | | - * |
45 | | - * The family is purely presentational — slots, grids, a sidebar and scroll ownership only, no data |
46 | | - * logic. It is meant to live inside a `PageContainer` right after the `NavBar`. |
47 | | - * |
48 | | - * @example List page (full-bleed table, count-derived grids) |
49 | | - * ```tsx |
50 | | - * <PageContainer> |
51 | | - * <NavBar>…</NavBar> |
52 | | - * <MetricsLayout.Root> |
53 | | - * <MetricsLayout.Filters> |
54 | | - * <div className="flex items-center gap-2">…search + TimeFilter…</div> |
55 | | - * <PaginationControls … /> |
56 | | - * </MetricsLayout.Filters> |
57 | | - * <MetricsLayout.Grid>…4 stat tiles…</MetricsLayout.Grid> |
58 | | - * <ChartSyncProvider> |
59 | | - * <MetricsLayout.Grid kind="charts">…4 chart tiles…</MetricsLayout.Grid> |
60 | | - * </ChartSyncProvider> |
61 | | - * <MetricsLayout.Content>…full-width table…</MetricsLayout.Content> |
62 | | - * </MetricsLayout.Root> |
63 | | - * </PageContainer> |
64 | | - * ``` |
65 | | - * |
66 | | - * @example Detail page (a single padded column) |
| 21 | + * @example |
67 | 22 | * ```tsx |
68 | 23 | * <MetricsLayout.Root> |
69 | 24 | * <MetricsLayout.Filters> |
70 | 25 | * <div className="flex items-center gap-2">…search + TimeFilter…</div> |
| 26 | + * <PaginationControls … /> |
71 | 27 | * </MetricsLayout.Filters> |
72 | | - * <MetricsLayout.Grid>…3 stat tiles…</MetricsLayout.Grid> |
73 | | - * <MetricsLayout.Content inset> |
74 | | - * <TabContainer>…</TabContainer> |
75 | | - * …active view (charts / keys)… |
76 | | - * </MetricsLayout.Content> |
77 | | - * </MetricsLayout.Root> |
78 | | - * ``` |
79 | | - * |
80 | | - * @example Page with a persistent config sidebar (fixed-width) |
81 | | - * ```tsx |
82 | | - * <MetricsLayout.Root> |
83 | | - * <MetricsLayout.Filters>…</MetricsLayout.Filters> |
84 | | - * <MetricsLayout.Content>…</MetricsLayout.Content> |
85 | | - * <MetricsLayout.Sidebar width="380px">…config panel…</MetricsLayout.Sidebar> |
86 | | - * </MetricsLayout.Root> |
87 | | - * ``` |
88 | | - * |
89 | | - * @example Resizable sidebar + independently-scrolling regions |
90 | | - * ```tsx |
91 | | - * // In the loader, hydrate the persisted split from a cookie: |
92 | | - * const sidebarSnapshot = await getResizableSnapshot(request, "my-page-sidebar"); |
93 | | - * |
94 | | - * <MetricsLayout.Root scroll="regions"> |
95 | | - * <div className="flex h-10 shrink-0 items-center …">…toolbar…</div> |
96 | | - * <div className="min-h-0 flex-1 overflow-y-auto">…scrolling body…</div> |
97 | | - * <MetricsLayout.Sidebar |
98 | | - * resizable |
99 | | - * autosaveId="my-page-sidebar" |
100 | | - * snapshot={sidebarSnapshot} |
101 | | - * min="280px" |
102 | | - * defaultSize="380px" |
103 | | - * max="500px" |
104 | | - * > |
105 | | - * …config panel… |
106 | | - * </MetricsLayout.Sidebar> |
| 28 | + * <MetricsLayout.Grid>…stat tiles…</MetricsLayout.Grid> |
| 29 | + * <MetricsLayout.Grid kind="charts">…chart tiles…</MetricsLayout.Grid> |
| 30 | + * <MetricsLayout.Content>…table…</MetricsLayout.Content> |
107 | 31 | * </MetricsLayout.Root> |
108 | 32 | * ``` |
109 | 33 | */ |
@@ -204,10 +128,8 @@ type MetricsLayoutSidebarProps = { |
204 | 128 | */ |
205 | 129 | width?: string; |
206 | 130 | /** |
207 | | - * When true, the `[main | sidebar]` split becomes draggable using the shared Resizable |
208 | | - * primitives. Persist the split by passing a stable `autosaveId` (the primitive writes the |
209 | | - * split to a cookie of that name) together with a `snapshot` read back from that cookie in the |
210 | | - * loader via `getResizableSnapshot(request, autosaveId)`. |
| 131 | + * Makes the split draggable. To persist it, pass an `autosaveId` (written to a cookie) plus the |
| 132 | + * `snapshot` read back in the loader via `getResizableSnapshot(request, autosaveId)`. |
211 | 133 | */ |
212 | 134 | resizable?: boolean; |
213 | 135 | /** Resizable only: min width of the sidebar panel. Defaults to `"280px"`. */ |
@@ -242,11 +164,9 @@ function isFiltersElement(child: ReactNode): child is ReactElement { |
242 | 164 | return isValidElement(child) && child.type === MetricsLayoutFilters; |
243 | 165 | } |
244 | 166 |
|
245 | | -// The main (left) column. The Filters slot is hoisted OUT of the scroll container so it stays |
246 | | -// pinned while the tiles/content scroll underneath (the dashboards' `[auto | 1fr]` pattern — |
247 | | -// sticky without z-index/backdrop bookkeeping). In `"page"` mode the rest scrolls as one and gets |
248 | | -// the baked column rhythm (vertical gap + top/bottom padding); in `"regions"` mode the page |
249 | | -// composes its own scrolling areas, so the container stays bare. |
| 167 | +// The main (left) column. Filters is hoisted out of the scroll container so it stays pinned while |
| 168 | +// the rest scrolls. `"page"` scrolls as one with the baked column rhythm; `"regions"` stays bare so |
| 169 | +// the page owns its own scrolling. |
250 | 170 | function MetricsLayoutMain({ children, scroll }: { children: ReactNode; scroll: MetricsScroll }) { |
251 | 171 | const arr = Children.toArray(children); |
252 | 172 | const filters = arr.find(isFiltersElement); |
|
0 commit comments