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. // // // // …StatCard… // …} tabs={}> // …table… // // // --------------------------------------------------------------------------- // 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 `` — 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 && ( )} {children} ); } 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 ( {children} ); } 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 ( {children} ); } 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 ``) 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 ( {toolbar} {toolbar && } {tabs && ( <> {tabs} )} {children} ); } DataCard.propTypes = { toolbar: PropTypes.node, tabs: PropTypes.node, children: PropTypes.node, fill: PropTypes.bool };