updates on the ui changes and table
This commit is contained in:
@@ -17,6 +17,7 @@ These components are imported across every page. Changes here have **fan-out imp
|
||||
|
||||
| Component | Use | Status |
|
||||
|---|---|---|
|
||||
| `PageLayout.js` | **The page design language**: `PageShell`, `KpiGrid`, `FilterToolbar`, `DataCard` | ✅ **Astryx** — use these for every operator page (see §2b) |
|
||||
| `PageHeader.js` | Page title + live subtitle + action slot | ✅ **Astryx** — `Heading`/`Text`/`HStack`/`StatusDot` |
|
||||
| `StatCard.js` | KPI / metric tile | ✅ **Astryx** — `Card`/`Text`/`Center`/`Skeleton` |
|
||||
| `StatusBadge.js` | Per-row lifecycle badge | ✅ **Astryx** — `Badge` driven by `themes/dt/status.js` |
|
||||
@@ -47,6 +48,51 @@ The design system is documented in the root `CLAUDE.md` §6. Read it before edit
|
||||
|
||||
---
|
||||
|
||||
## 2b. Page composition — use `PageLayout.js`, don't hand-roll spacing
|
||||
|
||||
Every operator page composes from the same four primitives. **Do not set
|
||||
`marginTop`, `gap` or `padding` on page sections by hand** — that is exactly how
|
||||
23 pages ended up with 23 different rhythms and the UI read as cramped and
|
||||
inconsistent.
|
||||
|
||||
```jsx
|
||||
import { PageShell, KpiGrid, FilterToolbar, DataCard } from 'components/nearle_components/PageLayout';
|
||||
|
||||
<PageShell> {/* gap={6} between regions */}
|
||||
<PageHeader title="…" subtitle="…" live action={<Button …/>} />
|
||||
<KpiGrid>…<StatCard/>…</KpiGrid> {/* fills the row, no dead gap */}
|
||||
<DataCard
|
||||
toolbar={<FilterToolbar>…selectors, date, search…</FilterToolbar>}
|
||||
tabs={<StatusTabs …/>}
|
||||
>
|
||||
…TableScroll + Table…
|
||||
</DataCard>
|
||||
</PageShell>
|
||||
```
|
||||
|
||||
Values come from the Astryx `dashboard` page template (`padding={6}` /
|
||||
`VStack gap={6}` / `Grid gap={4}`), not from taste.
|
||||
|
||||
**Three Astryx traps these primitives exist to absorb** — all three fail
|
||||
*silently*, with no lint or build error:
|
||||
|
||||
- **`Grid` has no `padding` prop.** It declares only `columns`/`gap`/`rowGap`/
|
||||
`columnGap`/`align`/`justify`/`width`/`height`. A `padding` you pass is spread
|
||||
onto the DOM and does nothing, so controls sit flush against the card edge.
|
||||
`FilterToolbar` puts the padding on a `VStack` wrapper instead.
|
||||
- **`Grid`'s `repeat` defaults to `'fill'`, which preserves empty tracks.** In a
|
||||
wide container that lays out more tracks than you have cards, so they bunch
|
||||
left and leave a dead gap. `KpiGrid` always passes `repeat: 'fit'`.
|
||||
- **Page-level actions belong in `PageHeader`; filters do not.**
|
||||
`LocationAutocomplete` carries its own width and ignores a wrapper's, so
|
||||
putting it in the header made it overlap its neighbour.
|
||||
|
||||
Before using any Astryx prop, check it is actually declared —
|
||||
`grep "export declare function <Name>" node_modules/@astryxdesign/core/dist/<Name>/<Name>.d.ts`.
|
||||
An undeclared prop is spread to the DOM and fails invisibly.
|
||||
|
||||
---
|
||||
|
||||
## 3. `LocationAutocomplete` — the zone picker
|
||||
|
||||
```jsx
|
||||
|
||||
190
src/components/nearle_components/PageLayout.js
Normal file
190
src/components/nearle_components/PageLayout.js
Normal file
@@ -0,0 +1,190 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Grid } from '@astryxdesign/core/Grid';
|
||||
import { VStack } from '@astryxdesign/core/VStack';
|
||||
import { Card } from '@astryxdesign/core/Card';
|
||||
import { Divider } from '@astryxdesign/core/Divider';
|
||||
|
||||
// ==============================|| PAGE LAYOUT PRIMITIVES ||============================== //
|
||||
//
|
||||
// The operator-page design language, in one place.
|
||||
//
|
||||
// Every page had been composing its own rhythm out of ad-hoc `marginTop: 16`,
|
||||
// `gap={2}` and `gap={3}` values, so no two pages agreed on spacing and the
|
||||
// result read as cramped. These four primitives fix the vertical rhythm, the
|
||||
// grid behaviour and the card chrome so a page only decides WHAT goes in each
|
||||
// slot, never how far apart things sit.
|
||||
//
|
||||
// The values come from the Astryx `dashboard` page template (which uses
|
||||
// LayoutContent padding={6} + VStack gap={6} + Grid gap={4}) — i.e. they are the
|
||||
// design system's own numbers, not invented here.
|
||||
//
|
||||
// <PageShell>
|
||||
// <PageHeader … />
|
||||
// <KpiGrid>…StatCard…</KpiGrid>
|
||||
// <DataCard toolbar={<FilterToolbar>…</FilterToolbar>} tabs={<StatusTabs …/>}>
|
||||
// …table…
|
||||
// </DataCard>
|
||||
// </PageShell>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PageShell — owns the vertical rhythm between a page's major regions.
|
||||
// gap={6} is the template's section spacing. Use this INSTEAD of putting
|
||||
// `style={{ marginTop: N }}` on individual sections.
|
||||
//
|
||||
// `fill` opts a page into FULL-HEIGHT mode: the header (and any KPI row) take
|
||||
// only the height they need and the last region — a `<DataCard fill>` — grows
|
||||
// to the bottom of the viewport, with its table scrolling inside itself.
|
||||
//
|
||||
// Why the `:has()` rule rather than a `height: 100%` chain or a `100vh` calc:
|
||||
//
|
||||
// AppShell (height="fill") gives its scroll container `.astryx-layout-content`
|
||||
// a definite `height: 100%` — so a real flex chain IS available. The chain
|
||||
// breaks at exactly one link: MainLayout's `.main-content-area`, the div that
|
||||
// holds the page padding, sets `min-height: 100%` and NOT `height`, so its own
|
||||
// height is auto. A percentage height on any descendant then resolves against
|
||||
// `auto` and is dropped, which is why full-height pages have had to guess with
|
||||
// `calc(100vh - 260px)` — a number that is wrong at every viewport the guess
|
||||
// wasn't written for, and that leaves dead space under the table.
|
||||
//
|
||||
// `:has(> .dt-page-fill)` re-definites that one link, and ONLY while a page
|
||||
// that asked for fill mode is mounted — every other page keeps the auto-height,
|
||||
// grow-with-content behaviour it has today. Scoping it this way is what makes
|
||||
// this safe to add to a shared primitive. Astryx's own stylesheet already
|
||||
// relies on `:has()`, so it is not a new browser requirement; where it is
|
||||
// unsupported the page simply falls back to today's auto-height layout.
|
||||
// ---------------------------------------------------------------------------
|
||||
const FILL_CLASS = 'dt-page-fill';
|
||||
|
||||
export function PageShell({ children, gap = 6, fill = false }) {
|
||||
return (
|
||||
<>
|
||||
{fill && (
|
||||
<style>{`
|
||||
.main-content-area:has(> .${FILL_CLASS}) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
`}</style>
|
||||
)}
|
||||
<VStack gap={gap} padding={0} className={fill ? FILL_CLASS : undefined} style={fill ? { flex: 1, minHeight: 0 } : undefined}>
|
||||
{children}
|
||||
</VStack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
PageShell.propTypes = {
|
||||
children: PropTypes.node,
|
||||
gap: PropTypes.number,
|
||||
fill: PropTypes.bool
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// KpiGrid — the row of StatCards under the page header.
|
||||
//
|
||||
// `repeat: 'fit'` is load-bearing and the reason this exists as a shared
|
||||
// primitive: Grid's default is `'fill'`, which PRESERVES empty tracks. With a
|
||||
// small minWidth in a wide container that lays out more tracks than there are
|
||||
// cards, so the cards bunch to the left and leave a dead gap on the right —
|
||||
// the single most visible layout bug on these pages. `'fit'` collapses the
|
||||
// empty tracks so the cards stretch to fill the row.
|
||||
//
|
||||
// `max` caps the column count so 3 cards don't each become 400px wide on an
|
||||
// ultrawide monitor; it defaults to the number of children.
|
||||
// ---------------------------------------------------------------------------
|
||||
export function KpiGrid({ children, minWidth = 240, max }) {
|
||||
const count = Array.isArray(children) ? children.filter(Boolean).length : 1;
|
||||
return (
|
||||
<Grid columns={{ minWidth, max: max || Math.max(count, 1), repeat: 'fit' }} gap={4}>
|
||||
{children}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
KpiGrid.propTypes = {
|
||||
children: PropTypes.node,
|
||||
minWidth: PropTypes.number,
|
||||
max: PropTypes.number
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FilterToolbar — the filter/search controls above a table.
|
||||
//
|
||||
// NOTE the padding lives on the VStack, not the Grid. `Grid` has no `padding`
|
||||
// prop (it declares only columns/gap/rowGap/columnGap/align/justify/width/
|
||||
// height), so passing one spreads it onto the DOM where it silently does
|
||||
// nothing — controls then sit flush against the card edge with no breathing
|
||||
// room. That bug is the reason this wrapper exists rather than each page
|
||||
// hand-rolling the pair.
|
||||
//
|
||||
// max 3 keeps a 6-control toolbar as two even rows of three instead of leaving
|
||||
// an orphan on a second row.
|
||||
// ---------------------------------------------------------------------------
|
||||
export function FilterToolbar({ children, minWidth = 220, max = 3, padding = 4 }) {
|
||||
return (
|
||||
<VStack padding={padding} gap={0}>
|
||||
<Grid columns={{ minWidth, max, repeat: 'fit' }} gap={4}>
|
||||
{children}
|
||||
</Grid>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
FilterToolbar.propTypes = {
|
||||
children: PropTypes.node,
|
||||
minWidth: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
padding: PropTypes.number
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DataCard — one card holding a table and everything that acts on it.
|
||||
//
|
||||
// Pages used to stack three separate Cards (filters, then tabs, then table),
|
||||
// which read as loose boxes with no relationship to each other. Grouping them
|
||||
// into a single card with dividers makes it obvious that the filters and tabs
|
||||
// act on the table below them.
|
||||
//
|
||||
// `tabs` gets its own padded row so the tab strip never sits flush against the
|
||||
// card edge or against the toolbar above it.
|
||||
//
|
||||
// `fill` (pairs with `<PageShell fill>`) makes the card a flex column that
|
||||
// claims the leftover viewport height: the toolbar and tab rows keep their
|
||||
// intrinsic height, and the content slot takes the rest with `min-height: 0`
|
||||
// so its scroll area can actually shrink rather than pushing the card past the
|
||||
// bottom of the screen. A table inside then only needs `maxHeight="100%"` —
|
||||
// no viewport arithmetic, and it re-measures on every resize for free.
|
||||
// ---------------------------------------------------------------------------
|
||||
export function DataCard({ toolbar, tabs, children, fill = false }) {
|
||||
return (
|
||||
<Card
|
||||
padding={0}
|
||||
elevation="none"
|
||||
style={fill ? { overflow: 'hidden', display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 } : { overflow: 'hidden' }}
|
||||
>
|
||||
{toolbar}
|
||||
{toolbar && <Divider />}
|
||||
{tabs && (
|
||||
<>
|
||||
<VStack gap={0} paddingInline={4} paddingBlock={2} style={{ overflowX: 'auto' }}>
|
||||
{tabs}
|
||||
</VStack>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
<VStack gap={0} padding={0} style={fill ? { flex: 1, minHeight: 0 } : undefined}>
|
||||
{children}
|
||||
</VStack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
DataCard.propTypes = {
|
||||
toolbar: PropTypes.node,
|
||||
tabs: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
fill: PropTypes.bool
|
||||
};
|
||||
Reference in New Issue
Block a user