@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,1018 @@
1
+ import type { JSX } from 'solid-js'
2
+
3
+ import { createContext, createEffect, createSignal, on, onMount, Show, useContext } from 'solid-js'
4
+
5
+ import { Plus } from 'lucide-solid'
6
+
7
+ import { ViewCustomizer } from '../../forms'
8
+
9
+ import { FilterBuilder } from '../../forms/FilterBuilder'
10
+
11
+ import {
12
+
13
+ formatFilterSummary,
14
+
15
+ formatFilterCode,
16
+
17
+ assignRuleNumbers,
18
+
19
+ hasRulesWithEmptyField,
20
+
21
+ type FilterGroup,
22
+
23
+ } from '../../../types/filter-types'
24
+
25
+ import { Drawer } from '../../overlays/Drawer'
26
+
27
+ import { ViewSwitcher } from '../../navigation/ViewSwitcher'
28
+
29
+ import type { TableViewConfig, TableViewsApi, ViewConfig } from './types'
30
+
31
+
32
+
33
+ const createFilterId = (prefix: string) =>
34
+
35
+ `${prefix}-${Math.random().toString(36).slice(2, 9)}`
36
+
37
+
38
+
39
+ export const emptyFilterGroup = (): FilterGroup => ({
40
+
41
+ id: createFilterId('group'),
42
+
43
+ type: 'and',
44
+
45
+ logic: 'and',
46
+
47
+ children: [],
48
+
49
+ })
50
+
51
+
52
+
53
+ function getDefaultViewId(views: ViewConfig[], systemDefaultId: string): string {
54
+
55
+ const pinned = views.find((v) => v.pinned)
56
+
57
+ return pinned?.id ?? systemDefaultId
58
+
59
+ }
60
+
61
+
62
+
63
+
64
+
65
+ export interface TableViewContextValue {
66
+
67
+ views: () => ViewConfig[]
68
+
69
+ activeView: () => string
70
+
71
+ setActiveView: (id: string) => void
72
+
73
+ activeViewConfig: () => ViewConfig | undefined
74
+
75
+ /** False when viewing the system default view (e.g. All contacts) - use to disable Customize button */
76
+
77
+ canCustomize: () => boolean
78
+
79
+ openCustomize: () => void
80
+
81
+ openFilters: () => void
82
+
83
+ openCustomizeForEdit: () => void
84
+
85
+ openCustomizeForCreate: () => void
86
+
87
+ /** Compact single-line summary of active filters (e.g. "Status is any of Active, Inactive") */
88
+
89
+ activeFilterSummary: () => string
90
+
91
+ /** Current filter group - use to filter displayed data */
92
+
93
+ activeFilterGroup: () => FilterGroup
94
+
95
+ /** Report filtered row count for badge display. Call from table/content when filters are applied. */
96
+
97
+ setFilteredRowCount: (count: number | null) => void
98
+
99
+ }
100
+
101
+
102
+
103
+ const TableViewContext = createContext<TableViewContextValue>()
104
+
105
+
106
+
107
+ export function useTableView() {
108
+
109
+ const ctx = useContext(TableViewContext)
110
+
111
+ if (!ctx) {
112
+
113
+ throw new Error('useTableView must be used within TableView')
114
+
115
+ }
116
+
117
+ return ctx
118
+
119
+ }
120
+
121
+
122
+
123
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
124
+
125
+ function isDbViewId(id: string) {
126
+
127
+ return UUID_REGEX.test(id)
128
+
129
+ }
130
+
131
+
132
+
133
+ export interface TableViewProps {
134
+
135
+ config: TableViewConfig
136
+
137
+ /** When provided, views are fetched from and saved to the API. Requires config.entityType. */
138
+
139
+ viewsApi?: TableViewsApi
140
+
141
+ /** Toolbar content. Receives API with openCustomizeForEdit, openFilters, etc. */
142
+
143
+ toolbar?: (api: TableViewContextValue) => JSX.Element
144
+
145
+ /** Main table/content area */
146
+
147
+ children: JSX.Element
148
+
149
+ class?: string
150
+
151
+ }
152
+
153
+
154
+
155
+ export function TableView(props: TableViewProps) {
156
+
157
+ const config = () => props.config
158
+
159
+ const [showCustomize, setShowCustomize] = createSignal(false)
160
+
161
+ const [customizeMode, setCustomizeMode] = createSignal<'edit' | 'create'>('edit')
162
+
163
+ const [showFilters, setShowFilters] = createSignal(false)
164
+
165
+ const [views, setViews] = createSignal(config().defaultViews)
166
+
167
+ const [activeView, setActiveView] = createSignal(
168
+
169
+ getDefaultViewId(config().defaultViews, config().systemDefaultViewId),
170
+
171
+ )
172
+
173
+ const [saveError, setSaveError] = createSignal<string | null>(null)
174
+
175
+ const [filterSaveError, setFilterSaveError] = createSignal<string | null>(null)
176
+
177
+
178
+
179
+ onMount(() => {
180
+
181
+ const api = props.viewsApi
182
+
183
+ const entityType = config().entityType
184
+
185
+ if (!api || !entityType) return
186
+
187
+ api.fetchViews(entityType).then(
188
+
189
+ (fetched: ViewConfig[]) => {
190
+
191
+ const systemIds = new Set(config().defaultViews.map((v) => v.id))
192
+
193
+ const merged = [
194
+
195
+ ...config().defaultViews,
196
+
197
+ ...fetched.filter((v: ViewConfig) => !systemIds.has(v.id)),
198
+
199
+ ]
200
+
201
+ setViews(merged)
202
+
203
+ setActiveView(getDefaultViewId(merged, config().systemDefaultViewId))
204
+
205
+ },
206
+
207
+ (err: unknown) => {
208
+
209
+ console.error('[TableView] Failed to fetch views:', err)
210
+
211
+ },
212
+
213
+ )
214
+
215
+ })
216
+
217
+ const [viewName, setViewName] = createSignal('')
218
+
219
+ const [usedFields, setUsedFields] = createSignal([...config().initialFields])
220
+
221
+ const [groupBy, setGroupBy] = createSignal('none')
222
+
223
+ const [filterGroup, setFilterGroup] = createSignal<FilterGroup>(emptyFilterGroup())
224
+
225
+ const [filteredRowCount, setFilteredRowCount] = createSignal<number | null>(null)
226
+
227
+ const [scope, setScope] = createSignal<'user' | 'tenant'>('user')
228
+
229
+ const [pinned, setPinned] = createSignal(false)
230
+
231
+
232
+
233
+ const createViewId = (name: string) => {
234
+
235
+ const base = name.toLowerCase().replace(/\s+/g, '-')
236
+
237
+ let candidate = base
238
+
239
+ let counter = 2
240
+
241
+ while (views().some((view) => view.id === candidate)) {
242
+
243
+ candidate = `${base}-${counter}`
244
+
245
+ counter += 1
246
+
247
+ }
248
+
249
+ return candidate
250
+
251
+ }
252
+
253
+
254
+
255
+ const updateView = async () => {
256
+
257
+ const name = viewName().trim()
258
+
259
+ if (!name) return
260
+
261
+ const entityType = config().entityType
262
+
263
+ const api = props.viewsApi
264
+
265
+ const viewId = activeView()
266
+
267
+
268
+
269
+ if (api && entityType && isDbViewId(viewId)) {
270
+
271
+ setSaveError(null)
272
+
273
+ try {
274
+
275
+ await api.updateView(entityType, {
276
+
277
+ id: viewId,
278
+
279
+ label: name,
280
+
281
+ fields: [...usedFields()],
282
+
283
+ groupBy: groupBy(),
284
+
285
+ filters: { ...filterGroup() },
286
+
287
+ scope: scope(),
288
+
289
+ pinned: pinned(),
290
+
291
+ })
292
+
293
+ } catch (err) {
294
+
295
+ setSaveError(err instanceof Error ? err.message : 'Failed to save view')
296
+
297
+ return
298
+
299
+ }
300
+
301
+ }
302
+
303
+
304
+
305
+ setViews((current) =>
306
+
307
+ current.map((view) =>
308
+
309
+ view.id === viewId
310
+
311
+ ? {
312
+
313
+ ...view,
314
+
315
+ label: name,
316
+
317
+ fields: [...usedFields()],
318
+
319
+ groupBy: groupBy(),
320
+
321
+ filters: { ...filterGroup() },
322
+
323
+ scope: scope(),
324
+
325
+ pinned: pinned(),
326
+
327
+ }
328
+
329
+ : view,
330
+
331
+ ),
332
+
333
+ )
334
+
335
+ setShowCustomize(false)
336
+
337
+ }
338
+
339
+
340
+
341
+ const saveViewAsNew = async () => {
342
+
343
+ const name = viewName().trim()
344
+
345
+ if (!name) return
346
+
347
+ const entityType = config().entityType
348
+
349
+ const api = props.viewsApi
350
+
351
+
352
+
353
+ if (api && entityType) {
354
+
355
+ setSaveError(null)
356
+
357
+ try {
358
+
359
+ const created = await api.createView(entityType, {
360
+
361
+ label: name,
362
+
363
+ fields: [...usedFields()],
364
+
365
+ groupBy: groupBy(),
366
+
367
+ filters: { ...filterGroup() },
368
+
369
+ scope: scope(),
370
+
371
+ pinned: pinned(),
372
+
373
+ })
374
+
375
+ setViews((current) => [...current, created])
376
+
377
+ setActiveView(created.id)
378
+
379
+ setShowCustomize(false)
380
+
381
+ return
382
+
383
+ } catch (err) {
384
+
385
+ setSaveError(err instanceof Error ? err.message : 'Failed to create view')
386
+
387
+ return
388
+
389
+ }
390
+
391
+ }
392
+
393
+
394
+
395
+ const id = createViewId(name)
396
+
397
+ setViews((current) => [
398
+
399
+ ...current,
400
+
401
+ {
402
+
403
+ id,
404
+
405
+ label: name,
406
+
407
+ fields: [...usedFields()],
408
+
409
+ groupBy: groupBy(),
410
+
411
+ filters: { ...filterGroup() },
412
+
413
+ scope: scope(),
414
+
415
+ pinned: pinned(),
416
+
417
+ },
418
+
419
+ ])
420
+
421
+ setActiveView(id)
422
+
423
+ setShowCustomize(false)
424
+
425
+ }
426
+
427
+
428
+
429
+ const openCustomizeForEdit = () => {
430
+
431
+ if (activeView() === config().systemDefaultViewId) return
432
+
433
+ setCustomizeMode('edit')
434
+
435
+ setShowCustomize(true)
436
+
437
+ }
438
+
439
+
440
+
441
+ const openCustomizeForCreate = () => {
442
+
443
+ setCustomizeMode('create')
444
+
445
+ setViewName('New view')
446
+
447
+ setUsedFields([...config().initialFields])
448
+
449
+ setGroupBy('none')
450
+
451
+ // Keep current filters so user can create a view from their filtered results (e.g. "Active contacts SMS")
452
+
453
+ // Use deep copy to avoid state mutation bugs
454
+
455
+ setFilterGroup(JSON.parse(JSON.stringify(filterGroup())))
456
+
457
+ setScope('user')
458
+
459
+ setPinned(false)
460
+
461
+ setShowCustomize(true)
462
+
463
+ }
464
+
465
+
466
+
467
+ const closeCustomize = () => {
468
+
469
+ setSaveError(null)
470
+
471
+ setShowCustomize(false)
472
+
473
+ }
474
+
475
+
476
+
477
+ /** Save editingFilterGroup to filterGroup and active view */
478
+
479
+ const saveFiltersToView = async (): Promise<boolean> => {
480
+
481
+ const group = editingFilterGroup()
482
+
483
+ if (!group) return true
484
+
485
+
486
+
487
+ const viewId = activeView()
488
+
489
+ const current = views().find((v) => v.id === viewId)
490
+
491
+ if (!current) return true
492
+
493
+
494
+
495
+ const filtersCopy = JSON.parse(JSON.stringify(group))
496
+
497
+ const originalFilters = current.filters // Store original for revert
498
+
499
+
500
+
501
+ // Update local views first for optimistic UI
502
+
503
+ setViews((prev) =>
504
+
505
+ prev.map((v) => (v.id === viewId ? { ...v, filters: filtersCopy } : v))
506
+
507
+ )
508
+
509
+
510
+
511
+ // Only commit to live filterGroup after successful API save
512
+
513
+ const api = props.viewsApi
514
+
515
+ const entityType = config().entityType
516
+
517
+ if (api && entityType && isDbViewId(viewId)) {
518
+
519
+ setFilterSaveError(null)
520
+
521
+ try {
522
+
523
+ await api.updateView(entityType, {
524
+
525
+ ...current,
526
+
527
+ filters: filtersCopy,
528
+
529
+ })
530
+
531
+ // Success: now commit to live filterGroup
532
+
533
+ setFilterGroup(filtersCopy)
534
+
535
+ } catch (err) {
536
+
537
+ // Failure: revert views to original filters and show error
538
+
539
+ setViews((prev) =>
540
+
541
+ prev.map((v) => (v.id === viewId ? { ...v, filters: originalFilters } : v))
542
+
543
+ )
544
+
545
+ setFilterSaveError(err instanceof Error ? err.message : 'Failed to save filters')
546
+
547
+ return false
548
+
549
+ }
550
+
551
+ } else {
552
+
553
+ // Non-db view: commit immediately
554
+
555
+ setFilterGroup(filtersCopy)
556
+
557
+ }
558
+
559
+
560
+
561
+ return true
562
+
563
+ }
564
+
565
+
566
+
567
+ const saveFilters = async () => {
568
+
569
+ const group = editingFilterGroup()
570
+
571
+ if (!group) return
572
+
573
+ if (hasRulesWithEmptyField(group)) {
574
+
575
+ setFilterSaveError('Please select a field for each condition.')
576
+
577
+ return
578
+
579
+ }
580
+
581
+ const ok = await saveFiltersToView()
582
+
583
+ if (ok) {
584
+
585
+ setFilterSaveError(null)
586
+
587
+ setEditingFilterGroup(null)
588
+
589
+ setShowFilters(false)
590
+
591
+ }
592
+
593
+ }
594
+
595
+
596
+
597
+ /** Local editing state for filter drawer - isolated from filterGroup to avoid sync/reactivity issues */
598
+
599
+ const [editingFilterGroup, setEditingFilterGroup] = createSignal<FilterGroup | null>(null)
600
+
601
+
602
+
603
+ const cancelFilters = () => {
604
+
605
+ setEditingFilterGroup(null)
606
+
607
+ setFilterSaveError(null)
608
+
609
+ setTimeout(() => setShowFilters(false), 0)
610
+
611
+ }
612
+
613
+
614
+
615
+ // Initialize local editing state ONLY when drawer opens; never overwrite during editing
616
+
617
+ createEffect(
618
+
619
+ on(showFilters, (open) => {
620
+
621
+ if (open) {
622
+
623
+ setEditingFilterGroup(JSON.parse(JSON.stringify(filterGroup())))
624
+
625
+ } else {
626
+
627
+ setEditingFilterGroup(null)
628
+
629
+ }
630
+
631
+ }),
632
+
633
+ )
634
+
635
+
636
+
637
+ const deleteView = async () => {
638
+
639
+ const current = views().find((v) => v.id === activeView())
640
+
641
+ if (!current || current.scope !== 'user') return
642
+
643
+ const entityType = config().entityType
644
+
645
+ const api = props.viewsApi
646
+
647
+
648
+
649
+ if (api && entityType && isDbViewId(current.id)) {
650
+
651
+ setSaveError(null)
652
+
653
+ try {
654
+
655
+ await api.deleteView(entityType, current.id)
656
+
657
+ } catch (err) {
658
+
659
+ setSaveError(err instanceof Error ? err.message : 'Failed to delete view')
660
+
661
+ return
662
+
663
+ }
664
+
665
+ }
666
+
667
+
668
+
669
+ const remaining = views().filter((v) => v.id !== current.id)
670
+
671
+ setViews(remaining)
672
+
673
+ setActiveView(getDefaultViewId(remaining, config().systemDefaultViewId))
674
+
675
+ setShowCustomize(false)
676
+
677
+ }
678
+
679
+
680
+
681
+ // Sync filterGroup when active view changes (so Filter drawer shows correct filters)
682
+
683
+ createEffect(() => {
684
+
685
+ if (showFilters()) return // Don't overwrite while user is editing in the filter drawer
686
+
687
+ const viewId = activeView()
688
+
689
+ const current = views().find((view) => view.id === viewId)
690
+
691
+ if (!current) return
692
+
693
+ setFilterGroup(JSON.parse(JSON.stringify(current.filters)))
694
+
695
+ })
696
+
697
+
698
+
699
+
700
+
701
+ // Load view data into Customize form when opening for edit
702
+
703
+ createEffect(() => {
704
+
705
+ if (!showCustomize() || customizeMode() !== 'edit') return
706
+
707
+ const current = views().find((view) => view.id === activeView())
708
+
709
+ if (!current) return
710
+
711
+ setViewName(current.label)
712
+
713
+ setUsedFields([...current.fields])
714
+
715
+ setGroupBy(current.groupBy)
716
+
717
+ setScope(current.scope)
718
+
719
+ setPinned(current.pinned ?? false)
720
+
721
+ setFilterGroup(JSON.parse(JSON.stringify(current.filters)))
722
+
723
+ })
724
+
725
+
726
+
727
+ const activeViewConfig = () => views().find((v) => v.id === activeView())
728
+
729
+ const canCustomize = () => activeView() !== config().systemDefaultViewId
730
+
731
+
732
+
733
+ const activeFilterSummary = () => {
734
+
735
+ const group = filterGroup()
736
+
737
+ if (!group.children.length) return ''
738
+
739
+ return formatFilterSummary(group, {
740
+
741
+ fields: config().filterFields,
742
+
743
+ getOperators: config().getOperatorsForType,
744
+
745
+ })
746
+
747
+ }
748
+
749
+
750
+
751
+ const contextValue: TableViewContextValue = {
752
+
753
+ views,
754
+
755
+ activeView,
756
+
757
+ setActiveView,
758
+
759
+ activeViewConfig,
760
+
761
+ canCustomize,
762
+
763
+ openCustomize: openCustomizeForEdit,
764
+
765
+ openFilters: () => setShowFilters(true),
766
+
767
+ openCustomizeForEdit,
768
+
769
+ openCustomizeForCreate,
770
+
771
+ activeFilterSummary,
772
+
773
+ activeFilterGroup: filterGroup,
774
+
775
+ setFilteredRowCount,
776
+
777
+ }
778
+
779
+
780
+
781
+ const sortedViews = () =>
782
+
783
+ [...views()].sort((a, b) => {
784
+
785
+ if (a.pinned && !b.pinned) return -1
786
+
787
+ if (!a.pinned && b.pinned) return 1
788
+
789
+ return 0
790
+
791
+ })
792
+
793
+
794
+
795
+ return (
796
+
797
+ <TableViewContext.Provider value={contextValue}>
798
+
799
+ <div class={props.class}>
800
+
801
+ <div class="min-w-0 w-full bg-ink-100/60">
802
+
803
+ <ViewSwitcher
804
+
805
+ views={sortedViews().map((view) => ({
806
+
807
+ id: view.id,
808
+
809
+ label: view.label,
810
+
811
+ count:
812
+
813
+ config().getFilteredCountForView?.(view) ??
814
+
815
+ (activeView() === view.id ? filteredRowCount() : null) ??
816
+
817
+ config().rowCount ??
818
+
819
+ 0,
820
+
821
+ scope: view.scope,
822
+
823
+ pinned: view.pinned,
824
+
825
+ }))}
826
+
827
+ activeId={activeView()}
828
+
829
+ onChange={setActiveView}
830
+
831
+ variant="embedded"
832
+
833
+ class="w-full"
834
+
835
+ addIcon={<Plus size={20} strokeWidth={2} />}
836
+
837
+ onAdd={openCustomizeForCreate}
838
+
839
+ />
840
+
841
+ </div>
842
+
843
+ {props.toolbar && (
844
+
845
+ <div class="border-b border-surface-border bg-surface-raised px-4 py-4">
846
+
847
+ {props.toolbar(contextValue)}
848
+
849
+ </div>
850
+
851
+ )}
852
+
853
+ <div class={config().rowCount === 0 ? 'p-10' : 'p-0'}>{props.children}</div>
854
+
855
+ </div>
856
+
857
+
858
+
859
+ <Drawer open={showCustomize()} onClose={closeCustomize} size="xl">
860
+
861
+ <ViewCustomizer
862
+
863
+ fields={config().allFields}
864
+
865
+ value={usedFields()}
866
+
867
+ onChange={setUsedFields}
868
+
869
+ allFields={config().allFields}
870
+
871
+ viewName={viewName()}
872
+
873
+ onViewNameChange={setViewName}
874
+
875
+ usedFields={usedFields()}
876
+
877
+ onUsedFieldsChange={setUsedFields}
878
+
879
+ groupBy={groupBy()}
880
+
881
+ onGroupByChange={setGroupBy}
882
+
883
+ mode={customizeMode()}
884
+
885
+ editingViewLabel={activeViewConfig()?.label}
886
+
887
+ onSaveChanges={updateView}
888
+
889
+ onSaveAsNew={saveViewAsNew}
890
+
891
+ saveError={saveError()}
892
+
893
+ onCancel={closeCustomize}
894
+
895
+ onClose={closeCustomize}
896
+
897
+ canDelete={activeViewConfig()?.scope === 'user'}
898
+
899
+ onDelete={deleteView}
900
+
901
+ scope={scope()}
902
+
903
+ onScopeChange={setScope}
904
+
905
+ canSetGlobalScope={true}
906
+
907
+ pinned={pinned()}
908
+
909
+ onPinnedChange={setPinned}
910
+
911
+ title={config().customizeTitle ?? 'Customize table'}
912
+
913
+ description={config().customizeDescription ?? 'Manage fields, grouping, and saved views.'}
914
+
915
+ />
916
+
917
+ </Drawer>
918
+
919
+
920
+
921
+ <Drawer
922
+
923
+ open={showFilters()}
924
+
925
+ onClose={cancelFilters}
926
+
927
+ onCancel={cancelFilters}
928
+
929
+ onSave={saveFilters}
930
+
931
+ size="2xl"
932
+
933
+ >
934
+
935
+ <div>
936
+
937
+ <h2 class="text-lg font-semibold text-ink-900">
938
+
939
+ {config().filterTitle ?? 'Filter'}
940
+
941
+ </h2>
942
+
943
+ <div class="mt-2 text-sm text-ink-500">
944
+
945
+ {config().filterDescription ?? 'Build filter rules with And/Or logic.'}
946
+
947
+ </div>
948
+
949
+ </div>
950
+
951
+ <Show when={filterSaveError()}>
952
+
953
+ <div role="alert" class="mt-4 rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700">
954
+
955
+ {filterSaveError()}
956
+
957
+ </div>
958
+
959
+ </Show>
960
+
961
+ <div class="mt-6 flex-1 overflow-y-auto overflow-x-visible min-h-0">
962
+
963
+ <Show when={editingFilterGroup()}>
964
+
965
+ <FilterBuilder
966
+
967
+ value={() => editingFilterGroup()!}
968
+
969
+ onChange={(g: any) => setEditingFilterGroup(g)}
970
+
971
+ fields={config().filterFields}
972
+
973
+ getOperators={config().getOperatorsForType}
974
+
975
+ />
976
+
977
+ </Show>
978
+
979
+ </div>
980
+
981
+ <Show when={editingFilterGroup() && editingFilterGroup()!.children.length > 0}>
982
+
983
+ <div class="mt-6 space-y-2 border-t border-ink-200 pt-4">
984
+
985
+ <div class="text-sm font-medium text-ink-700">Filter logic</div>
986
+
987
+ <code class="block rounded-md bg-ink-100 px-3 py-2 text-sm font-mono text-ink-800">
988
+
989
+ {formatFilterCode(editingFilterGroup()!, assignRuleNumbers(editingFilterGroup()!))}
990
+
991
+ </code>
992
+
993
+ <div class="text-sm font-medium text-ink-700">Summary</div>
994
+
995
+ <p class="text-sm text-ink-600">
996
+
997
+ {formatFilterSummary(editingFilterGroup()!, {
998
+
999
+ fields: config().filterFields,
1000
+
1001
+ getOperators: config().getOperatorsForType,
1002
+
1003
+ })}
1004
+
1005
+ </p>
1006
+
1007
+ </div>
1008
+
1009
+ </Show>
1010
+
1011
+ </Drawer>
1012
+
1013
+ </TableViewContext.Provider>
1014
+
1015
+ )
1016
+
1017
+ }
1018
+