@duro-app/ui 3.0.0 → 3.2.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/DragDrop/DragDrop.d.ts +67 -0
- package/dist/components/DragDrop/DragDrop.d.ts.map +1 -0
- package/dist/components/DragDrop/DragDrop.meta.d.ts +3 -0
- package/dist/components/DragDrop/DragDrop.meta.d.ts.map +1 -0
- package/dist/components/DragDrop/DragDrop.stories.d.ts +21 -0
- package/dist/components/DragDrop/DragDrop.stories.d.ts.map +1 -0
- package/dist/components/DragDrop/styles.css.d.ts +52 -0
- package/dist/components/DragDrop/styles.css.d.ts.map +1 -0
- 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.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2961 -2489
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/DragDrop/DragDrop.meta.ts +49 -0
- package/src/components/DragDrop/DragDrop.stories.tsx +249 -0
- package/src/components/DragDrop/DragDrop.tsx +502 -0
- package/src/components/DragDrop/styles.css.ts +62 -0
- 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/src/index.ts +7 -0
|
@@ -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
|
})
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,13 @@ export {Checkbox} from './components/Checkbox/Checkbox'
|
|
|
20
20
|
export {CheckboxGroup} from './components/CheckboxGroup/CheckboxGroup'
|
|
21
21
|
export {ColorInput} from './components/ColorInput/ColorInput'
|
|
22
22
|
export {ConfirmDialog} from './components/ConfirmDialog/ConfirmDialog'
|
|
23
|
+
export {
|
|
24
|
+
DragDrop,
|
|
25
|
+
useDragDrop,
|
|
26
|
+
type DragDropEvent,
|
|
27
|
+
type DragDropItemData,
|
|
28
|
+
type DragDropTarget,
|
|
29
|
+
} from './components/DragDrop/DragDrop'
|
|
23
30
|
export {Dialog, type DialogSize} from './components/Dialog/Dialog'
|
|
24
31
|
export {DetailPanel, type DetailPanelSize} from './components/DetailPanel/DetailPanel'
|
|
25
32
|
export {Drawer, type DrawerAnchor, type DrawerSize} from './components/Drawer/Drawer'
|