@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.
- package/dist/components/ActivityFeed.svelte +135 -0
- package/dist/components/ActivityFeed.svelte.d.ts +21 -0
- package/dist/components/ApprovalActionCard.svelte +248 -0
- package/dist/components/ApprovalActionCard.svelte.d.ts +22 -0
- package/dist/components/AutoTable.svelte +8 -0
- package/dist/components/AutoTable.svelte.d.ts +7 -1
- package/dist/components/CanvasAnnotation.svelte +389 -0
- package/dist/components/CanvasAnnotation.svelte.d.ts +17 -0
- package/dist/components/Cascader.svelte +205 -0
- package/dist/components/Cascader.svelte.d.ts +21 -0
- package/dist/components/ColumnHeaderFilter.svelte +178 -0
- package/dist/components/ColumnHeaderFilter.svelte.d.ts +12 -0
- package/dist/components/ColumnSettings.svelte +198 -0
- package/dist/components/ColumnSettings.svelte.d.ts +19 -0
- package/dist/components/DecisionTable.svelte +240 -0
- package/dist/components/DecisionTable.svelte.d.ts +22 -0
- package/dist/components/DraggableRowTable.svelte +117 -0
- package/dist/components/DraggableRowTable.svelte.d.ts +15 -0
- package/dist/components/DrawerForm.svelte +110 -26
- package/dist/components/DrawerForm.svelte.d.ts +20 -8
- package/dist/components/DynamicFormList.svelte +214 -0
- package/dist/components/DynamicFormList.svelte.d.ts +62 -0
- package/dist/components/EditableTable.svelte +195 -0
- package/dist/components/EditableTable.svelte.d.ts +25 -0
- package/dist/components/FieldRenderer.svelte +32 -0
- package/dist/components/FilterBuilder.svelte +310 -0
- package/dist/components/FilterBuilder.svelte.d.ts +23 -0
- package/dist/components/GanttChart.svelte +144 -0
- package/dist/components/GanttChart.svelte.d.ts +20 -0
- package/dist/components/ImageCropper.svelte +122 -0
- package/dist/components/ImageCropper.svelte.d.ts +12 -0
- package/dist/components/ImportWizard.svelte +336 -0
- package/dist/components/ImportWizard.svelte.d.ts +12 -0
- package/dist/components/JsonSchemaForm.svelte +149 -0
- package/dist/components/JsonSchemaForm.svelte.d.ts +10 -0
- package/dist/components/KanbanBoard.svelte +212 -0
- package/dist/components/KanbanBoard.svelte.d.ts +30 -0
- package/dist/components/MasterDetailView.svelte +134 -0
- package/dist/components/MasterDetailView.svelte.d.ts +16 -0
- package/dist/components/MediaLibraryModal.svelte +195 -0
- package/dist/components/MediaLibraryModal.svelte.d.ts +22 -0
- package/dist/components/MentionsInput.svelte +174 -0
- package/dist/components/MentionsInput.svelte.d.ts +21 -0
- package/dist/components/ModalForm.svelte +113 -28
- package/dist/components/ModalForm.svelte.d.ts +20 -8
- package/dist/components/MultiTabKeepAlive.svelte +143 -0
- package/dist/components/MultiTabKeepAlive.svelte.d.ts +21 -0
- package/dist/components/OfflineSyncBanner.svelte +172 -0
- package/dist/components/OfflineSyncBanner.svelte.d.ts +19 -0
- package/dist/components/PdfDocumentViewer.svelte +255 -0
- package/dist/components/PdfDocumentViewer.svelte.d.ts +22 -0
- package/dist/components/PivotTable.svelte +140 -0
- package/dist/components/PivotTable.svelte.d.ts +16 -0
- package/dist/components/PresenceAvatarGroup.svelte +89 -0
- package/dist/components/PresenceAvatarGroup.svelte.d.ts +15 -0
- package/dist/components/PrintableBill.svelte +149 -0
- package/dist/components/PrintableBill.svelte.d.ts +24 -0
- package/dist/components/SensitiveDataMask.svelte +125 -0
- package/dist/components/SensitiveDataMask.svelte.d.ts +13 -0
- package/dist/components/SignaturePad.svelte +264 -0
- package/dist/components/SignaturePad.svelte.d.ts +17 -0
- package/dist/components/SplitPaneLayout.svelte +113 -0
- package/dist/components/SplitPaneLayout.svelte.d.ts +13 -0
- package/dist/components/SpreadsheetView.svelte +363 -0
- package/dist/components/SpreadsheetView.svelte.d.ts +18 -0
- package/dist/components/StepForm.svelte +180 -0
- package/dist/components/StepForm.svelte.d.ts +23 -0
- package/dist/components/TableSummary.svelte +110 -0
- package/dist/components/TableSummary.svelte.d.ts +20 -0
- package/dist/components/Transfer.svelte +352 -0
- package/dist/components/Transfer.svelte.d.ts +28 -0
- package/dist/components/TreeSelect.svelte +307 -0
- package/dist/components/TreeSelect.svelte.d.ts +22 -0
- package/dist/components/TreeTable.svelte +237 -0
- package/dist/components/TreeTable.svelte.d.ts +28 -0
- package/dist/components/UnsavedChangesPrompt.svelte +44 -0
- package/dist/components/UnsavedChangesPrompt.svelte.d.ts +7 -0
- package/dist/components/VersionDiffViewer.svelte +196 -0
- package/dist/components/VersionDiffViewer.svelte.d.ts +12 -0
- package/dist/components/VirtualTable.svelte +86 -54
- package/dist/components/VirtualTable.svelte.d.ts +14 -21
- package/dist/components/Watermark.svelte +166 -0
- package/dist/components/Watermark.svelte.d.ts +26 -0
- package/dist/components/ui/navigation-menu/index.d.ts +11 -0
- package/dist/components/ui/navigation-menu/index.js +13 -0
- package/dist/components/ui/navigation-menu/navigation-menu-content.svelte +21 -0
- package/dist/components/ui/navigation-menu/navigation-menu-content.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-indicator.svelte +22 -0
- package/dist/components/ui/navigation-menu/navigation-menu-indicator.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-item.svelte +17 -0
- package/dist/components/ui/navigation-menu/navigation-menu-item.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-link.svelte +20 -0
- package/dist/components/ui/navigation-menu/navigation-menu-link.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-list.svelte +17 -0
- package/dist/components/ui/navigation-menu/navigation-menu-list.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-trigger.svelte +28 -0
- package/dist/components/ui/navigation-menu/navigation-menu-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-viewport.svelte +22 -0
- package/dist/components/ui/navigation-menu/navigation-menu-viewport.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu.svelte +24 -0
- package/dist/components/ui/navigation-menu/navigation-menu.svelte.d.ts +4 -0
- package/dist/index.d.ts +68 -3
- package/dist/index.js +42 -3
- 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;
|