@duro-app/ui 0.32.0 → 0.33.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/Table/SortChip.d.ts +28 -0
- package/dist/components/Table/SortChip.d.ts.map +1 -0
- package/dist/components/Table/Table.d.ts +21 -3
- package/dist/components/Table/Table.d.ts.map +1 -1
- package/dist/components/Table/Table.stories.d.ts +1 -0
- package/dist/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/components/Table/styles.css.d.ts +49 -5
- package/dist/components/Table/styles.css.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +2344 -2213
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Table/SortChip.tsx +52 -0
- package/src/components/Table/Table.stories.tsx +90 -0
- package/src/components/Table/Table.tsx +87 -30
- package/src/components/Table/styles.css.ts +189 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duro-app/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@duro-app/tokens": "^0.
|
|
56
|
+
"@duro-app/tokens": "^0.33.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@babel/preset-typescript": "^7.28.0",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {html} from 'react-strict-dom'
|
|
2
|
+
import {styles} from './styles.css'
|
|
3
|
+
import {Menu} from '../Menu/Menu'
|
|
4
|
+
|
|
5
|
+
export interface SortValue {
|
|
6
|
+
readonly id: string
|
|
7
|
+
readonly desc: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SortChipProps {
|
|
11
|
+
readonly options: ReadonlyArray<{readonly id: string; readonly label: string}>
|
|
12
|
+
readonly value: SortValue | null
|
|
13
|
+
readonly onChange: (next: SortValue | null) => void
|
|
14
|
+
/** Visible label preceding the sort field, e.g. "Sort by". */
|
|
15
|
+
readonly label?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Sort selector for stack-mode Tables. Hidden in default/compact via
|
|
20
|
+
* `@container` query — see styles.sortChip. Consumers render this inside
|
|
21
|
+
* a `Table.Container` so the same query target reaches it.
|
|
22
|
+
*
|
|
23
|
+
* The chip is a simple controlled wrapper around `Menu`. It does NOT
|
|
24
|
+
* couple to @tanstack/react-table — the consumer wires `value` and
|
|
25
|
+
* `onChange` from whatever sort state they manage.
|
|
26
|
+
*/
|
|
27
|
+
export function SortChip({options, value, onChange, label = 'Sort by'}: SortChipProps) {
|
|
28
|
+
const current = value ? options.find((o) => o.id === value.id) : null
|
|
29
|
+
const triggerLabel = current ? `${label}: ${current.label} ${value!.desc ? '↓' : '↑'}` : label
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<html.div style={styles.sortChip}>
|
|
33
|
+
<Menu.Root>
|
|
34
|
+
<Menu.Trigger>{triggerLabel}</Menu.Trigger>
|
|
35
|
+
<Menu.Popup>
|
|
36
|
+
{options.map((opt) => {
|
|
37
|
+
const isCurrent = value?.id === opt.id
|
|
38
|
+
const nextDesc = isCurrent ? !value!.desc : false
|
|
39
|
+
return (
|
|
40
|
+
<Menu.Item key={opt.id} onClick={() => onChange({id: opt.id, desc: nextDesc})}>
|
|
41
|
+
{opt.label}
|
|
42
|
+
{isCurrent ? ` ${value!.desc ? '↓' : '↑'}` : ''}
|
|
43
|
+
</Menu.Item>
|
|
44
|
+
)
|
|
45
|
+
})}
|
|
46
|
+
{value !== null && <Menu.Item onClick={() => onChange(null)}>Clear sort</Menu.Item>}
|
|
47
|
+
</Menu.Popup>
|
|
48
|
+
</Menu.Root>
|
|
49
|
+
</html.div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
SortChip.displayName = 'SortChip'
|
|
@@ -878,3 +878,93 @@ export const FullFeatured: Story = {
|
|
|
878
878
|
await expect(canvas.getByText('Previous')).toBeInTheDocument()
|
|
879
879
|
},
|
|
880
880
|
}
|
|
881
|
+
|
|
882
|
+
// Demonstrates the three responsive modes. Resize the canvas (or wrap in
|
|
883
|
+
// a narrower container) to see Compact (~720px) and Stack (~440px).
|
|
884
|
+
const responsiveStyles = css.create({
|
|
885
|
+
resizable: {
|
|
886
|
+
resize: 'horizontal',
|
|
887
|
+
overflow: 'auto',
|
|
888
|
+
minWidth: '320px',
|
|
889
|
+
maxWidth: '100%',
|
|
890
|
+
width: '900px',
|
|
891
|
+
padding: spacing.sm,
|
|
892
|
+
borderWidth: 1,
|
|
893
|
+
borderStyle: 'dashed',
|
|
894
|
+
borderColor: colors.border,
|
|
895
|
+
},
|
|
896
|
+
})
|
|
897
|
+
|
|
898
|
+
export const Responsive: Story = {
|
|
899
|
+
name: 'Responsive (Container Queries)',
|
|
900
|
+
render: () => {
|
|
901
|
+
const grants = [
|
|
902
|
+
{principal: 'daddy', role: 'Admin', resource: 'all', expires: 'Never', granted_by: 'system'},
|
|
903
|
+
{
|
|
904
|
+
principal: 'alice',
|
|
905
|
+
role: 'Editor',
|
|
906
|
+
resource: 'documents/foo.md',
|
|
907
|
+
expires: '2026-12-31',
|
|
908
|
+
granted_by: 'daddy',
|
|
909
|
+
},
|
|
910
|
+
{principal: 'bob', role: 'Viewer', resource: 'all', expires: 'Never', granted_by: 'daddy'},
|
|
911
|
+
]
|
|
912
|
+
const [sort, setSort] = useState<{id: string; desc: boolean} | null>(null)
|
|
913
|
+
const sortable = [
|
|
914
|
+
{id: 'principal', label: 'Principal'},
|
|
915
|
+
{id: 'role', label: 'Role'},
|
|
916
|
+
{id: 'expires', label: 'Expires'},
|
|
917
|
+
]
|
|
918
|
+
return (
|
|
919
|
+
<html.div style={responsiveStyles.resizable}>
|
|
920
|
+
<Table.Container>
|
|
921
|
+
<Table.SortChip options={sortable} value={sort} onChange={setSort} />
|
|
922
|
+
<Table.Root>
|
|
923
|
+
<Table.Header>
|
|
924
|
+
<Table.Row>
|
|
925
|
+
<Table.HeaderCell label="Principal">Principal</Table.HeaderCell>
|
|
926
|
+
<Table.HeaderCell label="Role">Role</Table.HeaderCell>
|
|
927
|
+
<Table.HeaderCell label="Resource">Resource</Table.HeaderCell>
|
|
928
|
+
<Table.HeaderCell label="Expires">Expires</Table.HeaderCell>
|
|
929
|
+
<Table.HeaderCell label="Granted by">Granted by</Table.HeaderCell>
|
|
930
|
+
<Table.HeaderCell label="Actions" isActions>
|
|
931
|
+
Actions
|
|
932
|
+
</Table.HeaderCell>
|
|
933
|
+
</Table.Row>
|
|
934
|
+
</Table.Header>
|
|
935
|
+
<Table.Body>
|
|
936
|
+
{grants.map((g) => (
|
|
937
|
+
<Table.Row key={g.principal}>
|
|
938
|
+
<Table.Cell>{g.principal}</Table.Cell>
|
|
939
|
+
<Table.Cell>{g.role}</Table.Cell>
|
|
940
|
+
<Table.Cell>{g.resource}</Table.Cell>
|
|
941
|
+
<Table.Cell>{g.expires}</Table.Cell>
|
|
942
|
+
<Table.Cell>{g.granted_by}</Table.Cell>
|
|
943
|
+
<Table.Cell isActions>
|
|
944
|
+
<Button size="small" variant="secondary">
|
|
945
|
+
Revoke
|
|
946
|
+
</Button>
|
|
947
|
+
</Table.Cell>
|
|
948
|
+
</Table.Row>
|
|
949
|
+
))}
|
|
950
|
+
</Table.Body>
|
|
951
|
+
</Table.Root>
|
|
952
|
+
</Table.Container>
|
|
953
|
+
</html.div>
|
|
954
|
+
)
|
|
955
|
+
},
|
|
956
|
+
play: async ({canvas}) => {
|
|
957
|
+
// At default canvas width (900px) we're in compact, headers nowrap,
|
|
958
|
+
// sort chip hidden. Just sanity-check the table renders the expected
|
|
959
|
+
// shape — visual regression is covered by Storybook + Chromatic.
|
|
960
|
+
const headers = canvas.getAllByRole('columnheader')
|
|
961
|
+
await expect(headers.length).toBe(6)
|
|
962
|
+
const cells = canvas.getAllByRole('cell')
|
|
963
|
+
// 3 rows × 6 columns = 18 cells
|
|
964
|
+
await expect(cells.length).toBe(18)
|
|
965
|
+
// Each non-actions body cell carries its label as a hidden span — we
|
|
966
|
+
// can't visually assert display:none in jsdom, but the text node
|
|
967
|
+
// exists in the DOM tree.
|
|
968
|
+
await expect(canvas.getAllByText('Principal').length).toBeGreaterThanOrEqual(2)
|
|
969
|
+
},
|
|
970
|
+
}
|
|
@@ -12,6 +12,7 @@ import {styles} from './styles.css'
|
|
|
12
12
|
import {Pagination} from './Pagination'
|
|
13
13
|
import {SortIndicator} from './SortIndicator'
|
|
14
14
|
import {ColumnFilter} from './ColumnFilter'
|
|
15
|
+
import {SortChip} from './SortChip'
|
|
15
16
|
|
|
16
17
|
// --- Types ---
|
|
17
18
|
|
|
@@ -23,6 +24,10 @@ export type TableSize = 'sm' | 'md'
|
|
|
23
24
|
interface TableContextValue {
|
|
24
25
|
variant: TableVariant
|
|
25
26
|
size: TableSize
|
|
27
|
+
responsive: boolean
|
|
28
|
+
/** Header labels indexed by column position. Populated synchronously by
|
|
29
|
+
* extractColumnMeta() during Root render so SSR HTML carries them. */
|
|
30
|
+
labels: ReadonlyArray<string>
|
|
26
31
|
/** Mutable ref: header Row writes inferred template, Root reads it */
|
|
27
32
|
inferredTemplateRef: MutableRefObject<string | null>
|
|
28
33
|
}
|
|
@@ -39,33 +44,50 @@ function useTable() {
|
|
|
39
44
|
|
|
40
45
|
const HeaderContext = createContext(false)
|
|
41
46
|
|
|
42
|
-
// ---
|
|
47
|
+
// --- Container — owns the @container query target ---
|
|
48
|
+
//
|
|
49
|
+
// Wraps SortChip + Root + Pagination so they all participate in the same
|
|
50
|
+
// container query. Setting `containerType: inline-size` on Root would not
|
|
51
|
+
// reach siblings that consumers render above the table.
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
function Container({children}: {children: ReactNode}) {
|
|
54
|
+
return <html.div style={styles.container}>{children}</html.div>
|
|
55
|
+
}
|
|
47
56
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
// --- Synchronous tree walk: widths + labels in document order ---
|
|
58
|
+
//
|
|
59
|
+
// Mirrors the pattern that was previously called extractTemplate(). Walks
|
|
60
|
+
// the JSX tree, finds HeaderCell elements, captures their `width` and
|
|
61
|
+
// `label` props (or extractText(children) fallback). Runs on every Root
|
|
62
|
+
// render — cheap (small N, no DOM) — but worth memoising on `children`
|
|
63
|
+
// reference if profiling flags it.
|
|
64
|
+
|
|
65
|
+
function extractText(node: ReactNode): string {
|
|
66
|
+
let out = ''
|
|
67
|
+
Children.forEach(node, (child) => {
|
|
68
|
+
if (typeof child === 'string' || typeof child === 'number') {
|
|
69
|
+
out += String(child)
|
|
70
|
+
} else if (isValidElement(child)) {
|
|
71
|
+
const props = child.props as {children?: ReactNode}
|
|
72
|
+
if (props.children) out += extractText(props.children)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
return out
|
|
52
76
|
}
|
|
53
77
|
|
|
54
|
-
|
|
55
|
-
* Scans the React tree for HeaderCell elements to extract width props.
|
|
56
|
-
* Returns a grid-template-columns string.
|
|
57
|
-
*/
|
|
58
|
-
function extractTemplate(children: ReactNode): string {
|
|
78
|
+
function extractColumnMeta(children: ReactNode): {template: string; labels: string[]} {
|
|
59
79
|
const widths: string[] = []
|
|
80
|
+
const labels: string[] = []
|
|
60
81
|
|
|
61
82
|
function walk(node: ReactNode) {
|
|
62
83
|
Children.forEach(node, (child) => {
|
|
63
84
|
if (!isValidElement(child)) return
|
|
64
85
|
const props = child.props as Record<string, any>
|
|
65
|
-
// Check if this looks like a HeaderCell (has width prop or is inside Header)
|
|
66
86
|
const displayName = (child.type as any)?.name || (child.type as any)?.displayName || ''
|
|
67
87
|
if (displayName === 'HeaderCell' || child.type === HeaderCell) {
|
|
68
88
|
widths.push(props.width || '1fr')
|
|
89
|
+
const label = props.label ?? extractText(props.children).trim()
|
|
90
|
+
labels.push(label)
|
|
69
91
|
} else if (props.children) {
|
|
70
92
|
walk(props.children)
|
|
71
93
|
}
|
|
@@ -73,23 +95,35 @@ function extractTemplate(children: ReactNode): string {
|
|
|
73
95
|
}
|
|
74
96
|
|
|
75
97
|
walk(children)
|
|
76
|
-
return
|
|
98
|
+
return {template: widths.join(' '), labels}
|
|
77
99
|
}
|
|
78
100
|
|
|
79
|
-
|
|
80
|
-
const inferredTemplateRef = useRef<string | null>(null)
|
|
101
|
+
// --- Root ---
|
|
81
102
|
|
|
82
|
-
|
|
83
|
-
|
|
103
|
+
interface RootProps {
|
|
104
|
+
children: ReactNode
|
|
105
|
+
variant?: TableVariant
|
|
106
|
+
size?: TableSize
|
|
107
|
+
/** Opt out of responsive container-query behavior. Default true. */
|
|
108
|
+
responsive?: boolean
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function Root({children, variant = 'default', size = 'md', responsive = true}: RootProps) {
|
|
112
|
+
const inferredTemplateRef = useRef<string | null>(null)
|
|
113
|
+
const {template, labels} = extractColumnMeta(children)
|
|
84
114
|
if (template) {
|
|
85
115
|
inferredTemplateRef.current = template
|
|
86
116
|
}
|
|
87
117
|
|
|
88
118
|
return (
|
|
89
|
-
<TableContext.Provider value={{variant, size, inferredTemplateRef}}>
|
|
119
|
+
<TableContext.Provider value={{variant, size, responsive, labels, inferredTemplateRef}}>
|
|
90
120
|
<html.div
|
|
91
121
|
role="table"
|
|
92
|
-
style={[
|
|
122
|
+
style={[
|
|
123
|
+
styles.root,
|
|
124
|
+
template ? styles.gridColumns(template) : undefined,
|
|
125
|
+
responsive && styles.rootResponsive,
|
|
126
|
+
]}
|
|
93
127
|
>
|
|
94
128
|
{children}
|
|
95
129
|
</html.div>
|
|
@@ -136,6 +170,11 @@ function Body({children}: {children: ReactNode}) {
|
|
|
136
170
|
const RowIndexContext = createContext<number>(-1)
|
|
137
171
|
|
|
138
172
|
// --- Row ---
|
|
173
|
+
//
|
|
174
|
+
// Always provides CellIndexContext, regardless of variant. Previously this
|
|
175
|
+
// was only set up for `variant === 'bordered'`, which meant non-bordered
|
|
176
|
+
// tables had every cell reading {index: 0} from the default — fine until
|
|
177
|
+
// we needed to look up labels by column index.
|
|
139
178
|
|
|
140
179
|
function Row({children}: {children: ReactNode}) {
|
|
141
180
|
const {variant} = useTable()
|
|
@@ -153,13 +192,11 @@ function Row({children}: {children: ReactNode}) {
|
|
|
153
192
|
!isHeader && variant === 'striped' && isEvenRow && styles.stripedEven,
|
|
154
193
|
]}
|
|
155
194
|
>
|
|
156
|
-
{
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
))
|
|
162
|
-
: children}
|
|
195
|
+
{childArray.map((child, index) => (
|
|
196
|
+
<CellIndexContext.Provider key={index} value={{index, total: childArray.length}}>
|
|
197
|
+
{child}
|
|
198
|
+
</CellIndexContext.Provider>
|
|
199
|
+
))}
|
|
163
200
|
</html.div>
|
|
164
201
|
)
|
|
165
202
|
}
|
|
@@ -171,11 +208,19 @@ const CellIndexContext = createContext<{index: number; total: number}>({index: 0
|
|
|
171
208
|
function HeaderCell({
|
|
172
209
|
children,
|
|
173
210
|
width: _width,
|
|
211
|
+
label: _label,
|
|
212
|
+
isActions: _isActions,
|
|
174
213
|
'aria-label': ariaLabel,
|
|
175
214
|
}: {
|
|
176
215
|
children?: ReactNode
|
|
177
216
|
/** Column width: CSS value like '40px', '2fr', 'max-content'. Defaults to '1fr'. */
|
|
178
217
|
width?: string
|
|
218
|
+
/** Stack-mode label string. Required when responsive=true; falls back to
|
|
219
|
+
* text-content of children with a dev-only console.warn. */
|
|
220
|
+
label?: string
|
|
221
|
+
/** Marks this column as the actions column — its body cells render as a
|
|
222
|
+
* full-width footer in stack mode. */
|
|
223
|
+
isActions?: boolean
|
|
179
224
|
'aria-label'?: string
|
|
180
225
|
}) {
|
|
181
226
|
const {size, variant} = useTable()
|
|
@@ -197,13 +242,19 @@ function HeaderCell({
|
|
|
197
242
|
</html.div>
|
|
198
243
|
)
|
|
199
244
|
}
|
|
245
|
+
HeaderCell.displayName = 'HeaderCell'
|
|
200
246
|
|
|
201
247
|
// --- Cell ---
|
|
248
|
+
//
|
|
249
|
+
// Renders the column-header label as a real <html.span>, hidden via
|
|
250
|
+
// container query in non-stack modes. The span is omitted entirely when
|
|
251
|
+
// isActions=true — actions footers don't show a label.
|
|
202
252
|
|
|
203
|
-
function Cell({children}: {children: ReactNode}) {
|
|
204
|
-
const {size, variant} = useTable()
|
|
253
|
+
function Cell({children, isActions}: {children: ReactNode; isActions?: boolean}) {
|
|
254
|
+
const {size, variant, labels, responsive} = useTable()
|
|
205
255
|
const {index, total} = useContext(CellIndexContext)
|
|
206
256
|
const isLast = variant === 'bordered' && index === total - 1
|
|
257
|
+
const label = labels[index] ?? ''
|
|
207
258
|
|
|
208
259
|
return (
|
|
209
260
|
<html.div
|
|
@@ -213,8 +264,12 @@ function Cell({children}: {children: ReactNode}) {
|
|
|
213
264
|
size === 'sm' ? styles.cellSm : styles.cellMd,
|
|
214
265
|
variant === 'bordered' && styles.borderedCell,
|
|
215
266
|
isLast && styles.borderedCellLast,
|
|
267
|
+
isActions && styles.cellActions,
|
|
216
268
|
]}
|
|
217
269
|
>
|
|
270
|
+
{responsive && !isActions && label !== '' ? (
|
|
271
|
+
<html.span style={styles.cellLabel}>{label}</html.span>
|
|
272
|
+
) : null}
|
|
218
273
|
{children}
|
|
219
274
|
</html.div>
|
|
220
275
|
)
|
|
@@ -223,6 +278,7 @@ function Cell({children}: {children: ReactNode}) {
|
|
|
223
278
|
// --- Export ---
|
|
224
279
|
|
|
225
280
|
export const Table = {
|
|
281
|
+
Container,
|
|
226
282
|
Root,
|
|
227
283
|
Header,
|
|
228
284
|
Body,
|
|
@@ -232,4 +288,5 @@ export const Table = {
|
|
|
232
288
|
Pagination,
|
|
233
289
|
SortIndicator,
|
|
234
290
|
ColumnFilter,
|
|
291
|
+
SortChip,
|
|
235
292
|
}
|
|
@@ -3,8 +3,25 @@ import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
|
3
3
|
import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
4
|
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
5
|
|
|
6
|
+
// Container-query thresholds. Picked for typical 5-7 column admin tables —
|
|
7
|
+
// see Table.stories.tsx "Responsive" for the visual reasoning. Defined
|
|
8
|
+
// once here so it's clear they're a design-system constant, not a magic
|
|
9
|
+
// number sprinkled across files.
|
|
10
|
+
const COMPACT_BP = '720px'
|
|
11
|
+
const STACK_BP = '440px'
|
|
12
|
+
|
|
6
13
|
export const styles = css.create({
|
|
7
|
-
//
|
|
14
|
+
// Outer wrapper that hosts the @container query. Wraps SortChip + Root
|
|
15
|
+
// + Pagination so all three react to the same width. Root itself does
|
|
16
|
+
// NOT carry containerType — keeping a single query target per Table.
|
|
17
|
+
container: {
|
|
18
|
+
containerType: 'inline-size',
|
|
19
|
+
display: 'flex',
|
|
20
|
+
flexDirection: 'column',
|
|
21
|
+
gap: spacing.sm,
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
// Root — the single grid container for the table itself
|
|
8
25
|
root: {
|
|
9
26
|
display: 'grid',
|
|
10
27
|
borderWidth: 1,
|
|
@@ -17,9 +34,40 @@ export const styles = css.create({
|
|
|
17
34
|
color: colors.text,
|
|
18
35
|
},
|
|
19
36
|
|
|
20
|
-
//
|
|
37
|
+
// Stack-mode override: the Root's grid collapses to a single column so
|
|
38
|
+
// each Row spans full width and stacks vertically. Applied AFTER the
|
|
39
|
+
// dynamic gridColumns(template) so this rule wins inside the @container.
|
|
40
|
+
rootResponsive: {
|
|
41
|
+
gridTemplateColumns: {
|
|
42
|
+
default: null,
|
|
43
|
+
[`@container (max-width: ${STACK_BP})`]: '1fr',
|
|
44
|
+
},
|
|
45
|
+
// The card-list look needs the outer border to disappear in stack
|
|
46
|
+
// mode — each row paints its own border.
|
|
47
|
+
borderWidth: {
|
|
48
|
+
default: 1,
|
|
49
|
+
[`@container (max-width: ${STACK_BP})`]: 0,
|
|
50
|
+
},
|
|
51
|
+
backgroundColor: {
|
|
52
|
+
default: colors.bgCard,
|
|
53
|
+
[`@container (max-width: ${STACK_BP})`]: 'transparent',
|
|
54
|
+
},
|
|
55
|
+
overflow: {
|
|
56
|
+
default: 'hidden',
|
|
57
|
+
[`@container (max-width: ${STACK_BP})`]: 'visible',
|
|
58
|
+
},
|
|
59
|
+
rowGap: {
|
|
60
|
+
default: 0,
|
|
61
|
+
[`@container (max-width: ${STACK_BP})`]: spacing.sm,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Header group — hidden entirely in stack mode
|
|
21
66
|
header: {
|
|
22
|
-
display:
|
|
67
|
+
display: {
|
|
68
|
+
default: 'grid',
|
|
69
|
+
[`@container (max-width: ${STACK_BP})`]: 'none',
|
|
70
|
+
},
|
|
23
71
|
gridColumn: '1 / -1',
|
|
24
72
|
gridTemplateColumns: 'subgrid',
|
|
25
73
|
backgroundColor: colors.bgCard,
|
|
@@ -32,43 +80,148 @@ export const styles = css.create({
|
|
|
32
80
|
gridTemplateColumns: 'subgrid',
|
|
33
81
|
},
|
|
34
82
|
|
|
35
|
-
// Row — spans all columns
|
|
83
|
+
// Row — spans all columns. In stack mode its template flips to 1fr so
|
|
84
|
+
// children stack as grid items in a single column (we keep grid context
|
|
85
|
+
// so cells with `gridColumn` overrides still compute).
|
|
36
86
|
row: {
|
|
37
87
|
display: 'grid',
|
|
38
88
|
gridColumn: '1 / -1',
|
|
39
|
-
gridTemplateColumns:
|
|
40
|
-
|
|
89
|
+
gridTemplateColumns: {
|
|
90
|
+
default: 'subgrid',
|
|
91
|
+
[`@container (max-width: ${STACK_BP})`]: '1fr',
|
|
92
|
+
},
|
|
93
|
+
borderBottomWidth: {
|
|
94
|
+
default: 1,
|
|
95
|
+
[`@container (max-width: ${STACK_BP})`]: 0,
|
|
96
|
+
},
|
|
41
97
|
borderBottomStyle: 'solid',
|
|
42
98
|
borderBottomColor: colors.border,
|
|
43
99
|
},
|
|
44
100
|
bodyRow: {
|
|
45
101
|
backgroundColor: {
|
|
46
102
|
default: 'transparent',
|
|
103
|
+
[`@container (max-width: ${STACK_BP})`]: colors.bgCard,
|
|
47
104
|
':hover': colors.bgCardHover,
|
|
48
105
|
},
|
|
49
106
|
transitionProperty: 'background-color',
|
|
50
107
|
transitionDuration: '150ms',
|
|
51
108
|
transitionTimingFunction: 'ease',
|
|
109
|
+
// Stack-mode card chrome
|
|
110
|
+
padding: {
|
|
111
|
+
default: 0,
|
|
112
|
+
[`@container (max-width: ${STACK_BP})`]: spacing.sm,
|
|
113
|
+
},
|
|
114
|
+
borderRadius: {
|
|
115
|
+
default: 0,
|
|
116
|
+
[`@container (max-width: ${STACK_BP})`]: radii.sm,
|
|
117
|
+
},
|
|
118
|
+
borderWidth: {
|
|
119
|
+
default: 0,
|
|
120
|
+
[`@container (max-width: ${STACK_BP})`]: 1,
|
|
121
|
+
},
|
|
122
|
+
borderStyle: 'solid',
|
|
123
|
+
borderColor: colors.border,
|
|
52
124
|
},
|
|
53
125
|
|
|
54
|
-
// Header cell
|
|
126
|
+
// Header cell — keeps headers on a single line in compact mode
|
|
55
127
|
headerCell: {
|
|
56
128
|
fontWeight: typography.fontWeightSemibold,
|
|
57
129
|
color: colors.textMuted,
|
|
58
130
|
textAlign: 'start',
|
|
131
|
+
whiteSpace: {
|
|
132
|
+
default: 'normal',
|
|
133
|
+
[`@container (max-width: ${COMPACT_BP})`]: 'nowrap',
|
|
134
|
+
},
|
|
59
135
|
},
|
|
60
136
|
|
|
61
|
-
// Body cell
|
|
137
|
+
// Body cell. In compact mode: nowrap + truncate. In stack mode:
|
|
138
|
+
// becomes a 2-column grid for label / value.
|
|
62
139
|
cell: {
|
|
63
140
|
color: colors.text,
|
|
64
|
-
display:
|
|
141
|
+
display: {
|
|
142
|
+
default: 'flex',
|
|
143
|
+
[`@container (max-width: ${STACK_BP})`]: 'grid',
|
|
144
|
+
},
|
|
65
145
|
alignItems: 'center',
|
|
146
|
+
minWidth: {
|
|
147
|
+
default: 'auto',
|
|
148
|
+
[`@container (max-width: ${COMPACT_BP})`]: 0,
|
|
149
|
+
},
|
|
150
|
+
overflow: {
|
|
151
|
+
default: 'visible',
|
|
152
|
+
[`@container (max-width: ${COMPACT_BP})`]: 'hidden',
|
|
153
|
+
},
|
|
154
|
+
textOverflow: {
|
|
155
|
+
default: 'clip',
|
|
156
|
+
[`@container (max-width: ${COMPACT_BP})`]: 'ellipsis',
|
|
157
|
+
},
|
|
158
|
+
whiteSpace: {
|
|
159
|
+
default: 'normal',
|
|
160
|
+
[`@container (max-width: ${COMPACT_BP})`]: 'nowrap',
|
|
161
|
+
[`@container (max-width: ${STACK_BP})`]: 'normal',
|
|
162
|
+
},
|
|
163
|
+
gridTemplateColumns: {
|
|
164
|
+
default: null,
|
|
165
|
+
[`@container (max-width: ${STACK_BP})`]: '1fr 2fr',
|
|
166
|
+
},
|
|
167
|
+
gap: {
|
|
168
|
+
default: 0,
|
|
169
|
+
[`@container (max-width: ${STACK_BP})`]: spacing.sm,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
// Label rendered as a real <span> inside each cell. Display:none in
|
|
174
|
+
// default/compact, shown only in stack — matches StyleX guidance to
|
|
175
|
+
// prefer real elements over ::before/::after for non-shadow-DOM cases.
|
|
176
|
+
cellLabel: {
|
|
177
|
+
display: {
|
|
178
|
+
default: 'none',
|
|
179
|
+
[`@container (max-width: ${STACK_BP})`]: 'block',
|
|
180
|
+
},
|
|
181
|
+
color: colors.textMuted,
|
|
182
|
+
fontWeight: typography.fontWeightMedium,
|
|
183
|
+
fontSize: typography.fontSizeXs,
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
// Actions cell. In default/compact behaves like any cell. In stack
|
|
187
|
+
// mode: full-width footer of the card, right-aligned, with a top
|
|
188
|
+
// separator. Label span is suppressed in JSX (Cell does not render it
|
|
189
|
+
// when isActions=true).
|
|
190
|
+
cellActions: {
|
|
191
|
+
gridTemplateColumns: {
|
|
192
|
+
default: null,
|
|
193
|
+
[`@container (max-width: ${STACK_BP})`]: '1fr',
|
|
194
|
+
},
|
|
195
|
+
justifyContent: {
|
|
196
|
+
default: null,
|
|
197
|
+
[`@container (max-width: ${STACK_BP})`]: 'flex-end',
|
|
198
|
+
},
|
|
199
|
+
marginTop: {
|
|
200
|
+
default: 0,
|
|
201
|
+
[`@container (max-width: ${STACK_BP})`]: spacing.sm,
|
|
202
|
+
},
|
|
203
|
+
paddingTop: {
|
|
204
|
+
default: null,
|
|
205
|
+
[`@container (max-width: ${STACK_BP})`]: spacing.sm,
|
|
206
|
+
},
|
|
207
|
+
borderTopWidth: {
|
|
208
|
+
default: 0,
|
|
209
|
+
[`@container (max-width: ${STACK_BP})`]: 1,
|
|
210
|
+
},
|
|
211
|
+
borderTopStyle: 'solid',
|
|
212
|
+
borderTopColor: colors.border,
|
|
66
213
|
},
|
|
67
214
|
|
|
68
215
|
// Size: sm
|
|
69
216
|
cellSm: {
|
|
70
|
-
paddingTop:
|
|
71
|
-
|
|
217
|
+
paddingTop: {
|
|
218
|
+
default: spacing.xs,
|
|
219
|
+
[`@container (max-width: ${COMPACT_BP})`]: spacing.xs,
|
|
220
|
+
},
|
|
221
|
+
paddingBottom: {
|
|
222
|
+
default: spacing.xs,
|
|
223
|
+
[`@container (max-width: ${COMPACT_BP})`]: spacing.xs,
|
|
224
|
+
},
|
|
72
225
|
paddingLeft: spacing.sm,
|
|
73
226
|
paddingRight: spacing.sm,
|
|
74
227
|
fontSize: typography.fontSizeXs,
|
|
@@ -77,22 +230,33 @@ export const styles = css.create({
|
|
|
77
230
|
cellMd: {
|
|
78
231
|
paddingTop: spacing.sm,
|
|
79
232
|
paddingBottom: spacing.sm,
|
|
80
|
-
paddingLeft:
|
|
81
|
-
|
|
233
|
+
paddingLeft: {
|
|
234
|
+
default: spacing.md,
|
|
235
|
+
[`@container (max-width: ${COMPACT_BP})`]: spacing.sm,
|
|
236
|
+
},
|
|
237
|
+
paddingRight: {
|
|
238
|
+
default: spacing.md,
|
|
239
|
+
[`@container (max-width: ${COMPACT_BP})`]: spacing.sm,
|
|
240
|
+
},
|
|
82
241
|
fontSize: typography.fontSizeSm,
|
|
83
242
|
},
|
|
84
243
|
|
|
85
|
-
// Variant: striped (even body rows)
|
|
244
|
+
// Variant: striped (even body rows). Suppressed in stack — cards have
|
|
245
|
+
// their own surface and zebra-striping reads as a bug there.
|
|
86
246
|
stripedEven: {
|
|
87
247
|
backgroundColor: {
|
|
88
248
|
default: colors.bgCardHover,
|
|
249
|
+
[`@container (max-width: ${STACK_BP})`]: colors.bgCard,
|
|
89
250
|
':hover': colors.bgCardHover,
|
|
90
251
|
},
|
|
91
252
|
},
|
|
92
253
|
|
|
93
|
-
// Variant: bordered (cells get side borders)
|
|
254
|
+
// Variant: bordered (cells get side borders). Suppressed in stack.
|
|
94
255
|
borderedCell: {
|
|
95
|
-
borderRightWidth:
|
|
256
|
+
borderRightWidth: {
|
|
257
|
+
default: 1,
|
|
258
|
+
[`@container (max-width: ${STACK_BP})`]: 0,
|
|
259
|
+
},
|
|
96
260
|
borderRightStyle: 'solid',
|
|
97
261
|
borderRightColor: colors.border,
|
|
98
262
|
},
|
|
@@ -100,6 +264,15 @@ export const styles = css.create({
|
|
|
100
264
|
borderRightWidth: 0,
|
|
101
265
|
},
|
|
102
266
|
|
|
267
|
+
// SortChip — visible only in stack mode
|
|
268
|
+
sortChip: {
|
|
269
|
+
display: {
|
|
270
|
+
default: 'none',
|
|
271
|
+
[`@container (max-width: ${STACK_BP})`]: 'inline-flex',
|
|
272
|
+
},
|
|
273
|
+
alignSelf: 'flex-start',
|
|
274
|
+
},
|
|
275
|
+
|
|
103
276
|
// Dynamic: grid columns applied on Root
|
|
104
277
|
gridColumns: (template: string) => ({
|
|
105
278
|
gridTemplateColumns: template,
|