@duro-app/ui 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Grid/Grid.d.ts +13 -3
- package/dist/components/Grid/Grid.d.ts.map +1 -1
- package/dist/components/Grid/Grid.meta.d.ts.map +1 -1
- package/dist/components/Grid/Grid.stories.d.ts +15 -0
- package/dist/components/Grid/Grid.stories.d.ts.map +1 -1
- package/dist/components/Grid/columns.d.ts +27 -0
- package/dist/components/Grid/columns.d.ts.map +1 -0
- package/dist/components/Grid/styles.css.d.ts +71 -0
- package/dist/components/Grid/styles.css.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +457 -343
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Grid/Grid.meta.ts +8 -2
- package/src/components/Grid/Grid.stories.tsx +83 -1
- package/src/components/Grid/Grid.tsx +117 -7
- package/src/components/Grid/columns.ts +58 -0
- package/src/components/Grid/styles.css.ts +72 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duro-app/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@tanstack/react-virtual": "^3.14.6",
|
|
67
|
-
"@duro-app/tokens": "^3.
|
|
67
|
+
"@duro-app/tokens": "^3.1.0"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@babel/preset-typescript": "^7.28.0",
|
|
@@ -2,11 +2,11 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'
|
|
5
|
+
'Grid layout. Columns are a count (1-6), a list of weights ([1, 2] for one-third / two-thirds), responsive auto-fit via minColumnWidth, or a named split layout (list/detail, nav/content) that collapses to one column on the width of its own container, so it is safe to nest and to place beside a DetailPanel. Weights rather than CSS template strings, so the same props render as CSS grid on web and as a wrapping flex row on native.',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'Card grids, dashboard layouts, multi-column forms',
|
|
8
8
|
'Responsive layouts that should auto-adjust column count',
|
|
9
|
-
'A list/detail or nav/content screen — layout="split" | "split-wide", never a hand-rolled gridTemplateColumns with its own @media',
|
|
9
|
+
'A list/detail or nav/content screen — layout="split" | "split-wide", never a hand-rolled gridTemplateColumns with its own @media; a split inside a split is fine, each collapses on its own room',
|
|
10
10
|
],
|
|
11
11
|
whenNotToUse: [
|
|
12
12
|
'Single-column vertical layout — use Stack',
|
|
@@ -42,5 +42,11 @@ export const meta: ComponentMeta = {
|
|
|
42
42
|
<Card>A</Card>
|
|
43
43
|
<Card>B</Card>
|
|
44
44
|
<Card>C</Card>
|
|
45
|
+
</Grid>
|
|
46
|
+
|
|
47
|
+
// Weighted: a one-third / two-thirds split (works on native too)
|
|
48
|
+
<Grid columns={[1, 2]} gap="md">
|
|
49
|
+
<Card>Sidebar</Card>
|
|
50
|
+
<Card>Content</Card>
|
|
45
51
|
</Grid>`,
|
|
46
52
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {Meta, StoryObj} from '@storybook/react'
|
|
2
|
+
import {expect} from 'storybook/test'
|
|
2
3
|
import {css, html} from 'react-strict-dom'
|
|
3
4
|
import {Grid} from './Grid'
|
|
4
5
|
import {Stack} from '../Stack/Stack'
|
|
@@ -19,6 +20,7 @@ const meta: Meta<typeof Grid> = {
|
|
|
19
20
|
columns: {
|
|
20
21
|
control: 'select',
|
|
21
22
|
options: [1, 2, 3, 4, 5, 6],
|
|
23
|
+
description: 'A count, or weights such as [1, 2] for a one-third / two-thirds split',
|
|
22
24
|
},
|
|
23
25
|
minColumnWidth: {control: 'text'},
|
|
24
26
|
layout: {control: 'select', options: [undefined, 'split', 'split-wide']},
|
|
@@ -41,6 +43,12 @@ const localStyles = css.create({
|
|
|
41
43
|
fontSize: typography.fontSizeXs,
|
|
42
44
|
color: colors.textMuted,
|
|
43
45
|
},
|
|
46
|
+
frame: (width: number) => ({
|
|
47
|
+
width,
|
|
48
|
+
borderWidth: 1,
|
|
49
|
+
borderStyle: 'dashed',
|
|
50
|
+
borderColor: colors.border,
|
|
51
|
+
}),
|
|
44
52
|
})
|
|
45
53
|
|
|
46
54
|
const Cell = ({children}: {children: string}) => (
|
|
@@ -89,7 +97,8 @@ export const Split: Story = {
|
|
|
89
97
|
render: () => (
|
|
90
98
|
<Stack gap="sm">
|
|
91
99
|
<html.span style={localStyles.label}>
|
|
92
|
-
layout="split" — list ≥ 240px beside the detail, one column
|
|
100
|
+
layout="split" — list ≥ 240px beside the detail, one column when the container is
|
|
101
|
+
narrower than sm; split-wide collapses below md
|
|
93
102
|
</html.span>
|
|
94
103
|
<Grid layout="split" gap="lg">
|
|
95
104
|
<Cell>list</Cell>
|
|
@@ -103,6 +112,79 @@ export const Split: Story = {
|
|
|
103
112
|
),
|
|
104
113
|
}
|
|
105
114
|
|
|
115
|
+
/**
|
|
116
|
+
* A split inside a split: a list/detail board in the content column of a
|
|
117
|
+
* nav/content shell. Each collapses on its OWN container: at 1120 both split;
|
|
118
|
+
* at 900 the shell still splits (≥ md) while the board — left 588px, under
|
|
119
|
+
* sm — stacks; at 700 the shell stacks and the board, now given the whole
|
|
120
|
+
* width, splits again; at 600 both stack. Keyed on the viewport, both would
|
|
121
|
+
* open at 768 and leave the detail pane ~150px.
|
|
122
|
+
*/
|
|
123
|
+
export const NestedSplits: Story = {
|
|
124
|
+
render: () => (
|
|
125
|
+
<Stack gap="lg">
|
|
126
|
+
{[1120, 900, 700, 600].map((width) => (
|
|
127
|
+
<Stack key={width} gap="sm">
|
|
128
|
+
<html.span style={localStyles.label}>frame {width}px</html.span>
|
|
129
|
+
<html.div style={localStyles.frame(width)} data-testid={`frame-${width}`}>
|
|
130
|
+
<Grid layout="split-wide" gap="xl">
|
|
131
|
+
<Cell>nav</Cell>
|
|
132
|
+
<Grid layout="split" gap="md">
|
|
133
|
+
<Cell>list</Cell>
|
|
134
|
+
<Cell>detail</Cell>
|
|
135
|
+
</Grid>
|
|
136
|
+
</Grid>
|
|
137
|
+
</html.div>
|
|
138
|
+
</Stack>
|
|
139
|
+
))}
|
|
140
|
+
</Stack>
|
|
141
|
+
),
|
|
142
|
+
play: async ({canvas}) => {
|
|
143
|
+
const tracks = (frame: HTMLElement, text: string) => {
|
|
144
|
+
const grid = canvas.getAllByText(text).find((el) => frame.contains(el))
|
|
145
|
+
?.parentElement as HTMLElement
|
|
146
|
+
return getComputedStyle(grid).gridTemplateColumns.split(' ').length
|
|
147
|
+
}
|
|
148
|
+
const expected: Record<number, [number, number]> = {
|
|
149
|
+
1120: [2, 2],
|
|
150
|
+
900: [2, 1],
|
|
151
|
+
700: [1, 2],
|
|
152
|
+
600: [1, 1],
|
|
153
|
+
}
|
|
154
|
+
for (const [width, [outer, inner]] of Object.entries(expected)) {
|
|
155
|
+
const frame = canvas.getByTestId(`frame-${width}`)
|
|
156
|
+
await expect(tracks(frame, 'nav'), `shell at ${width}`).toBe(outer)
|
|
157
|
+
await expect(tracks(frame, 'list'), `board at ${width}`).toBe(inner)
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Weighted columns: the portable form of `grid-template-columns: 1fr 2fr`.
|
|
164
|
+
* Weights are what the native Grid can honour too (as flex bases), where a
|
|
165
|
+
* CSS template string would be dropped.
|
|
166
|
+
*/
|
|
167
|
+
export const WeightedColumns: Story = {
|
|
168
|
+
render: () => (
|
|
169
|
+
<Grid gap="md" columns={[1, 2]}>
|
|
170
|
+
<Cell>Sidebar (1)</Cell>
|
|
171
|
+
<Cell>Content (2)</Cell>
|
|
172
|
+
<Cell>Sidebar (1)</Cell>
|
|
173
|
+
<Cell>Content (2)</Cell>
|
|
174
|
+
</Grid>
|
|
175
|
+
),
|
|
176
|
+
play: async ({canvas}) => {
|
|
177
|
+
// The grid is the cells' parent (Storybook wraps the story in its own
|
|
178
|
+
// divs). The browser resolves `1fr 2fr` to pixel tracks, so assert the
|
|
179
|
+
// ratio rather than the string.
|
|
180
|
+
const root = canvas.getAllByText('Sidebar (1)')[0].parentElement as HTMLElement
|
|
181
|
+
const tracks = getComputedStyle(root).gridTemplateColumns.split(' ').map(Number.parseFloat)
|
|
182
|
+
await expect(tracks).toHaveLength(2)
|
|
183
|
+
// Second track is twice the first, within a pixel of rounding.
|
|
184
|
+
await expect(Math.abs(tracks[1] - 2 * tracks[0])).toBeLessThan(1.5)
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
|
|
106
188
|
export const WithContainerQuery: Story = {
|
|
107
189
|
render: function Render() {
|
|
108
190
|
const {ref, size} = useContainerQuery<HTMLDivElement>()
|
|
@@ -1,18 +1,29 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import {Children, type ReactNode} from 'react'
|
|
2
2
|
import {html} from 'react-strict-dom'
|
|
3
3
|
import {styles} from './styles.css'
|
|
4
|
+
import {isNative} from '../../platform'
|
|
5
|
+
import {cellBasis, cellPosition, columnWeights, gridTemplate, type GridColumns} from './columns'
|
|
4
6
|
import type {SpacingToken} from '@duro-app/tokens/keys'
|
|
5
7
|
|
|
6
8
|
export type GridLayout = 'split' | 'split-wide'
|
|
7
9
|
|
|
8
10
|
interface GridProps {
|
|
9
11
|
gap?: SpacingToken
|
|
10
|
-
|
|
12
|
+
/** A column count (equal columns) or weights: `[1, 2]` gives a one-third /
|
|
13
|
+
* two-thirds split. Weights are the portable form of a track list — a CSS
|
|
14
|
+
* template string would be silently ignored on native. */
|
|
15
|
+
columns?: GridColumns
|
|
16
|
+
/** Responsive auto-fit: as many columns of at least this width as fit.
|
|
17
|
+
* Wins over `columns` when both are set. On native this approximates to
|
|
18
|
+
* cells of that minimum width that grow and wrap. */
|
|
11
19
|
minColumnWidth?: string
|
|
12
20
|
/**
|
|
13
21
|
* A named responsive layout. `split` is list/detail (1:2, list ≥ 240px),
|
|
14
|
-
* `split-wide` is nav/content (1:3, nav ≥ 280px)
|
|
15
|
-
* column
|
|
22
|
+
* `split-wide` is nav/content (1:3, nav ≥ 280px). Each collapses to one
|
|
23
|
+
* column on the width of its OWN container — `split` below `sm` (640px),
|
|
24
|
+
* `split-wide` below `md` (768px) — so a split nested inside another
|
|
25
|
+
* split, or beside an open DetailPanel, collapses when it runs out of room
|
|
26
|
+
* rather than when the window does. Wins over `columns` / `minColumnWidth`.
|
|
16
27
|
*/
|
|
17
28
|
layout?: GridLayout
|
|
18
29
|
children: ReactNode
|
|
@@ -29,6 +40,8 @@ const gapMap = {
|
|
|
29
40
|
xxxl: styles.gapXxxl,
|
|
30
41
|
} as const satisfies Record<SpacingToken, unknown>
|
|
31
42
|
|
|
43
|
+
// Fixed counts keep their static classes so existing web consumers emit the
|
|
44
|
+
// exact CSS they did before; only weight lists go through the dynamic style.
|
|
32
45
|
const columnsMap = {
|
|
33
46
|
1: styles.col1,
|
|
34
47
|
2: styles.col2,
|
|
@@ -43,14 +56,111 @@ const layoutMap = {
|
|
|
43
56
|
'split-wide': styles.splitWide,
|
|
44
57
|
} as const satisfies Record<GridLayout, unknown>
|
|
45
58
|
|
|
59
|
+
// On native the named layouts are their weights; there is no media query to
|
|
60
|
+
// collapse them, so a split stays a split on a phone (see NativeGrid).
|
|
61
|
+
const layoutWeights = {
|
|
62
|
+
split: [1, 2],
|
|
63
|
+
'split-wide': [1, 3],
|
|
64
|
+
} as const satisfies Record<GridLayout, readonly number[]>
|
|
65
|
+
|
|
66
|
+
const cellGapLeftMap = {
|
|
67
|
+
xs: styles.cellGapLeftXs,
|
|
68
|
+
sm: styles.cellGapLeftSm,
|
|
69
|
+
ms: styles.cellGapLeftMs,
|
|
70
|
+
md: styles.cellGapLeftMd,
|
|
71
|
+
lg: styles.cellGapLeftLg,
|
|
72
|
+
xl: styles.cellGapLeftXl,
|
|
73
|
+
xxl: styles.cellGapLeftXxl,
|
|
74
|
+
xxxl: styles.cellGapLeftXxxl,
|
|
75
|
+
} as const satisfies Record<SpacingToken, unknown>
|
|
76
|
+
|
|
77
|
+
const cellGapTopMap = {
|
|
78
|
+
xs: styles.cellGapTopXs,
|
|
79
|
+
sm: styles.cellGapTopSm,
|
|
80
|
+
ms: styles.cellGapTopMs,
|
|
81
|
+
md: styles.cellGapTopMd,
|
|
82
|
+
lg: styles.cellGapTopLg,
|
|
83
|
+
xl: styles.cellGapTopXl,
|
|
84
|
+
xxl: styles.cellGapTopXxl,
|
|
85
|
+
xxxl: styles.cellGapTopXxxl,
|
|
86
|
+
} as const satisfies Record<SpacingToken, unknown>
|
|
87
|
+
|
|
46
88
|
export function Grid({gap = 'md', columns, minColumnWidth, layout, children}: GridProps) {
|
|
89
|
+
if (isNative) {
|
|
90
|
+
return (
|
|
91
|
+
<NativeGrid
|
|
92
|
+
gap={gap}
|
|
93
|
+
columns={layout ? layoutWeights[layout] : columns}
|
|
94
|
+
minColumnWidth={layout ? undefined : minColumnWidth}
|
|
95
|
+
>
|
|
96
|
+
{children}
|
|
97
|
+
</NativeGrid>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
47
101
|
const columnStyle = layout
|
|
48
102
|
? layoutMap[layout]
|
|
49
103
|
: minColumnWidth
|
|
50
104
|
? styles.autoFit(minColumnWidth)
|
|
51
|
-
: columns
|
|
105
|
+
: typeof columns === 'number'
|
|
52
106
|
? columnsMap[columns]
|
|
53
|
-
:
|
|
107
|
+
: columns
|
|
108
|
+
? styles.template(gridTemplate(columnWeights(columns) ?? [1]))
|
|
109
|
+
: undefined
|
|
110
|
+
|
|
111
|
+
const grid = <html.div style={[styles.base, gapMap[gap], columnStyle]}>{children}</html.div>
|
|
112
|
+
// A named layout answers its container query from a wrapper: an element
|
|
113
|
+
// cannot query its own size.
|
|
114
|
+
return layout ? <html.div style={styles.splitContainer}>{grid}</html.div> : grid
|
|
115
|
+
}
|
|
54
116
|
|
|
55
|
-
|
|
117
|
+
/**
|
|
118
|
+
* React Native has no grid, so the native Grid is a wrapping flex row with
|
|
119
|
+
* each child in a cell:
|
|
120
|
+
*
|
|
121
|
+
* - Weighted / counted columns: the cell's `flexBasis` is its column's share
|
|
122
|
+
* of the row. Percentages plus a real `gap` would overflow and wrap early,
|
|
123
|
+
* so the gap is padding on the cell instead — left on every cell that does
|
|
124
|
+
* not open a row, top on every row after the first. RN is border-box, so
|
|
125
|
+
* the bases still sum to exactly 100%, and the gap stays a token.
|
|
126
|
+
* - `minColumnWidth`: cells start at that width and grow, and the row uses a
|
|
127
|
+
* real `gap`, since here wrapping is width-driven and row membership is
|
|
128
|
+
* not known from the index.
|
|
129
|
+
*
|
|
130
|
+
* Only this branch wraps children; on web grid children stay direct, so
|
|
131
|
+
* existing DOM trees are untouched.
|
|
132
|
+
*/
|
|
133
|
+
function NativeGrid({gap = 'md', columns, minColumnWidth, children}: GridProps) {
|
|
134
|
+
const items = Children.toArray(children)
|
|
135
|
+
if (minColumnWidth) {
|
|
136
|
+
return (
|
|
137
|
+
<html.div style={[styles.nativeRow, gapMap[gap]]}>
|
|
138
|
+
{items.map((child, index) => (
|
|
139
|
+
<html.div key={index} style={styles.nativeMinCell(minColumnWidth)}>
|
|
140
|
+
{child}
|
|
141
|
+
</html.div>
|
|
142
|
+
))}
|
|
143
|
+
</html.div>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
const weights = columnWeights(columns) ?? [1]
|
|
147
|
+
return (
|
|
148
|
+
<html.div style={styles.nativeRow}>
|
|
149
|
+
{items.map((child, index) => {
|
|
150
|
+
const {opensRow, firstRow} = cellPosition(index, weights.length)
|
|
151
|
+
return (
|
|
152
|
+
<html.div
|
|
153
|
+
key={index}
|
|
154
|
+
style={[
|
|
155
|
+
styles.nativeCell(cellBasis(weights, index)),
|
|
156
|
+
!opensRow && cellGapLeftMap[gap],
|
|
157
|
+
!firstRow && cellGapTopMap[gap],
|
|
158
|
+
]}
|
|
159
|
+
>
|
|
160
|
+
{child}
|
|
161
|
+
</html.div>
|
|
162
|
+
)
|
|
163
|
+
})}
|
|
164
|
+
</html.div>
|
|
165
|
+
)
|
|
56
166
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Pure column arithmetic behind Grid. No react-strict-dom, no platform: this
|
|
2
|
+
// is what the unit project tests, and what both the web and native renderers
|
|
3
|
+
// derive their styles from.
|
|
4
|
+
|
|
5
|
+
export type GridColumnCount = 1 | 2 | 3 | 4 | 5 | 6
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Column spec: a count (equal columns) or a list of weights (`[1, 2]` is a
|
|
9
|
+
* one-third / two-thirds split). Weights are the portable form of
|
|
10
|
+
* `grid-template-columns: 1fr 2fr` — the only shape both runtimes can
|
|
11
|
+
* honour, since React Native has no grid.
|
|
12
|
+
*/
|
|
13
|
+
export type GridColumns = GridColumnCount | readonly number[]
|
|
14
|
+
|
|
15
|
+
/** Normalise a column spec to weights; `undefined` when no spec was given
|
|
16
|
+
* (the caller then falls back to auto-fit or a single column). An invalid
|
|
17
|
+
* weight list (empty, non-finite or non-positive entries) is a programming
|
|
18
|
+
* error, reported in development and treated as one column. */
|
|
19
|
+
export function columnWeights(columns: GridColumns | undefined): readonly number[] | undefined {
|
|
20
|
+
if (columns === undefined) return undefined
|
|
21
|
+
if (typeof columns === 'number') return Array.from({length: columns}, () => 1)
|
|
22
|
+
const valid = columns.length > 0 && columns.every((w) => Number.isFinite(w) && w > 0)
|
|
23
|
+
if (!valid) {
|
|
24
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
25
|
+
console.error(
|
|
26
|
+
`Grid: \`columns\` must be a non-empty list of positive numbers, got ${JSON.stringify(columns)}. Falling back to one column.`,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
return [1]
|
|
30
|
+
}
|
|
31
|
+
return columns
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Web track list: `[1, 2]` → `"1fr 2fr"`. */
|
|
35
|
+
export function gridTemplate(weights: readonly number[]): string {
|
|
36
|
+
return weights.map((w) => `${w}fr`).join(' ')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Native cell basis for the child at `index`: its column's share of the row
|
|
40
|
+
* as a percentage string Yoga accepts (`"33.3333%"`). Children flow into
|
|
41
|
+
* columns in order and wrap, exactly like grid auto-placement. */
|
|
42
|
+
export function cellBasis(weights: readonly number[], index: number): string {
|
|
43
|
+
const total = weights.reduce((sum, w) => sum + w, 0)
|
|
44
|
+
const share = (weights[index % weights.length] / total) * 100
|
|
45
|
+
// Four decimals keeps a row summing to 100% within Yoga's tolerance while
|
|
46
|
+
// never exceeding it (which would wrap the last cell).
|
|
47
|
+
return `${Math.floor(share * 10000) / 10000}%`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Where the child at `index` sits in an `columnCount`-wide flow: whether it
|
|
51
|
+
* opens a row (no gap to its left) and whether it is on the first row (no
|
|
52
|
+
* gap above). Drives the native gap-as-padding scheme. */
|
|
53
|
+
export function cellPosition(
|
|
54
|
+
index: number,
|
|
55
|
+
columnCount: number,
|
|
56
|
+
): {opensRow: boolean; firstRow: boolean} {
|
|
57
|
+
return {opensRow: index % columnCount === 0, firstRow: index < columnCount}
|
|
58
|
+
}
|
|
@@ -2,9 +2,15 @@ import {css} from 'react-strict-dom'
|
|
|
2
2
|
import {spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
3
3
|
import {breakpoints} from '@duro-app/tokens/tokens/breakpoints.css'
|
|
4
4
|
|
|
5
|
-
// Where a split collapses to one column
|
|
6
|
-
// the
|
|
7
|
-
|
|
5
|
+
// Where a split collapses to one column — measured on the split's OWN
|
|
6
|
+
// container, not the viewport. A split nested in another split (a list/detail
|
|
7
|
+
// board inside a nav/content shell) has far less room than the window: at a
|
|
8
|
+
// 768px viewport the shell leaves ~408px for its content column, and a
|
|
9
|
+
// viewport-keyed inner split would still open there and squeeze its detail
|
|
10
|
+
// pane to ~150px. Container queries collapse each split on what it actually
|
|
11
|
+
// has. defineConsts strings, inlined into the @container text at build time.
|
|
12
|
+
const SPLIT_WIDE_BP = breakpoints.md // 280px rail + real content
|
|
13
|
+
const SPLIT_BP = breakpoints.sm // 240px list + ≥ 384px detail
|
|
8
14
|
|
|
9
15
|
export const styles = css.create({
|
|
10
16
|
base: {
|
|
@@ -19,22 +25,34 @@ export const styles = css.create({
|
|
|
19
25
|
autoFit: (minWidth: string) => ({
|
|
20
26
|
gridTemplateColumns: `repeat(auto-fill, minmax(${minWidth}, 1fr))`,
|
|
21
27
|
}),
|
|
28
|
+
// Hosts the @container query for a named layout. A container cannot query
|
|
29
|
+
// itself, so the grid sits inside this wrapper (the Table.rootContainer
|
|
30
|
+
// pattern). Only `layout` grids get it: counted and weighted grids keep
|
|
31
|
+
// their DOM exactly as before.
|
|
32
|
+
splitContainer: {
|
|
33
|
+
containerType: 'inline-size',
|
|
34
|
+
},
|
|
22
35
|
// Asymmetric list/detail splits: a bounded first column, the rest for the
|
|
23
|
-
// detail; one column
|
|
24
|
-
// parameter because the pair (widths +
|
|
25
|
-
// decision — a screen picks a split, it does
|
|
36
|
+
// detail; one column when the container is narrower than the breakpoint.
|
|
37
|
+
// Static entries rather than a parameter because the pair (widths +
|
|
38
|
+
// collapse point) is the design decision — a screen picks a split, it does
|
|
39
|
+
// not tune one.
|
|
26
40
|
split: {
|
|
27
41
|
gridTemplateColumns: {
|
|
28
42
|
default: '1fr',
|
|
29
|
-
[`@
|
|
43
|
+
[`@container (min-width: ${SPLIT_BP})`]: 'minmax(240px, 1fr) minmax(0, 2fr)',
|
|
30
44
|
},
|
|
31
45
|
},
|
|
32
46
|
splitWide: {
|
|
33
47
|
gridTemplateColumns: {
|
|
34
48
|
default: '1fr',
|
|
35
|
-
[`@
|
|
49
|
+
[`@container (min-width: ${SPLIT_WIDE_BP})`]: 'minmax(280px, 1fr) minmax(0, 3fr)',
|
|
36
50
|
},
|
|
37
51
|
},
|
|
52
|
+
// Weighted columns: `[1, 2]` → `1fr 2fr` (see columns.ts).
|
|
53
|
+
template: (tracks: string) => ({
|
|
54
|
+
gridTemplateColumns: tracks,
|
|
55
|
+
}),
|
|
38
56
|
gapXs: {gap: spacing.xs},
|
|
39
57
|
gapSm: {gap: spacing.sm},
|
|
40
58
|
gapMs: {gap: spacing.ms},
|
|
@@ -43,4 +61,50 @@ export const styles = css.create({
|
|
|
43
61
|
gapXl: {gap: spacing.xl},
|
|
44
62
|
gapXxl: {gap: spacing.xxl},
|
|
45
63
|
gapXxxl: {gap: spacing.xxxl},
|
|
64
|
+
|
|
65
|
+
// --- native only (see NativeGrid in Grid.tsx) -----------------------------
|
|
66
|
+
// React Native honours `display: 'flex'` only, and RSD gives it web flex
|
|
67
|
+
// semantics, so the row direction is explicit.
|
|
68
|
+
nativeRow: {
|
|
69
|
+
display: 'flex',
|
|
70
|
+
flexDirection: 'row',
|
|
71
|
+
flexWrap: 'wrap',
|
|
72
|
+
alignItems: 'stretch',
|
|
73
|
+
// A definite width so percentage bases resolve in Yoga's measure pass;
|
|
74
|
+
// against an auto width the line breaks come out wrong and the container
|
|
75
|
+
// under-reports its height (verified on the iOS simulator).
|
|
76
|
+
width: '100%',
|
|
77
|
+
},
|
|
78
|
+
// border-box is explicit: react-strict-dom mirrors the web default
|
|
79
|
+
// (content-box) on native, which adds the gap padding on top of the basis
|
|
80
|
+
// and overflows the row so every cell wraps.
|
|
81
|
+
nativeCell: (basis: string) => ({
|
|
82
|
+
flexBasis: basis,
|
|
83
|
+
flexShrink: 0,
|
|
84
|
+
boxSizing: 'border-box',
|
|
85
|
+
}),
|
|
86
|
+
// Auto-fit approximation: start at the minimum width, grow to fill. flexGrow
|
|
87
|
+
// is fine here: this style is native-only, so RSD-web's forced flex-grow: 0
|
|
88
|
+
// never applies.
|
|
89
|
+
nativeMinCell: (minWidth: string) => ({
|
|
90
|
+
flexBasis: minWidth,
|
|
91
|
+
flexGrow: 1,
|
|
92
|
+
boxSizing: 'border-box',
|
|
93
|
+
}),
|
|
94
|
+
cellGapLeftXs: {paddingLeft: spacing.xs},
|
|
95
|
+
cellGapLeftSm: {paddingLeft: spacing.sm},
|
|
96
|
+
cellGapLeftMs: {paddingLeft: spacing.ms},
|
|
97
|
+
cellGapLeftMd: {paddingLeft: spacing.md},
|
|
98
|
+
cellGapLeftLg: {paddingLeft: spacing.lg},
|
|
99
|
+
cellGapLeftXl: {paddingLeft: spacing.xl},
|
|
100
|
+
cellGapLeftXxl: {paddingLeft: spacing.xxl},
|
|
101
|
+
cellGapLeftXxxl: {paddingLeft: spacing.xxxl},
|
|
102
|
+
cellGapTopXs: {paddingTop: spacing.xs},
|
|
103
|
+
cellGapTopSm: {paddingTop: spacing.sm},
|
|
104
|
+
cellGapTopMs: {paddingTop: spacing.ms},
|
|
105
|
+
cellGapTopMd: {paddingTop: spacing.md},
|
|
106
|
+
cellGapTopLg: {paddingTop: spacing.lg},
|
|
107
|
+
cellGapTopXl: {paddingTop: spacing.xl},
|
|
108
|
+
cellGapTopXxl: {paddingTop: spacing.xxl},
|
|
109
|
+
cellGapTopXxxl: {paddingTop: spacing.xxxl},
|
|
46
110
|
})
|