@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.
- 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
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils.js';
|
|
3
|
+
import { Badge } from './ui/badge/index.js';
|
|
4
|
+
import { User, Hash } from '@lucide/svelte';
|
|
5
|
+
|
|
6
|
+
export interface MentionOption {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
type?: 'user' | 'tag' | 'entity';
|
|
10
|
+
avatar?: string;
|
|
11
|
+
subtitle?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
value?: string;
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
triggers?: string[];
|
|
18
|
+
users?: MentionOption[];
|
|
19
|
+
tags?: MentionOption[];
|
|
20
|
+
rows?: number;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
onchange?: (value: string) => void;
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
value = $bindable(''),
|
|
28
|
+
placeholder = 'Type @ to mention team members, or # to link tags...',
|
|
29
|
+
triggers = ['@', '#'],
|
|
30
|
+
users = [],
|
|
31
|
+
tags = [],
|
|
32
|
+
rows = 3,
|
|
33
|
+
disabled = false,
|
|
34
|
+
onchange,
|
|
35
|
+
class: className = '',
|
|
36
|
+
}: Props = $props();
|
|
37
|
+
|
|
38
|
+
let textareaEl: HTMLTextAreaElement | undefined = $state();
|
|
39
|
+
let showDropdown = $state(false);
|
|
40
|
+
let activeTrigger = $state<string | null>(null);
|
|
41
|
+
let searchQuery = $state('');
|
|
42
|
+
let selectedIndex = $state(0);
|
|
43
|
+
let triggerIndex = $state(-1);
|
|
44
|
+
|
|
45
|
+
const activeOptions = $derived<MentionOption[]>(
|
|
46
|
+
(() => {
|
|
47
|
+
const list = activeTrigger === '#' ? tags : users;
|
|
48
|
+
if (!searchQuery.trim()) return list;
|
|
49
|
+
return list.filter((item) =>
|
|
50
|
+
item.label.toLowerCase().includes(searchQuery.toLowerCase().trim())
|
|
51
|
+
);
|
|
52
|
+
})()
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
function handleInput(e: Event) {
|
|
56
|
+
const target = e.currentTarget as HTMLTextAreaElement;
|
|
57
|
+
value = target.value;
|
|
58
|
+
onchange?.(value);
|
|
59
|
+
|
|
60
|
+
const cursorPos = target.selectionStart ?? 0;
|
|
61
|
+
const textBeforeCursor = value.slice(0, cursorPos);
|
|
62
|
+
|
|
63
|
+
let matchTrigger: string | null = null;
|
|
64
|
+
let matchIdx = -1;
|
|
65
|
+
|
|
66
|
+
for (const trig of triggers) {
|
|
67
|
+
const idx = textBeforeCursor.lastIndexOf(trig);
|
|
68
|
+
if (idx !== -1 && (idx === 0 || /\s/.test(textBeforeCursor[idx - 1]))) {
|
|
69
|
+
const query = textBeforeCursor.slice(idx + 1);
|
|
70
|
+
if (!/\s/.test(query)) {
|
|
71
|
+
matchTrigger = trig;
|
|
72
|
+
matchIdx = idx;
|
|
73
|
+
searchQuery = query;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (matchTrigger !== null) {
|
|
80
|
+
activeTrigger = matchTrigger;
|
|
81
|
+
triggerIndex = matchIdx;
|
|
82
|
+
showDropdown = true;
|
|
83
|
+
selectedIndex = 0;
|
|
84
|
+
} else {
|
|
85
|
+
showDropdown = false;
|
|
86
|
+
activeTrigger = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function insertMention(opt: MentionOption) {
|
|
91
|
+
if (!textareaEl || triggerIndex === -1 || !activeTrigger) return;
|
|
92
|
+
const before = value.slice(0, triggerIndex);
|
|
93
|
+
const after = value.slice(textareaEl.selectionStart ?? triggerIndex);
|
|
94
|
+
const mentionToken = `${activeTrigger}${opt.label} `;
|
|
95
|
+
value = `${before}${mentionToken}${after}`;
|
|
96
|
+
onchange?.(value);
|
|
97
|
+
showDropdown = false;
|
|
98
|
+
activeTrigger = null;
|
|
99
|
+
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
if (textareaEl) {
|
|
102
|
+
const newPos = before.length + mentionToken.length;
|
|
103
|
+
textareaEl.focus();
|
|
104
|
+
textareaEl.setSelectionRange(newPos, newPos);
|
|
105
|
+
}
|
|
106
|
+
}, 0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
110
|
+
if (!showDropdown || activeOptions.length === 0) return;
|
|
111
|
+
|
|
112
|
+
if (e.key === 'ArrowDown') {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
selectedIndex = (selectedIndex + 1) % activeOptions.length;
|
|
115
|
+
} else if (e.key === 'ArrowUp') {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
selectedIndex = (selectedIndex - 1 + activeOptions.length) % activeOptions.length;
|
|
118
|
+
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
if (activeOptions[selectedIndex]) {
|
|
121
|
+
insertMention(activeOptions[selectedIndex]);
|
|
122
|
+
}
|
|
123
|
+
} else if (e.key === 'Escape') {
|
|
124
|
+
showDropdown = false;
|
|
125
|
+
activeTrigger = null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
</script>
|
|
129
|
+
|
|
130
|
+
<div class={cn('relative w-full text-xs', className)}>
|
|
131
|
+
<textarea
|
|
132
|
+
bind:this={textareaEl}
|
|
133
|
+
{value}
|
|
134
|
+
{placeholder}
|
|
135
|
+
{rows}
|
|
136
|
+
{disabled}
|
|
137
|
+
class="w-full rounded-md border border-input bg-background p-2.5 text-xs text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 resize-y"
|
|
138
|
+
oninput={handleInput}
|
|
139
|
+
onkeydown={handleKeyDown}
|
|
140
|
+
></textarea>
|
|
141
|
+
|
|
142
|
+
{#if showDropdown && activeOptions.length > 0}
|
|
143
|
+
<div
|
|
144
|
+
class="absolute left-0 top-full mt-1 z-50 min-w-48 max-h-56 overflow-y-auto rounded-lg border border-border bg-popover text-popover-foreground shadow-md p-1 animate-in fade-in-0 zoom-in-95"
|
|
145
|
+
>
|
|
146
|
+
<div class="px-2 py-1 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
|
|
147
|
+
{activeTrigger === '#' ? 'Select Tag' : 'Mention Member'}
|
|
148
|
+
</div>
|
|
149
|
+
{#each activeOptions as opt, idx (opt.id)}
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
class={cn(
|
|
153
|
+
'w-full flex items-center justify-between gap-2 px-2 py-1.5 rounded-md text-left text-xs transition-colors cursor-pointer border-0',
|
|
154
|
+
idx === selectedIndex ? 'bg-accent text-accent-foreground font-medium' : 'bg-transparent text-foreground hover:bg-muted/60'
|
|
155
|
+
)}
|
|
156
|
+
onclick={() => insertMention(opt)}
|
|
157
|
+
>
|
|
158
|
+
<div class="flex items-center gap-2 truncate">
|
|
159
|
+
{#if activeTrigger === '#'}
|
|
160
|
+
<Hash class="h-3.5 w-3.5 text-primary shrink-0" />
|
|
161
|
+
{:else}
|
|
162
|
+
<User class="h-3.5 w-3.5 text-primary shrink-0" />
|
|
163
|
+
{/if}
|
|
164
|
+
<span class="truncate">{opt.label}</span>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{#if opt.subtitle}
|
|
168
|
+
<Badge variant="secondary" class="text-[10px] px-1 py-0">{opt.subtitle}</Badge>
|
|
169
|
+
{/if}
|
|
170
|
+
</button>
|
|
171
|
+
{/each}
|
|
172
|
+
</div>
|
|
173
|
+
{/if}
|
|
174
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface MentionOption {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
type?: 'user' | 'tag' | 'entity';
|
|
5
|
+
avatar?: string;
|
|
6
|
+
subtitle?: string;
|
|
7
|
+
}
|
|
8
|
+
interface Props {
|
|
9
|
+
value?: string;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
triggers?: string[];
|
|
12
|
+
users?: MentionOption[];
|
|
13
|
+
tags?: MentionOption[];
|
|
14
|
+
rows?: number;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
onchange?: (value: string) => void;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
declare const MentionsInput: import("svelte").Component<Props, {}, "value">;
|
|
20
|
+
type MentionsInput = ReturnType<typeof MentionsInput>;
|
|
21
|
+
export default MentionsInput;
|
|
@@ -1,38 +1,123 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import { useTranslation } from '@svadmin/core/i18n';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
4
3
|
import * as Dialog from './ui/dialog/index.js';
|
|
5
|
-
import
|
|
4
|
+
import { Button } from './ui/button/index.js';
|
|
5
|
+
import { Loader2 } from '@lucide/svelte';
|
|
6
|
+
import { cn } from '../utils.js';
|
|
6
7
|
|
|
7
|
-
|
|
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;
|
|
17
|
+
widthClass?: string;
|
|
18
|
+
isSubmitting?: boolean;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
onsubmit?: () => void | Promise<void>;
|
|
21
|
+
oncancel?: () => void;
|
|
22
|
+
class?: string;
|
|
23
|
+
triggerSnippet?: Snippet;
|
|
24
|
+
children?: Snippet;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
open = $bindable(false),
|
|
29
|
+
title,
|
|
30
|
+
description,
|
|
31
|
+
triggerText = 'Open Form',
|
|
32
|
+
triggerVariant = 'default',
|
|
33
|
+
triggerSize = 'sm',
|
|
34
|
+
submitText = 'Confirm',
|
|
35
|
+
cancelText = 'Cancel',
|
|
36
|
+
widthClass = 'sm:max-w-lg',
|
|
37
|
+
isSubmitting = false,
|
|
38
|
+
disabled = false,
|
|
39
|
+
onsubmit,
|
|
40
|
+
oncancel,
|
|
41
|
+
class: className = '',
|
|
42
|
+
triggerSnippet,
|
|
43
|
+
children,
|
|
44
|
+
}: Props = $props();
|
|
8
45
|
|
|
9
|
-
let
|
|
10
|
-
resourceName: string;
|
|
11
|
-
mode?: 'create' | 'edit';
|
|
12
|
-
id?: string | number;
|
|
13
|
-
open: boolean;
|
|
14
|
-
onSuccess?: () => void;
|
|
15
|
-
}>();
|
|
46
|
+
let loading = $state(false);
|
|
16
47
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
48
|
+
async function handleSubmit(e: SubmitEvent) {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
loading = true;
|
|
51
|
+
try {
|
|
52
|
+
await onsubmit?.();
|
|
53
|
+
open = false;
|
|
54
|
+
} finally {
|
|
55
|
+
loading = false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
20
58
|
|
|
21
|
-
function
|
|
59
|
+
function handleCancel() {
|
|
60
|
+
oncancel?.();
|
|
22
61
|
open = false;
|
|
23
|
-
onSuccess?.();
|
|
24
62
|
}
|
|
25
63
|
</script>
|
|
26
64
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
<Dialog.
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
{
|
|
33
|
-
</
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
{
|
|
65
|
+
<Dialog.Root bind:open>
|
|
66
|
+
{#if triggerSnippet}
|
|
67
|
+
<Dialog.Trigger>
|
|
68
|
+
{#snippet child({ props })}
|
|
69
|
+
<div {...props} class="inline-flex">
|
|
70
|
+
{@render triggerSnippet()}
|
|
71
|
+
</div>
|
|
72
|
+
{/snippet}
|
|
73
|
+
</Dialog.Trigger>
|
|
74
|
+
{:else if triggerText}
|
|
75
|
+
<Dialog.Trigger>
|
|
76
|
+
{#snippet child({ props })}
|
|
77
|
+
<Button {...props} variant={triggerVariant} size={triggerSize} {disabled}>
|
|
78
|
+
{triggerText}
|
|
79
|
+
</Button>
|
|
80
|
+
{/snippet}
|
|
81
|
+
</Dialog.Trigger>
|
|
82
|
+
{/if}
|
|
83
|
+
|
|
84
|
+
<Dialog.Content class={cn(widthClass, 'max-h-[90vh] overflow-y-auto', className)}>
|
|
85
|
+
<form onsubmit={handleSubmit} class="space-y-4">
|
|
86
|
+
<Dialog.Header>
|
|
87
|
+
<Dialog.Title>{title}</Dialog.Title>
|
|
88
|
+
{#if description}
|
|
89
|
+
<Dialog.Description>{description}</Dialog.Description>
|
|
90
|
+
{/if}
|
|
91
|
+
</Dialog.Header>
|
|
92
|
+
|
|
93
|
+
<div class="py-2 text-sm text-foreground">
|
|
94
|
+
{#if children}
|
|
95
|
+
{@render children()}
|
|
96
|
+
{/if}
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<Dialog.Footer class="gap-2 pt-2 border-t border-border/50">
|
|
100
|
+
<Button
|
|
101
|
+
type="button"
|
|
102
|
+
variant="outline"
|
|
103
|
+
size="sm"
|
|
104
|
+
disabled={loading || isSubmitting}
|
|
105
|
+
onclick={handleCancel}
|
|
106
|
+
>
|
|
107
|
+
{cancelText}
|
|
108
|
+
</Button>
|
|
109
|
+
<Button
|
|
110
|
+
type="submit"
|
|
111
|
+
size="sm"
|
|
112
|
+
disabled={loading || isSubmitting || disabled}
|
|
113
|
+
class="gap-1.5 min-w-20"
|
|
114
|
+
>
|
|
115
|
+
{#if loading || isSubmitting}
|
|
116
|
+
<Loader2 class="h-4 w-4 animate-spin" />
|
|
117
|
+
{/if}
|
|
118
|
+
{submitText}
|
|
119
|
+
</Button>
|
|
120
|
+
</Dialog.Footer>
|
|
121
|
+
</form>
|
|
122
|
+
</Dialog.Content>
|
|
123
|
+
</Dialog.Root>
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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;
|
|
11
|
+
widthClass?: string;
|
|
12
|
+
isSubmitting?: boolean;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
onsubmit?: () => void | Promise<void>;
|
|
15
|
+
oncancel?: () => void;
|
|
16
|
+
class?: string;
|
|
17
|
+
triggerSnippet?: Snippet;
|
|
18
|
+
children?: Snippet;
|
|
19
|
+
}
|
|
20
|
+
declare const ModalForm: import("svelte").Component<Props, {}, "open">;
|
|
9
21
|
type ModalForm = ReturnType<typeof ModalForm>;
|
|
10
22
|
export default ModalForm;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { X, Pin, RefreshCw, XCircle } from '@lucide/svelte';
|
|
3
|
+
import { Button } from './ui/button/index.js';
|
|
4
|
+
import { cn } from '../utils.js';
|
|
5
|
+
|
|
6
|
+
export interface WorkspaceTab {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
path: string;
|
|
10
|
+
closable?: boolean;
|
|
11
|
+
pinned?: boolean;
|
|
12
|
+
icon?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
tabs?: WorkspaceTab[];
|
|
17
|
+
activeTabId?: string;
|
|
18
|
+
onselect?: (tab: WorkspaceTab) => void;
|
|
19
|
+
onclose?: (tabId: string) => void;
|
|
20
|
+
oncloseothers?: (tabId: string) => void;
|
|
21
|
+
oncloseall?: () => void;
|
|
22
|
+
onrefresh?: (tab: WorkspaceTab) => void;
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
tabs = $bindable([]),
|
|
28
|
+
activeTabId = $bindable(''),
|
|
29
|
+
onselect,
|
|
30
|
+
onclose,
|
|
31
|
+
oncloseothers,
|
|
32
|
+
oncloseall,
|
|
33
|
+
onrefresh,
|
|
34
|
+
class: className = '',
|
|
35
|
+
}: Props = $props();
|
|
36
|
+
|
|
37
|
+
function handleSelect(tab: WorkspaceTab) {
|
|
38
|
+
activeTabId = tab.id;
|
|
39
|
+
onselect?.(tab);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleClose(e: MouseEvent, tabId: string) {
|
|
43
|
+
e.stopPropagation();
|
|
44
|
+
const idx = tabs.findIndex((t) => t.id === tabId);
|
|
45
|
+
if (idx === -1) return;
|
|
46
|
+
const isCurrentActive = activeTabId === tabId;
|
|
47
|
+
tabs = tabs.filter((t) => t.id !== tabId);
|
|
48
|
+
onclose?.(tabId);
|
|
49
|
+
|
|
50
|
+
if (isCurrentActive && tabs.length > 0) {
|
|
51
|
+
const nextTab = tabs[Math.min(idx, tabs.length - 1)];
|
|
52
|
+
if (nextTab) {
|
|
53
|
+
activeTabId = nextTab.id;
|
|
54
|
+
onselect?.(nextTab);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleCloseAll() {
|
|
60
|
+
const pinned = tabs.filter((t) => t.pinned);
|
|
61
|
+
tabs = pinned;
|
|
62
|
+
if (pinned.length > 0 && pinned[0]) {
|
|
63
|
+
activeTabId = pinned[0].id;
|
|
64
|
+
onselect?.(pinned[0]);
|
|
65
|
+
}
|
|
66
|
+
oncloseall?.();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handleContextMenu(e: MouseEvent, tabId: string) {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
if (oncloseothers) {
|
|
72
|
+
tabs = tabs.filter((t) => t.pinned || t.id === tabId);
|
|
73
|
+
activeTabId = tabId;
|
|
74
|
+
oncloseothers(tabId);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<div class={cn('flex items-center justify-between border-b border-border/80 bg-muted/30 px-2 text-xs select-none', className)}>
|
|
80
|
+
<!-- Tabs list -->
|
|
81
|
+
<div class="flex items-center gap-1 overflow-x-auto py-1 scrollbar-none">
|
|
82
|
+
{#each tabs as tab (tab.id)}
|
|
83
|
+
{@const isActive = tab.id === activeTabId}
|
|
84
|
+
<div
|
|
85
|
+
role="button"
|
|
86
|
+
tabindex="0"
|
|
87
|
+
class={cn(
|
|
88
|
+
'group relative flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all cursor-pointer border',
|
|
89
|
+
isActive
|
|
90
|
+
? 'bg-card text-foreground border-border shadow-xs'
|
|
91
|
+
: 'bg-transparent text-muted-foreground border-transparent hover:bg-muted/60 hover:text-foreground'
|
|
92
|
+
)}
|
|
93
|
+
onclick={() => handleSelect(tab)}
|
|
94
|
+
onkeydown={(e) => { if (e.key === 'Enter') handleSelect(tab); }}
|
|
95
|
+
oncontextmenu={(e) => handleContextMenu(e, tab.id)}
|
|
96
|
+
>
|
|
97
|
+
{#if tab.pinned}
|
|
98
|
+
<Pin class="h-3 w-3 text-primary shrink-0 rotate-45" />
|
|
99
|
+
{/if}
|
|
100
|
+
|
|
101
|
+
<span class="truncate max-w-32">{tab.title}</span>
|
|
102
|
+
|
|
103
|
+
{#if tab.closable !== false && !tab.pinned}
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
class="flex h-3.5 w-3.5 items-center justify-center rounded-full text-muted-foreground/60 hover:bg-destructive/10 hover:text-destructive transition-colors p-0 border-0 cursor-pointer"
|
|
107
|
+
onclick={(e) => handleClose(e, tab.id)}
|
|
108
|
+
>
|
|
109
|
+
<X class="h-2.5 w-2.5" />
|
|
110
|
+
</button>
|
|
111
|
+
{/if}
|
|
112
|
+
</div>
|
|
113
|
+
{/each}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<!-- Actions (Close All / Refresh) -->
|
|
117
|
+
<div class="flex items-center gap-1 pl-2 shrink-0">
|
|
118
|
+
{#if onrefresh}
|
|
119
|
+
{@const curTab = tabs.find((t) => t.id === activeTabId)}
|
|
120
|
+
{#if curTab}
|
|
121
|
+
<Button
|
|
122
|
+
variant="ghost"
|
|
123
|
+
size="sm"
|
|
124
|
+
class="h-7 w-7 p-0 text-muted-foreground hover:text-foreground"
|
|
125
|
+
onclick={() => onrefresh(curTab)}
|
|
126
|
+
>
|
|
127
|
+
<RefreshCw class="h-3 w-3" />
|
|
128
|
+
</Button>
|
|
129
|
+
{/if}
|
|
130
|
+
{/if}
|
|
131
|
+
|
|
132
|
+
{#if tabs.some((t) => !t.pinned && t.closable !== false)}
|
|
133
|
+
<Button
|
|
134
|
+
variant="ghost"
|
|
135
|
+
size="sm"
|
|
136
|
+
class="h-7 w-7 p-0 text-muted-foreground hover:text-destructive"
|
|
137
|
+
onclick={handleCloseAll}
|
|
138
|
+
>
|
|
139
|
+
<XCircle class="h-3.5 w-3.5" />
|
|
140
|
+
</Button>
|
|
141
|
+
{/if}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface WorkspaceTab {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
path: string;
|
|
5
|
+
closable?: boolean;
|
|
6
|
+
pinned?: boolean;
|
|
7
|
+
icon?: string;
|
|
8
|
+
}
|
|
9
|
+
interface Props {
|
|
10
|
+
tabs?: WorkspaceTab[];
|
|
11
|
+
activeTabId?: string;
|
|
12
|
+
onselect?: (tab: WorkspaceTab) => void;
|
|
13
|
+
onclose?: (tabId: string) => void;
|
|
14
|
+
oncloseothers?: (tabId: string) => void;
|
|
15
|
+
oncloseall?: () => void;
|
|
16
|
+
onrefresh?: (tab: WorkspaceTab) => void;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
declare const MultiTabKeepAlive: import("svelte").Component<Props, {}, "tabs" | "activeTabId">;
|
|
20
|
+
type MultiTabKeepAlive = ReturnType<typeof MultiTabKeepAlive>;
|
|
21
|
+
export default MultiTabKeepAlive;
|