@loke/design-system
Version:
A design system with individually importable components
312 lines (239 loc) • 10.7 kB
Markdown
---
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