@svadmin/ui 0.63.0 → 0.65.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.
Files changed (104) hide show
  1. package/dist/components/ActivityFeed.svelte +135 -0
  2. package/dist/components/ActivityFeed.svelte.d.ts +21 -0
  3. package/dist/components/ApprovalActionCard.svelte +248 -0
  4. package/dist/components/ApprovalActionCard.svelte.d.ts +22 -0
  5. package/dist/components/AutoTable.svelte +8 -0
  6. package/dist/components/AutoTable.svelte.d.ts +7 -1
  7. package/dist/components/CanvasAnnotation.svelte +389 -0
  8. package/dist/components/CanvasAnnotation.svelte.d.ts +17 -0
  9. package/dist/components/Cascader.svelte +205 -0
  10. package/dist/components/Cascader.svelte.d.ts +21 -0
  11. package/dist/components/ColumnHeaderFilter.svelte +178 -0
  12. package/dist/components/ColumnHeaderFilter.svelte.d.ts +12 -0
  13. package/dist/components/ColumnSettings.svelte +198 -0
  14. package/dist/components/ColumnSettings.svelte.d.ts +19 -0
  15. package/dist/components/DecisionTable.svelte +240 -0
  16. package/dist/components/DecisionTable.svelte.d.ts +22 -0
  17. package/dist/components/DraggableRowTable.svelte +117 -0
  18. package/dist/components/DraggableRowTable.svelte.d.ts +15 -0
  19. package/dist/components/DrawerForm.svelte +110 -26
  20. package/dist/components/DrawerForm.svelte.d.ts +20 -8
  21. package/dist/components/DynamicFormList.svelte +214 -0
  22. package/dist/components/DynamicFormList.svelte.d.ts +62 -0
  23. package/dist/components/EditableTable.svelte +195 -0
  24. package/dist/components/EditableTable.svelte.d.ts +25 -0
  25. package/dist/components/FieldRenderer.svelte +32 -0
  26. package/dist/components/FilterBuilder.svelte +310 -0
  27. package/dist/components/FilterBuilder.svelte.d.ts +23 -0
  28. package/dist/components/GanttChart.svelte +144 -0
  29. package/dist/components/GanttChart.svelte.d.ts +20 -0
  30. package/dist/components/ImageCropper.svelte +122 -0
  31. package/dist/components/ImageCropper.svelte.d.ts +12 -0
  32. package/dist/components/ImportWizard.svelte +336 -0
  33. package/dist/components/ImportWizard.svelte.d.ts +12 -0
  34. package/dist/components/JsonSchemaForm.svelte +149 -0
  35. package/dist/components/JsonSchemaForm.svelte.d.ts +10 -0
  36. package/dist/components/KanbanBoard.svelte +212 -0
  37. package/dist/components/KanbanBoard.svelte.d.ts +30 -0
  38. package/dist/components/MasterDetailView.svelte +134 -0
  39. package/dist/components/MasterDetailView.svelte.d.ts +16 -0
  40. package/dist/components/MediaLibraryModal.svelte +195 -0
  41. package/dist/components/MediaLibraryModal.svelte.d.ts +22 -0
  42. package/dist/components/MentionsInput.svelte +174 -0
  43. package/dist/components/MentionsInput.svelte.d.ts +21 -0
  44. package/dist/components/ModalForm.svelte +113 -28
  45. package/dist/components/ModalForm.svelte.d.ts +20 -8
  46. package/dist/components/MultiTabKeepAlive.svelte +143 -0
  47. package/dist/components/MultiTabKeepAlive.svelte.d.ts +21 -0
  48. package/dist/components/OfflineSyncBanner.svelte +172 -0
  49. package/dist/components/OfflineSyncBanner.svelte.d.ts +19 -0
  50. package/dist/components/PdfDocumentViewer.svelte +255 -0
  51. package/dist/components/PdfDocumentViewer.svelte.d.ts +22 -0
  52. package/dist/components/PivotTable.svelte +140 -0
  53. package/dist/components/PivotTable.svelte.d.ts +16 -0
  54. package/dist/components/PresenceAvatarGroup.svelte +89 -0
  55. package/dist/components/PresenceAvatarGroup.svelte.d.ts +15 -0
  56. package/dist/components/PrintableBill.svelte +149 -0
  57. package/dist/components/PrintableBill.svelte.d.ts +24 -0
  58. package/dist/components/SensitiveDataMask.svelte +125 -0
  59. package/dist/components/SensitiveDataMask.svelte.d.ts +13 -0
  60. package/dist/components/SignaturePad.svelte +264 -0
  61. package/dist/components/SignaturePad.svelte.d.ts +17 -0
  62. package/dist/components/SplitPaneLayout.svelte +113 -0
  63. package/dist/components/SplitPaneLayout.svelte.d.ts +13 -0
  64. package/dist/components/SpreadsheetView.svelte +363 -0
  65. package/dist/components/SpreadsheetView.svelte.d.ts +18 -0
  66. package/dist/components/StepForm.svelte +180 -0
  67. package/dist/components/StepForm.svelte.d.ts +23 -0
  68. package/dist/components/TableSummary.svelte +110 -0
  69. package/dist/components/TableSummary.svelte.d.ts +20 -0
  70. package/dist/components/Transfer.svelte +352 -0
  71. package/dist/components/Transfer.svelte.d.ts +28 -0
  72. package/dist/components/TreeSelect.svelte +307 -0
  73. package/dist/components/TreeSelect.svelte.d.ts +22 -0
  74. package/dist/components/TreeTable.svelte +237 -0
  75. package/dist/components/TreeTable.svelte.d.ts +28 -0
  76. package/dist/components/UnsavedChangesPrompt.svelte +44 -0
  77. package/dist/components/UnsavedChangesPrompt.svelte.d.ts +7 -0
  78. package/dist/components/VersionDiffViewer.svelte +196 -0
  79. package/dist/components/VersionDiffViewer.svelte.d.ts +12 -0
  80. package/dist/components/VirtualTable.svelte +86 -54
  81. package/dist/components/VirtualTable.svelte.d.ts +14 -21
  82. package/dist/components/Watermark.svelte +166 -0
  83. package/dist/components/Watermark.svelte.d.ts +26 -0
  84. package/dist/components/ui/navigation-menu/index.d.ts +11 -0
  85. package/dist/components/ui/navigation-menu/index.js +13 -0
  86. package/dist/components/ui/navigation-menu/navigation-menu-content.svelte +21 -0
  87. package/dist/components/ui/navigation-menu/navigation-menu-content.svelte.d.ts +4 -0
  88. package/dist/components/ui/navigation-menu/navigation-menu-indicator.svelte +22 -0
  89. package/dist/components/ui/navigation-menu/navigation-menu-indicator.svelte.d.ts +4 -0
  90. package/dist/components/ui/navigation-menu/navigation-menu-item.svelte +17 -0
  91. package/dist/components/ui/navigation-menu/navigation-menu-item.svelte.d.ts +4 -0
  92. package/dist/components/ui/navigation-menu/navigation-menu-link.svelte +20 -0
  93. package/dist/components/ui/navigation-menu/navigation-menu-link.svelte.d.ts +4 -0
  94. package/dist/components/ui/navigation-menu/navigation-menu-list.svelte +17 -0
  95. package/dist/components/ui/navigation-menu/navigation-menu-list.svelte.d.ts +4 -0
  96. package/dist/components/ui/navigation-menu/navigation-menu-trigger.svelte +28 -0
  97. package/dist/components/ui/navigation-menu/navigation-menu-trigger.svelte.d.ts +4 -0
  98. package/dist/components/ui/navigation-menu/navigation-menu-viewport.svelte +22 -0
  99. package/dist/components/ui/navigation-menu/navigation-menu-viewport.svelte.d.ts +4 -0
  100. package/dist/components/ui/navigation-menu/navigation-menu.svelte +24 -0
  101. package/dist/components/ui/navigation-menu/navigation-menu.svelte.d.ts +4 -0
  102. package/dist/index.d.ts +68 -3
  103. package/dist/index.js +42 -3
  104. package/package.json +2 -2
@@ -0,0 +1,178 @@
1
+ <script lang="ts">
2
+ import { untrack } from 'svelte';
3
+ import type { FieldDefinition, CrudOperator } from '@svadmin/core';
4
+ import { useTranslation } from '@svadmin/core/i18n';
5
+ import * as Popover from './ui/popover/index.js';
6
+ import { Button } from './ui/button/index.js';
7
+ import { Filter, Check, RotateCcw } from '@lucide/svelte';
8
+ import { cn } from '../utils.js';
9
+
10
+ interface Props {
11
+ field: FieldDefinition;
12
+ currentValue?: unknown;
13
+ currentOperator?: CrudOperator;
14
+ onapply?: (operator: CrudOperator, value: unknown) => void;
15
+ onclear?: () => void;
16
+ class?: string;
17
+ }
18
+
19
+ let {
20
+ field,
21
+ currentValue,
22
+ currentOperator = 'contains',
23
+ onapply,
24
+ onclear,
25
+ class: className = '',
26
+ }: Props = $props();
27
+
28
+ const i18n = useTranslation();
29
+ let open = $state(false);
30
+ let operator = $state<CrudOperator>(untrack(() => currentOperator));
31
+ let filterVal = $state<string>(untrack(() => String(currentValue ?? '')));
32
+
33
+ const hasActiveFilter = $derived(currentValue !== undefined && String(currentValue).trim().length > 0);
34
+
35
+ const applicableOperators = $derived.by(() => {
36
+ switch (field.type) {
37
+ case 'number':
38
+ case 'currency':
39
+ return [
40
+ { value: 'eq' as const, label: '= Equal' },
41
+ { value: 'gt' as const, label: '> Greater than' },
42
+ { value: 'gte' as const, label: '>= Greater equal' },
43
+ { value: 'lt' as const, label: '< Less than' },
44
+ { value: 'lte' as const, label: '<= Less equal' },
45
+ ];
46
+ case 'date':
47
+ return [
48
+ { value: 'eq' as const, label: '= On date' },
49
+ { value: 'gte' as const, label: '>= After or on' },
50
+ { value: 'lte' as const, label: '<= Before or on' },
51
+ ];
52
+ case 'boolean':
53
+ return [
54
+ { value: 'eq' as const, label: 'Is' },
55
+ ];
56
+ default:
57
+ return [
58
+ { value: 'contains' as const, label: 'Contains' },
59
+ { value: 'eq' as const, label: 'Equals' },
60
+ { value: 'ne' as const, label: 'Not equals' },
61
+ ];
62
+ }
63
+ });
64
+
65
+ function handleApply() {
66
+ let finalVal: unknown = filterVal.trim();
67
+ if (field.type === 'number' || field.type === 'currency') {
68
+ const num = Number(finalVal);
69
+ finalVal = isNaN(num) ? finalVal : num;
70
+ }
71
+ onapply?.(operator, finalVal);
72
+ open = false;
73
+ }
74
+
75
+ function handleClear() {
76
+ filterVal = '';
77
+ onclear?.();
78
+ open = false;
79
+ }
80
+ </script>
81
+
82
+ <Popover.Root bind:open>
83
+ <Popover.Trigger>
84
+ {#snippet child({ props })}
85
+ <button
86
+ {...props}
87
+ type="button"
88
+ class={cn(
89
+ 'inline-flex items-center justify-center h-6 w-6 rounded-md transition-colors cursor-pointer',
90
+ hasActiveFilter
91
+ ? 'bg-primary/15 text-primary'
92
+ : 'text-muted-foreground/60 hover:text-foreground hover:bg-muted/50',
93
+ className
94
+ )}
95
+ aria-label="Filter {field.label}"
96
+ >
97
+ <Filter class="h-3 w-3" />
98
+ </button>
99
+ {/snippet}
100
+ </Popover.Trigger>
101
+ <Popover.Content class="w-64 p-3 text-xs" align="start">
102
+ <div class="space-y-3">
103
+ <div class="font-medium text-foreground pb-1 border-b border-border/50">
104
+ Filter {field.label}
105
+ </div>
106
+
107
+ <div class="space-y-1">
108
+ <label class="block text-[11px] text-muted-foreground" for="header_filter_op_{field.key}">Operator</label>
109
+ <select
110
+ id="header_filter_op_{field.key}"
111
+ bind:value={operator}
112
+ class="h-7.5 w-full rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
113
+ >
114
+ {#each applicableOperators as op (op.value)}
115
+ <option value={op.value}>{op.label}</option>
116
+ {/each}
117
+ </select>
118
+ </div>
119
+
120
+ <div class="space-y-1">
121
+ <label class="block text-[11px] text-muted-foreground" for="header_filter_val_{field.key}">Value</label>
122
+ {#if field.type === 'select' && field.options}
123
+ <select
124
+ id="header_filter_val_{field.key}"
125
+ bind:value={filterVal}
126
+ class="h-7.5 w-full rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
127
+ >
128
+ <option value="">All</option>
129
+ {#each field.options as opt (opt.value)}
130
+ <option value={String(opt.value)}>{opt.label}</option>
131
+ {/each}
132
+ </select>
133
+ {:else if field.type === 'boolean'}
134
+ <select
135
+ id="header_filter_val_{field.key}"
136
+ bind:value={filterVal}
137
+ class="h-7.5 w-full rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
138
+ >
139
+ <option value="">All</option>
140
+ <option value="true">Yes / True</option>
141
+ <option value="false">No / False</option>
142
+ </select>
143
+ {:else if field.type === 'date'}
144
+ <input
145
+ id="header_filter_val_{field.key}"
146
+ type="date"
147
+ bind:value={filterVal}
148
+ class="h-7.5 w-full rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
149
+ />
150
+ {:else}
151
+ <input
152
+ id="header_filter_val_{field.key}"
153
+ type="text"
154
+ bind:value={filterVal}
155
+ placeholder="Search {field.label}..."
156
+ class="h-7.5 w-full rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
157
+ />
158
+ {/if}
159
+ </div>
160
+
161
+ <div class="flex items-center justify-between pt-1 border-t border-border/50">
162
+ <Button
163
+ variant="ghost"
164
+ size="sm"
165
+ class="h-7 px-2 text-xs text-muted-foreground hover:text-foreground gap-1"
166
+ onclick={handleClear}
167
+ >
168
+ <RotateCcw class="h-3 w-3" />
169
+ {i18n.t('common.clear', { defaultValue: 'Clear' })}
170
+ </Button>
171
+ <Button size="sm" class="h-7 px-2.5 text-xs gap-1" onclick={handleApply}>
172
+ <Check class="h-3 w-3" />
173
+ {i18n.t('common.apply', { defaultValue: 'Apply' })}
174
+ </Button>
175
+ </div>
176
+ </div>
177
+ </Popover.Content>
178
+ </Popover.Root>
@@ -0,0 +1,12 @@
1
+ import type { FieldDefinition, CrudOperator } from '@svadmin/core';
2
+ interface Props {
3
+ field: FieldDefinition;
4
+ currentValue?: unknown;
5
+ currentOperator?: CrudOperator;
6
+ onapply?: (operator: CrudOperator, value: unknown) => void;
7
+ onclear?: () => void;
8
+ class?: string;
9
+ }
10
+ declare const ColumnHeaderFilter: import("svelte").Component<Props, {}, "">;
11
+ type ColumnHeaderFilter = ReturnType<typeof ColumnHeaderFilter>;
12
+ export default ColumnHeaderFilter;
@@ -0,0 +1,198 @@
1
+ <script lang="ts">
2
+ import { useTranslation } from '@svadmin/core/i18n';
3
+ import { Button } from './ui/button/index.js';
4
+ import { Checkbox } from './ui/checkbox/index.js';
5
+ import * as Popover from './ui/popover/index.js';
6
+ import { SlidersHorizontal, RotateCcw, ArrowUp, ArrowDown, Search } from '@lucide/svelte';
7
+ import { cn } from '../utils.js';
8
+
9
+ export interface ColumnItem {
10
+ key: string;
11
+ label: string;
12
+ visible: boolean;
13
+ fixed?: 'left' | 'right';
14
+ }
15
+
16
+ interface Props {
17
+ columns: ColumnItem[];
18
+ onchange?: (columns: ColumnItem[]) => void;
19
+ onreset?: () => void;
20
+ storageKey?: string;
21
+ title?: string;
22
+ triggerVariant?: 'outline' | 'ghost' | 'secondary';
23
+ triggerSize?: 'default' | 'sm' | 'icon' | 'icon-sm';
24
+ class?: string;
25
+ }
26
+
27
+ let {
28
+ columns = $bindable([]),
29
+ onchange,
30
+ onreset,
31
+ storageKey,
32
+ title,
33
+ triggerVariant = 'outline',
34
+ triggerSize = 'sm',
35
+ class: className = '',
36
+ }: Props = $props();
37
+
38
+ const i18n = useTranslation();
39
+ let filterText = $state('');
40
+ let open = $state(false);
41
+
42
+ // Snapshot initial configuration for reset
43
+ let initialSnapshot = $state<ColumnItem[]>(JSON.parse(JSON.stringify(columns)));
44
+
45
+ const displayTitle = $derived(title ?? i18n.t('common.columnSettings', { defaultValue: 'Columns' }));
46
+ const resetLabel = $derived(i18n.t('common.reset', { defaultValue: 'Reset' }));
47
+
48
+ const filteredColumns = $derived(
49
+ filterText.trim()
50
+ ? columns.filter((c) => c.label.toLowerCase().includes(filterText.toLowerCase()) || c.key.toLowerCase().includes(filterText.toLowerCase()))
51
+ : columns
52
+ );
53
+
54
+ const allVisible = $derived(columns.every((c) => c.visible));
55
+ const someVisible = $derived(columns.some((c) => c.visible) && !allVisible);
56
+
57
+ function savePreference(cols: ColumnItem[]) {
58
+ if (storageKey && typeof window !== 'undefined') {
59
+ try {
60
+ localStorage.setItem(storageKey, JSON.stringify(cols));
61
+ } catch {
62
+ /* storage unavailable */
63
+ }
64
+ }
65
+ }
66
+
67
+ function toggleColumn(key: string) {
68
+ columns = columns.map((c) => (c.key === key ? { ...c, visible: !c.visible } : c));
69
+ savePreference(columns);
70
+ onchange?.(columns);
71
+ }
72
+
73
+ function toggleAll(checked: boolean) {
74
+ columns = columns.map((c) => ({ ...c, visible: checked }));
75
+ savePreference(columns);
76
+ onchange?.(columns);
77
+ }
78
+
79
+ function moveColumn(index: number, direction: 'up' | 'down') {
80
+ const targetIndex = direction === 'up' ? index - 1 : index + 1;
81
+ if (targetIndex < 0 || targetIndex >= columns.length) return;
82
+
83
+ const next = [...columns];
84
+ const [moved] = next.splice(index, 1);
85
+ next.splice(targetIndex, 0, moved);
86
+
87
+ columns = next;
88
+ savePreference(columns);
89
+ onchange?.(columns);
90
+ }
91
+
92
+ function handleReset() {
93
+ columns = JSON.parse(JSON.stringify(initialSnapshot));
94
+ if (storageKey && typeof window !== 'undefined') {
95
+ try {
96
+ localStorage.removeItem(storageKey);
97
+ } catch {
98
+ /* storage unavailable */
99
+ }
100
+ }
101
+ onreset?.();
102
+ onchange?.(columns);
103
+ }
104
+ </script>
105
+
106
+ <Popover.Root bind:open>
107
+ <Popover.Trigger>
108
+ {#snippet child({ props })}
109
+ <Button
110
+ {...props}
111
+ variant={triggerVariant}
112
+ size={triggerSize}
113
+ class={cn('gap-1.5 text-xs font-normal', className)}
114
+ aria-label={displayTitle}
115
+ >
116
+ <SlidersHorizontal class="h-3.5 w-3.5 text-muted-foreground" data-icon="inline-start" />
117
+ <span>{displayTitle}</span>
118
+ </Button>
119
+ {/snippet}
120
+ </Popover.Trigger>
121
+ <Popover.Content class="w-72 p-3 text-xs" align="end">
122
+ <!-- Header -->
123
+ <div class="flex items-center justify-between pb-2 border-b border-border/60">
124
+ <div class="flex items-center gap-2">
125
+ <Checkbox
126
+ checked={allVisible}
127
+ indeterminate={someVisible}
128
+ onCheckedChange={(checked) => toggleAll(checked === true)}
129
+ aria-label={i18n.t('common.selectAll', { defaultValue: 'Select All' })}
130
+ />
131
+ <span class="font-medium text-foreground">{displayTitle}</span>
132
+ </div>
133
+ <Button
134
+ variant="ghost"
135
+ size="sm"
136
+ class="h-6 px-1.5 text-[11px] text-muted-foreground hover:text-foreground gap-1"
137
+ onclick={handleReset}
138
+ >
139
+ <RotateCcw class="h-3 w-3" />
140
+ {resetLabel}
141
+ </Button>
142
+ </div>
143
+
144
+ <!-- Filter input if many columns -->
145
+ {#if columns.length > 6}
146
+ <div class="relative mt-2 mb-1">
147
+ <Search class="absolute left-2 top-2 h-3.5 w-3.5 text-muted-foreground" />
148
+ <input
149
+ type="text"
150
+ bind:value={filterText}
151
+ placeholder={i18n.t('common.search', { defaultValue: 'Search columns...' })}
152
+ class="h-7.5 w-full rounded-md border border-input bg-muted/40 pl-7 pr-2 text-xs placeholder:text-muted-foreground/60 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
153
+ />
154
+ </div>
155
+ {/if}
156
+
157
+ <!-- Column list with ordering -->
158
+ <div class="mt-2 max-h-60 overflow-y-auto space-y-1 pr-1">
159
+ {#each filteredColumns as col (col.key)}
160
+ {@const realIdx = columns.findIndex((c) => c.key === col.key)}
161
+ <div class="flex items-center justify-between p-1 rounded-md hover:bg-muted/50 group transition-colors">
162
+ <label class="flex items-center gap-2 cursor-pointer select-none truncate flex-1 pr-2">
163
+ <Checkbox
164
+ checked={col.visible}
165
+ onCheckedChange={() => toggleColumn(col.key)}
166
+ />
167
+ <span class="truncate text-foreground {col.visible ? '' : 'text-muted-foreground line-through opacity-60'}">
168
+ {col.label}
169
+ </span>
170
+ </label>
171
+
172
+ <div class="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
173
+ <Button
174
+ variant="ghost"
175
+ size="icon"
176
+ class="h-5 w-5 text-muted-foreground hover:text-foreground"
177
+ disabled={realIdx === 0}
178
+ onclick={() => moveColumn(realIdx, 'up')}
179
+ aria-label="Move up"
180
+ >
181
+ <ArrowUp class="h-3 w-3" />
182
+ </Button>
183
+ <Button
184
+ variant="ghost"
185
+ size="icon"
186
+ class="h-5 w-5 text-muted-foreground hover:text-foreground"
187
+ disabled={realIdx === columns.length - 1}
188
+ onclick={() => moveColumn(realIdx, 'down')}
189
+ aria-label="Move down"
190
+ >
191
+ <ArrowDown class="h-3 w-3" />
192
+ </Button>
193
+ </div>
194
+ </div>
195
+ {/each}
196
+ </div>
197
+ </Popover.Content>
198
+ </Popover.Root>
@@ -0,0 +1,19 @@
1
+ export interface ColumnItem {
2
+ key: string;
3
+ label: string;
4
+ visible: boolean;
5
+ fixed?: 'left' | 'right';
6
+ }
7
+ interface Props {
8
+ columns: ColumnItem[];
9
+ onchange?: (columns: ColumnItem[]) => void;
10
+ onreset?: () => void;
11
+ storageKey?: string;
12
+ title?: string;
13
+ triggerVariant?: 'outline' | 'ghost' | 'secondary';
14
+ triggerSize?: 'default' | 'sm' | 'icon' | 'icon-sm';
15
+ class?: string;
16
+ }
17
+ declare const ColumnSettings: import("svelte").Component<Props, {}, "columns">;
18
+ type ColumnSettings = ReturnType<typeof ColumnSettings>;
19
+ export default ColumnSettings;
@@ -0,0 +1,240 @@
1
+ <script lang="ts">
2
+ import { Button } from './ui/button/index.js';
3
+ import { Badge } from './ui/badge/index.js';
4
+ import { Plus, Trash2, Play, CheckCircle2 } from '@lucide/svelte';
5
+ import { cn } from '../utils.js';
6
+
7
+ export interface DecisionColumn {
8
+ key: string;
9
+ label: string;
10
+ type: 'condition' | 'action';
11
+ dataType?: 'string' | 'number' | 'boolean';
12
+ }
13
+
14
+ export interface DecisionRule {
15
+ id: string;
16
+ description?: string;
17
+ values: Record<string, string>; // e.g. { age: "> 18", score: ">= 80", result: "Approved" }
18
+ }
19
+
20
+ interface Props {
21
+ title?: string;
22
+ columns?: DecisionColumn[];
23
+ rules?: DecisionRule[];
24
+ hitPolicy?: 'FIRST' | 'COLLECT' | 'UNIQUE';
25
+ onchange?: (rules: DecisionRule[]) => void;
26
+ class?: string;
27
+ }
28
+
29
+ let {
30
+ title = 'Business Decision Table',
31
+ columns = [
32
+ { key: 'tier', label: 'Customer Tier', type: 'condition' },
33
+ { key: 'amount', label: 'Order Amount', type: 'condition' },
34
+ { key: 'discount', label: 'Discount %', type: 'action' },
35
+ { key: 'approval', label: 'Require Approval', type: 'action' },
36
+ ],
37
+ rules = $bindable([
38
+ { id: 'r1', description: 'VIP high volume', values: { tier: 'VIP', amount: '>= 10000', discount: '20%', approval: 'No' } },
39
+ { id: 'r2', description: 'VIP standard', values: { tier: 'VIP', amount: '< 10000', discount: '15%', approval: 'No' } },
40
+ { id: 'r3', description: 'Standard high volume', values: { tier: 'Standard', amount: '>= 10000', discount: '10%', approval: 'Yes' } },
41
+ { id: 'r4', description: 'Default fallback', values: { tier: '-', amount: '-', discount: '0%', approval: 'No' } },
42
+ ]),
43
+ hitPolicy = 'FIRST',
44
+ onchange,
45
+ class: className = '',
46
+ }: Props = $props();
47
+
48
+ let testInputs = $state<Record<string, string>>({
49
+ tier: 'VIP',
50
+ amount: '12000',
51
+ });
52
+
53
+ let matchedRuleId = $state<string | null>(null);
54
+
55
+ const conditionCols = $derived(columns.filter((c) => c.type === 'condition'));
56
+ const actionCols = $derived(columns.filter((c) => c.type === 'action'));
57
+
58
+ function updateRuleValue(ruleId: string, colKey: string, val: string) {
59
+ const nextRules = rules.map((r) =>
60
+ r.id === ruleId ? { ...r, values: { ...r.values, [colKey]: val } } : r
61
+ );
62
+ rules = nextRules;
63
+ onchange?.(rules);
64
+ }
65
+
66
+ function addRule() {
67
+ const newRule: DecisionRule = {
68
+ id: `rule_${Date.now()}`,
69
+ description: 'New Rule',
70
+ values: {},
71
+ };
72
+ for (const c of columns) {
73
+ newRule.values[c.key] = '-';
74
+ }
75
+ rules = [...rules, newRule];
76
+ onchange?.(rules);
77
+ }
78
+
79
+ function deleteRule(id: string) {
80
+ rules = rules.filter((r) => r.id !== id);
81
+ onchange?.(rules);
82
+ }
83
+
84
+ function evaluateRules() {
85
+ for (const r of rules) {
86
+ let isMatch = true;
87
+ for (const cond of conditionCols) {
88
+ const ruleVal = (r.values[cond.key] ?? '').trim();
89
+ const testVal = (testInputs[cond.key] ?? '').trim();
90
+
91
+ if (ruleVal === '-' || ruleVal === '') continue;
92
+
93
+ if (ruleVal.startsWith('>=')) {
94
+ const numR = parseFloat(ruleVal.slice(2));
95
+ const numT = parseFloat(testVal);
96
+ if (isNaN(numT) || numT < numR) isMatch = false;
97
+ } else if (ruleVal.startsWith('<=')) {
98
+ const numR = parseFloat(ruleVal.slice(2));
99
+ const numT = parseFloat(testVal);
100
+ if (isNaN(numT) || numT > numR) isMatch = false;
101
+ } else if (ruleVal.startsWith('>')) {
102
+ const numR = parseFloat(ruleVal.slice(1));
103
+ const numT = parseFloat(testVal);
104
+ if (isNaN(numT) || numT <= numR) isMatch = false;
105
+ } else if (ruleVal.startsWith('<')) {
106
+ const numR = parseFloat(ruleVal.slice(1));
107
+ const numT = parseFloat(testVal);
108
+ if (isNaN(numT) || numT >= numR) isMatch = false;
109
+ } else {
110
+ if (ruleVal.toLowerCase() !== testVal.toLowerCase()) {
111
+ isMatch = false;
112
+ }
113
+ }
114
+ }
115
+
116
+ if (isMatch) {
117
+ matchedRuleId = r.id;
118
+ return;
119
+ }
120
+ }
121
+ matchedRuleId = null;
122
+ }
123
+ </script>
124
+
125
+ <div class={cn('rounded-xl border border-border bg-card p-4 shadow-xs text-xs space-y-4', className)}>
126
+ <!-- Header -->
127
+ <div class="flex flex-wrap items-center justify-between gap-2 pb-2 border-b border-border/60">
128
+ <div class="flex items-center gap-2">
129
+ <span class="font-semibold text-foreground">{title}</span>
130
+ <Badge variant="outline" class="font-mono text-[10px] uppercase">Hit Policy: {hitPolicy}</Badge>
131
+ </div>
132
+
133
+ <Button size="sm" class="h-7 text-xs gap-1" onclick={addRule}>
134
+ <Plus class="h-3.5 w-3.5" />
135
+ Add Rule
136
+ </Button>
137
+ </div>
138
+
139
+ <!-- Decision Matrix Table -->
140
+ <div class="overflow-x-auto rounded-lg border border-border/60">
141
+ <table class="w-full border-collapse text-left">
142
+ <thead>
143
+ <!-- Top Category Header -->
144
+ <tr class="text-[11px] font-semibold">
145
+ <th rowspan="2" class="w-12 p-2 text-center border-r border-b border-border/60 bg-muted/80 text-muted-foreground">#</th>
146
+ <th colspan={conditionCols.length} class="p-2 border-r border-b border-border/60 bg-primary/10 text-primary text-center font-bold">
147
+ IF (Conditions)
148
+ </th>
149
+ <th colspan={actionCols.length} class="p-2 border-r border-b border-border/60 bg-success/10 text-success text-center font-bold">
150
+ THEN (Actions)
151
+ </th>
152
+ <th rowspan="2" class="w-10 p-2 text-center border-b border-border/60 bg-muted/80 text-muted-foreground">Del</th>
153
+ </tr>
154
+ <!-- Column Names Header -->
155
+ <tr class="bg-muted/40 text-muted-foreground border-b border-border/60 font-medium">
156
+ {#each conditionCols as col (col.key)}
157
+ <th class="p-2 border-r border-border/40 text-foreground">{col.label}</th>
158
+ {/each}
159
+ {#each actionCols as col (col.key)}
160
+ <th class="p-2 border-r border-border/40 text-foreground">{col.label}</th>
161
+ {/each}
162
+ </tr>
163
+ </thead>
164
+ <tbody class="divide-y divide-border/40 font-mono text-xs">
165
+ {#each rules as rule, idx (rule.id)}
166
+ {@const isMatched = matchedRuleId === rule.id}
167
+ <tr class={cn('hover:bg-muted/20 transition-colors', isMatched ? 'bg-success/15 font-semibold' : '')}>
168
+ <td class="p-2 text-center border-r border-border/40 text-muted-foreground font-sans">
169
+ {#if isMatched}
170
+ <span class="inline-flex text-success"><CheckCircle2 class="h-3.5 w-3.5" /></span>
171
+ {:else}
172
+ {idx + 1}
173
+ {/if}
174
+ </td>
175
+ {#each conditionCols as col (col.key)}
176
+ <td class="p-1.5 border-r border-border/40">
177
+ <input
178
+ type="text"
179
+ value={rule.values[col.key] ?? ''}
180
+ class="h-7 w-full rounded border border-input bg-background px-2 text-xs text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
181
+ oninput={(e) => updateRuleValue(rule.id, col.key, e.currentTarget.value)}
182
+ />
183
+ </td>
184
+ {/each}
185
+ {#each actionCols as col (col.key)}
186
+ <td class="p-1.5 border-r border-border/40">
187
+ <input
188
+ type="text"
189
+ value={rule.values[col.key] ?? ''}
190
+ class="h-7 w-full rounded border border-input bg-background px-2 text-xs font-semibold text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
191
+ oninput={(e) => updateRuleValue(rule.id, col.key, e.currentTarget.value)}
192
+ />
193
+ </td>
194
+ {/each}
195
+ <td class="p-1.5 text-center">
196
+ <button
197
+ type="button"
198
+ class="text-muted-foreground hover:text-destructive transition-colors p-1 rounded border-0 bg-transparent cursor-pointer"
199
+ onclick={() => deleteRule(rule.id)}
200
+ >
201
+ <Trash2 class="h-3.5 w-3.5" />
202
+ </button>
203
+ </td>
204
+ </tr>
205
+ {/each}
206
+ </tbody>
207
+ </table>
208
+ </div>
209
+
210
+ <!-- Live Rule Test Evaluator -->
211
+ <div class="p-3 rounded-lg bg-muted/30 border border-border/60 space-y-2">
212
+ <div class="flex items-center justify-between">
213
+ <span class="font-semibold text-foreground">Rule Test Runner</span>
214
+ <Button variant="outline" size="sm" class="h-6 text-xs gap-1" onclick={evaluateRules}>
215
+ <Play class="h-3 w-3" />
216
+ Test Execution
217
+ </Button>
218
+ </div>
219
+
220
+ <div class="flex flex-wrap items-center gap-3">
221
+ {#each conditionCols as cond (cond.key)}
222
+ <div class="flex items-center gap-1.5">
223
+ <span class="text-muted-foreground font-medium">{cond.label}:</span>
224
+ <input
225
+ type="text"
226
+ bind:value={testInputs[cond.key]}
227
+ class="h-6 w-24 rounded border border-input bg-background px-1.5 text-xs text-foreground focus-visible:outline-none"
228
+ />
229
+ </div>
230
+ {/each}
231
+
232
+ {#if matchedRuleId}
233
+ <div class="flex items-center gap-1 text-success font-medium">
234
+ <CheckCircle2 class="h-3.5 w-3.5" />
235
+ <span>Fired: {rules.find((r) => r.id === matchedRuleId)?.description ?? matchedRuleId}</span>
236
+ </div>
237
+ {/if}
238
+ </div>
239
+ </div>
240
+ </div>
@@ -0,0 +1,22 @@
1
+ export interface DecisionColumn {
2
+ key: string;
3
+ label: string;
4
+ type: 'condition' | 'action';
5
+ dataType?: 'string' | 'number' | 'boolean';
6
+ }
7
+ export interface DecisionRule {
8
+ id: string;
9
+ description?: string;
10
+ values: Record<string, string>;
11
+ }
12
+ interface Props {
13
+ title?: string;
14
+ columns?: DecisionColumn[];
15
+ rules?: DecisionRule[];
16
+ hitPolicy?: 'FIRST' | 'COLLECT' | 'UNIQUE';
17
+ onchange?: (rules: DecisionRule[]) => void;
18
+ class?: string;
19
+ }
20
+ declare const DecisionTable: import("svelte").Component<Props, {}, "rules">;
21
+ type DecisionTable = ReturnType<typeof DecisionTable>;
22
+ export default DecisionTable;