@torch-ui/solid 0.1.3

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.
Files changed (118) hide show
  1. package/README.md +166 -0
  2. package/package.json +67 -0
  3. package/src/components/actions/Button.tsx +612 -0
  4. package/src/components/actions/ButtonGroup.tsx +728 -0
  5. package/src/components/actions/Copy.tsx +98 -0
  6. package/src/components/actions/DarkModeToggle.tsx +80 -0
  7. package/src/components/actions/Link.tsx +37 -0
  8. package/src/components/actions/index.ts +19 -0
  9. package/src/components/actions/useCopyToClipboard.ts +90 -0
  10. package/src/components/charts/Chart.tsx +331 -0
  11. package/src/components/charts/Sparkline.tsx +156 -0
  12. package/src/components/charts/index.ts +13 -0
  13. package/src/components/data-display/Avatar.tsx +208 -0
  14. package/src/components/data-display/AvatarGroup.tsx +228 -0
  15. package/src/components/data-display/Badge.tsx +70 -0
  16. package/src/components/data-display/Carousel.tsx +214 -0
  17. package/src/components/data-display/ColorSwatch.tsx +56 -0
  18. package/src/components/data-display/DataTable.tsx +886 -0
  19. package/src/components/data-display/EmptyState.tsx +61 -0
  20. package/src/components/data-display/Image.tsx +277 -0
  21. package/src/components/data-display/Kbd.tsx +114 -0
  22. package/src/components/data-display/Persona.tsx +78 -0
  23. package/src/components/data-display/StatCard.tsx +338 -0
  24. package/src/components/data-display/Table.tsx +147 -0
  25. package/src/components/data-display/Tag.tsx +91 -0
  26. package/src/components/data-display/Timeline.tsx +200 -0
  27. package/src/components/data-display/TreeView.tsx +172 -0
  28. package/src/components/data-display/Video.tsx +95 -0
  29. package/src/components/data-display/avatar-utils.ts +32 -0
  30. package/src/components/data-display/index.ts +81 -0
  31. package/src/components/feedback/Loading.tsx +159 -0
  32. package/src/components/feedback/Progress.tsx +321 -0
  33. package/src/components/feedback/Skeleton.tsx +62 -0
  34. package/src/components/feedback/SkeletonBlocks.tsx +222 -0
  35. package/src/components/feedback/Toast.tsx +648 -0
  36. package/src/components/feedback/index.ts +44 -0
  37. package/src/components/feedback/password/PasswordStrengthIndicator.tsx +232 -0
  38. package/src/components/feedback/password/password-strength.ts +115 -0
  39. package/src/components/feedback/password/password-validation-data.ts +66 -0
  40. package/src/components/feedback/password/password-validation.ts +93 -0
  41. package/src/components/forms/Autocomplete.tsx +268 -0
  42. package/src/components/forms/Checkbox.tsx +155 -0
  43. package/src/components/forms/CodeInput.tsx +237 -0
  44. package/src/components/forms/ColorPicker/ColorPicker.tsx +469 -0
  45. package/src/components/forms/ColorPicker/color-utils.ts +75 -0
  46. package/src/components/forms/ColorPicker/index.ts +2 -0
  47. package/src/components/forms/DatePicker.tsx +516 -0
  48. package/src/components/forms/DateRangePicker.tsx +464 -0
  49. package/src/components/forms/FieldPicker.tsx +64 -0
  50. package/src/components/forms/FileUpload.tsx +614 -0
  51. package/src/components/forms/FilterBuilder/FilterGroupBlock.ts +6 -0
  52. package/src/components/forms/FilterBuilder.tsx +16 -0
  53. package/src/components/forms/FilterRuleRow.tsx +68 -0
  54. package/src/components/forms/Input.tsx +200 -0
  55. package/src/components/forms/MultiSelect.tsx +361 -0
  56. package/src/components/forms/NumberField.tsx +145 -0
  57. package/src/components/forms/RadioGroup.tsx +135 -0
  58. package/src/components/forms/RelativeDateDefaultInput.tsx +62 -0
  59. package/src/components/forms/ReorderableList.tsx +163 -0
  60. package/src/components/forms/Select.tsx +268 -0
  61. package/src/components/forms/Slider.tsx +260 -0
  62. package/src/components/forms/Switch.tsx +135 -0
  63. package/src/components/forms/TextArea.tsx +202 -0
  64. package/src/components/forms/ViewCustomizer.tsx +44 -0
  65. package/src/components/forms/index.ts +43 -0
  66. package/src/components/layout/Accordion.tsx +110 -0
  67. package/src/components/layout/Alert.tsx +156 -0
  68. package/src/components/layout/BlockQuote.tsx +70 -0
  69. package/src/components/layout/Card.tsx +166 -0
  70. package/src/components/layout/CodeBlock/CodeBlock.tsx +477 -0
  71. package/src/components/layout/CodeBlock/code-block-tokens.css +104 -0
  72. package/src/components/layout/CodeBlock/prism.ts +81 -0
  73. package/src/components/layout/Collapsible.tsx +84 -0
  74. package/src/components/layout/Container.tsx +55 -0
  75. package/src/components/layout/Divider.tsx +64 -0
  76. package/src/components/layout/Form.tsx +39 -0
  77. package/src/components/layout/FormActions.tsx +50 -0
  78. package/src/components/layout/Grid.tsx +53 -0
  79. package/src/components/layout/PageHeading.tsx +46 -0
  80. package/src/components/layout/PromptWithAction.tsx +49 -0
  81. package/src/components/layout/Section.tsx +60 -0
  82. package/src/components/layout/TablePanel.tsx +24 -0
  83. package/src/components/layout/TableView/TableView.tsx +1018 -0
  84. package/src/components/layout/TableView/index.ts +3 -0
  85. package/src/components/layout/TableView/types.ts +51 -0
  86. package/src/components/layout/WizardStep.tsx +40 -0
  87. package/src/components/layout/WizardStepper.tsx +173 -0
  88. package/src/components/layout/index.ts +96 -0
  89. package/src/components/navigation/Breadcrumbs.tsx +66 -0
  90. package/src/components/navigation/DropdownMenu.tsx +86 -0
  91. package/src/components/navigation/MegaMenu.tsx +480 -0
  92. package/src/components/navigation/NavigationMenu.tsx +305 -0
  93. package/src/components/navigation/Pagination.tsx +298 -0
  94. package/src/components/navigation/Sidebar.tsx +280 -0
  95. package/src/components/navigation/Tabs.tsx +122 -0
  96. package/src/components/navigation/ViewSwitcher.tsx +314 -0
  97. package/src/components/navigation/index.ts +66 -0
  98. package/src/components/overlays/AlertDialog.tsx +174 -0
  99. package/src/components/overlays/ContextMenu.tsx +65 -0
  100. package/src/components/overlays/Dialog.tsx +279 -0
  101. package/src/components/overlays/Drawer.tsx +370 -0
  102. package/src/components/overlays/HoverCard.tsx +107 -0
  103. package/src/components/overlays/Popover.tsx +73 -0
  104. package/src/components/overlays/Tooltip.tsx +31 -0
  105. package/src/components/overlays/index.ts +71 -0
  106. package/src/components/typography/Code.tsx +72 -0
  107. package/src/components/typography/Icon.tsx +36 -0
  108. package/src/components/typography/index.ts +10 -0
  109. package/src/env.d.ts +9 -0
  110. package/src/index.ts +13 -0
  111. package/src/styles/theme.css +226 -0
  112. package/src/types/avatar-types.ts +11 -0
  113. package/src/types/filter-types.ts +35 -0
  114. package/src/utilities/classNames.ts +6 -0
  115. package/src/utilities/componentSize.ts +46 -0
  116. package/src/utilities/i18n.tsx +60 -0
  117. package/src/utilities/mergeRefs.ts +12 -0
  118. package/src/utilities/relativeDateDefault.ts +14 -0
@@ -0,0 +1,886 @@
1
+ import { type JSX, Show, For, createMemo, splitProps } from 'solid-js'
2
+
3
+ import { Plus } from 'lucide-solid'
4
+
5
+ import { Button } from '../actions'
6
+
7
+ import { Input } from '../forms'
8
+
9
+ import { Dialog, AlertDialog } from '../layout'
10
+
11
+ import { EmptyState } from './EmptyState'
12
+
13
+ import { cn } from '../../utilities/classNames'
14
+
15
+ import {
16
+
17
+ Table,
18
+
19
+ TableHeader,
20
+
21
+ TableBody,
22
+
23
+ TableRow,
24
+
25
+ TableHead,
26
+
27
+ TableCell,
28
+
29
+ } from './Table'
30
+
31
+ import { Pagination } from '../navigation/Pagination'
32
+
33
+ import type { PaginationProps } from '../navigation/Pagination'
34
+
35
+
36
+
37
+ /** Shared table container styling: white background, border, rounded. Use with overflow-x-auto or overflow-hidden. */
38
+
39
+ export const TABLE_CONTAINER_CLASS =
40
+
41
+ 'rounded-xl border border-surface-border bg-surface-raised'
42
+
43
+
44
+
45
+ export interface DataTableSearchProps {
46
+
47
+ value: string
48
+
49
+ onChange: (value: string) => void
50
+
51
+ placeholder: string
52
+
53
+ }
54
+
55
+
56
+
57
+ export interface DataTableButtonProps {
58
+
59
+ label: string
60
+
61
+ onClick: () => void
62
+
63
+ startIcon?: JSX.Element
64
+
65
+ }
66
+
67
+
68
+
69
+ export interface DataTableAddRowProps {
70
+
71
+ showAddForm: boolean
72
+
73
+ onToggleAddForm: () => void
74
+
75
+ addButtonLabel: string
76
+
77
+ renderAddCells: () => JSX.Element
78
+
79
+ addError?: string
80
+
81
+ }
82
+
83
+
84
+
85
+ export interface DataTableEditModalProps {
86
+
87
+ open: boolean
88
+
89
+ title: string
90
+
91
+ onClose: () => void
92
+
93
+ children: JSX.Element
94
+
95
+ editError?: string
96
+
97
+ onSave: () => void
98
+
99
+ saving?: boolean
100
+
101
+ }
102
+
103
+
104
+
105
+ export interface DataTableDeleteDialogProps {
106
+
107
+ open: boolean
108
+
109
+ title: string
110
+
111
+ description: string
112
+
113
+ onClose: () => void
114
+
115
+ onConfirm: () => void
116
+
117
+ }
118
+
119
+
120
+
121
+ export interface ColumnDef<T> {
122
+
123
+ id: string
124
+
125
+ /** Column header label or JSX. */
126
+
127
+ header: JSX.Element | string
128
+
129
+ /** Class applied to the <TableHead>. */
130
+
131
+ headClass?: string
132
+
133
+ /** Class applied to each <TableCell> in this column (data rows and skeleton). */
134
+
135
+ cellClass?: string
136
+
137
+ /** Return cell content; DataTable wraps it in <TableCell class={cellClass}>. */
138
+
139
+ cell: (item: T) => JSX.Element | string | number | null
140
+
141
+ /** Custom skeleton element for loading state. Defaults to a pulse bar. */
142
+
143
+ skeleton?: JSX.Element
144
+
145
+ }
146
+
147
+
148
+
149
+ export interface DataTableGroupByProps<T> {
150
+
151
+ /** Return group key for each item; null = uncategorized. When set, rows are rendered in groups with a header row per group. */
152
+
153
+ groupBy: (item: T) => string | null
154
+
155
+ /** Render the group header row (one cell with colSpan). Required when groupBy is set. */
156
+
157
+ renderGroupHeader: (groupKey: string | null) => JSX.Element
158
+
159
+ /** Sort group keys: null (uncategorized) first, then others by key. Override for custom order. */
160
+
161
+ groupOrder?: (a: string | null, b: string | null) => number
162
+
163
+ }
164
+
165
+
166
+
167
+ export type DataTablePagination = Pick<
168
+
169
+ PaginationProps,
170
+
171
+ 'totalItems' | 'page' | 'totalPages' | 'pageSize' | 'onPageChange' | 'onPageSizeChange' | 'pageSizeOptions' | 'maxPages' | 'showFirstLast'
172
+
173
+ >
174
+
175
+
176
+
177
+ export type DataTablePagingProps =
178
+
179
+ | { pagination: DataTablePagination; loadMore?: never }
180
+
181
+ | { loadMore: { hasMore: boolean; onLoadMore: () => void; loading?: boolean }; pagination?: never }
182
+
183
+ | { pagination?: undefined; loadMore?: undefined }
184
+
185
+
186
+
187
+ export type DataTableProps<T> = JSX.HTMLAttributes<HTMLDivElement> & DataTablePagingProps & {
188
+
189
+ description?: JSX.Element
190
+
191
+ search?: DataTableSearchProps
192
+
193
+ toolbarContent?: JSX.Element
194
+
195
+ toolbarActions?: JSX.Element
196
+
197
+ primaryButton?: DataTableButtonProps
198
+
199
+ secondaryButton?: DataTableButtonProps
200
+
201
+ addRow?: DataTableAddRowProps
202
+
203
+ editModal?: DataTableEditModalProps
204
+
205
+ deleteDialog?: DataTableDeleteDialogProps
206
+
207
+ groupBy?: DataTableGroupByProps<T>
208
+
209
+ /** When true, the table header row is not rendered. */
210
+
211
+ hideHeader?: boolean
212
+
213
+ emptyState?: {
214
+
215
+ title: string
216
+
217
+ description?: string
218
+
219
+ icon?: JSX.Element
220
+
221
+ actions?: JSX.Element
222
+
223
+ }
224
+
225
+ loading?: boolean
226
+
227
+ error?: Error | unknown
228
+
229
+ items: T[]
230
+
231
+ /** Column definitions. Drives header, body cells, skeleton, and colSpan automatically. */
232
+
233
+ columns: ColumnDef<T>[]
234
+
235
+ /** Escape hatch: return a complete <TableRow> to override, or null/undefined for column-based default. Returning anything else (fragment, bare text, false) produces invalid table markup. */
236
+
237
+ renderRowOverride?: (item: T) => JSX.Element | null | undefined
238
+
239
+ emptyMessage: string
240
+
241
+ /** Number of skeleton rows to show while loading. Default: 5. */
242
+
243
+ skeletonRows?: number
244
+
245
+ }
246
+
247
+
248
+
249
+ /** Default group order: null (uncategorized) first, then keys by localeCompare. */
250
+
251
+ function defaultGroupOrder(a: string | null, b: string | null): number {
252
+
253
+ if (a === b) return 0
254
+
255
+ if (a == null) return -1
256
+
257
+ if (b == null) return 1
258
+
259
+ return a.localeCompare(b, undefined, { sensitivity: 'base' })
260
+
261
+ }
262
+
263
+
264
+
265
+ /**
266
+
267
+ * Data table with optional toolbar, inline add row, edit modal, delete dialog, and row grouping.
268
+
269
+ * Use groupBy to render rows in groups with a header row per group. All action buttons use type="button" so the table can be used inside a form without causing submit.
270
+
271
+ */
272
+
273
+ export function DataTable<T>(props: DataTableProps<T>) {
274
+
275
+ const [local, others] = splitProps(props, [
276
+
277
+ 'description', 'search', 'toolbarContent', 'toolbarActions',
278
+
279
+ 'primaryButton', 'secondaryButton', 'addRow', 'editModal',
280
+
281
+ 'deleteDialog', 'groupBy', 'pagination', 'hideHeader',
282
+
283
+ 'emptyState', 'loadMore', 'loading', 'error', 'items',
284
+
285
+ 'columns', 'renderRowOverride', 'emptyMessage',
286
+
287
+ 'skeletonRows', 'class',
288
+
289
+ ])
290
+
291
+
292
+
293
+ if (import.meta.env.DEV && local.columns.length === 0) {
294
+
295
+ console.warn('DataTable: columns must not be empty')
296
+
297
+ }
298
+
299
+
300
+
301
+ const colSpan = () => local.columns.length
302
+
303
+ const skeletonRowCount = createMemo(() => Array.from({ length: local.skeletonRows ?? 5 }))
304
+
305
+ const hasToolbar = () =>
306
+
307
+ local.description != null ||
308
+
309
+ local.search != null ||
310
+
311
+ local.toolbarContent != null ||
312
+
313
+ local.toolbarActions != null ||
314
+
315
+ local.primaryButton != null ||
316
+
317
+ local.secondaryButton != null ||
318
+
319
+ local.addRow != null
320
+
321
+
322
+
323
+ const groupedRows = createMemo((): { key: string | null; items: T[] }[] | null => {
324
+
325
+ const g = local.groupBy
326
+
327
+ if (!g) return null
328
+
329
+ if (local.items.length === 0) return []
330
+
331
+ const order = g.groupOrder ?? defaultGroupOrder
332
+
333
+ const map = new Map<string | null, T[]>()
334
+
335
+ for (const item of local.items) {
336
+
337
+ const key = g.groupBy(item)
338
+
339
+ const list = map.get(key)
340
+
341
+ if (list) list.push(item)
342
+
343
+ else map.set(key, [item])
344
+
345
+ }
346
+
347
+ const keys = [...map.keys()].sort(order)
348
+
349
+ return keys.map((key) => ({ key, items: map.get(key)! }))
350
+
351
+ })
352
+
353
+
354
+
355
+ function renderEmptyRow() {
356
+
357
+ return (
358
+
359
+ <Show when={!local.addRow?.showAddForm}>
360
+
361
+ <TableRow hover={false}>
362
+
363
+ <TableCell colSpan={colSpan()} class="p-0 align-top">
364
+
365
+ {local.emptyState ? (
366
+
367
+ <EmptyState
368
+
369
+ title={local.emptyState.title}
370
+
371
+ description={local.emptyState.description}
372
+
373
+ icon={local.emptyState.icon}
374
+
375
+ actions={local.emptyState.actions}
376
+
377
+ />
378
+
379
+ ) : (
380
+
381
+ <div class="py-8 text-center text-sm text-ink-500">
382
+
383
+ {local.emptyMessage}
384
+
385
+ </div>
386
+
387
+ )}
388
+
389
+ </TableCell>
390
+
391
+ </TableRow>
392
+
393
+ </Show>
394
+
395
+ )
396
+
397
+ }
398
+
399
+
400
+
401
+ function renderItem(item: T) {
402
+
403
+ const overridden = local.renderRowOverride?.(item)
404
+
405
+ if (import.meta.env.DEV && overridden != null && (typeof overridden !== 'object' || Array.isArray(overridden))) {
406
+
407
+ console.warn('DataTable: renderRowOverride must return a single <TableRow> element or null/undefined, not an array or fragment')
408
+
409
+ }
410
+
411
+ return overridden ?? (
412
+
413
+ <TableRow>
414
+
415
+ <For each={local.columns}>
416
+
417
+ {(col) => <TableCell class={col.cellClass}>{col.cell(item)}</TableCell>}
418
+
419
+ </For>
420
+
421
+ </TableRow>
422
+
423
+ )
424
+
425
+ }
426
+
427
+
428
+
429
+ function FlatRows() {
430
+
431
+ return (
432
+
433
+ <Show when={local.items.length > 0} fallback={renderEmptyRow()}>
434
+
435
+ <For each={local.items}>{(item) => renderItem(item)}</For>
436
+
437
+ </Show>
438
+
439
+ )
440
+
441
+ }
442
+
443
+
444
+
445
+ return (
446
+
447
+ <div {...others} class={cn('space-y-4', local.class)}>
448
+
449
+ {local.description}
450
+
451
+ <Show when={hasToolbar()}>
452
+
453
+ <div class="flex flex-wrap items-center gap-2">
454
+
455
+ <Show when={local.search}>
456
+
457
+ {(search) => (
458
+
459
+ <div class="min-w-[200px] flex-1">
460
+
461
+ <Input
462
+
463
+ bare
464
+
465
+ compact
466
+
467
+ value={search().value}
468
+
469
+ onValueChange={search().onChange}
470
+
471
+ placeholder={search().placeholder}
472
+
473
+ class="w-full"
474
+
475
+ inputClass="rounded-lg"
476
+
477
+ />
478
+
479
+ </div>
480
+
481
+ )}
482
+
483
+ </Show>
484
+
485
+ <Show when={local.toolbarContent}>
486
+
487
+ {local.toolbarContent}
488
+
489
+ </Show>
490
+
491
+ <Show when={local.toolbarActions}>
492
+
493
+ <div class="flex items-center gap-2 shrink-0">
494
+
495
+ {local.toolbarActions}
496
+
497
+ </div>
498
+
499
+ </Show>
500
+
501
+ <Show when={local.addRow || local.primaryButton || local.secondaryButton}>
502
+
503
+ <div class="ml-auto flex items-center gap-2 shrink-0">
504
+
505
+ <Show when={local.addRow}>
506
+
507
+ {(addRow) => (
508
+
509
+ <Button
510
+
511
+ type="button"
512
+
513
+ variant="primary"
514
+
515
+ size="sm"
516
+
517
+ class="shrink-0 rounded-lg"
518
+
519
+ label={addRow().addButtonLabel}
520
+
521
+ startIcon={<Plus size={16} />}
522
+
523
+ onClick={() => addRow().onToggleAddForm()}
524
+
525
+ />
526
+
527
+ )}
528
+
529
+ </Show>
530
+
531
+ <Show when={!local.addRow && local.primaryButton}>
532
+
533
+ {(btn) => (
534
+
535
+ <Button
536
+
537
+ type="button"
538
+
539
+ variant="primary"
540
+
541
+ size="sm"
542
+
543
+ class="shrink-0 rounded-lg"
544
+
545
+ label={btn().label}
546
+
547
+ startIcon={btn().startIcon}
548
+
549
+ onClick={() => btn().onClick()}
550
+
551
+ />
552
+
553
+ )}
554
+
555
+ </Show>
556
+
557
+ <Show when={local.secondaryButton}>
558
+
559
+ {(btn) => (
560
+
561
+ <Button
562
+
563
+ type="button"
564
+
565
+ variant="outlined"
566
+
567
+ size="sm"
568
+
569
+ class="shrink-0 rounded-lg"
570
+
571
+ label={btn().label}
572
+
573
+ startIcon={btn().startIcon}
574
+
575
+ onClick={() => btn().onClick()}
576
+
577
+ />
578
+
579
+ )}
580
+
581
+ </Show>
582
+
583
+ </div>
584
+
585
+ </Show>
586
+
587
+ </div>
588
+
589
+ </Show>
590
+
591
+
592
+
593
+ <Show when={local.error}>
594
+
595
+ <p class="text-sm text-red-600 dark:text-red-400">
596
+
597
+ {local.error instanceof Error ? local.error.message : 'Failed to load'}
598
+
599
+ </p>
600
+
601
+ </Show>
602
+
603
+ <Show when={local.loading}>
604
+
605
+ <div role="status" aria-live="polite" class="sr-only">Loading</div>
606
+
607
+ </Show>
608
+
609
+ <div class={cn('overflow-x-auto', TABLE_CONTAINER_CLASS)}>
610
+
611
+ <Table class="min-w-full">
612
+
613
+ <Show when={!local.hideHeader}>
614
+
615
+ <TableHeader>
616
+
617
+ <TableRow>
618
+
619
+ <For each={local.columns}>
620
+
621
+ {(col) => (
622
+
623
+ <TableHead class={col.headClass}>
624
+
625
+ {typeof col.header === 'string' ? col.header : col.header}
626
+
627
+ </TableHead>
628
+
629
+ )}
630
+
631
+ </For>
632
+
633
+ </TableRow>
634
+
635
+ </TableHeader>
636
+
637
+ </Show>
638
+
639
+ <TableBody aria-busy={local.loading ? true : undefined}>
640
+
641
+ <Show
642
+
643
+ when={!local.loading}
644
+
645
+ fallback={
646
+
647
+ <For each={skeletonRowCount()}>
648
+
649
+ {() => (
650
+
651
+ <TableRow hover={false}>
652
+
653
+ <For each={local.columns}>
654
+
655
+ {(col) => (
656
+
657
+ <TableCell class={cn('py-3', col.cellClass)}>
658
+
659
+ {col.skeleton ?? (
660
+
661
+ <div class="h-4 w-full max-w-48 animate-pulse rounded bg-ink-200" />
662
+
663
+ )}
664
+
665
+ </TableCell>
666
+
667
+ )}
668
+
669
+ </For>
670
+
671
+ </TableRow>
672
+
673
+ )}
674
+
675
+ </For>
676
+
677
+ }
678
+
679
+ >
680
+
681
+ <Show when={local.addRow?.showAddForm}>
682
+
683
+ <TableRow class="bg-primary-50/50 dark:bg-primary-950/20" hover={false}>
684
+
685
+ {local.addRow!.renderAddCells()}
686
+
687
+ </TableRow>
688
+
689
+ </Show>
690
+
691
+ <Show when={groupedRows()} fallback={<FlatRows />}>
692
+
693
+ {(groups) => (
694
+
695
+ <Show when={groups().length > 0} fallback={renderEmptyRow()}>
696
+
697
+ <For each={groups()}>
698
+
699
+ {({ key, items: groupItems }) => [
700
+
701
+ <TableRow class="bg-ink-50/50 font-medium text-ink-700" hover={false}>
702
+
703
+ <TableCell colSpan={colSpan()} class="py-2 pl-4 text-sm font-medium" role="rowheader">
704
+
705
+ {local.groupBy!.renderGroupHeader(key)}
706
+
707
+ </TableCell>
708
+
709
+ </TableRow>,
710
+
711
+ ...groupItems.map((item) => renderItem(item)),
712
+
713
+ ]}
714
+
715
+ </For>
716
+
717
+ </Show>
718
+
719
+ )}
720
+
721
+ </Show>
722
+
723
+ <Show when={local.addRow?.addError && local.addRow?.showAddForm}>
724
+
725
+ <TableRow hover={false}>
726
+
727
+ <TableCell colSpan={colSpan()} class="bg-red-50/80 dark:bg-red-950/30 text-sm text-red-600 dark:text-red-400">
728
+
729
+ {local.addRow!.addError}
730
+
731
+ </TableCell>
732
+
733
+ </TableRow>
734
+
735
+ </Show>
736
+
737
+ </Show>
738
+
739
+ </TableBody>
740
+
741
+ </Table>
742
+
743
+ <Show when={local.loadMore?.hasMore}>
744
+
745
+ <div class="flex justify-center border-t border-surface-border bg-surface-raised px-6 py-4">
746
+
747
+ <Button
748
+
749
+ type="button"
750
+
751
+ variant="outlined"
752
+
753
+ size="sm"
754
+
755
+ class="rounded-lg"
756
+
757
+ label="Load more"
758
+
759
+ loading={local.loadMore!.loading}
760
+
761
+ onClick={() => local.loadMore!.onLoadMore()}
762
+
763
+ />
764
+
765
+ </div>
766
+
767
+ </Show>
768
+
769
+ <Show when={local.pagination}>
770
+
771
+ {(pagination) => (
772
+
773
+ <Pagination
774
+
775
+ {...pagination()}
776
+
777
+ class="border-t border-surface-border bg-surface-raised px-6 py-3"
778
+
779
+ />
780
+
781
+ )}
782
+
783
+ </Show>
784
+
785
+ </div>
786
+
787
+
788
+
789
+ <Show when={local.editModal}>
790
+
791
+ {(modal) => (
792
+
793
+ <Dialog
794
+
795
+ open={modal().open}
796
+
797
+ onClose={modal().onClose}
798
+
799
+ size="md"
800
+
801
+ header={<h3 class="text-lg font-semibold text-ink-900">{modal().title}</h3>}
802
+
803
+ footer={
804
+
805
+ <div class="flex justify-end gap-2">
806
+
807
+ <Button type="button" variant="link" size="sm" onClick={modal().onClose} class="rounded-lg">
808
+
809
+ Cancel
810
+
811
+ </Button>
812
+
813
+ <Button
814
+
815
+ type="button"
816
+
817
+ variant="primary"
818
+
819
+ size="sm"
820
+
821
+ onClick={modal().onSave}
822
+
823
+ loading={modal().saving}
824
+
825
+ class="rounded-lg"
826
+
827
+ >
828
+
829
+ Save
830
+
831
+ </Button>
832
+
833
+ </div>
834
+
835
+ }
836
+
837
+ >
838
+
839
+ <div class="space-y-4">{modal().children}</div>
840
+
841
+ <Show when={modal().editError}>
842
+
843
+ <p class="mt-3 text-sm text-red-600 dark:text-red-400">{modal().editError}</p>
844
+
845
+ </Show>
846
+
847
+ </Dialog>
848
+
849
+ )}
850
+
851
+ </Show>
852
+
853
+
854
+
855
+ <Show when={local.deleteDialog}>
856
+
857
+ {(dialog) => (
858
+
859
+ <AlertDialog
860
+
861
+ open={dialog().open}
862
+
863
+ onOpenChange={(open) => !open && dialog().onClose()}
864
+
865
+ title={dialog().title}
866
+
867
+ description={dialog().description}
868
+
869
+ confirmLabel="Delete"
870
+
871
+ destructive
872
+
873
+ onConfirm={dialog().onConfirm}
874
+
875
+ />
876
+
877
+ )}
878
+
879
+ </Show>
880
+
881
+ </div>
882
+
883
+ )
884
+
885
+ }
886
+