@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,389 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { Button } from './ui/button/index.js';
4
+ import {
5
+ Pencil,
6
+ Square,
7
+ Circle,
8
+ MoveUpRight,
9
+ Type,
10
+ Eraser,
11
+ RotateCcw,
12
+ Trash2,
13
+ Download,
14
+ } from '@lucide/svelte';
15
+ import { cn } from '../utils.js';
16
+
17
+ export type AnnotationTool = 'pen' | 'rect' | 'circle' | 'arrow' | 'text' | 'eraser';
18
+
19
+ interface Props {
20
+ imageUrl?: string;
21
+ width?: number;
22
+ height?: number;
23
+ activeTool?: AnnotationTool;
24
+ strokeWidth?: number;
25
+ onchange?: (dataUrl: string) => void;
26
+ onexport?: (dataUrl: string) => void;
27
+ class?: string;
28
+ }
29
+
30
+ let {
31
+ imageUrl = '',
32
+ width = 700,
33
+ height = 450,
34
+ activeTool = $bindable('pen'),
35
+ strokeWidth = 3,
36
+ onchange,
37
+ onexport,
38
+ class: className = '',
39
+ }: Props = $props();
40
+
41
+ let canvasEl = $state<HTMLCanvasElement | null>(null);
42
+ let isDrawing = $state(false);
43
+ let startX = 0;
44
+ let startY = 0;
45
+ let history = $state<ImageData[]>([]);
46
+ let bgImage: HTMLImageElement | null = null;
47
+ let textInputPrompt = $state('');
48
+ let showTextPrompt = $state(false);
49
+ let textPos = $state({ x: 0, y: 0 });
50
+
51
+ interface Point {
52
+ x: number;
53
+ y: number;
54
+ }
55
+
56
+ function getCanvasPos(e: MouseEvent | TouchEvent): Point | null {
57
+ if (!canvasEl) return null;
58
+ const rect = canvasEl.getBoundingClientRect();
59
+ const scaleX = canvasEl.width / rect.width;
60
+ const scaleY = canvasEl.height / rect.height;
61
+
62
+ if ('touches' in e) {
63
+ if (e.touches.length === 0) return null;
64
+ const touch = e.touches[0];
65
+ if (!touch) return null;
66
+ return {
67
+ x: (touch.clientX - rect.left) * scaleX,
68
+ y: (touch.clientY - rect.top) * scaleY,
69
+ };
70
+ } else {
71
+ return {
72
+ x: (e.clientX - rect.left) * scaleX,
73
+ y: (e.clientY - rect.top) * scaleY,
74
+ };
75
+ }
76
+ }
77
+
78
+ function saveHistory() {
79
+ if (!canvasEl) return;
80
+ const ctx = canvasEl.getContext('2d');
81
+ if (ctx) {
82
+ history = [...history, ctx.getImageData(0, 0, canvasEl.width, canvasEl.height)];
83
+ }
84
+ }
85
+
86
+ function drawArrow(ctx: CanvasRenderingContext2D, fromX: number, fromY: number, toX: number, toY: number) {
87
+ const headlen = 12;
88
+ const angle = Math.atan2(toY - fromY, toX - fromX);
89
+ ctx.beginPath();
90
+ ctx.moveTo(fromX, fromY);
91
+ ctx.lineTo(toX, toY);
92
+ ctx.lineTo(toX - headlen * Math.cos(angle - Math.PI / 6), toY - headlen * Math.sin(angle - Math.PI / 6));
93
+ ctx.moveTo(toX, toY);
94
+ ctx.lineTo(toX - headlen * Math.cos(angle + Math.PI / 6), toY - headlen * Math.sin(angle + Math.PI / 6));
95
+ ctx.stroke();
96
+ }
97
+
98
+ function startAction(e: MouseEvent | TouchEvent) {
99
+ if (!canvasEl) return;
100
+ const pos = getCanvasPos(e);
101
+ if (!pos) return;
102
+
103
+ if (activeTool === 'text') {
104
+ textPos = pos;
105
+ showTextPrompt = true;
106
+ return;
107
+ }
108
+
109
+ saveHistory();
110
+ isDrawing = true;
111
+ startX = pos.x;
112
+ startY = pos.y;
113
+
114
+ const ctx = canvasEl.getContext('2d');
115
+ if (!ctx) return;
116
+ ctx.lineWidth = strokeWidth;
117
+ ctx.lineCap = 'round';
118
+ ctx.lineJoin = 'round';
119
+
120
+ if (activeTool === 'eraser') {
121
+ ctx.globalCompositeOperation = 'destination-out';
122
+ } else {
123
+ ctx.globalCompositeOperation = 'source-over';
124
+ ctx.strokeStyle = getComputedStyle(canvasEl).color || 'rgb(225, 29, 72)';
125
+ }
126
+
127
+ if (activeTool === 'pen') {
128
+ ctx.beginPath();
129
+ ctx.moveTo(pos.x, pos.y);
130
+ }
131
+ }
132
+
133
+ function handleMove(e: MouseEvent | TouchEvent) {
134
+ if (!isDrawing || !canvasEl) return;
135
+ const pos = getCanvasPos(e);
136
+ if (!pos) return;
137
+
138
+ const ctx = canvasEl.getContext('2d');
139
+ if (!ctx) return;
140
+
141
+ if (activeTool === 'pen' || activeTool === 'eraser') {
142
+ ctx.lineTo(pos.x, pos.y);
143
+ ctx.stroke();
144
+ } else if (activeTool === 'rect' || activeTool === 'circle' || activeTool === 'arrow') {
145
+ const last = history[history.length - 1];
146
+ if (last) {
147
+ ctx.putImageData(last, 0, 0);
148
+ }
149
+ ctx.strokeStyle = getComputedStyle(canvasEl).color || 'rgb(225, 29, 72)';
150
+ ctx.lineWidth = strokeWidth;
151
+
152
+ if (activeTool === 'rect') {
153
+ ctx.strokeRect(startX, startY, pos.x - startX, pos.y - startY);
154
+ } else if (activeTool === 'circle') {
155
+ ctx.beginPath();
156
+ const rx = Math.abs(pos.x - startX) / 2;
157
+ const ry = Math.abs(pos.y - startY) / 2;
158
+ const cx = Math.min(startX, pos.x) + rx;
159
+ const cy = Math.min(startY, pos.y) + ry;
160
+ ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
161
+ ctx.stroke();
162
+ } else if (activeTool === 'arrow') {
163
+ drawArrow(ctx, startX, startY, pos.x, pos.y);
164
+ }
165
+ }
166
+ }
167
+
168
+ function stopAction() {
169
+ if (!isDrawing || !canvasEl) return;
170
+ isDrawing = false;
171
+ const ctx = canvasEl.getContext('2d');
172
+ if (ctx) {
173
+ ctx.globalCompositeOperation = 'source-over';
174
+ }
175
+ const dataUrl = canvasEl.toDataURL('image/png');
176
+ onchange?.(dataUrl);
177
+ }
178
+
179
+ function submitText() {
180
+ if (!canvasEl || !textInputPrompt.trim()) {
181
+ showTextPrompt = false;
182
+ textInputPrompt = '';
183
+ return;
184
+ }
185
+ saveHistory();
186
+ const ctx = canvasEl.getContext('2d');
187
+ if (ctx) {
188
+ ctx.fillStyle = getComputedStyle(canvasEl).color || 'rgb(225, 29, 72)';
189
+ ctx.font = 'bold 14px sans-serif';
190
+ ctx.fillText(textInputPrompt.trim(), textPos.x, textPos.y);
191
+ }
192
+ showTextPrompt = false;
193
+ textInputPrompt = '';
194
+ const dataUrl = canvasEl.toDataURL('image/png');
195
+ onchange?.(dataUrl);
196
+ }
197
+
198
+ export function undo() {
199
+ if (!canvasEl || history.length === 0) return;
200
+ const ctx = canvasEl.getContext('2d');
201
+ if (!ctx) return;
202
+ const previous = history[history.length - 1];
203
+ history = history.slice(0, -1);
204
+ if (previous) {
205
+ ctx.putImageData(previous, 0, 0);
206
+ }
207
+ const dataUrl = canvasEl.toDataURL('image/png');
208
+ onchange?.(dataUrl);
209
+ }
210
+
211
+ export function clear() {
212
+ if (!canvasEl) return;
213
+ saveHistory();
214
+ const ctx = canvasEl.getContext('2d');
215
+ if (!ctx) return;
216
+ ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
217
+ if (bgImage) {
218
+ ctx.drawImage(bgImage, 0, 0, canvasEl.width, canvasEl.height);
219
+ }
220
+ const dataUrl = canvasEl.toDataURL('image/png');
221
+ onchange?.(dataUrl);
222
+ }
223
+
224
+ function handleExport() {
225
+ if (!canvasEl) return;
226
+ const dataUrl = canvasEl.toDataURL('image/png');
227
+ onexport?.(dataUrl);
228
+
229
+ const a = document.createElement('a');
230
+ a.href = dataUrl;
231
+ a.download = `annotation_${Date.now()}.png`;
232
+ a.click();
233
+ }
234
+
235
+ function redrawBg() {
236
+ if (!canvasEl || !imageUrl) return;
237
+ bgImage = new Image();
238
+ bgImage.crossOrigin = 'anonymous';
239
+ bgImage.onload = () => {
240
+ if (!canvasEl) return;
241
+ const ctx = canvasEl.getContext('2d');
242
+ if (ctx) {
243
+ ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
244
+ if (bgImage) ctx.drawImage(bgImage, 0, 0, canvasEl.width, canvasEl.height);
245
+ }
246
+ };
247
+ bgImage.src = imageUrl;
248
+ }
249
+
250
+ onMount(() => {
251
+ redrawBg();
252
+ });
253
+ </script>
254
+
255
+ <div class={cn('rounded-xl border border-border bg-card p-4 shadow-xs text-xs space-y-3', className)}>
256
+ <!-- Header & Toolbar -->
257
+ <div class="flex flex-wrap items-center justify-between gap-2 pb-2 border-b border-border/60">
258
+ <div class="flex items-center gap-1 bg-muted/40 p-1 rounded-lg border border-border/60">
259
+ <Button
260
+ variant={activeTool === 'pen' ? 'default' : 'ghost'}
261
+ size="sm"
262
+ class="h-7 w-7 p-0"
263
+ onclick={() => { activeTool = 'pen'; }}
264
+ title="Pen"
265
+ >
266
+ <Pencil class="h-3.5 w-3.5" />
267
+ </Button>
268
+
269
+ <Button
270
+ variant={activeTool === 'rect' ? 'default' : 'ghost'}
271
+ size="sm"
272
+ class="h-7 w-7 p-0"
273
+ onclick={() => { activeTool = 'rect'; }}
274
+ title="Rectangle"
275
+ >
276
+ <Square class="h-3.5 w-3.5" />
277
+ </Button>
278
+
279
+ <Button
280
+ variant={activeTool === 'circle' ? 'default' : 'ghost'}
281
+ size="sm"
282
+ class="h-7 w-7 p-0"
283
+ onclick={() => { activeTool = 'circle'; }}
284
+ title="Circle"
285
+ >
286
+ <Circle class="h-3.5 w-3.5" />
287
+ </Button>
288
+
289
+ <Button
290
+ variant={activeTool === 'arrow' ? 'default' : 'ghost'}
291
+ size="sm"
292
+ class="h-7 w-7 p-0"
293
+ onclick={() => { activeTool = 'arrow'; }}
294
+ title="Arrow"
295
+ >
296
+ <MoveUpRight class="h-3.5 w-3.5" />
297
+ </Button>
298
+
299
+ <Button
300
+ variant={activeTool === 'text' ? 'default' : 'ghost'}
301
+ size="sm"
302
+ class="h-7 w-7 p-0"
303
+ onclick={() => { activeTool = 'text'; }}
304
+ title="Text Note"
305
+ >
306
+ <Type class="h-3.5 w-3.5" />
307
+ </Button>
308
+
309
+ <Button
310
+ variant={activeTool === 'eraser' ? 'default' : 'ghost'}
311
+ size="sm"
312
+ class="h-7 w-7 p-0"
313
+ onclick={() => { activeTool = 'eraser'; }}
314
+ title="Eraser"
315
+ >
316
+ <Eraser class="h-3.5 w-3.5" />
317
+ </Button>
318
+ </div>
319
+
320
+ <div class="flex items-center gap-1.5">
321
+ <Button
322
+ variant="ghost"
323
+ size="sm"
324
+ class="h-7 text-xs gap-1 text-muted-foreground hover:text-foreground"
325
+ disabled={history.length === 0}
326
+ onclick={undo}
327
+ >
328
+ <RotateCcw class="h-3 w-3" />
329
+ Undo
330
+ </Button>
331
+
332
+ <Button
333
+ variant="ghost"
334
+ size="sm"
335
+ class="h-7 text-xs gap-1 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
336
+ onclick={clear}
337
+ >
338
+ <Trash2 class="h-3 w-3" />
339
+ Clear
340
+ </Button>
341
+
342
+ <Button
343
+ variant="outline"
344
+ size="sm"
345
+ class="h-7 text-xs gap-1"
346
+ onclick={handleExport}
347
+ >
348
+ <Download class="h-3 w-3" />
349
+ Export
350
+ </Button>
351
+ </div>
352
+ </div>
353
+
354
+ <!-- Canvas Container -->
355
+ <div class="relative w-full rounded-lg border border-border/80 bg-muted/20 overflow-hidden flex items-center justify-center">
356
+ <canvas
357
+ bind:this={canvasEl}
358
+ {width}
359
+ {height}
360
+ class="touch-none cursor-crosshair max-w-full h-auto block bg-card"
361
+ onmousedown={startAction}
362
+ onmousemove={handleMove}
363
+ onmouseup={stopAction}
364
+ onmouseleave={stopAction}
365
+ ontouchstart={(e) => { e.preventDefault(); startAction(e); }}
366
+ ontouchmove={(e) => { e.preventDefault(); handleMove(e); }}
367
+ ontouchend={(e) => { e.preventDefault(); stopAction(); }}
368
+ ></canvas>
369
+
370
+ {#if showTextPrompt}
371
+ <div
372
+ class="absolute z-50 p-2 rounded-lg border border-border bg-popover shadow-md flex items-center gap-1.5"
373
+ style={`left: ${Math.min(textPos.x, width - 180)}px; top: ${Math.min(textPos.y, height - 50)}px;`}
374
+ >
375
+ <input
376
+ type="text"
377
+ bind:value={textInputPrompt}
378
+ placeholder="Annotation text..."
379
+ class="h-7 w-36 rounded border border-input bg-background px-2 text-xs text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
380
+ onkeydown={(e) => {
381
+ if (e.key === 'Enter') submitText();
382
+ else if (e.key === 'Escape') showTextPrompt = false;
383
+ }}
384
+ />
385
+ <Button size="sm" class="h-7 text-xs px-2" onclick={submitText}>Add</Button>
386
+ </div>
387
+ {/if}
388
+ </div>
389
+ </div>
@@ -0,0 +1,17 @@
1
+ export type AnnotationTool = 'pen' | 'rect' | 'circle' | 'arrow' | 'text' | 'eraser';
2
+ interface Props {
3
+ imageUrl?: string;
4
+ width?: number;
5
+ height?: number;
6
+ activeTool?: AnnotationTool;
7
+ strokeWidth?: number;
8
+ onchange?: (dataUrl: string) => void;
9
+ onexport?: (dataUrl: string) => void;
10
+ class?: string;
11
+ }
12
+ declare const CanvasAnnotation: import("svelte").Component<Props, {
13
+ undo: () => void;
14
+ clear: () => void;
15
+ }, "activeTool">;
16
+ type CanvasAnnotation = ReturnType<typeof CanvasAnnotation>;
17
+ export default CanvasAnnotation;
@@ -0,0 +1,205 @@
1
+ <script lang="ts">
2
+ import { ChevronRight, Check, X, ChevronsUpDown } from '@lucide/svelte';
3
+ import { cn } from '../utils.js';
4
+ import * as Popover from './ui/popover/index.js';
5
+ import { useTranslation } from '@svadmin/core/i18n';
6
+
7
+ export interface CascaderOption {
8
+ value: string | number;
9
+ label: string;
10
+ children?: CascaderOption[];
11
+ disabled?: boolean;
12
+ isLeaf?: boolean;
13
+ }
14
+
15
+ const i18n = useTranslation();
16
+
17
+ interface Props {
18
+ options?: CascaderOption[];
19
+ value?: (string | number)[];
20
+ separator?: string;
21
+ placeholder?: string;
22
+ changeOnSelect?: boolean;
23
+ disabled?: boolean;
24
+ allowClear?: boolean;
25
+ class?: string;
26
+ onchange?: (value: (string | number)[] | undefined, selectedOptions: CascaderOption[]) => void;
27
+ }
28
+
29
+ let {
30
+ options = [],
31
+ value = $bindable(undefined),
32
+ separator = ' / ',
33
+ placeholder,
34
+ changeOnSelect = false,
35
+ disabled = false,
36
+ allowClear = true,
37
+ class: className,
38
+ onchange,
39
+ }: Props = $props();
40
+
41
+ let open = $state(false);
42
+ // Active selected path while navigating panels
43
+ let activePath = $state<(string | number)[]>([]);
44
+
45
+ $effect(() => {
46
+ if (value && Array.isArray(value)) {
47
+ activePath = [...value];
48
+ } else {
49
+ activePath = [];
50
+ }
51
+ });
52
+
53
+ // Calculate columns based on activePath
54
+ const columns = $derived.by<CascaderOption[][]>(() => {
55
+ const cols: CascaderOption[][] = [options];
56
+ let currentOptions = options;
57
+
58
+ for (const val of activePath) {
59
+ const selectedOpt = currentOptions.find((o) => o.value === val);
60
+ if (selectedOpt && selectedOpt.children && selectedOpt.children.length > 0) {
61
+ cols.push(selectedOpt.children);
62
+ currentOptions = selectedOpt.children;
63
+ } else {
64
+ break;
65
+ }
66
+ }
67
+
68
+ return cols;
69
+ });
70
+
71
+ // Selected options objects corresponding to value
72
+ const selectedOptions = $derived.by<CascaderOption[]>(() => {
73
+ if (!value || !Array.isArray(value) || value.length === 0) return [];
74
+ const opts: CascaderOption[] = [];
75
+ let currentLevel = options;
76
+
77
+ for (const val of value) {
78
+ const found = currentLevel.find((o) => o.value === val);
79
+ if (found) {
80
+ opts.push(found);
81
+ currentLevel = found.children ?? [];
82
+ } else {
83
+ break;
84
+ }
85
+ }
86
+ return opts;
87
+ });
88
+
89
+ const displayLabel = $derived.by(() => {
90
+ if (selectedOptions.length === 0) return '';
91
+ return selectedOptions.map((o) => o.label).join(separator);
92
+ });
93
+
94
+ function handleSelectOption(colIndex: number, option: CascaderOption) {
95
+ if (option.disabled) return;
96
+
97
+ const nextPath = [...activePath.slice(0, colIndex), option.value];
98
+ activePath = nextPath;
99
+
100
+ const hasChildren = Boolean(option.children && option.children.length > 0 && !option.isLeaf);
101
+
102
+ if (!hasChildren || changeOnSelect) {
103
+ value = nextPath;
104
+ const currentSelected = getOptionsForPath(nextPath);
105
+ onchange?.(nextPath, currentSelected);
106
+ if (!hasChildren) {
107
+ open = false;
108
+ }
109
+ }
110
+ }
111
+
112
+ function getOptionsForPath(path: (string | number)[]): CascaderOption[] {
113
+ const result: CascaderOption[] = [];
114
+ let currentLevel = options;
115
+ for (const val of path) {
116
+ const found = currentLevel.find((o) => o.value === val);
117
+ if (found) {
118
+ result.push(found);
119
+ currentLevel = found.children ?? [];
120
+ }
121
+ }
122
+ return result;
123
+ }
124
+
125
+ function clearAll(event?: MouseEvent) {
126
+ event?.stopPropagation();
127
+ value = undefined;
128
+ activePath = [];
129
+ onchange?.(undefined, []);
130
+ }
131
+ </script>
132
+
133
+ <div class={cn('relative w-full', className)} data-testid="cascader">
134
+ <Popover.Root bind:open>
135
+ <Popover.Trigger class="w-full">
136
+ {#snippet child({ props })}
137
+ <button
138
+ type="button"
139
+ {...props}
140
+ {disabled}
141
+ class={cn(
142
+ 'flex min-h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-1.5 text-xs ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 text-left',
143
+ open && 'ring-2 ring-ring ring-offset-2'
144
+ )}
145
+ >
146
+ <span class={cn('truncate', displayLabel ? 'text-foreground' : 'text-muted-foreground')}>
147
+ {displayLabel || (placeholder ?? (i18n.t('field.selectPlaceholder', undefined) ?? '请选择...'))}
148
+ </span>
149
+
150
+ <div class="flex items-center gap-1 shrink-0 ml-1 text-muted-foreground">
151
+ {#if allowClear && displayLabel && !disabled}
152
+ <span
153
+ role="button"
154
+ tabindex="0"
155
+ class="hover:text-foreground cursor-pointer p-0.5"
156
+ onclick={clearAll}
157
+ onkeydown={(e) => {
158
+ if (e.key === 'Enter') clearAll(e as never);
159
+ }}
160
+ >
161
+ <X class="h-3.5 w-3.5" />
162
+ </span>
163
+ {/if}
164
+ <ChevronsUpDown class="h-3.5 w-3.5 opacity-50" />
165
+ </div>
166
+ </button>
167
+ {/snippet}
168
+ </Popover.Trigger>
169
+
170
+ <Popover.Content class="w-auto min-w-[180px] p-0 shadow-lg border border-border" align="start">
171
+ <div class="flex divide-x divide-border max-h-64 overflow-x-auto">
172
+ {#each columns as columnOptions, colIndex (colIndex)}
173
+ <div class="w-40 min-w-[140px] max-h-64 overflow-y-auto p-1 space-y-0.5">
174
+ {#each columnOptions as option (option.value)}
175
+ {@const isSelectedInPath = activePath[colIndex] === option.value}
176
+ {@const hasChildren = Boolean(option.children && option.children.length > 0 && !option.isLeaf)}
177
+ <button
178
+ type="button"
179
+ class={cn(
180
+ 'flex w-full items-center justify-between rounded-md px-2.5 py-1.5 text-xs text-left transition-colors',
181
+ isSelectedInPath
182
+ ? 'bg-primary/10 text-primary font-medium'
183
+ : 'hover:bg-muted/60 text-foreground',
184
+ option.disabled && 'opacity-50 cursor-not-allowed pointer-events-none'
185
+ )}
186
+ onclick={() => handleSelectOption(colIndex, option)}
187
+ >
188
+ <span class="truncate flex-1">{option.label}</span>
189
+ {#if hasChildren}
190
+ <ChevronRight class="h-3.5 w-3.5 shrink-0 opacity-60 ml-1" />
191
+ {:else if isSelectedInPath && value && value[colIndex] === option.value}
192
+ <Check class="h-3.5 w-3.5 shrink-0 text-primary ml-1" />
193
+ {/if}
194
+ </button>
195
+ {:else}
196
+ <div class="py-4 text-center text-xs text-muted-foreground">
197
+ {i18n.t('common.noData', undefined) ?? '无数据'}
198
+ </div>
199
+ {/each}
200
+ </div>
201
+ {/each}
202
+ </div>
203
+ </Popover.Content>
204
+ </Popover.Root>
205
+ </div>
@@ -0,0 +1,21 @@
1
+ export interface CascaderOption {
2
+ value: string | number;
3
+ label: string;
4
+ children?: CascaderOption[];
5
+ disabled?: boolean;
6
+ isLeaf?: boolean;
7
+ }
8
+ interface Props {
9
+ options?: CascaderOption[];
10
+ value?: (string | number)[];
11
+ separator?: string;
12
+ placeholder?: string;
13
+ changeOnSelect?: boolean;
14
+ disabled?: boolean;
15
+ allowClear?: boolean;
16
+ class?: string;
17
+ onchange?: (value: (string | number)[] | undefined, selectedOptions: CascaderOption[]) => void;
18
+ }
19
+ declare const Cascader: import("svelte").Component<Props, {}, "value">;
20
+ type Cascader = ReturnType<typeof Cascader>;
21
+ export default Cascader;