jattac.libs.web.responsive-table 0.12.0 → 0.14.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.
@@ -0,0 +1,630 @@
1
+ # Row Expansion and Collapse
2
+ ## Progressive Disclosure of Row Detail
3
+
4
+ Expandable rows reveal arbitrary content below any row on demand. A chevron in a dedicated 2rem left column indicates state (▶ collapsed, ▾ expanded). The feature is surface-agnostic — it behaves identically in the desktop table layout and the mobile card layout — and composes transparently with all other table features: sorting, filtering, selection, `onRowClick`, and `dataSource`.
5
+
6
+ ---
7
+
8
+ [← Previous: Technical Implementation Guide](./examples.md) | [Return to API Reference →](./api.md)
9
+
10
+ ---
11
+
12
+ ## Table of Contents
13
+
14
+ - [Props at a Glance](#props-at-a-glance)
15
+ - [expandRowRenderer](#expandrowrenderer)
16
+ - [Signature](#signature)
17
+ - [All Rows Expandable](#all-rows-expandable)
18
+ - [Selective Expansion](#selective-expansion)
19
+ - [Using rowIndex](#using-rowindex)
20
+ - [expandChevronClassName](#expandchevronclassname)
21
+ - [Overriding Color](#overriding-color)
22
+ - [Overriding Size](#overriding-size)
23
+ - [Combining Overrides](#combining-overrides)
24
+ - [Expansion State and Row Identity](#expansion-state-and-row-identity)
25
+ - [Lazy Mounting](#lazy-mounting)
26
+ - [Multiple Rows Expanded Simultaneously](#multiple-rows-expanded-simultaneously)
27
+ - [Accessibility and Keyboard Navigation](#accessibility-and-keyboard-navigation)
28
+ - [Combining with onRowClick](#combining-with-onrowclick)
29
+ - [Combining with Selection](#combining-with-selection)
30
+ - [Combining with dataSource](#combining-with-datasource)
31
+ - [Common Patterns](#common-patterns)
32
+ - [Master-Detail](#master-detail-orders--line-items)
33
+ - [Inline Editing Panel](#inline-editing-panel)
34
+ - [Nested Table](#nested-table)
35
+ - [Charts and Rich Media](#charts-and-rich-media)
36
+ - [Visual Anatomy](#visual-anatomy)
37
+ - [Animation System](#animation-system)
38
+ - [CSS Customization Reference](#css-customization-reference)
39
+
40
+ ---
41
+
42
+ ## Props at a Glance
43
+
44
+ | Prop | Type | Default | Description |
45
+ | :--- | :--- | :--- | :--- |
46
+ | `expandRowRenderer` | `(row: TData, rowIndex: number) => ReactNode` | — | Renderer for the detail panel. Return `null` or `undefined` to suppress the toggle on that row. |
47
+ | `expandChevronClassName` | `string` | — | CSS class applied to the chevron `<span>`. Use to override color, size, or other styles. Do not override `transform` or `transition`. |
48
+
49
+ ---
50
+
51
+ ## `expandRowRenderer`
52
+
53
+ ### Signature
54
+
55
+ ```typescript
56
+ expandRowRenderer?: (row: TData, rowIndex: number) => React.ReactNode
57
+ ```
58
+
59
+ The renderer function is called for every row on each render pass. The return value determines both whether the toggle is shown and what content is displayed when expanded.
60
+
61
+ | Return value | Effect |
62
+ | :--- | :--- |
63
+ | A `ReactNode` | Chevron appears in the expand column. Content is shown when expanded. |
64
+ | `null` or `undefined` | No chevron is rendered for that row. The row renders flat with no visual affordance. |
65
+
66
+ ### All Rows Expandable
67
+
68
+ The simplest case: every row receives a toggle.
69
+
70
+ ```tsx
71
+ import ResponsiveTable from 'jattac.libs.web.responsive-table';
72
+
73
+ <ResponsiveTable
74
+ data={orders}
75
+ columnDefinitions={columns}
76
+ expandRowRenderer={(order) => (
77
+ <div style={{ padding: '1rem' }}>
78
+ <strong>Notes:</strong> {order.notes}
79
+ </div>
80
+ )}
81
+ />
82
+ ```
83
+
84
+ ### Selective Expansion
85
+
86
+ Return `null` or `undefined` for rows that should not be expandable. The chevron is completely absent — not hidden — for those rows, so there is no visual affordance for non-expandable rows.
87
+
88
+ ```tsx
89
+ <ResponsiveTable
90
+ data={orders}
91
+ columnDefinitions={columns}
92
+ expandRowRenderer={(order) =>
93
+ order.lineItems.length > 0
94
+ ? <OrderLineItems order={order} />
95
+ : null
96
+ }
97
+ />
98
+ ```
99
+
100
+ This is the canonical pattern for master-detail layouts where only some records have associated detail data.
101
+
102
+ ### Using `rowIndex`
103
+
104
+ The renderer receives the **display-order** index of the row as its second argument. This is the post-sort, post-filter position in the rendered list — it changes when the user re-sorts or filters.
105
+
106
+ ```tsx
107
+ <ResponsiveTable
108
+ data={employees}
109
+ columnDefinitions={columns}
110
+ expandRowRenderer={(employee, rowIndex) => (
111
+ <EmployeeDetail
112
+ employee={employee}
113
+ zebra={rowIndex % 2 === 0} // alternating panel background
114
+ />
115
+ )}
116
+ />
117
+ ```
118
+
119
+ > **Do not use `rowIndex` as a stable identifier.** Its value changes whenever the visible order changes. For data correlation that must survive re-renders, use the row object's own identifier field (`row.id`, etc.).
120
+
121
+ ---
122
+
123
+ ## `expandChevronClassName`
124
+
125
+ ### Signature
126
+
127
+ ```typescript
128
+ expandChevronClassName?: string
129
+ ```
130
+
131
+ Applies a CSS class to the `<span>` wrapping the chevron icon. The chevron defaults to `1.125rem` and inherits its color from `--primary-color` (`#3b82f6` fallback). Use this prop to override any style without forking the component.
132
+
133
+ > **Do not override `transform` or `transition`** on this class. Both properties drive the rotation animation and will be reset internally on each toggle.
134
+
135
+ ### Overriding Color
136
+
137
+ ```tsx
138
+ <ResponsiveTable
139
+ expandChevronClassName={styles.customChevron}
140
+ expandRowRenderer={(row) => <Detail row={row} />}
141
+ data={rows}
142
+ columnDefinitions={columns}
143
+ />
144
+ ```
145
+
146
+ ```css
147
+ .customChevron {
148
+ color: #7c3aed; /* violet accent */
149
+ }
150
+ ```
151
+
152
+ Prefer a CSS variable or design token over a hardcoded hex value to maintain theming consistency:
153
+
154
+ ```css
155
+ .customChevron {
156
+ color: var(--brand-accent);
157
+ }
158
+ ```
159
+
160
+ ### Overriding Size
161
+
162
+ ```tsx
163
+ <ResponsiveTable
164
+ expandChevronClassName={styles.compactChevron}
165
+ expandRowRenderer={(row) => <Detail row={row} />}
166
+ data={rows}
167
+ columnDefinitions={columns}
168
+ />
169
+ ```
170
+
171
+ ```css
172
+ .compactChevron {
173
+ font-size: 0.9rem; /* smaller than the 1.125rem default */
174
+ }
175
+ ```
176
+
177
+ ### Combining Overrides
178
+
179
+ Multiple style properties can be applied in a single class:
180
+
181
+ ```css
182
+ .brandedChevron {
183
+ color: var(--brand-primary);
184
+ font-size: 1rem;
185
+ /* Do NOT set transform or transition here */
186
+ }
187
+ ```
188
+
189
+ ```tsx
190
+ <ResponsiveTable
191
+ expandChevronClassName={styles.brandedChevron}
192
+ expandRowRenderer={(row) => <Detail row={row} />}
193
+ data={rows}
194
+ columnDefinitions={columns}
195
+ />
196
+ ```
197
+
198
+ ---
199
+
200
+ ## Expansion State and Row Identity
201
+
202
+ Each row's expanded/collapsed state is tracked internally using a `Set` of row identifiers. The key used to identify rows is determined as follows:
203
+
204
+ | Condition | Key used |
205
+ | :--- | :--- |
206
+ | `selectionProps.rowIdKey` is provided | `row[rowIdKey]` — a stable data-derived identifier |
207
+ | `selectionProps` is not provided | Array index (display-order position) |
208
+
209
+ **With a stable key**, expanded panels survive re-sorts and filter changes. A user who expands "Order #1042" and then re-sorts by date will find "Order #1042" still expanded in its new position.
210
+
211
+ **Without a stable key**, expansion state is tied to position. Re-sorting resets all expanded panels because the indices no longer correspond to the same rows.
212
+
213
+ ```tsx
214
+ // Stable: expanded state survives sort and filter
215
+ <ResponsiveTable
216
+ data={orders}
217
+ columnDefinitions={columns}
218
+ selectionProps={{
219
+ rowIdKey: 'id',
220
+ mode: 'multiple',
221
+ onSelectionChange: setSelected,
222
+ }}
223
+ expandRowRenderer={(order) => <OrderLineItems order={order} />}
224
+ />
225
+
226
+ // Index-based: expanded state resets on sort/filter
227
+ <ResponsiveTable
228
+ data={orders}
229
+ columnDefinitions={columns}
230
+ expandRowRenderer={(order) => <OrderLineItems order={order} />}
231
+ />
232
+ ```
233
+
234
+ You can provide `selectionProps` solely to anchor expand state, even if row selection is not otherwise needed:
235
+
236
+ ```tsx
237
+ // selectionProps used only to provide a stable expand key
238
+ // No selection UI is rendered unless selection is also visually wired
239
+ <ResponsiveTable
240
+ data={orders}
241
+ columnDefinitions={columns}
242
+ selectionProps={{
243
+ rowIdKey: 'id',
244
+ onSelectionChange: () => {}, // no-op — selection result is not consumed
245
+ }}
246
+ expandRowRenderer={(order) => <OrderLineItems order={order} />}
247
+ />
248
+ ```
249
+
250
+ ---
251
+
252
+ ## Lazy Mounting
253
+
254
+ Detail panel components are **not instantiated until the first time a row is expanded**. After that, the component stays mounted permanently. Collapse is achieved entirely by CSS (`grid-template-rows: 1fr → 0fr`) rather than by unmounting.
255
+
256
+ **Implications by scenario:**
257
+
258
+ | Scenario | Behaviour |
259
+ | :--- | :--- |
260
+ | Initial table render with 100 expandable rows | 0 detail components instantiated |
261
+ | First expand of a row | Detail component mounts; `useEffect` hooks fire |
262
+ | Subsequent collapse of the same row | Component stays mounted; only CSS changes |
263
+ | Second expand of the same row | No remount; component resumes from its current state |
264
+ | Component manages form state internally | Form state (values, validation) persists across collapse/expand cycles |
265
+
266
+ ```tsx
267
+ // This API call fires on first expand, not on table render
268
+ function OrderDetail({ orderId }: { orderId: string }) {
269
+ const { data } = useOrderDetail(orderId); // fires once, on first expand
270
+ return <LineItemTable data={data} />;
271
+ }
272
+
273
+ <ResponsiveTable
274
+ data={orders}
275
+ columnDefinitions={columns}
276
+ expandRowRenderer={(order) => <OrderDetail orderId={order.id} />}
277
+ />
278
+ ```
279
+
280
+ ---
281
+
282
+ ## Multiple Rows Expanded Simultaneously
283
+
284
+ Rows expand independently. Any number of rows can be open at the same time. There is no built-in accordion mode where opening one row closes others.
285
+
286
+ ```tsx
287
+ // Alice and Bob can both be expanded — this is valid and supported
288
+ <ResponsiveTable
289
+ data={[
290
+ { id: 1, name: 'Alice' },
291
+ { id: 2, name: 'Bob' },
292
+ { id: 3, name: 'Carol' },
293
+ ]}
294
+ columnDefinitions={columns}
295
+ expandRowRenderer={(row) => <Detail row={row} />}
296
+ />
297
+ ```
298
+
299
+ If your use case requires accordion behaviour — only one row open at a time — implement it by controlling which rows return content from the renderer:
300
+
301
+ ```tsx
302
+ const [openId, setOpenId] = useState<string | null>(null);
303
+
304
+ // selectionProps provides stable keying for the single-open panel
305
+ <ResponsiveTable
306
+ data={rows}
307
+ columnDefinitions={columns}
308
+ selectionProps={{ rowIdKey: 'id', onSelectionChange: () => {} }}
309
+ expandRowRenderer={(row) => {
310
+ if (row.id !== openId) return null;
311
+ return <Detail row={row} onClose={() => setOpenId(null)} />;
312
+ }}
313
+ />
314
+ ```
315
+
316
+ > When `openId` changes, affected rows re-render and return a different value (`null` vs content). Because expansion state is managed internally by the table, the visual toggle reflects the content availability — a row with `null` content has no toggle, and a row with content has a toggle.
317
+
318
+ ---
319
+
320
+ ## Accessibility and Keyboard Navigation
321
+
322
+ The chevron in the expand column is fully accessible without any additional configuration.
323
+
324
+ | Attribute / Behaviour | Detail |
325
+ | :--- | :--- |
326
+ | `role="button"` | Chevron is announced as an interactive button to assistive technologies. |
327
+ | `tabIndex={0}` | Chevron is keyboard-focusable via Tab. |
328
+ | `aria-expanded="true/false"` | State is announced on each toggle; screen readers report the change. |
329
+ | **Enter** / **Space** | Toggles the panel from the keyboard. Default scroll behaviour is suppressed on Space. |
330
+ | `data-rt-ignore-row-click` | Chevron is excluded from the `onRowClick` event chain. Activating the toggle never fires the row click handler. |
331
+
332
+ The chevron is a separate focusable element from the row itself. When `onRowClick` is also present, both the row and the chevron are keyboard-focusable independently: Tab reaches the row, Tab again reaches the chevron in the expand column.
333
+
334
+ ---
335
+
336
+ ## Combining with `onRowClick`
337
+
338
+ The expand toggle and `onRowClick` are fully isolated. Clicking the chevron never fires `onRowClick` because it carries `data-rt-ignore-row-click` internally.
339
+
340
+ ```tsx
341
+ <ResponsiveTable
342
+ data={orders}
343
+ columnDefinitions={columns}
344
+ onRowClick={(order) => navigate(`/orders/${order.id}`)}
345
+ expandRowRenderer={(order) => <OrderLineItems order={order} />}
346
+ />
347
+ ```
348
+
349
+ Clicking the row body navigates. Clicking the chevron expands the panel. The two actions are fully independent and do not interfere with each other.
350
+
351
+ ---
352
+
353
+ ## Combining with Selection
354
+
355
+ Expand state and selection state share the same row identity key from `selectionProps.rowIdKey`. Providing a stable ID ensures both selection highlights and expanded panels survive sort and filter operations.
356
+
357
+ ```tsx
358
+ <ResponsiveTable
359
+ data={orders}
360
+ columnDefinitions={columns}
361
+ selectionProps={{
362
+ rowIdKey: 'id',
363
+ mode: 'multiple',
364
+ onSelectionChange: setSelectedOrders,
365
+ }}
366
+ expandRowRenderer={(order) =>
367
+ order.lineItems.length > 0
368
+ ? <OrderLineItems order={order} />
369
+ : null
370
+ }
371
+ />
372
+ ```
373
+
374
+ ---
375
+
376
+ ## Combining with `dataSource`
377
+
378
+ `expandRowRenderer` is fully compatible with `dataSource` and its infinite scroll behaviour. As new pages load and rows append, previously expanded rows remain expanded. Newly loaded rows arrive in the collapsed state.
379
+
380
+ ```tsx
381
+ <ResponsiveTable
382
+ dataSource={async ({ page, pageSize }) => {
383
+ const response = await api.orders.list({ page, limit: pageSize });
384
+ return { items: response.data, totalCount: response.total };
385
+ }}
386
+ pageSize={20}
387
+ columnDefinitions={columns}
388
+ selectionProps={{ rowIdKey: 'id', onSelectionChange: () => {} }}
389
+ expandRowRenderer={(order) =>
390
+ order.lineItems.length > 0
391
+ ? <OrderLineItems order={order} />
392
+ : null
393
+ }
394
+ />
395
+ ```
396
+
397
+ Providing `selectionProps.rowIdKey` is especially important with `dataSource`. Without a stable key, each new page fetch reassigns indices, resetting expand state for all currently visible rows.
398
+
399
+ ---
400
+
401
+ ## Common Patterns
402
+
403
+ ### Master-Detail (Orders → Line Items)
404
+
405
+ The canonical use case. The summary row displays key fields; expanding reveals the full record or a sub-table.
406
+
407
+ ```tsx
408
+ type Order = {
409
+ id: string;
410
+ reference: string;
411
+ customer: string;
412
+ total: number;
413
+ lineItems: { sku: string; qty: number; price: number }[];
414
+ };
415
+
416
+ const columns: ColumnDefinition<Order>[] = [
417
+ { displayLabel: 'Reference', cellRenderer: (o) => o.reference },
418
+ { displayLabel: 'Customer', cellRenderer: (o) => o.customer },
419
+ { displayLabel: 'Total', cellRenderer: (o) => `$${o.total.toFixed(2)}` },
420
+ ];
421
+
422
+ <ResponsiveTable
423
+ data={orders}
424
+ columnDefinitions={columns}
425
+ selectionProps={{ rowIdKey: 'id', onSelectionChange: () => {} }}
426
+ expandRowRenderer={(order) =>
427
+ order.lineItems.length === 0 ? null : (
428
+ <table style={{ width: '100%', padding: '0.75rem 1.5rem', fontSize: '0.875rem' }}>
429
+ <thead>
430
+ <tr>
431
+ <th style={{ textAlign: 'left' }}>SKU</th>
432
+ <th style={{ textAlign: 'center' }}>Qty</th>
433
+ <th style={{ textAlign: 'right' }}>Unit Price</th>
434
+ </tr>
435
+ </thead>
436
+ <tbody>
437
+ {order.lineItems.map((item) => (
438
+ <tr key={item.sku}>
439
+ <td>{item.sku}</td>
440
+ <td style={{ textAlign: 'center' }}>{item.qty}</td>
441
+ <td style={{ textAlign: 'right' }}>${item.price.toFixed(2)}</td>
442
+ </tr>
443
+ ))}
444
+ </tbody>
445
+ </table>
446
+ )
447
+ }
448
+ />
449
+ ```
450
+
451
+ ### Inline Editing Panel
452
+
453
+ Reveals an edit form below the row without navigating away. Because the component stays mounted after first expand, form state (draft values, validation errors) persists across collapse/expand cycles.
454
+
455
+ ```tsx
456
+ function EditForm({ employee, onSave }: { employee: Employee; onSave: (e: Employee) => void }) {
457
+ const [draft, setDraft] = useState(employee);
458
+
459
+ return (
460
+ <form
461
+ style={{ padding: '1rem 1.5rem', display: 'flex', gap: '1rem', flexWrap: 'wrap' }}
462
+ onSubmit={(ev) => { ev.preventDefault(); onSave(draft); }}
463
+ >
464
+ <input
465
+ value={draft.name}
466
+ onChange={(ev) => setDraft({ ...draft, name: ev.target.value })}
467
+ data-rt-ignore-row-click
468
+ />
469
+ <button type="submit" data-rt-ignore-row-click>Save</button>
470
+ </form>
471
+ );
472
+ }
473
+
474
+ <ResponsiveTable
475
+ data={employees}
476
+ columnDefinitions={columns}
477
+ selectionProps={{ rowIdKey: 'id', onSelectionChange: () => {} }}
478
+ onRowClick={(employee) => setPreview(employee)}
479
+ expandRowRenderer={(employee) => (
480
+ <EditForm employee={employee} onSave={handleSave} />
481
+ )}
482
+ />
483
+ ```
484
+
485
+ > Add `data-rt-ignore-row-click` to interactive elements inside the detail panel when the table also uses `onRowClick`. See [Handling Interactive Elements](./handling-interactive-elements.md).
486
+
487
+ ### Nested Table
488
+
489
+ Render a full child `ResponsiveTable` inside the detail panel for hierarchical data.
490
+
491
+ ```tsx
492
+ <ResponsiveTable
493
+ data={departments}
494
+ columnDefinitions={deptColumns}
495
+ selectionProps={{ rowIdKey: 'id', onSelectionChange: () => {} }}
496
+ expandRowRenderer={(dept) =>
497
+ dept.employees.length === 0 ? null : (
498
+ <div style={{ padding: '0.75rem 1.5rem' }}>
499
+ <ResponsiveTable
500
+ data={dept.employees}
501
+ columnDefinitions={employeeColumns}
502
+ mobileBreakpoint={400}
503
+ />
504
+ </div>
505
+ )
506
+ }
507
+ />
508
+ ```
509
+
510
+ ### Charts and Rich Media
511
+
512
+ Any `ReactNode` is valid detail content. Heavy libraries (charting, map rendering, rich text editors) mount lazily on first expand, keeping initial render cost low.
513
+
514
+ ```tsx
515
+ import { LineChart, Line, XAxis, YAxis, Tooltip } from 'recharts';
516
+
517
+ <ResponsiveTable
518
+ data={assets}
519
+ columnDefinitions={assetColumns}
520
+ selectionProps={{ rowIdKey: 'id', onSelectionChange: () => {} }}
521
+ expandRowRenderer={(asset) => (
522
+ <div style={{ padding: '1rem 1.5rem', height: 240 }}>
523
+ <LineChart width={580} height={200} data={asset.priceHistory}>
524
+ <XAxis dataKey="date" />
525
+ <YAxis />
526
+ <Tooltip />
527
+ <Line type="monotone" dataKey="price" dot={false} />
528
+ </LineChart>
529
+ </div>
530
+ )}
531
+ />
532
+ ```
533
+
534
+ ---
535
+
536
+ ## Visual Anatomy
537
+
538
+ When `expandRowRenderer` returns content for a row, the following structure is rendered (desktop shown; mobile uses the same visual model in card layout):
539
+
540
+ ```
541
+ ┌─── expand column (2rem) ──┬── data columns ───────────────────────────────┐
542
+ │ ▶── chevron (25% opacity) │ Alice │ alice@example.com │ Admin │
543
+ │ -90deg, quiet idle │ │ │ │
544
+ └────────────────────────────┴─────────┴──────────────────────┴──────────────┘
545
+ ┌── toggle bar (0→2rem on expand, left chevron, muted blue) ────────┐
546
+ │ ▾ (rotated 0deg, full opacity) │
547
+ └────────────────────────────────────────────────────────────────────┘
548
+ ┌── detail panel (CSS grid: 0fr → 1fr, content inset 1.5rem) ───────┐
549
+ │ [rendered detail content] │
550
+ └────────────────────────────────────────────────────────────────────┘
551
+ ```
552
+
553
+ The expand column is a dedicated `<th>`/`<td>` — the first column in the table — ensuring the chevron is always left-aligned and never overlaps data cells.
554
+
555
+ **State summary:**
556
+
557
+ | State | Chevron | Toggle bar | Detail panel |
558
+ | :--- | :--- | :--- | :--- |
559
+ | **Idle** (no expand) | Hidden (no chevron) | Hidden | Hidden |
560
+ | **Idle** (expandable) | 25% opacity, -90deg | Hidden (height: 0) | Hidden (0fr) |
561
+ | **Hover** | 60% opacity, -90deg | Hidden | Hidden |
562
+ | **Expanded** | 100% opacity, 0deg | 2rem, chevron left | Visible (1fr) |
563
+
564
+ ---
565
+
566
+ ## Animation System
567
+
568
+ All transitions are CSS-driven with no JavaScript animation loop.
569
+
570
+ **Toggle transitions:**
571
+
572
+ | Element | Technique | Duration |
573
+ | :--- | :--- | :--- |
574
+ | Chevron rotation (▶ → ▾) | `transform: rotate(-90deg) → rotate(0deg)` | 250ms ease |
575
+ | Chevron opacity (idle → hover) | `opacity: 0.25 → 0.6` | 150ms ease |
576
+ | Chevron opacity (idle → expanded) | `opacity: 0.25 → 1` | 250ms ease |
577
+ | Toggle bar slide in | `height: 0 → 2rem` | 250ms ease |
578
+ | Panel open/close | `grid-template-rows: 0fr → 1fr` | 300ms ease |
579
+
580
+ **Greeting animation (plays once per component mount):**
581
+
582
+ The chevrons perform a staggered multi-pulse wave to draw attention to the expandable feature:
583
+
584
+ ```
585
+ @keyframes chevronGreeting:
586
+ 0% → opacity 0, scale 0.6
587
+ 12% → opacity 1, scale 1.3 (first pop)
588
+ 22% → opacity 0.35, scale 1.0
589
+ 32% → opacity 1, scale 1.15 (second pop)
590
+ 45% → opacity 0.25, scale 1.0 (settle to idle)
591
+ 100% → opacity 0.25, scale 1.0 (stay at idle)
592
+ ```
593
+
594
+ Each row's chevron delays its animation by `rowIndex × 60ms`, creating a wave from top to bottom. The greeting times out after 3.2s via a React `useEffect` timer.
595
+
596
+ ---
597
+
598
+ ## CSS Customization Reference
599
+
600
+ The following CSS module classes govern the expand/collapse visual system. They are not part of the public API — use `expandChevronClassName` for chevron customization. The table is provided for reference when applying global theme overrides via CSS variables.
601
+
602
+ | Class | Element | Key properties |
603
+ | :--- | :--- | :--- |
604
+ | `.expandColumn` | `<th>` / `<td>` for the dedicated chevron column | `width: 2rem`, `min-width: 2rem`, `padding: 0`, `text-align: center` |
605
+ | `.expandChevron` | Chevron `<span>` | `opacity: 0.25`, `transform: rotate(-90deg)`, `transition: opacity 0.15s, transform 0.25s` |
606
+ | `.expandChevronGreeting` | Applied during mount greeting | `animation: chevronGreeting 2.2s ease-out forwards`, delay via `--row-idx` |
607
+ | `.expandChevronHovered` | Applied on row hover | `opacity: 0.6` |
608
+ | `.expandChevronExpanded` | Applied when the panel is open | `opacity: 1`, `transform: rotate(0deg)` |
609
+ | `.detailCell` | `<td>` wrapping the entire detail row | `padding: 0`, `background: rgba(0,0,0,0.012)` |
610
+ | `.detailCellHasContent` | Applied when renderer returns content | `padding-bottom: 0.5rem` |
611
+ | `.detailCellExpanded` | Applied when the panel is open | `border-left: 2px solid var(--primary-color, #3b82f6)` |
612
+ | `.detailToggleBar` | Toggle bar inside detail pane (base) | `height: 0`, `overflow: hidden` |
613
+ | `.detailToggleBarExpanded` | Toggle bar when panel is open | `height: 2rem`, `background: rgba(59,130,246,0.08)`, `cursor: pointer` |
614
+ | `.detailToggleChevron` | Chevron inside the toggle bar (always visible when bar is shown) | `color: var(--primary-color)`, `font-size: 1.25rem`, left-aligned |
615
+ | `.detailContentWrapper` | Grid container for the detail content | `grid-template-rows: 0fr`, transitions to `1fr` on expand |
616
+ | `.detailContentInner` | Inner content container | `overflow: hidden`, `padding-left: 1.5rem` |
617
+ | `.mobileDetailOuter` | Wrapper for toggle bar + content in mobile view | `overflow: hidden`, `background: rgba(0,0,0,0.012)` |
618
+ | `.mobileExpandRow` | Chevron row at top of mobile card | Flex container, chevron left-aligned, bottom border separator |
619
+
620
+ To apply brand colors across the entire expand/collapse system (chevron, border indicator, and selection highlights simultaneously), override the `--primary-color` variable in your global stylesheet:
621
+
622
+ ```css
623
+ :root {
624
+ --primary-color: #7c3aed;
625
+ }
626
+ ```
627
+
628
+ ---
629
+
630
+ **Previous:** [Technical Implementation Guide](./examples.md) | **Next:** [API Reference](./api.md)
package/docs/features.md CHANGED
@@ -6,6 +6,7 @@ This document provides a high-level overview of the technical capabilities and a
6
6
  ## Table of Contents
7
7
  * [Automated Layout Orchestration](#automated-layout-orchestration)
8
8
  * [Asynchronous Data Stream Support](#asynchronous-data-stream-support)
9
+ * [Progressive Row Disclosure](#progressive-row-disclosure)
9
10
  * [Persistent Plugin Lifecycle](#persistent-plugin-lifecycle)
10
11
  * [Structural Alignment Management](#structural-alignment-management)
11
12
 
@@ -27,6 +28,11 @@ Native integration for high-volume data sets via an infinite scrolling orchestra
27
28
  Automatic error detection and recovery for `dataSource` operations. When a fetch fails, the component surfaces the error with a retry mechanism, accessible both through the UI and programmatically.
28
29
  * [Implementation Example: Error Handling and Retry](./examples.md#13-error-handling-and-retry)
29
30
 
31
+ ### Progressive Row Disclosure
32
+ The component supports collapsible detail panels attached below any row. A chevron toggle bar is rendered automatically when `expandRowRenderer` returns content for a row; returning `null` suppresses the toggle entirely for that row. Detail components are lazy-mounted — not instantiated until first expand — and stay mounted thereafter so the collapse animation plays correctly and component state is preserved across open/close cycles. Expand state is keyed by a stable row identifier when `selectionProps.rowIdKey` is provided, ensuring open panels survive re-sorts and filter changes. The feature operates identically in desktop table and mobile card layouts and composes transparently with selection, `onRowClick`, and `dataSource`.
33
+ * [Full Implementation Guide: Row Expansion and Collapse](./expand-collapse.md)
34
+ * [Implementation Example: Expandable Rows](./examples.md#14-expandable-rows)
35
+
30
36
  ### Persistent Plugin Lifecycle
31
37
  The component features a robust internal lifecycle management system for plugins. State for sorting, filtering, and selection is persisted across re-renders and data updates using non-reactive references, optimizing performance and ensuring data consistency.
32
38
  * [Implementation Example: Sortable Columns](./examples.md#2-implementing-sortable-columns)