@svadmin/ui 0.64.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/CanvasAnnotation.svelte +389 -0
- package/dist/components/CanvasAnnotation.svelte.d.ts +17 -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/EditableTable.svelte +195 -0
- package/dist/components/EditableTable.svelte.d.ts +25 -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/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/index.d.ts +58 -3
- package/dist/index.js +35 -3
- package/package.json +2 -2
|
@@ -1,38 +1,122 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import { Button } from './ui/button/index.js';
|
|
5
|
+
import { Loader2 } from '@lucide/svelte';
|
|
6
|
+
import { cn } from '../utils.js';
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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>
|