UNPKG

@loke/design-system

Version:

A design system with individually importable components

312 lines (239 loc) 10.7 kB
--- name: layout description: > Structure pages with responsive layout primitives. Box (responsive props for display, flex, gap, padding, margin, width, height, position, overflow), Stack (vertical flex, default gap=2), Inline (horizontal flex, default gap=2), Columns/Column (CSS grid 1-12 + auto-fill, responsive column count), MaxWidthWrapper (container centering), PageLayout (sidebar-aware header + content). ResponsiveValue type: T | [T,T] | [T,T,T] | [T,T,T,T] mapping to base/sm/md/lg breakpoints. Activate when building page layouts or using responsive spacing. type: core library: '@loke/design-system' library_version: '2.0.0-rc.6' requires: - getting-started sources: - 'LOKE/merchant-frontends:packages/design-system/src/layout/box' - 'LOKE/merchant-frontends:packages/design-system/src/layout/stack' - 'LOKE/merchant-frontends:packages/design-system/src/layout/inline' - 'LOKE/merchant-frontends:packages/design-system/src/layout/columns' - 'LOKE/merchant-frontends:packages/design-system/src/layout/max-width-wrapper' - 'LOKE/merchant-frontends:packages/design-system/src/layout/page-layout' - 'LOKE/merchant-frontends:packages/design-system/src/lib/responsive' --- # Layout Primitives This skill builds on **getting-started**. Read it first for setup and imports. ## Setup Layout primitives compose together to build page structures. Import each from its subpath: ```tsx import { Box } from "@loke/design-system/box"; import { Stack } from "@loke/design-system/stack"; import { Inline } from "@loke/design-system/inline"; import { Columns, Column } from "@loke/design-system/columns"; import { MaxWidthWrapper } from "@loke/design-system/max-width-wrapper"; import { PageLayout, PageLayoutSidebarProvider } from "@loke/design-system/page-layout"; ``` Basic composition -- Stack for vertical flow, Inline for horizontal, Box for custom: ```tsx <Stack gap={4}> <Inline gap={2}> <Box padding={4} background="card" borderRadius="lg"> Card content </Box> <Box padding={4} background="card" borderRadius="lg"> Another card </Box> </Inline> <Box paddingY={2}>Footer area</Box> </Stack> ``` ## Core Patterns ### Box with responsive props Box is the foundational layout primitive. Every variant prop accepts a `ResponsiveValue<T>` -- either a single value or an array mapping to breakpoints: | Format | Breakpoints | |--------|-------------| | `gap={4}` | All breakpoints | | `gap={[2, 4]}` | base=2, sm=4 | | `gap={[2, 4, 6]}` | base=2, sm=4, md=6 | | `gap={[2, 4, 6, 8]}` | base=2, sm=4, md=6, lg=8 | The `ResponsiveValue` type is defined as: ```ts type ResponsiveValue<T> = T | [T, T] | [T, T, T] | [T, T, T, T]; ``` Breakpoints map to Tailwind prefixes: `""` (base), `"sm:"`, `"md:"`, `"lg:"`. **All responsive Box props:** | Category | Props | |----------|-------| | Display | `display` (`block`, `flex`, `grid`, `hidden`, `inline`, `inline-block`, `inline-flex`, `inline-grid`, `table-cell`) | | Flex | `flexDirection` (`row`, `col`, `row-reverse`, `col-reverse`), `flexWrap` (`wrap`, `nowrap`, `wrap-reverse`), `flexGrow`, `flexShrink` | | Alignment | `alignItems` (`start`, `center`, `end`, `baseline`, `stretch`), `justifyContent` (`start`, `center`, `end`, `between`, `around`, `evenly`) | | Spacing | `gap`, `gapX`, `gapY`, `spaceX`, `spaceY` | | Padding | `padding`, `paddingX`, `paddingY`, `paddingTop`, `paddingBottom`, `paddingLeft`, `paddingRight` | | Margin | `margin`, `marginX`, `marginY`, `marginTop`, `marginBottom`, `marginLeft`, `marginRight` (all support `auto`) | | Dimensions | `width`, `height`, `minWidth`, `minHeight`, `maxWidth`, `maxHeight` | | Position | `position` (`relative`, `absolute`, `fixed`, `sticky`, `static`), `top`, `right`, `bottom`, `left` (support negative values), `zIndex` | | Overflow | `overflow`, `overflowX`, `overflowY` (`auto`, `hidden`, `scroll`, `visible`) | | Border | `border`, `borderTop`, `borderBottom`, `borderLeft`, `borderRight` (boolean), `borderColor`, `borderRadius` (`sm`, `md`, `lg`, `xl`, `2xl`, `3xl`, `full`, `none`) | | Visual | `background` (color tokens), `boxShadow` (`sm`, `md`, `lg`, `xl`, `2xl`, `inner`, `none`), `cursor`, `pointerEvents`, `textAlign` | | Other | `container` (boolean), `as` (HTML element tag), `asChild` (slot pattern) | Size values are numeric: `0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 16, 20, 24, 28, 32` or named: `none`, `xxsmall`, `xsmall`, `small`, `medium`, `large`, `xlarge`, `xxlarge`. ```tsx <Box display="flex" flexDirection={["col", "row"]} gap={[2, 4, 6, 8]} padding={[2, 4]} background="card" borderRadius="lg" overflow="hidden" > {children} </Box> ``` The `as` prop changes the rendered HTML element (default `"div"`): ```tsx <Box as="section" padding={4}> Section content </Box> ``` ### Stack and Inline **Stack** renders a vertical flex container (`display="flex"`, `flexDirection="col"`). Default `gap={2}`. Accepts all Box props except `display` and `flexDirection`. ```tsx import { Stack } from "@loke/design-system/stack"; <Stack gap={4} padding={2} alignItems="start"> <div>First item</div> <div>Second item</div> <div>Third item</div> </Stack> ``` **Inline** renders a horizontal inline-flex container (`display="inline-flex"`, `flexDirection="row"`). Default `gap={2}`. Same prop restrictions as Stack. ```tsx import { Inline } from "@loke/design-system/inline"; <Inline gap={3} alignItems="center" flexWrap="wrap"> <Tag>React</Tag> <Tag>TypeScript</Tag> <Tag>Tailwind</Tag> </Inline> ``` Both support responsive gap: ```tsx <Stack gap={[2, 4, 6]}> {items} </Stack> ``` ### Columns for grid layouts **Columns** renders a CSS grid. Props: `columns` (1-12 or `"auto-fill"`, default 2), `gap` (default 1). Both are responsive. **Column** sets `colSpan` (1-12, default 1) on a grid child. Also responsive. ```tsx import { Columns, Column } from "@loke/design-system/columns"; <Columns columns={[1, 2, 3]} gap={4}> <Column colSpan={1}>Sidebar</Column> <Column colSpan={[1, 1, 2]}>Main content</Column> </Columns> ``` A 12-column responsive layout: ```tsx <Columns columns={12} gap={4}> <Column colSpan={[12, 4, 3]}>Navigation</Column> <Column colSpan={[12, 8, 9]}>Content</Column> </Columns> ``` Auto-fill columns: ```tsx <Columns columns="auto-fill" gap={2}> {cards.map((card) => ( <div key={card.id}>{card.title}</div> ))} </Columns> ``` ### MaxWidthWrapper for container centering Centers content with a max-width container and responsive horizontal padding (`px-2.5` base, `md:px-20`). ```tsx import { MaxWidthWrapper } from "@loke/design-system/max-width-wrapper"; <MaxWidthWrapper> <h1>Page content</h1> </MaxWidthWrapper> ``` Accepts `className` and `style` for customization. ### PageLayout with sidebar Full-page layout with a sidebar-aware header and scrollable content area. Includes a loading state with a centered Spinner. ```tsx import { PageLayout, PageLayoutSidebarProvider } from "@loke/design-system/page-layout"; import { Settings } from "@loke/icons"; // With sidebar context (wrap at app level) <PageLayoutSidebarProvider sidebar={{ trigger: <SidebarTrigger />, imageUrl: "/logo.svg" }} > <PageLayout header={{ icon: Settings, title: "Settings", belowTitle: <span>Manage your account</span>, rightAside: <Button>Save</Button>, }} > <div>Page content here</div> </PageLayout> </PageLayoutSidebarProvider> ``` **PageLayoutProps:** | Prop | Type | Description | |------|------|-------------| | `header.icon` | `LokeIcon` | Icon component from `@loke/icons` | | `header.title` | `string` | Page heading text | | `header.belowTitle` | `ReactNode` (optional) | Content below the title | | `header.rightAside` | `ReactNode` (optional) | Right-aligned header content | | `sidebar` | `SidebarConfig` (optional) | Override sidebar from context | | `loading` | `boolean` (optional) | Shows centered Spinner instead of children | The sidebar can be provided directly via the `sidebar` prop or through `PageLayoutSidebarProvider` context. The direct prop takes precedence. On mobile (`md:hidden`), the sidebar renders a top bar with the trigger, a centered logo image, and a spacer. `HEADER_HEIGHT` is exported as `"8rem"` for layout calculations. ## Common Mistakes ### HIGH: Mixing responsive array props with className Responsive prop arrays and Tailwind responsive className prefixes target the same breakpoints and will conflict: ```tsx // WRONG -- responsive gap prop and sm:gap-8 className conflict <Box gap={[2, 4, 6]} className="sm:gap-8" /> // CORRECT -- use responsive props exclusively for responsive behavior <Box gap={[2, 4, 8, 8]} /> // CORRECT -- className for non-responsive additions only <Box gap={[2, 4, 6]} className="border-dashed" /> ``` Use responsive prop arrays for responsive behavior. Use `className` only for static styles that the prop API does not cover. ### MEDIUM: Using PageLayout without SidebarProvider PageLayout reads `PageLayoutSidebarContext` to render the mobile sidebar bar. If no sidebar is provided via props or context, the mobile top bar is omitted silently -- which may cause a broken layout on small screens. ```tsx // WRONG -- no sidebar context, mobile layout has no top bar <PageLayout header={{ icon: Settings, title: "Settings" }}> {children} </PageLayout> // CORRECT -- provide sidebar via context <PageLayoutSidebarProvider sidebar={{ trigger: <MenuButton />, imageUrl: "/logo.svg" }}> <PageLayout header={{ icon: Settings, title: "Settings" }}> {children} </PageLayout> </PageLayoutSidebarProvider> // CORRECT -- provide sidebar via prop <PageLayout sidebar={{ trigger: <MenuButton />, imageUrl: "/logo.svg" }} header={{ icon: Settings, title: "Settings" }} > {children} </PageLayout> ``` ### MEDIUM: Inconsistent responsive breakpoint arrays A 3-value array `[1, 2, 3]` maps to base/sm/md. The `lg` breakpoint inherits the `md` value. This is fine when intentional, but can cause confusion when you expect all four breakpoints covered. ```tsx // 3 values: base=1, sm=2, md=3, lg inherits md (3) <Columns columns={[1, 2, 3]} /> // Explicit 4 values: base=1, sm=2, md=3, lg=4 <Columns columns={[1, 2, 3, 4]} /> ``` When targeting all breakpoints, prefer the explicit 4-value form to avoid ambiguity. --- **Tension note:** Responsive props vs className escape hatch -- Layout responsive prop arrays conflict with Tailwind className responsive prefixes. Use props for responsive behavior, className for non-responsive static styles. See also: theming/SKILL.md ## See also - display-components/SKILL.md - interactive-components/SKILL.md - theming/SKILL.md