@svadmin/ui 0.64.0 → 0.65.1

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 (73) 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/CanvasAnnotation.svelte +389 -0
  6. package/dist/components/CanvasAnnotation.svelte.d.ts +17 -0
  7. package/dist/components/ColumnHeaderFilter.svelte +178 -0
  8. package/dist/components/ColumnHeaderFilter.svelte.d.ts +12 -0
  9. package/dist/components/ColumnSettings.svelte +198 -0
  10. package/dist/components/ColumnSettings.svelte.d.ts +19 -0
  11. package/dist/components/DecisionTable.svelte +240 -0
  12. package/dist/components/DecisionTable.svelte.d.ts +22 -0
  13. package/dist/components/DraggableRowTable.svelte +117 -0
  14. package/dist/components/DraggableRowTable.svelte.d.ts +15 -0
  15. package/dist/components/DrawerForm.svelte +110 -26
  16. package/dist/components/DrawerForm.svelte.d.ts +20 -8
  17. package/dist/components/EditableTable.svelte +195 -0
  18. package/dist/components/EditableTable.svelte.d.ts +25 -0
  19. package/dist/components/GanttChart.svelte +144 -0
  20. package/dist/components/GanttChart.svelte.d.ts +20 -0
  21. package/dist/components/ImageCropper.svelte +122 -0
  22. package/dist/components/ImageCropper.svelte.d.ts +12 -0
  23. package/dist/components/ImportWizard.svelte +336 -0
  24. package/dist/components/ImportWizard.svelte.d.ts +12 -0
  25. package/dist/components/JsonSchemaForm.svelte +149 -0
  26. package/dist/components/JsonSchemaForm.svelte.d.ts +10 -0
  27. package/dist/components/KanbanBoard.svelte +212 -0
  28. package/dist/components/KanbanBoard.svelte.d.ts +30 -0
  29. package/dist/components/MasterDetailView.svelte +134 -0
  30. package/dist/components/MasterDetailView.svelte.d.ts +16 -0
  31. package/dist/components/MediaLibraryModal.svelte +195 -0
  32. package/dist/components/MediaLibraryModal.svelte.d.ts +22 -0
  33. package/dist/components/MentionsInput.svelte +174 -0
  34. package/dist/components/MentionsInput.svelte.d.ts +21 -0
  35. package/dist/components/ModalForm.svelte +113 -28
  36. package/dist/components/ModalForm.svelte.d.ts +20 -8
  37. package/dist/components/MultiTabKeepAlive.svelte +143 -0
  38. package/dist/components/MultiTabKeepAlive.svelte.d.ts +21 -0
  39. package/dist/components/OfflineSyncBanner.svelte +172 -0
  40. package/dist/components/OfflineSyncBanner.svelte.d.ts +19 -0
  41. package/dist/components/PdfDocumentViewer.svelte +255 -0
  42. package/dist/components/PdfDocumentViewer.svelte.d.ts +22 -0
  43. package/dist/components/PivotTable.svelte +140 -0
  44. package/dist/components/PivotTable.svelte.d.ts +16 -0
  45. package/dist/components/PresenceAvatarGroup.svelte +89 -0
  46. package/dist/components/PresenceAvatarGroup.svelte.d.ts +15 -0
  47. package/dist/components/PrintableBill.svelte +149 -0
  48. package/dist/components/PrintableBill.svelte.d.ts +24 -0
  49. package/dist/components/SensitiveDataMask.svelte +125 -0
  50. package/dist/components/SensitiveDataMask.svelte.d.ts +13 -0
  51. package/dist/components/SignaturePad.svelte +264 -0
  52. package/dist/components/SignaturePad.svelte.d.ts +17 -0
  53. package/dist/components/SplitPaneLayout.svelte +113 -0
  54. package/dist/components/SplitPaneLayout.svelte.d.ts +13 -0
  55. package/dist/components/SpreadsheetView.svelte +363 -0
  56. package/dist/components/SpreadsheetView.svelte.d.ts +18 -0
  57. package/dist/components/StepForm.svelte +180 -0
  58. package/dist/components/StepForm.svelte.d.ts +23 -0
  59. package/dist/components/TableSummary.svelte +110 -0
  60. package/dist/components/TableSummary.svelte.d.ts +20 -0
  61. package/dist/components/TreeTable.svelte +237 -0
  62. package/dist/components/TreeTable.svelte.d.ts +28 -0
  63. package/dist/components/UnsavedChangesPrompt.svelte +44 -0
  64. package/dist/components/UnsavedChangesPrompt.svelte.d.ts +7 -0
  65. package/dist/components/VersionDiffViewer.svelte +196 -0
  66. package/dist/components/VersionDiffViewer.svelte.d.ts +12 -0
  67. package/dist/components/VirtualTable.svelte +86 -54
  68. package/dist/components/VirtualTable.svelte.d.ts +14 -21
  69. package/dist/components/Watermark.svelte +166 -0
  70. package/dist/components/Watermark.svelte.d.ts +26 -0
  71. package/dist/index.d.ts +58 -3
  72. package/dist/index.js +35 -3
  73. package/package.json +2 -2
@@ -1,38 +1,122 @@
1
1
  <script lang="ts">
2
- import { getResource } from '@svadmin/core';
3
- import { useTranslation } from '@svadmin/core/i18n';
4
-
2
+ import type { Snippet } from 'svelte';
5
3
  import * as Sheet from './ui/sheet/index.js';
6
- import AutoForm from './AutoForm.svelte';
7
-
8
- const i18n = useTranslation();
4
+ import { Button } from './ui/button/index.js';
5
+ import { Loader2 } from '@lucide/svelte';
6
+ import { cn } from '../utils.js';
9
7
 
10
- let { resourceName, mode = 'create', id, open = $bindable(false), side = 'right', onSuccess } = $props<{
11
- resourceName: string;
12
- mode?: 'create' | 'edit';
13
- id?: string | number;
14
- open: boolean;
8
+ interface Props {
9
+ open?: boolean;
10
+ title: string;
11
+ description?: string;
12
+ triggerText?: string;
13
+ triggerVariant?: 'default' | 'outline' | 'secondary' | 'ghost' | 'destructive';
14
+ triggerSize?: 'default' | 'sm' | 'lg' | 'icon';
15
+ submitText?: string;
16
+ cancelText?: string;
15
17
  side?: 'left' | 'right';
16
- onSuccess?: () => void;
17
- }>();
18
+ widthClass?: string;
19
+ isSubmitting?: boolean;
20
+ disabled?: boolean;
21
+ onsubmit?: () => void | Promise<void>;
22
+ oncancel?: () => void;
23
+ class?: string;
24
+ triggerSnippet?: Snippet;
25
+ children?: Snippet;
26
+ }
18
27
 
19
- const resource = $derived(getResource(resourceName));
28
+ let {
29
+ open = $bindable(false),
30
+ title,
31
+ description,
32
+ triggerText = 'Open Drawer',
33
+ triggerVariant = 'default',
34
+ triggerSize = 'sm',
35
+ submitText = 'Save',
36
+ cancelText = 'Cancel',
37
+ side = 'right',
38
+ widthClass = 'sm:max-w-md w-full',
39
+ isSubmitting = false,
40
+ disabled = false,
41
+ onsubmit,
42
+ oncancel,
43
+ class: className = '',
44
+ triggerSnippet,
45
+ children,
46
+ }: Props = $props();
20
47
 
21
- function handleSuccess() {
48
+ let loading = $state(false);
49
+
50
+ async function handleSubmit(e: SubmitEvent) {
51
+ e.preventDefault();
52
+ loading = true;
53
+ try {
54
+ await onsubmit?.();
55
+ open = false;
56
+ } finally {
57
+ loading = false;
58
+ }
59
+ }
60
+
61
+ function handleCancel() {
62
+ oncancel?.();
22
63
  open = false;
23
- onSuccess?.();
24
64
  }
25
65
  </script>
26
66
 
27
- <Sheet.Root bind:open {side}>
28
- <Sheet.Content>
29
- <Sheet.Header>
30
- <Sheet.Title>
31
- {mode === 'create' ? `${i18n.t('common.create')}${resource.label}` : `${i18n.t('common.edit')}${resource.label}`}
32
- </Sheet.Title>
33
- </Sheet.Header>
34
- <div class="flex-1 overflow-y-auto">
35
- <AutoForm {resourceName} {mode} {id} showHeader={false} onSuccess={handleSuccess} />
36
- </div>
67
+ {#if triggerSnippet}
68
+ <button
69
+ type="button"
70
+ class="inline-flex cursor-pointer bg-transparent border-0 p-0 text-left"
71
+ onclick={() => { open = true; }}
72
+ {disabled}
73
+ >
74
+ {@render triggerSnippet()}
75
+ </button>
76
+ {:else if triggerText}
77
+ <Button variant={triggerVariant} size={triggerSize} {disabled} onclick={() => { open = true; }}>
78
+ {triggerText}
79
+ </Button>
80
+ {/if}
81
+
82
+ <Sheet.Root bind:open {side} onClose={handleCancel} class={cn(widthClass, 'p-0 overflow-hidden', className)}>
83
+ <Sheet.Content class="flex flex-col h-full gap-0 p-0">
84
+ <form onsubmit={handleSubmit} class="flex flex-col h-full">
85
+ <Sheet.Header class="px-6 py-4 border-b border-border/60">
86
+ <Sheet.Title>{title}</Sheet.Title>
87
+ {#if description}
88
+ <Sheet.Description>{description}</Sheet.Description>
89
+ {/if}
90
+ </Sheet.Header>
91
+
92
+ <div class="flex-1 overflow-y-auto p-6 text-sm text-foreground space-y-4">
93
+ {#if children}
94
+ {@render children()}
95
+ {/if}
96
+ </div>
97
+
98
+ <Sheet.Footer class="gap-2 px-6 py-4 border-t border-border/60 mt-auto">
99
+ <Button
100
+ type="button"
101
+ variant="outline"
102
+ size="sm"
103
+ disabled={loading || isSubmitting}
104
+ onclick={handleCancel}
105
+ >
106
+ {cancelText}
107
+ </Button>
108
+ <Button
109
+ type="submit"
110
+ size="sm"
111
+ disabled={loading || isSubmitting || disabled}
112
+ class="gap-1.5 min-w-20"
113
+ >
114
+ {#if loading || isSubmitting}
115
+ <Loader2 class="h-4 w-4 animate-spin" />
116
+ {/if}
117
+ {submitText}
118
+ </Button>
119
+ </Sheet.Footer>
120
+ </form>
37
121
  </Sheet.Content>
38
122
  </Sheet.Root>
@@ -1,11 +1,23 @@
1
- type $$ComponentProps = {
2
- resourceName: string;
3
- mode?: 'create' | 'edit';
4
- id?: string | number;
5
- open: boolean;
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ open?: boolean;
4
+ title: string;
5
+ description?: string;
6
+ triggerText?: string;
7
+ triggerVariant?: 'default' | 'outline' | 'secondary' | 'ghost' | 'destructive';
8
+ triggerSize?: 'default' | 'sm' | 'lg' | 'icon';
9
+ submitText?: string;
10
+ cancelText?: string;
6
11
  side?: 'left' | 'right';
7
- onSuccess?: () => void;
8
- };
9
- declare const DrawerForm: import("svelte").Component<$$ComponentProps, {}, "open">;
12
+ widthClass?: string;
13
+ isSubmitting?: boolean;
14
+ disabled?: boolean;
15
+ onsubmit?: () => void | Promise<void>;
16
+ oncancel?: () => void;
17
+ class?: string;
18
+ triggerSnippet?: Snippet;
19
+ children?: Snippet;
20
+ }
21
+ declare const DrawerForm: import("svelte").Component<Props, {}, "open">;
10
22
  type DrawerForm = ReturnType<typeof DrawerForm>;
11
23
  export default DrawerForm;
@@ -0,0 +1,195 @@
1
+ <script lang="ts">
2
+ import { Button } from './ui/button/index.js';
3
+ import { Plus, Trash2, Save, Loader2 } from '@lucide/svelte';
4
+ import { cn } from '../utils.js';
5
+
6
+ export interface EditableTableColumn {
7
+ key: string;
8
+ label: string;
9
+ type?: 'text' | 'number' | 'select' | 'boolean' | 'date';
10
+ options?: Array<{ label: string; value: string | number }>;
11
+ required?: boolean;
12
+ width?: string;
13
+ readonly?: boolean;
14
+ }
15
+
16
+ interface Props {
17
+ columns: EditableTableColumn[];
18
+ data?: Record<string, unknown>[];
19
+ rowKey?: string;
20
+ allowAdd?: boolean;
21
+ allowDelete?: boolean;
22
+ onchange?: (data: Record<string, unknown>[]) => void;
23
+ onsave?: (data: Record<string, unknown>[]) => void | Promise<void>;
24
+ class?: string;
25
+ }
26
+
27
+ let {
28
+ columns,
29
+ data = $bindable([]),
30
+ rowKey = 'id',
31
+ allowAdd = true,
32
+ allowDelete = true,
33
+ onchange,
34
+ onsave,
35
+ class: className = '',
36
+ }: Props = $props();
37
+
38
+ let isSaving = $state(false);
39
+
40
+ function updateCell(rowIndex: number, columnKey: string, value: unknown) {
41
+ const nextData = [...data];
42
+ nextData[rowIndex] = {
43
+ ...nextData[rowIndex],
44
+ [columnKey]: value,
45
+ };
46
+ data = nextData;
47
+ onchange?.(data);
48
+ }
49
+
50
+ function handleAddRow() {
51
+ const newRow: Record<string, unknown> = {
52
+ [rowKey]: `row_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
53
+ };
54
+ for (const col of columns) {
55
+ if (col.type === 'number') newRow[col.key] = 0;
56
+ else if (col.type === 'boolean') newRow[col.key] = false;
57
+ else newRow[col.key] = '';
58
+ }
59
+ data = [...data, newRow];
60
+ onchange?.(data);
61
+ }
62
+
63
+ function handleDeleteRow(index: number) {
64
+ data = data.filter((_, idx) => idx !== index);
65
+ onchange?.(data);
66
+ }
67
+
68
+ async function handleSave() {
69
+ isSaving = true;
70
+ try {
71
+ await onsave?.(data);
72
+ } finally {
73
+ isSaving = false;
74
+ }
75
+ }
76
+ </script>
77
+
78
+ <div class={cn('space-y-3 rounded-xl border border-border bg-card p-4 shadow-xs text-xs', className)}>
79
+ <div class="flex items-center justify-between gap-2 pb-2 border-b border-border/60">
80
+ <div class="font-semibold text-foreground">
81
+ Editable Grid <span class="text-muted-foreground font-normal">({data.length} records)</span>
82
+ </div>
83
+
84
+ <div class="flex items-center gap-2">
85
+ {#if allowAdd}
86
+ <Button variant="outline" size="sm" class="h-8 text-xs gap-1" onclick={handleAddRow}>
87
+ <Plus class="h-3.5 w-3.5" />
88
+ Add Row
89
+ </Button>
90
+ {/if}
91
+ {#if onsave}
92
+ <Button size="sm" class="h-8 text-xs gap-1 min-w-20" disabled={isSaving} onclick={handleSave}>
93
+ {#if isSaving}
94
+ <Loader2 class="h-3.5 w-3.5 animate-spin" />
95
+ {:else}
96
+ <Save class="h-3.5 w-3.5" />
97
+ {/if}
98
+ Save All
99
+ </Button>
100
+ {/if}
101
+ </div>
102
+ </div>
103
+
104
+ <div class="overflow-x-auto rounded-lg border border-border/60">
105
+ <table class="w-full text-left border-collapse">
106
+ <thead class="bg-muted/40 font-semibold text-muted-foreground border-b border-border/60">
107
+ <tr>
108
+ {#each columns as col (col.key)}
109
+ <th style={col.width ? `width: ${col.width};` : ''} class="px-3 py-2.5">
110
+ {col.label}
111
+ {#if col.required}<span class="text-destructive">*</span>{/if}
112
+ </th>
113
+ {/each}
114
+ {#if allowDelete}
115
+ <th class="w-12 px-3 py-2.5 text-center">Action</th>
116
+ {/if}
117
+ </tr>
118
+ </thead>
119
+ <tbody class="divide-y divide-border/40">
120
+ {#each data as row, rowIndex (row[rowKey] ?? rowIndex)}
121
+ <tr class="hover:bg-muted/20 transition-colors">
122
+ {#each columns as col (col.key)}
123
+ <td class="p-1.5">
124
+ {#if col.readonly}
125
+ <div class="px-2 py-1 text-muted-foreground font-mono truncate">{row[col.key] ?? '—'}</div>
126
+ {:else if col.type === 'select' && col.options}
127
+ <select
128
+ 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"
129
+ value={String(row[col.key] ?? '')}
130
+ onchange={(e) => updateCell(rowIndex, col.key, e.currentTarget.value)}
131
+ >
132
+ {#each col.options as opt (opt.value)}
133
+ <option value={String(opt.value)}>{opt.label}</option>
134
+ {/each}
135
+ </select>
136
+ {:else if col.type === 'boolean'}
137
+ <div class="flex items-center justify-center py-1">
138
+ <input
139
+ type="checkbox"
140
+ class="h-4 w-4 rounded border-border text-primary focus:ring-primary/20 cursor-pointer"
141
+ checked={Boolean(row[col.key])}
142
+ onchange={(e) => updateCell(rowIndex, col.key, e.currentTarget.checked)}
143
+ />
144
+ </div>
145
+ {:else if col.type === 'number'}
146
+ <input
147
+ type="number"
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
+ value={Number(row[col.key] ?? 0)}
150
+ oninput={(e) => updateCell(rowIndex, col.key, Number(e.currentTarget.value))}
151
+ />
152
+ {:else if col.type === 'date'}
153
+ <input
154
+ type="date"
155
+ 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"
156
+ value={String(row[col.key] ?? '')}
157
+ oninput={(e) => updateCell(rowIndex, col.key, e.currentTarget.value)}
158
+ />
159
+ {:else}
160
+ <input
161
+ type="text"
162
+ 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"
163
+ value={String(row[col.key] ?? '')}
164
+ oninput={(e) => updateCell(rowIndex, col.key, e.currentTarget.value)}
165
+ />
166
+ {/if}
167
+ </td>
168
+ {/each}
169
+
170
+ {#if allowDelete}
171
+ <td class="p-1.5 text-center">
172
+ <Button
173
+ variant="ghost"
174
+ size="sm"
175
+ class="h-7 w-7 p-0 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
176
+ onclick={() => handleDeleteRow(rowIndex)}
177
+ >
178
+ <Trash2 class="h-3.5 w-3.5" />
179
+ </Button>
180
+ </td>
181
+ {/if}
182
+ </tr>
183
+ {/each}
184
+
185
+ {#if data.length === 0}
186
+ <tr>
187
+ <td colspan={columns.length + (allowDelete ? 1 : 0)} class="py-8 text-center text-muted-foreground">
188
+ No rows. Click "Add Row" to create the first record.
189
+ </td>
190
+ </tr>
191
+ {/if}
192
+ </tbody>
193
+ </table>
194
+ </div>
195
+ </div>
@@ -0,0 +1,25 @@
1
+ export interface EditableTableColumn {
2
+ key: string;
3
+ label: string;
4
+ type?: 'text' | 'number' | 'select' | 'boolean' | 'date';
5
+ options?: Array<{
6
+ label: string;
7
+ value: string | number;
8
+ }>;
9
+ required?: boolean;
10
+ width?: string;
11
+ readonly?: boolean;
12
+ }
13
+ interface Props {
14
+ columns: EditableTableColumn[];
15
+ data?: Record<string, unknown>[];
16
+ rowKey?: string;
17
+ allowAdd?: boolean;
18
+ allowDelete?: boolean;
19
+ onchange?: (data: Record<string, unknown>[]) => void;
20
+ onsave?: (data: Record<string, unknown>[]) => void | Promise<void>;
21
+ class?: string;
22
+ }
23
+ declare const EditableTable: import("svelte").Component<Props, {}, "data">;
24
+ type EditableTable = ReturnType<typeof EditableTable>;
25
+ export default EditableTable;
@@ -0,0 +1,144 @@
1
+ <script lang="ts">
2
+ import { Badge } from './ui/badge/index.js';
3
+ import { cn } from '../utils.js';
4
+
5
+ export interface GanttTask {
6
+ id: string;
7
+ title: string;
8
+ startDay: number; // 0-based offset
9
+ durationDays: number;
10
+ progress?: number; // 0 to 100
11
+ category?: string;
12
+ assignee?: string;
13
+ status?: 'planned' | 'in_progress' | 'completed' | 'delayed';
14
+ }
15
+
16
+ interface Props {
17
+ tasks: GanttTask[];
18
+ totalDays?: number;
19
+ dayLabelPrefix?: string;
20
+ onselecttask?: (task: GanttTask) => void;
21
+ class?: string;
22
+ }
23
+
24
+ let {
25
+ tasks = [],
26
+ totalDays = 14,
27
+ dayLabelPrefix = 'D',
28
+ onselecttask,
29
+ class: className = '',
30
+ }: Props = $props();
31
+
32
+ const days = $derived(
33
+ Array.from({ length: totalDays }, (_, i) => `${dayLabelPrefix}${i + 1}`)
34
+ );
35
+
36
+ function getStatusBadgeVariant(status?: string) {
37
+ switch (status) {
38
+ case 'completed':
39
+ return 'bg-success/15 text-success border-success/30';
40
+ case 'delayed':
41
+ return 'bg-destructive/15 text-destructive border-destructive/30';
42
+ case 'in_progress':
43
+ return 'bg-primary/15 text-primary border-primary/30';
44
+ default:
45
+ return 'bg-muted text-muted-foreground border-border/40';
46
+ }
47
+ }
48
+
49
+ function getBarColor(status?: string) {
50
+ switch (status) {
51
+ case 'completed':
52
+ return 'bg-success text-success-foreground';
53
+ case 'delayed':
54
+ return 'bg-destructive text-destructive-foreground';
55
+ default:
56
+ return 'bg-primary text-primary-foreground';
57
+ }
58
+ }
59
+ </script>
60
+
61
+ <div class={cn('rounded-xl border border-border bg-card p-4 shadow-xs text-xs space-y-3', className)}>
62
+ <div class="flex items-center justify-between pb-2 border-b border-border/60">
63
+ <div class="font-semibold text-foreground">
64
+ Project Gantt Schedule <span class="text-muted-foreground font-normal">({tasks.length} tasks / {totalDays} days)</span>
65
+ </div>
66
+ </div>
67
+
68
+ <div class="overflow-x-auto rounded-lg border border-border/60">
69
+ <div class="min-w-[700px]">
70
+ <!-- Timeline Header -->
71
+ <div class="flex border-b border-border/60 bg-muted/40 font-semibold text-muted-foreground">
72
+ <div class="w-56 p-2.5 border-r border-border/60 text-left shrink-0">Task Name</div>
73
+ <div class="flex-1 flex">
74
+ {#each days as day (day)}
75
+ <div class="flex-1 p-2 text-center border-r border-border/40 text-[11px] font-mono last:border-r-0">
76
+ {day}
77
+ </div>
78
+ {/each}
79
+ </div>
80
+ </div>
81
+
82
+ <!-- Task Rows -->
83
+ <div class="divide-y divide-border/40">
84
+ {#each tasks as task (task.id)}
85
+ {@const leftPct = (task.startDay / totalDays) * 100}
86
+ {@const widthPct = (task.durationDays / totalDays) * 100}
87
+ <div
88
+ role="button"
89
+ tabindex="0"
90
+ class="flex items-center hover:bg-muted/20 transition-colors cursor-pointer"
91
+ onclick={() => onselecttask?.(task)}
92
+ onkeydown={(e) => { if (e.key === 'Enter') onselecttask?.(task); }}
93
+ >
94
+ <!-- Task Info Column -->
95
+ <div class="w-56 p-2.5 border-r border-border/60 flex items-center justify-between gap-2 shrink-0">
96
+ <div class="truncate font-medium text-foreground">{task.title}</div>
97
+ {#if task.status}
98
+ <Badge variant="outline" class={cn('text-[9px] px-1 py-0 uppercase', getStatusBadgeVariant(task.status))}>
99
+ {task.status}
100
+ </Badge>
101
+ {/if}
102
+ </div>
103
+
104
+ <!-- Task Bar Grid Area -->
105
+ <div class="flex-1 relative h-10 flex items-center px-1">
106
+ <!-- Grid vertical guideline background -->
107
+ <div class="absolute inset-0 flex pointer-events-none">
108
+ {#each days as _, idx (idx)}
109
+ <div class="flex-1 border-r border-border/20 last:border-r-0"></div>
110
+ {/each}
111
+ </div>
112
+
113
+ <!-- Bar element -->
114
+ <div
115
+ class={cn(
116
+ 'relative h-6 rounded-md shadow-xs flex items-center justify-between px-2 text-[10px] font-medium overflow-hidden transition-all group hover:brightness-110',
117
+ getBarColor(task.status)
118
+ )}
119
+ style={`margin-left: ${leftPct}%; width: ${Math.max(widthPct, 4)}%;`}
120
+ >
121
+ <!-- Inner progress fill -->
122
+ {#if task.progress !== undefined}
123
+ <div
124
+ class="absolute inset-y-0 left-0 bg-foreground/20 pointer-events-none"
125
+ style={`width: ${task.progress}%;`}
126
+ ></div>
127
+ {/if}
128
+
129
+ <span class="truncate relative z-10">{task.title}</span>
130
+ {#if task.progress !== undefined}
131
+ <span class="relative z-10 text-[9px] opacity-90 font-mono">{task.progress}%</span>
132
+ {/if}
133
+ </div>
134
+ </div>
135
+ </div>
136
+ {/each}
137
+
138
+ {#if tasks.length === 0}
139
+ <div class="py-8 text-center text-muted-foreground">No schedule tasks available</div>
140
+ {/if}
141
+ </div>
142
+ </div>
143
+ </div>
144
+ </div>
@@ -0,0 +1,20 @@
1
+ export interface GanttTask {
2
+ id: string;
3
+ title: string;
4
+ startDay: number;
5
+ durationDays: number;
6
+ progress?: number;
7
+ category?: string;
8
+ assignee?: string;
9
+ status?: 'planned' | 'in_progress' | 'completed' | 'delayed';
10
+ }
11
+ interface Props {
12
+ tasks: GanttTask[];
13
+ totalDays?: number;
14
+ dayLabelPrefix?: string;
15
+ onselecttask?: (task: GanttTask) => void;
16
+ class?: string;
17
+ }
18
+ declare const GanttChart: import("svelte").Component<Props, {}, "">;
19
+ type GanttChart = ReturnType<typeof GanttChart>;
20
+ export default GanttChart;
@@ -0,0 +1,122 @@
1
+ <script lang="ts">
2
+ import { Button } from './ui/button/index.js';
3
+ import { RotateCw, ZoomIn, ZoomOut, Crop } from '@lucide/svelte';
4
+ import { cn } from '../utils.js';
5
+
6
+ interface Props {
7
+ imageUrl: string;
8
+ aspectRatio?: number;
9
+ zoom?: number;
10
+ rotation?: number;
11
+ oncrop?: (croppedDataUrl: string) => void;
12
+ oncancel?: () => void;
13
+ class?: string;
14
+ }
15
+
16
+ let {
17
+ imageUrl,
18
+ aspectRatio = 1,
19
+ zoom = $bindable(1),
20
+ rotation = $bindable(0),
21
+ oncrop,
22
+ oncancel,
23
+ class: className = '',
24
+ }: Props = $props();
25
+
26
+ let canvasEl = $state<HTMLCanvasElement | null>(null);
27
+
28
+ function handleRotate() {
29
+ rotation = (rotation + 90) % 360;
30
+ }
31
+
32
+ function handleCrop() {
33
+ if (!canvasEl) return;
34
+ const img = new Image();
35
+ img.crossOrigin = 'anonymous';
36
+ img.onload = () => {
37
+ const ctx = canvasEl?.getContext('2d');
38
+ if (!ctx || !canvasEl) return;
39
+
40
+ const size = 300;
41
+ canvasEl.width = size;
42
+ canvasEl.height = size / aspectRatio;
43
+
44
+ ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
45
+ ctx.save();
46
+ ctx.translate(canvasEl.width / 2, canvasEl.height / 2);
47
+ ctx.rotate((rotation * Math.PI) / 180);
48
+ ctx.scale(zoom, zoom);
49
+ ctx.drawImage(img, -size / 2, (-size / 2) / aspectRatio, size, size / aspectRatio);
50
+ ctx.restore();
51
+
52
+ const dataUrl = canvasEl.toDataURL('image/png');
53
+ oncrop?.(dataUrl);
54
+ };
55
+ img.src = imageUrl;
56
+ }
57
+ </script>
58
+
59
+ <div class={cn('rounded-xl border border-border bg-card p-4 shadow-xs space-y-4 text-xs', className)}>
60
+ <div class="flex items-center justify-between pb-2 border-b border-border/60">
61
+ <h4 class="font-semibold text-foreground flex items-center gap-1.5">
62
+ <Crop class="h-4 w-4 text-primary" />
63
+ Image Cropper
64
+ </h4>
65
+ </div>
66
+
67
+ <!-- Crop Viewport -->
68
+ <div class="relative flex items-center justify-center h-64 w-full rounded-lg bg-muted/40 border border-border/60 overflow-hidden select-none">
69
+ <div
70
+ style="transform: scale({zoom}) rotate({rotation}deg); transition: transform 150ms ease;"
71
+ class="max-h-56 max-w-56 flex items-center justify-center"
72
+ >
73
+ <img src={imageUrl} alt="Crop Preview" class="object-contain max-h-56 max-w-56 rounded pointer-events-none" />
74
+ </div>
75
+
76
+ <!-- Aspect Ratio Overlay Grid -->
77
+ <div
78
+ style="aspect-ratio: {aspectRatio};"
79
+ class="absolute w-48 border-2 border-primary/80 rounded pointer-events-none shadow-[0_0_0_9999px_rgba(0,0,0,0.45)]"
80
+ ></div>
81
+ </div>
82
+
83
+ <!-- Controls Bar -->
84
+ <div class="flex items-center justify-between gap-3 pt-2">
85
+ <div class="flex items-center gap-2">
86
+ <Button
87
+ variant="outline"
88
+ size="sm"
89
+ class="h-7 text-xs gap-1"
90
+ onclick={() => { zoom = Math.max(0.5, Number((zoom - 0.1).toFixed(1))); }}
91
+ >
92
+ <ZoomOut class="h-3 w-3" />
93
+ </Button>
94
+ <span class="tabular-nums font-mono text-[11px] text-muted-foreground w-10 text-center">{Math.round(zoom * 100)}%</span>
95
+ <Button
96
+ variant="outline"
97
+ size="sm"
98
+ class="h-7 text-xs gap-1"
99
+ onclick={() => { zoom = Math.min(3, Number((zoom + 0.1).toFixed(1))); }}
100
+ >
101
+ <ZoomIn class="h-3 w-3" />
102
+ </Button>
103
+
104
+ <Button variant="outline" size="sm" class="h-7 text-xs gap-1 ml-2" onclick={handleRotate}>
105
+ <RotateCw class="h-3 w-3" />
106
+ Rotate
107
+ </Button>
108
+ </div>
109
+
110
+ <div class="flex items-center gap-2">
111
+ {#if oncancel}
112
+ <Button variant="outline" size="sm" class="h-7 text-xs" onclick={oncancel}>Cancel</Button>
113
+ {/if}
114
+ <Button size="sm" class="h-7 text-xs gap-1" onclick={handleCrop}>
115
+ Apply Crop
116
+ </Button>
117
+ </div>
118
+ </div>
119
+
120
+ <!-- Hidden Working Canvas -->
121
+ <canvas bind:this={canvasEl} class="hidden"></canvas>
122
+ </div>