flowbite-svelte 1.25.0 → 1.27.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/clipboard-manager/ClipboardManager.svelte +491 -0
- package/dist/clipboard-manager/ClipboardManager.svelte.d.ts +4 -0
- package/dist/clipboard-manager/index.d.ts +2 -0
- package/dist/clipboard-manager/index.js +2 -0
- package/dist/clipboard-manager/theme.d.ts +169 -0
- package/dist/clipboard-manager/theme.js +73 -0
- package/dist/command-palette/CommandPalette.svelte +1 -1
- package/dist/command-palette/CommandPalette.svelte.d.ts +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/kanban/KanbanBoard.svelte +2 -1
- package/dist/kanban/KanbanCard.svelte +1 -1
- package/dist/kanban/KanbanCard.svelte.d.ts +1 -1
- package/dist/kanban/theme.js +7 -7
- package/dist/scroll-spy/ScrollSpy.svelte +1 -1
- package/dist/scroll-spy/ScrollSpy.svelte.d.ts +1 -1
- package/dist/spinner/Spinner.svelte +72 -14
- package/dist/spinner/Spinner.svelte.d.ts +2 -1
- package/dist/spinner/theme.d.ts +23 -2
- package/dist/spinner/theme.js +29 -22
- package/dist/split-pane/Divider.svelte +1 -1
- package/dist/split-pane/Divider.svelte.d.ts +1 -1
- package/dist/split-pane/Pane.svelte +1 -1
- package/dist/split-pane/Pane.svelte.d.ts +1 -1
- package/dist/split-pane/SplitPane.svelte +1 -1
- package/dist/split-pane/SplitPane.svelte.d.ts +1 -1
- package/dist/step-indicator/StepIndicator.svelte +1 -1
- package/dist/step-indicator/StepIndicator.svelte.d.ts +1 -1
- package/dist/stepper/BreadcrumbStepper.svelte +1 -1
- package/dist/stepper/BreadcrumbStepper.svelte.d.ts +1 -1
- package/dist/stepper/DetailedStepper.svelte +1 -1
- package/dist/stepper/DetailedStepper.svelte.d.ts +1 -1
- package/dist/stepper/ProgressStepper.svelte +1 -1
- package/dist/stepper/ProgressStepper.svelte.d.ts +1 -1
- package/dist/stepper/Stepper.svelte +1 -1
- package/dist/stepper/Stepper.svelte.d.ts +1 -1
- package/dist/stepper/TimelineStepper.svelte +1 -1
- package/dist/stepper/TimelineStepper.svelte.d.ts +1 -1
- package/dist/stepper/VerticalStepper.svelte +1 -1
- package/dist/stepper/VerticalStepper.svelte.d.ts +1 -1
- package/dist/table/Table.svelte +1 -1
- package/dist/table/Table.svelte.d.ts +1 -1
- package/dist/table/TableBody.svelte +1 -1
- package/dist/table/TableBody.svelte.d.ts +1 -1
- package/dist/table/TableBodyCell.svelte +1 -1
- package/dist/table/TableBodyCell.svelte.d.ts +1 -1
- package/dist/table/TableBodyRow.svelte +1 -1
- package/dist/table/TableBodyRow.svelte.d.ts +1 -1
- package/dist/table/TableHead.svelte +1 -1
- package/dist/table/TableHead.svelte.d.ts +1 -1
- package/dist/table/TableHeadCell.svelte +1 -1
- package/dist/table/TableHeadCell.svelte.d.ts +1 -1
- package/dist/table/TableSearch.svelte +1 -1
- package/dist/table/TableSearch.svelte.d.ts +1 -1
- package/dist/tabs/TabItem.svelte +1 -1
- package/dist/tabs/TabItem.svelte.d.ts +1 -1
- package/dist/tabs/Tabs.svelte +1 -1
- package/dist/tabs/Tabs.svelte.d.ts +1 -1
- package/dist/theme/themes.d.ts +6 -5
- package/dist/theme/themes.js +6 -6
- package/dist/timeline/Activity.svelte +1 -1
- package/dist/timeline/Activity.svelte.d.ts +1 -1
- package/dist/timeline/ActivityItem.svelte +1 -1
- package/dist/timeline/ActivityItem.svelte.d.ts +1 -1
- package/dist/timeline/Group.svelte +1 -1
- package/dist/timeline/Group.svelte.d.ts +1 -1
- package/dist/timeline/GroupItem.svelte +1 -1
- package/dist/timeline/GroupItem.svelte.d.ts +1 -1
- package/dist/timeline/Timeline.svelte +1 -1
- package/dist/timeline/Timeline.svelte.d.ts +1 -1
- package/dist/timeline/TimelineItem.svelte +1 -1
- package/dist/timeline/TimelineItem.svelte.d.ts +1 -1
- package/dist/toast/Toast.svelte +1 -1
- package/dist/toast/Toast.svelte.d.ts +1 -1
- package/dist/tooltip/Tooltip.svelte +1 -1
- package/dist/tooltip/Tooltip.svelte.d.ts +1 -1
- package/dist/types.d.ts +39 -3
- package/dist/typography/a/A.svelte +1 -1
- package/dist/typography/a/A.svelte.d.ts +1 -1
- package/dist/typography/blockquote/Blockquote.svelte +1 -1
- package/dist/typography/blockquote/Blockquote.svelte.d.ts +1 -1
- package/dist/typography/descriptionlist/DescriptionList.svelte +1 -1
- package/dist/typography/descriptionlist/DescriptionList.svelte.d.ts +1 -1
- package/dist/typography/heading/Heading.svelte +1 -1
- package/dist/typography/heading/Heading.svelte.d.ts +1 -1
- package/dist/typography/img/EnhancedImg.svelte +1 -1
- package/dist/typography/img/EnhancedImg.svelte.d.ts +1 -1
- package/dist/typography/img/Img.svelte +1 -1
- package/dist/typography/img/Img.svelte.d.ts +1 -1
- package/dist/typography/layout/Layout.svelte +1 -1
- package/dist/typography/layout/Layout.svelte.d.ts +1 -1
- package/dist/typography/list/Li.svelte +1 -1
- package/dist/typography/list/Li.svelte.d.ts +1 -1
- package/dist/typography/list/List.svelte +1 -1
- package/dist/typography/list/List.svelte.d.ts +1 -1
- package/dist/typography/mark/Mark.svelte +1 -1
- package/dist/typography/mark/Mark.svelte.d.ts +1 -1
- package/dist/typography/paragraph/P.svelte +1 -1
- package/dist/typography/paragraph/P.svelte.d.ts +1 -1
- package/dist/typography/secondary/Secondary.svelte +1 -1
- package/dist/typography/secondary/Secondary.svelte.d.ts +1 -1
- package/dist/typography/span/Span.svelte +1 -1
- package/dist/typography/span/Span.svelte.d.ts +1 -1
- package/dist/utils/Arrow.svelte +1 -1
- package/dist/utils/Arrow.svelte.d.ts +1 -1
- package/dist/utils/Popper.svelte +1 -1
- package/dist/utils/Popper.svelte.d.ts +1 -1
- package/dist/video/Video.svelte +1 -1
- package/dist/video/Video.svelte.d.ts +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Badge, Modal } from "..";
|
|
3
|
+
import { formatDistanceToNow } from "date-fns";
|
|
4
|
+
import { clipboardManager } from "./theme";
|
|
5
|
+
import type { ClipboardItem, ClipboardManagerProps } from "../types";
|
|
6
|
+
import { getTheme } from "../theme/themeUtils";
|
|
7
|
+
import clsx from "clsx";
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
items: initialItems = [],
|
|
11
|
+
placeholder = "Type and save to clipboard",
|
|
12
|
+
saveLabel = "Save",
|
|
13
|
+
clearLabel = "Clear All",
|
|
14
|
+
limit = 20,
|
|
15
|
+
saveToStorage = true,
|
|
16
|
+
toastDuration = 2000,
|
|
17
|
+
filterSensitive = true,
|
|
18
|
+
maxLength = 10000,
|
|
19
|
+
enableSelectionMenu = false,
|
|
20
|
+
selectionTarget = "body",
|
|
21
|
+
showInput = true,
|
|
22
|
+
class: className = "",
|
|
23
|
+
classes,
|
|
24
|
+
storageKey,
|
|
25
|
+
children,
|
|
26
|
+
emptyState,
|
|
27
|
+
open = $bindable(), // If undefined, renders inline; if defined, renders as modal
|
|
28
|
+
badgeProps = { color: "blue", class: "text-xs" },
|
|
29
|
+
modalProps,
|
|
30
|
+
detectSensitiveData
|
|
31
|
+
}: ClipboardManagerProps = $props();
|
|
32
|
+
|
|
33
|
+
const theme = getTheme("clipboardManager");
|
|
34
|
+
|
|
35
|
+
const isModal = $derived(open !== undefined);
|
|
36
|
+
|
|
37
|
+
const styles = $derived(clipboardManager());
|
|
38
|
+
|
|
39
|
+
let items = $state<ClipboardItem[]>(initialItems);
|
|
40
|
+
let newText = $state("");
|
|
41
|
+
let searchQuery = $state("");
|
|
42
|
+
let toast = $state<{ message: string; type: "success" | "error" | "info" } | null>(null);
|
|
43
|
+
|
|
44
|
+
// Selection menu state
|
|
45
|
+
let selectionMenu = $state<{
|
|
46
|
+
show: boolean;
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
text: string;
|
|
50
|
+
}>({ show: false, x: 0, y: 0, text: "" });
|
|
51
|
+
|
|
52
|
+
const STORAGE_KEY = storageKey ?? "flowbite-clipboard-manager";
|
|
53
|
+
|
|
54
|
+
// Save to localStorage whenever items change (but skip the initial load)
|
|
55
|
+
let isFirstLoad = true;
|
|
56
|
+
|
|
57
|
+
$effect(() => {
|
|
58
|
+
if (saveToStorage && typeof window !== "undefined") {
|
|
59
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
60
|
+
if (saved) {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(saved);
|
|
63
|
+
if (initialItems.length === 0) {
|
|
64
|
+
items = parsed;
|
|
65
|
+
} else if (isFirstLoad) {
|
|
66
|
+
// First load: use initial items and save them
|
|
67
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
|
|
68
|
+
}
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error("Failed to parse clipboard data:", e);
|
|
71
|
+
}
|
|
72
|
+
} else if (initialItems.length > 0) {
|
|
73
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
isFirstLoad = false;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
$effect(() => {
|
|
80
|
+
if (saveToStorage && typeof window !== "undefined") {
|
|
81
|
+
if (isFirstLoad) {
|
|
82
|
+
isFirstLoad = false;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// --- Selection Menu Logic ---
|
|
90
|
+
$effect(() => {
|
|
91
|
+
if (!enableSelectionMenu || typeof window === "undefined") return;
|
|
92
|
+
|
|
93
|
+
const targetElements = document.querySelectorAll(selectionTarget);
|
|
94
|
+
const elements = targetElements.length > 0 ? Array.from(targetElements) : [document.body];
|
|
95
|
+
|
|
96
|
+
const handleMouseUp = () => {
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
const selection = window.getSelection();
|
|
99
|
+
const selectedText = selection?.toString().trim();
|
|
100
|
+
|
|
101
|
+
if (selectedText && selectedText.length > 0) {
|
|
102
|
+
const range = selection!.getRangeAt(0);
|
|
103
|
+
const rect = range.getBoundingClientRect();
|
|
104
|
+
|
|
105
|
+
selectionMenu = {
|
|
106
|
+
show: true,
|
|
107
|
+
x: rect.left + rect.width / 2,
|
|
108
|
+
y: rect.top - 10,
|
|
109
|
+
text: selectedText
|
|
110
|
+
};
|
|
111
|
+
} else {
|
|
112
|
+
selectionMenu = { show: false, x: 0, y: 0, text: "" };
|
|
113
|
+
}
|
|
114
|
+
}, 10);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const handleMouseDown = (e: MouseEvent) => {
|
|
118
|
+
// Close menu if clicking outside
|
|
119
|
+
if (selectionMenu.show && !(e.target as HTMLElement).closest(".selection-menu")) {
|
|
120
|
+
selectionMenu = { show: false, x: 0, y: 0, text: "" };
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
elements.forEach((el) => el.addEventListener("mouseup", handleMouseUp));
|
|
125
|
+
document.addEventListener("mousedown", handleMouseDown);
|
|
126
|
+
|
|
127
|
+
return () => {
|
|
128
|
+
elements.forEach((el) => el.removeEventListener("mouseup", handleMouseUp));
|
|
129
|
+
document.removeEventListener("mousedown", handleMouseDown);
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// --- Sensitive data detection ---
|
|
134
|
+
const defaultContainsSensitiveData = (text: string): boolean => {
|
|
135
|
+
if (!filterSensitive) return false;
|
|
136
|
+
|
|
137
|
+
const ccPattern = /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/;
|
|
138
|
+
const passwordPattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d@$!%*?&]{12,}$/;
|
|
139
|
+
const apiKeyPattern = /\b[A-Za-z0-9_-]{32,}\b/;
|
|
140
|
+
const credentialPattern = /(password|passwd|pwd|token|secret|api[_-]?key)[\s:=]/i;
|
|
141
|
+
|
|
142
|
+
return ccPattern.test(text) || passwordPattern.test(text) || apiKeyPattern.test(text) || credentialPattern.test(text);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Use user-provided function if available, otherwise fallback
|
|
146
|
+
const containsSensitiveData = (text: string) => {
|
|
147
|
+
if (typeof detectSensitiveData === "function") {
|
|
148
|
+
return detectSensitiveData(text);
|
|
149
|
+
}
|
|
150
|
+
return defaultContainsSensitiveData(text);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// --- Helpers ---
|
|
154
|
+
const showToast = (message: string, type: "success" | "error" | "info" = "success") => {
|
|
155
|
+
toast = { message, type };
|
|
156
|
+
setTimeout(() => (toast = null), toastDuration);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const sortItems = (itemsList: ClipboardItem[]): ClipboardItem[] => {
|
|
160
|
+
const pinned = itemsList.filter((i) => i.pinned).sort((a, b) => b.timestamp - a.timestamp);
|
|
161
|
+
const unpinned = itemsList.filter((i) => !i.pinned).sort((a, b) => b.timestamp - a.timestamp);
|
|
162
|
+
return [...pinned, ...unpinned];
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const filteredItems = $derived(searchQuery.trim() ? items.filter((i) => i.text.toLowerCase().includes(searchQuery.toLowerCase())) : items);
|
|
166
|
+
|
|
167
|
+
// --- Save from selection menu ---
|
|
168
|
+
const saveSelection = async () => {
|
|
169
|
+
const text = selectionMenu.text;
|
|
170
|
+
if (!text) return;
|
|
171
|
+
|
|
172
|
+
if (text.length > maxLength) {
|
|
173
|
+
showToast(`Text too long (max ${maxLength} characters)`, "error");
|
|
174
|
+
selectionMenu = { show: false, x: 0, y: 0, text: "" };
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (containsSensitiveData(text)) {
|
|
179
|
+
showToast("Sensitive data detected. Not saved for security.", "error");
|
|
180
|
+
selectionMenu = { show: false, x: 0, y: 0, text: "" };
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const duplicate = items.find((i) => i.text === text);
|
|
185
|
+
if (duplicate) {
|
|
186
|
+
showToast("Already saved", "info");
|
|
187
|
+
selectionMenu = { show: false, x: 0, y: 0, text: "" };
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const item: ClipboardItem = {
|
|
192
|
+
id: Date.now(),
|
|
193
|
+
text,
|
|
194
|
+
timestamp: Date.now()
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
items = sortItems([item, ...items]).slice(0, limit);
|
|
198
|
+
showToast("Saved to clipboard manager");
|
|
199
|
+
selectionMenu = { show: false, x: 0, y: 0, text: "" };
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// --- Clipboard actions ---
|
|
203
|
+
const addToClipboard = async () => {
|
|
204
|
+
const trimmed = newText.trim();
|
|
205
|
+
if (!trimmed) return;
|
|
206
|
+
|
|
207
|
+
if (trimmed.length > maxLength) {
|
|
208
|
+
showToast(`Text too long (max ${maxLength} characters)`, "error");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (containsSensitiveData(trimmed)) {
|
|
213
|
+
showToast("Sensitive data detected. Not saved for security.", "error");
|
|
214
|
+
newText = "";
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const duplicate = items.find((i) => i.text === trimmed);
|
|
219
|
+
if (duplicate) {
|
|
220
|
+
showToast("This text is already in your clipboard", "info");
|
|
221
|
+
newText = "";
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const item: ClipboardItem = {
|
|
226
|
+
id: Date.now(),
|
|
227
|
+
text: trimmed,
|
|
228
|
+
timestamp: Date.now()
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
items = sortItems([item, ...items]).slice(0, limit);
|
|
232
|
+
newText = "";
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
await navigator.clipboard.writeText(item.text);
|
|
236
|
+
showToast("Saved and copied to clipboard");
|
|
237
|
+
} catch (e) {
|
|
238
|
+
console.error("Clipboard write failed:", e);
|
|
239
|
+
showToast("Saved but clipboard access denied", "error");
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const copyItem = async (item: ClipboardItem) => {
|
|
244
|
+
try {
|
|
245
|
+
await navigator.clipboard.writeText(item.text);
|
|
246
|
+
showToast("Copied to clipboard");
|
|
247
|
+
} catch (e) {
|
|
248
|
+
showToast("Clipboard access denied", "error");
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const deleteItem = (id: number) => {
|
|
253
|
+
items = items.filter((i) => i.id !== id);
|
|
254
|
+
showToast("Deleted");
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const togglePin = (id: number) => {
|
|
258
|
+
items = items.map((i) => (i.id === id ? { ...i, pinned: !i.pinned } : i));
|
|
259
|
+
items = sortItems(items);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const clearAll = () => {
|
|
263
|
+
if (confirm("Clear all clipboard items?")) {
|
|
264
|
+
items = [];
|
|
265
|
+
showToast("All items cleared");
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const handleKeydown = (e: KeyboardEvent) => {
|
|
270
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
271
|
+
e.preventDefault();
|
|
272
|
+
addToClipboard();
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
</script>
|
|
276
|
+
|
|
277
|
+
{#snippet inputArea()}
|
|
278
|
+
<div class={styles.inputSection({ class: clsx(theme?.inputSection, classes?.inputSection) })}>
|
|
279
|
+
<div class={styles.inputWrapper({ class: clsx(theme?.inputWrapper, classes?.inputWrapper) })}>
|
|
280
|
+
<input type="text" bind:value={newText} onkeydown={handleKeydown} {placeholder} class={styles.input({ class: clsx(theme?.input, classes?.input) })} />
|
|
281
|
+
<button onclick={addToClipboard} disabled={!newText.trim()} class={styles.addToClipboard({ class: clsx(theme?.addToClipboard, classes?.addToClipboard) })}>
|
|
282
|
+
{saveLabel}
|
|
283
|
+
</button>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<!-- Search and Clear -->
|
|
287
|
+
{#if items.length > 0}
|
|
288
|
+
<div class={styles.searchWrapper({ class: clsx(theme?.searchWrapper, classes?.searchWrapper) })}>
|
|
289
|
+
<div class={styles.searchContainer({ class: clsx(theme?.searchContainer, classes?.searchContainer) })}>
|
|
290
|
+
<input type="text" bind:value={searchQuery} placeholder="Search clipboard..." class={styles.searchInput({ class: clsx(theme?.searchInput, classes?.searchInput) })} />
|
|
291
|
+
<svg class={styles.searchIcon({ class: clsx(theme?.searchIcon, classes?.searchIcon) })} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
292
|
+
<circle cx="11" cy="11" r="8" />
|
|
293
|
+
<path d="m21 21-4.35-4.35" />
|
|
294
|
+
</svg>
|
|
295
|
+
</div>
|
|
296
|
+
<button onclick={clearAll} class={styles.clearAll({ class: clsx(theme?.clearAll, classes?.clearAll) })}>
|
|
297
|
+
{clearLabel}
|
|
298
|
+
</button>
|
|
299
|
+
</div>
|
|
300
|
+
{/if}
|
|
301
|
+
</div>
|
|
302
|
+
{/snippet}
|
|
303
|
+
|
|
304
|
+
{#snippet searchClear()}
|
|
305
|
+
<!-- Show only search and clear when input is hidden -->
|
|
306
|
+
<div class={styles.inputSection({ class: clsx(theme?.inputSection, classes?.inputSection) })}>
|
|
307
|
+
<div class={styles.searchWrapper({ class: clsx(theme?.searchWrapper, classes?.searchWrapper) })}>
|
|
308
|
+
<div class={styles.searchContainer({ class: clsx(theme?.searchContainer, classes?.searchContainer) })}>
|
|
309
|
+
<input type="text" bind:value={searchQuery} placeholder="Search clipboard..." class={styles.searchInput({ class: clsx(theme?.searchInput, classes?.searchInput) })} />
|
|
310
|
+
<svg class={styles.searchIcon({ class: clsx(theme?.searchIcon, classes?.searchIcon) })} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
311
|
+
<circle cx="11" cy="11" r="8" />
|
|
312
|
+
<path d="m21 21-4.35-4.35" />
|
|
313
|
+
</svg>
|
|
314
|
+
</div>
|
|
315
|
+
<button onclick={clearAll} class={styles.clearAll({ class: clsx(theme?.clearAll, classes?.clearAll) })}>
|
|
316
|
+
{clearLabel}
|
|
317
|
+
</button>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
{/snippet}
|
|
321
|
+
|
|
322
|
+
{#snippet itemList()}
|
|
323
|
+
<!-- Items list -->
|
|
324
|
+
<div class={styles.itemsList({ class: clsx(theme?.itemsList, classes?.itemsList) })}>
|
|
325
|
+
{#if filteredItems.length === 0}
|
|
326
|
+
{#if emptyState}
|
|
327
|
+
{@render emptyState()}
|
|
328
|
+
{:else}
|
|
329
|
+
<div class={styles.emptyState({ class: clsx(theme?.emptyState, classes?.emptyState) })}>
|
|
330
|
+
{#if items.length === 0}
|
|
331
|
+
<svg class={styles.emptyIcon({ class: clsx(theme?.emptyIcon, classes?.emptyIcon) })} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
332
|
+
<path
|
|
333
|
+
stroke-linecap="round"
|
|
334
|
+
stroke-linejoin="round"
|
|
335
|
+
stroke-width="2"
|
|
336
|
+
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
337
|
+
/>
|
|
338
|
+
</svg>
|
|
339
|
+
<p class={styles.emptyText({ class: clsx(theme?.emptyText, classes?.emptyText) })}>No clipboard items yet.</p>
|
|
340
|
+
<p class={styles.emptySubtext({ class: clsx(theme?.emptySubtext, classes?.emptySubtext) })}>
|
|
341
|
+
{#if enableSelectionMenu}
|
|
342
|
+
Select any text and click "Save" to add it here
|
|
343
|
+
{:else}
|
|
344
|
+
Start typing above to save text
|
|
345
|
+
{/if}
|
|
346
|
+
</p>
|
|
347
|
+
{:else}
|
|
348
|
+
<p class={styles.emptyText({ class: clsx(theme?.emptyText, classes?.emptyText) })}>No items match "{searchQuery}"</p>
|
|
349
|
+
{/if}
|
|
350
|
+
</div>
|
|
351
|
+
{/if}
|
|
352
|
+
{:else}
|
|
353
|
+
{#each filteredItems as item (item.id)}
|
|
354
|
+
{#if children}
|
|
355
|
+
{@render children({ item, copyItem, deleteItem, togglePin })}
|
|
356
|
+
{:else}
|
|
357
|
+
<div class={styles.item({ class: clsx(theme?.item, classes?.item) })}>
|
|
358
|
+
<!-- Content -->
|
|
359
|
+
<div class={styles.itemContent({ class: clsx(theme?.itemContent, classes?.itemContent) })}>
|
|
360
|
+
<div class={styles.itemHeader({ class: clsx(theme?.itemHeader, classes?.itemHeader) })}>
|
|
361
|
+
{#if item.pinned}
|
|
362
|
+
<Badge {...badgeProps}>Pinned</Badge>
|
|
363
|
+
{/if}
|
|
364
|
+
<span class={styles.itemTimestamp({ class: clsx(theme?.itemTimestamp, classes?.itemTimestamp) })}>
|
|
365
|
+
{formatDistanceToNow(item.timestamp, { addSuffix: true })}
|
|
366
|
+
</span>
|
|
367
|
+
</div>
|
|
368
|
+
<p class={styles.itemText({ class: clsx(theme?.itemText, classes?.itemText) })}>
|
|
369
|
+
{item.text}
|
|
370
|
+
</p>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
<!-- Actions -->
|
|
374
|
+
<div class={styles.itemActions({ class: clsx(theme?.itemActions, classes?.itemActions) })}>
|
|
375
|
+
<!-- Copy -->
|
|
376
|
+
<button onclick={() => copyItem(item)} class={styles.actionButton({ class: clsx(theme?.actionButton, classes?.actionButton) })} aria-label="Copy">
|
|
377
|
+
<svg
|
|
378
|
+
class={styles.actionIcon({ class: clsx(theme?.actionIcon, classes?.actionIcon) })}
|
|
379
|
+
fill="none"
|
|
380
|
+
stroke="currentColor"
|
|
381
|
+
stroke-width="2"
|
|
382
|
+
stroke-linecap="round"
|
|
383
|
+
stroke-linejoin="round"
|
|
384
|
+
viewBox="0 0 24 24"
|
|
385
|
+
>
|
|
386
|
+
<rect width="8" height="8" x="8" y="8" rx="2" ry="2" />
|
|
387
|
+
<path d="M4 12V4a2 2 0 0 1 2-2h8" />
|
|
388
|
+
</svg>
|
|
389
|
+
</button>
|
|
390
|
+
|
|
391
|
+
<!-- Pin / Unpin -->
|
|
392
|
+
<button
|
|
393
|
+
onclick={() => togglePin(item.id)}
|
|
394
|
+
class={styles.pinButton({ pinned: item.pinned, class: clsx(theme?.pinButton, classes?.pinButton) })}
|
|
395
|
+
aria-label={item.pinned ? "Unpin" : "Pin"}
|
|
396
|
+
>
|
|
397
|
+
<svg
|
|
398
|
+
class={styles.actionIcon({ class: clsx(theme?.actionIcon, classes?.actionIcon) })}
|
|
399
|
+
fill="none"
|
|
400
|
+
stroke="currentColor"
|
|
401
|
+
stroke-width="2"
|
|
402
|
+
stroke-linecap="round"
|
|
403
|
+
stroke-linejoin="round"
|
|
404
|
+
viewBox="0 0 24 24"
|
|
405
|
+
>
|
|
406
|
+
<path d="M12 17v5" />
|
|
407
|
+
<path d="M8 13h8l1-5h-10z" />
|
|
408
|
+
<path d="M10 3h4v5h-4z" />
|
|
409
|
+
</svg>
|
|
410
|
+
</button>
|
|
411
|
+
|
|
412
|
+
<!-- Delete -->
|
|
413
|
+
<button onclick={() => deleteItem(item.id)} class={styles.deleteButton({ class: clsx(theme?.deleteButton, classes?.deleteButton) })} aria-label="Delete">
|
|
414
|
+
<svg
|
|
415
|
+
class={styles.actionIcon({ class: clsx(theme?.actionIcon, classes?.actionIcon) })}
|
|
416
|
+
fill="none"
|
|
417
|
+
stroke="currentColor"
|
|
418
|
+
stroke-width="2"
|
|
419
|
+
stroke-linecap="round"
|
|
420
|
+
stroke-linejoin="round"
|
|
421
|
+
viewBox="0 0 24 24"
|
|
422
|
+
>
|
|
423
|
+
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14Z" />
|
|
424
|
+
<path d="M10 11v6M14 11v6" />
|
|
425
|
+
</svg>
|
|
426
|
+
</button>
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
{/if}
|
|
430
|
+
{/each}
|
|
431
|
+
{/if}
|
|
432
|
+
</div>
|
|
433
|
+
{/snippet}
|
|
434
|
+
|
|
435
|
+
{#if isModal}
|
|
436
|
+
<Modal title="Clipboard Manager" bind:open {...modalProps}>
|
|
437
|
+
<div class={styles.base({ class: clsx(theme?.base, className) })}>
|
|
438
|
+
{#if showInput}
|
|
439
|
+
{@render inputArea()}
|
|
440
|
+
{:else if items.length > 0}
|
|
441
|
+
{@render searchClear()}
|
|
442
|
+
{/if}
|
|
443
|
+
|
|
444
|
+
{@render itemList()}
|
|
445
|
+
</div>
|
|
446
|
+
</Modal>
|
|
447
|
+
{:else}
|
|
448
|
+
<div class={styles.base({ class: clsx(theme?.base, className) })}>
|
|
449
|
+
{#if showInput}
|
|
450
|
+
{@render inputArea()}
|
|
451
|
+
{:else if items.length > 0}
|
|
452
|
+
{@render searchClear()}
|
|
453
|
+
{/if}
|
|
454
|
+
|
|
455
|
+
{@render itemList()}
|
|
456
|
+
</div>
|
|
457
|
+
{/if}
|
|
458
|
+
|
|
459
|
+
<!-- Selection Bubble Menu -->
|
|
460
|
+
{#if selectionMenu.show}
|
|
461
|
+
<div class={styles.selectionMenu({ class: clsx(theme?.selectionMenu, classes?.selectionMenu) })} style="left: {selectionMenu.x}px; top: {selectionMenu.y}px;">
|
|
462
|
+
<div class={styles.selectionBubble({ class: clsx(theme?.selectionBubble, classes?.selectionBubble) })}>
|
|
463
|
+
<span class={styles.selectionText({ class: clsx(theme?.selectionText, classes?.selectionText) })}>
|
|
464
|
+
{selectionMenu.text.slice(0, 50)}{selectionMenu.text.length > 50 ? "..." : ""}
|
|
465
|
+
</span>
|
|
466
|
+
<button onclick={saveSelection} class={styles.selectionButton()}>Save to Clipboard</button>
|
|
467
|
+
</div>
|
|
468
|
+
<!-- Arrow -->
|
|
469
|
+
<div class={styles.selectionArrow({ class: clsx(theme?.selectionArrow, classes?.selectionArrow) })}></div>
|
|
470
|
+
</div>
|
|
471
|
+
{/if}
|
|
472
|
+
|
|
473
|
+
<!-- Toast notification -->
|
|
474
|
+
{#if toast}
|
|
475
|
+
<div class={styles.toastContainer({ class: clsx(theme?.toastContainer, classes?.toastContainer) })}>
|
|
476
|
+
<div class={styles.toast({ type: toast.type, class: clsx(theme?.toast, classes?.toast) })}>
|
|
477
|
+
<svg class={styles.toastIcon({ class: clsx(theme?.toastIcon, classes?.toastIcon) })} fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
478
|
+
{#if toast.type === "success"}
|
|
479
|
+
<path d="M5 13l4 4L19 7" />
|
|
480
|
+
{:else if toast.type === "error"}
|
|
481
|
+
<circle cx="12" cy="12" r="10" />
|
|
482
|
+
<path d="M15 9l-6 6M9 9l6 6" />
|
|
483
|
+
{:else}
|
|
484
|
+
<circle cx="12" cy="12" r="10" />
|
|
485
|
+
<path d="M12 16v-4M12 8h.01" />
|
|
486
|
+
{/if}
|
|
487
|
+
</svg>
|
|
488
|
+
<span class={styles.toastText({ class: clsx(theme?.toastText, classes?.toastText) })}>{toast.message}</span>
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
491
|
+
{/if}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { type VariantProps } from "tailwind-variants";
|
|
2
|
+
import type { Classes } from "../theme/themeUtils";
|
|
3
|
+
export type ClipboardManagerVariants = VariantProps<typeof clipboardManager> & Classes<typeof clipboardManager>;
|
|
4
|
+
export declare const clipboardManager: import("tailwind-variants").TVReturnType<{
|
|
5
|
+
pinned: {
|
|
6
|
+
true: {
|
|
7
|
+
pinButton: string;
|
|
8
|
+
};
|
|
9
|
+
false: {
|
|
10
|
+
pinButton: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
type: {
|
|
14
|
+
success: {
|
|
15
|
+
toast: string;
|
|
16
|
+
};
|
|
17
|
+
error: {
|
|
18
|
+
toast: string;
|
|
19
|
+
};
|
|
20
|
+
info: {
|
|
21
|
+
toast: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
}, {
|
|
25
|
+
base: string;
|
|
26
|
+
inputSection: string;
|
|
27
|
+
inputWrapper: string;
|
|
28
|
+
input: string;
|
|
29
|
+
searchWrapper: string;
|
|
30
|
+
searchContainer: string;
|
|
31
|
+
searchInput: string;
|
|
32
|
+
searchIcon: string;
|
|
33
|
+
itemsList: string;
|
|
34
|
+
emptyState: string;
|
|
35
|
+
emptyIcon: string;
|
|
36
|
+
emptyText: string;
|
|
37
|
+
emptySubtext: string;
|
|
38
|
+
item: string;
|
|
39
|
+
itemContent: string;
|
|
40
|
+
itemHeader: string;
|
|
41
|
+
itemTimestamp: string;
|
|
42
|
+
itemText: string;
|
|
43
|
+
itemActions: string;
|
|
44
|
+
actionButton: string;
|
|
45
|
+
actionIcon: string;
|
|
46
|
+
pinButton: string;
|
|
47
|
+
deleteButton: string;
|
|
48
|
+
toastContainer: string;
|
|
49
|
+
toast: string;
|
|
50
|
+
toastIcon: string;
|
|
51
|
+
toastText: string;
|
|
52
|
+
addToClipboard: string;
|
|
53
|
+
clearAll: string;
|
|
54
|
+
selectionMenu: string;
|
|
55
|
+
selectionBubble: string;
|
|
56
|
+
selectionText: string;
|
|
57
|
+
selectionButton: string;
|
|
58
|
+
selectionArrow: string;
|
|
59
|
+
}, undefined, {
|
|
60
|
+
pinned: {
|
|
61
|
+
true: {
|
|
62
|
+
pinButton: string;
|
|
63
|
+
};
|
|
64
|
+
false: {
|
|
65
|
+
pinButton: string;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
type: {
|
|
69
|
+
success: {
|
|
70
|
+
toast: string;
|
|
71
|
+
};
|
|
72
|
+
error: {
|
|
73
|
+
toast: string;
|
|
74
|
+
};
|
|
75
|
+
info: {
|
|
76
|
+
toast: string;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
}, {
|
|
80
|
+
base: string;
|
|
81
|
+
inputSection: string;
|
|
82
|
+
inputWrapper: string;
|
|
83
|
+
input: string;
|
|
84
|
+
searchWrapper: string;
|
|
85
|
+
searchContainer: string;
|
|
86
|
+
searchInput: string;
|
|
87
|
+
searchIcon: string;
|
|
88
|
+
itemsList: string;
|
|
89
|
+
emptyState: string;
|
|
90
|
+
emptyIcon: string;
|
|
91
|
+
emptyText: string;
|
|
92
|
+
emptySubtext: string;
|
|
93
|
+
item: string;
|
|
94
|
+
itemContent: string;
|
|
95
|
+
itemHeader: string;
|
|
96
|
+
itemTimestamp: string;
|
|
97
|
+
itemText: string;
|
|
98
|
+
itemActions: string;
|
|
99
|
+
actionButton: string;
|
|
100
|
+
actionIcon: string;
|
|
101
|
+
pinButton: string;
|
|
102
|
+
deleteButton: string;
|
|
103
|
+
toastContainer: string;
|
|
104
|
+
toast: string;
|
|
105
|
+
toastIcon: string;
|
|
106
|
+
toastText: string;
|
|
107
|
+
addToClipboard: string;
|
|
108
|
+
clearAll: string;
|
|
109
|
+
selectionMenu: string;
|
|
110
|
+
selectionBubble: string;
|
|
111
|
+
selectionText: string;
|
|
112
|
+
selectionButton: string;
|
|
113
|
+
selectionArrow: string;
|
|
114
|
+
}, import("tailwind-variants").TVReturnType<{
|
|
115
|
+
pinned: {
|
|
116
|
+
true: {
|
|
117
|
+
pinButton: string;
|
|
118
|
+
};
|
|
119
|
+
false: {
|
|
120
|
+
pinButton: string;
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
type: {
|
|
124
|
+
success: {
|
|
125
|
+
toast: string;
|
|
126
|
+
};
|
|
127
|
+
error: {
|
|
128
|
+
toast: string;
|
|
129
|
+
};
|
|
130
|
+
info: {
|
|
131
|
+
toast: string;
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
}, {
|
|
135
|
+
base: string;
|
|
136
|
+
inputSection: string;
|
|
137
|
+
inputWrapper: string;
|
|
138
|
+
input: string;
|
|
139
|
+
searchWrapper: string;
|
|
140
|
+
searchContainer: string;
|
|
141
|
+
searchInput: string;
|
|
142
|
+
searchIcon: string;
|
|
143
|
+
itemsList: string;
|
|
144
|
+
emptyState: string;
|
|
145
|
+
emptyIcon: string;
|
|
146
|
+
emptyText: string;
|
|
147
|
+
emptySubtext: string;
|
|
148
|
+
item: string;
|
|
149
|
+
itemContent: string;
|
|
150
|
+
itemHeader: string;
|
|
151
|
+
itemTimestamp: string;
|
|
152
|
+
itemText: string;
|
|
153
|
+
itemActions: string;
|
|
154
|
+
actionButton: string;
|
|
155
|
+
actionIcon: string;
|
|
156
|
+
pinButton: string;
|
|
157
|
+
deleteButton: string;
|
|
158
|
+
toastContainer: string;
|
|
159
|
+
toast: string;
|
|
160
|
+
toastIcon: string;
|
|
161
|
+
toastText: string;
|
|
162
|
+
addToClipboard: string;
|
|
163
|
+
clearAll: string;
|
|
164
|
+
selectionMenu: string;
|
|
165
|
+
selectionBubble: string;
|
|
166
|
+
selectionText: string;
|
|
167
|
+
selectionButton: string;
|
|
168
|
+
selectionArrow: string;
|
|
169
|
+
}, undefined, unknown, unknown, undefined>>;
|