@joewinke/jatui 0.1.0 → 0.1.2
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/package.json +4 -1
- package/src/lib/components/EmojiPicker.svelte +157 -0
- package/src/lib/components/LinkShortener.svelte +274 -0
- package/src/lib/components/SearchDropdown.svelte +41 -33
- package/src/lib/components/pipeline/DESIGN.md +361 -0
- package/src/lib/components/pipeline/Pipeline.svelte +391 -0
- package/src/lib/components/pipeline/PipelineCard.svelte +94 -0
- package/src/lib/components/pipeline/PipelineColumn.svelte +158 -0
- package/src/lib/data/emojis.ts +1208 -0
- package/src/lib/index.ts +37 -0
- package/src/lib/types/pipeline.ts +150 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joewinke/jatui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared Svelte 5 component library for JAT projects",
|
|
6
6
|
"type": "module",
|
|
@@ -42,5 +42,8 @@
|
|
|
42
42
|
"tailwindcss": "^4.0.0",
|
|
43
43
|
"typescript": "^5.0.0",
|
|
44
44
|
"vite": "^6.0.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"svelte-dnd-action": "^0.9.69"
|
|
45
48
|
}
|
|
46
49
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* EmojiPicker — Full Unicode emoji selector with search-by-name.
|
|
4
|
+
* Shows a small trigger button with the current emoji, opens a categorized grid dropdown.
|
|
5
|
+
* For assigning emoji icons to items (bases, channels, categories, etc.).
|
|
6
|
+
*/
|
|
7
|
+
import { EMOJI_DATA, searchEmojis } from '../data/emojis';
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
selected = null,
|
|
11
|
+
onSelect,
|
|
12
|
+
size = 'sm',
|
|
13
|
+
}: {
|
|
14
|
+
selected: string | null;
|
|
15
|
+
onSelect: (emoji: string | null) => void;
|
|
16
|
+
size?: 'sm' | 'md';
|
|
17
|
+
} = $props();
|
|
18
|
+
|
|
19
|
+
let open = $state(false);
|
|
20
|
+
let searchQuery = $state('');
|
|
21
|
+
let triggerEl: HTMLButtonElement | undefined = $state(undefined);
|
|
22
|
+
let searchInputEl: HTMLInputElement | undefined = $state(undefined);
|
|
23
|
+
|
|
24
|
+
const searchResults = $derived.by(() => {
|
|
25
|
+
if (!searchQuery.trim()) return null;
|
|
26
|
+
return searchEmojis(searchQuery);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function handleSelect(emoji: string) {
|
|
30
|
+
onSelect(emoji);
|
|
31
|
+
open = false;
|
|
32
|
+
searchQuery = '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function handleClear(e: MouseEvent) {
|
|
36
|
+
e.stopPropagation();
|
|
37
|
+
onSelect(null);
|
|
38
|
+
open = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function handleClickOutside(e: MouseEvent) {
|
|
42
|
+
if (triggerEl && !triggerEl.contains(e.target as Node)) {
|
|
43
|
+
const dropdown = document.querySelector('.emoji-picker-dropdown');
|
|
44
|
+
if (dropdown && !dropdown.contains(e.target as Node)) {
|
|
45
|
+
open = false;
|
|
46
|
+
searchQuery = '';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function handleToggle(e: MouseEvent) {
|
|
52
|
+
e.stopPropagation();
|
|
53
|
+
open = !open;
|
|
54
|
+
if (!open) {
|
|
55
|
+
searchQuery = '';
|
|
56
|
+
} else {
|
|
57
|
+
requestAnimationFrame(() => searchInputEl?.focus());
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const btnSize = $derived(size === 'sm' ? 'w-6 h-6 text-sm' : 'w-8 h-8 text-base');
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<svelte:window onclick={handleClickOutside} />
|
|
65
|
+
|
|
66
|
+
<div class="relative inline-flex">
|
|
67
|
+
<button
|
|
68
|
+
bind:this={triggerEl}
|
|
69
|
+
class="emoji-trigger {btnSize} flex items-center justify-center rounded transition-all duration-100 cursor-pointer"
|
|
70
|
+
style="background: {selected ? 'oklch(0.25 0.02 250)' : 'oklch(0.20 0.01 250)'}; border: 1px solid {open ? 'oklch(0.45 0.10 240)' : 'oklch(0.28 0.02 250)'};"
|
|
71
|
+
onclick={handleToggle}
|
|
72
|
+
title={selected ? `Icon: ${selected} (click to change)` : 'Set icon'}
|
|
73
|
+
>
|
|
74
|
+
{#if selected}
|
|
75
|
+
<span>{selected}</span>
|
|
76
|
+
{:else}
|
|
77
|
+
<span style="color: oklch(0.40 0.02 250); font-size: 0.65em;">+</span>
|
|
78
|
+
{/if}
|
|
79
|
+
</button>
|
|
80
|
+
|
|
81
|
+
{#if open}
|
|
82
|
+
<div
|
|
83
|
+
class="emoji-picker-dropdown absolute left-0 top-full mt-1 z-50 rounded-lg overflow-hidden"
|
|
84
|
+
style="background: oklch(0.18 0.02 250); border: 1px solid oklch(0.30 0.02 250); box-shadow: 0 8px 32px oklch(0 0 0 / 0.5); width: 280px;"
|
|
85
|
+
onclick={(e) => e.stopPropagation()}
|
|
86
|
+
>
|
|
87
|
+
<!-- Search -->
|
|
88
|
+
<div class="px-2 py-1.5" style="border-bottom: 1px solid oklch(0.24 0.01 250);">
|
|
89
|
+
<input
|
|
90
|
+
bind:this={searchInputEl}
|
|
91
|
+
type="text"
|
|
92
|
+
placeholder="Search emojis..."
|
|
93
|
+
bind:value={searchQuery}
|
|
94
|
+
class="w-full px-2 py-1 rounded text-xs border-0 outline-none"
|
|
95
|
+
style="background: oklch(0.22 0.01 250); color: oklch(0.85 0.01 250);"
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<!-- Emoji grid -->
|
|
100
|
+
<div class="max-h-64 overflow-y-auto px-1.5 py-1.5">
|
|
101
|
+
{#if searchResults}
|
|
102
|
+
{#if searchResults.length === 0}
|
|
103
|
+
<div class="text-center py-4 text-xs" style="color: oklch(0.45 0.02 250);">
|
|
104
|
+
No emojis found
|
|
105
|
+
</div>
|
|
106
|
+
{:else}
|
|
107
|
+
<div class="grid grid-cols-8 gap-0.5">
|
|
108
|
+
{#each searchResults as entry}
|
|
109
|
+
<button
|
|
110
|
+
class="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer transition-all duration-75 hover:scale-110"
|
|
111
|
+
style="background: {selected === entry.char ? 'oklch(0.45 0.12 240 / 0.3)' : 'transparent'};"
|
|
112
|
+
onclick={() => handleSelect(entry.char)}
|
|
113
|
+
title={entry.name}
|
|
114
|
+
>
|
|
115
|
+
{entry.char}
|
|
116
|
+
</button>
|
|
117
|
+
{/each}
|
|
118
|
+
</div>
|
|
119
|
+
{/if}
|
|
120
|
+
{:else}
|
|
121
|
+
{#each EMOJI_DATA as group}
|
|
122
|
+
<div class="mb-1.5">
|
|
123
|
+
<div class="text-[9px] font-mono uppercase tracking-wider px-1 py-0.5 sticky top-0" style="color: oklch(0.45 0.02 250); background: oklch(0.18 0.02 250);">
|
|
124
|
+
{group.label}
|
|
125
|
+
</div>
|
|
126
|
+
<div class="grid grid-cols-8 gap-0.5">
|
|
127
|
+
{#each group.emojis as entry}
|
|
128
|
+
<button
|
|
129
|
+
class="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer transition-all duration-75 hover:scale-110"
|
|
130
|
+
style="background: {selected === entry.char ? 'oklch(0.45 0.12 240 / 0.3)' : 'transparent'};"
|
|
131
|
+
onclick={() => handleSelect(entry.char)}
|
|
132
|
+
title={entry.name}
|
|
133
|
+
>
|
|
134
|
+
{entry.char}
|
|
135
|
+
</button>
|
|
136
|
+
{/each}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
{/each}
|
|
140
|
+
{/if}
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<!-- Clear button -->
|
|
144
|
+
{#if selected}
|
|
145
|
+
<div class="px-2 py-1.5" style="border-top: 1px solid oklch(0.24 0.01 250);">
|
|
146
|
+
<button
|
|
147
|
+
class="w-full text-center text-[10px] py-1 rounded cursor-pointer transition-colors duration-100"
|
|
148
|
+
style="color: oklch(0.55 0.02 250); background: transparent;"
|
|
149
|
+
onclick={handleClear}
|
|
150
|
+
>
|
|
151
|
+
Clear icon
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
{/if}
|
|
155
|
+
</div>
|
|
156
|
+
{/if}
|
|
157
|
+
</div>
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export interface ShortenedLink {
|
|
3
|
+
id: string
|
|
4
|
+
slug: string
|
|
5
|
+
destination_url: string
|
|
6
|
+
title?: string | null
|
|
7
|
+
click_count: number
|
|
8
|
+
created_at: string
|
|
9
|
+
expires_at?: string | null
|
|
10
|
+
is_active: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
links: ShortenedLink[]
|
|
15
|
+
baseUrl?: string
|
|
16
|
+
showHeader?: boolean
|
|
17
|
+
onCreate?: (data: { title: string; destination_url: string; slug: string; expires_at: string | null }) => void
|
|
18
|
+
onDelete?: (id: string) => void
|
|
19
|
+
onToggle?: (id: string, is_active: boolean) => void
|
|
20
|
+
class?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let { links, baseUrl = '', showHeader = true, onCreate, onDelete, onToggle, class: className = '' }: Props = $props()
|
|
24
|
+
|
|
25
|
+
let showForm = $state(false)
|
|
26
|
+
let title = $state('')
|
|
27
|
+
let destinationUrl = $state('')
|
|
28
|
+
let customSlug = $state('')
|
|
29
|
+
let expiresAt = $state('')
|
|
30
|
+
let copiedId = $state<string | null>(null)
|
|
31
|
+
let submitting = $state(false)
|
|
32
|
+
|
|
33
|
+
function generateSlug(): string {
|
|
34
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
|
35
|
+
let result = ''
|
|
36
|
+
for (let i = 0; i < 6; i++) {
|
|
37
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
|
38
|
+
}
|
|
39
|
+
return result
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleSubmit(e: Event) {
|
|
43
|
+
e.preventDefault()
|
|
44
|
+
if (!destinationUrl || submitting) return
|
|
45
|
+
|
|
46
|
+
submitting = true
|
|
47
|
+
const slug = customSlug.trim() || generateSlug()
|
|
48
|
+
|
|
49
|
+
onCreate?.({
|
|
50
|
+
title: title.trim(),
|
|
51
|
+
destination_url: destinationUrl.trim(),
|
|
52
|
+
slug,
|
|
53
|
+
expires_at: expiresAt || null
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
title = ''
|
|
57
|
+
destinationUrl = ''
|
|
58
|
+
customSlug = ''
|
|
59
|
+
expiresAt = ''
|
|
60
|
+
showForm = false
|
|
61
|
+
submitting = false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function copyUrl(link: ShortenedLink) {
|
|
65
|
+
const url = `${baseUrl}/l/${link.slug}`
|
|
66
|
+
await navigator.clipboard.writeText(url)
|
|
67
|
+
copiedId = link.id
|
|
68
|
+
setTimeout(() => { copiedId = null }, 2000)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isExpired(link: ShortenedLink): boolean {
|
|
72
|
+
if (!link.expires_at) return false
|
|
73
|
+
return new Date(link.expires_at) < new Date()
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<div class="space-y-4 {className}">
|
|
78
|
+
{#if showHeader}
|
|
79
|
+
<div class="flex items-center justify-between">
|
|
80
|
+
<h2 class="text-xl font-bold">Short Links</h2>
|
|
81
|
+
{#if !showForm}
|
|
82
|
+
<button class="btn btn-primary btn-sm" onclick={() => showForm = true}>
|
|
83
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
84
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
85
|
+
</svg>
|
|
86
|
+
New Link
|
|
87
|
+
</button>
|
|
88
|
+
{/if}
|
|
89
|
+
</div>
|
|
90
|
+
{:else if !showForm}
|
|
91
|
+
<div class="flex justify-end">
|
|
92
|
+
<button class="btn btn-primary btn-sm" onclick={() => showForm = true}>
|
|
93
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
94
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
95
|
+
</svg>
|
|
96
|
+
New Link
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
{/if}
|
|
100
|
+
|
|
101
|
+
{#if showForm}
|
|
102
|
+
<form class="card bg-base-200" onsubmit={handleSubmit}>
|
|
103
|
+
<div class="card-body gap-4">
|
|
104
|
+
<div class="grid sm:grid-cols-2 gap-3">
|
|
105
|
+
<div class="flex flex-col gap-1.5">
|
|
106
|
+
<label class="text-sm font-medium" for="link-title">Title</label>
|
|
107
|
+
<input
|
|
108
|
+
id="link-title"
|
|
109
|
+
type="text"
|
|
110
|
+
class="input input-bordered input-sm w-full"
|
|
111
|
+
placeholder="My Campaign Link"
|
|
112
|
+
bind:value={title}
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="flex flex-col gap-1.5">
|
|
116
|
+
<label class="text-sm font-medium" for="link-destination">
|
|
117
|
+
Destination URL <span class="text-error">*</span>
|
|
118
|
+
</label>
|
|
119
|
+
<input
|
|
120
|
+
id="link-destination"
|
|
121
|
+
type="url"
|
|
122
|
+
class="input input-bordered input-sm w-full"
|
|
123
|
+
placeholder="https://example.com/page"
|
|
124
|
+
bind:value={destinationUrl}
|
|
125
|
+
required
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div class="grid sm:grid-cols-2 gap-3">
|
|
131
|
+
<div class="flex flex-col gap-1.5">
|
|
132
|
+
<label class="text-sm font-medium" for="link-slug">Custom Slug</label>
|
|
133
|
+
<div class="join w-full">
|
|
134
|
+
<span class="join-item flex items-center px-3 bg-base-300 text-xs text-base-content/50 border border-base-content/20">/l/</span>
|
|
135
|
+
<input
|
|
136
|
+
id="link-slug"
|
|
137
|
+
type="text"
|
|
138
|
+
class="input input-bordered input-sm join-item flex-1 min-w-0"
|
|
139
|
+
placeholder="auto-generated"
|
|
140
|
+
bind:value={customSlug}
|
|
141
|
+
pattern="[a-zA-Z0-9_-]+"
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="flex flex-col gap-1.5">
|
|
146
|
+
<label class="text-sm font-medium" for="link-expires">Expires</label>
|
|
147
|
+
<input
|
|
148
|
+
id="link-expires"
|
|
149
|
+
type="date"
|
|
150
|
+
class="input input-bordered input-sm w-full"
|
|
151
|
+
bind:value={expiresAt}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div class="flex justify-end gap-2 pt-1">
|
|
157
|
+
<button type="button" class="btn btn-ghost btn-sm" onclick={() => showForm = false}>Cancel</button>
|
|
158
|
+
<button type="submit" class="btn btn-primary btn-sm" disabled={!destinationUrl || submitting}>
|
|
159
|
+
{#if submitting}
|
|
160
|
+
<span class="loading loading-spinner loading-xs"></span>
|
|
161
|
+
{/if}
|
|
162
|
+
Create Link
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</form>
|
|
167
|
+
{/if}
|
|
168
|
+
|
|
169
|
+
{#if links.length === 0 && !showForm}
|
|
170
|
+
<div class="card bg-base-200">
|
|
171
|
+
<div class="card-body text-center py-8">
|
|
172
|
+
<p class="text-base-content/60">No short links yet. Create your first one above.</p>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
{:else if links.length > 0}
|
|
176
|
+
<div class="overflow-x-auto">
|
|
177
|
+
<table class="table">
|
|
178
|
+
<thead>
|
|
179
|
+
<tr>
|
|
180
|
+
<th>Link</th>
|
|
181
|
+
<th>Destination</th>
|
|
182
|
+
<th class="text-center">Clicks</th>
|
|
183
|
+
<th class="text-center">Status</th>
|
|
184
|
+
<th class="text-right">Actions</th>
|
|
185
|
+
</tr>
|
|
186
|
+
</thead>
|
|
187
|
+
<tbody>
|
|
188
|
+
{#each links as link (link.id)}
|
|
189
|
+
<tr class="hover">
|
|
190
|
+
<td>
|
|
191
|
+
<div>
|
|
192
|
+
{#if link.title}
|
|
193
|
+
<div class="font-medium text-sm">{link.title}</div>
|
|
194
|
+
{/if}
|
|
195
|
+
<div class="text-primary text-sm font-mono">/l/{link.slug}</div>
|
|
196
|
+
</div>
|
|
197
|
+
</td>
|
|
198
|
+
<td>
|
|
199
|
+
<a
|
|
200
|
+
href={link.destination_url}
|
|
201
|
+
target="_blank"
|
|
202
|
+
rel="noopener noreferrer"
|
|
203
|
+
class="link link-hover text-sm max-w-xs truncate block"
|
|
204
|
+
title={link.destination_url}
|
|
205
|
+
>
|
|
206
|
+
{link.destination_url}
|
|
207
|
+
</a>
|
|
208
|
+
</td>
|
|
209
|
+
<td class="text-center">
|
|
210
|
+
<span class="badge badge-ghost badge-sm">{link.click_count}</span>
|
|
211
|
+
</td>
|
|
212
|
+
<td class="text-center">
|
|
213
|
+
{#if isExpired(link)}
|
|
214
|
+
<span class="badge badge-error badge-sm">Expired</span>
|
|
215
|
+
{:else if !link.is_active}
|
|
216
|
+
<span class="badge badge-ghost badge-sm">Inactive</span>
|
|
217
|
+
{:else}
|
|
218
|
+
<span class="badge badge-success badge-sm">Active</span>
|
|
219
|
+
{/if}
|
|
220
|
+
</td>
|
|
221
|
+
<td>
|
|
222
|
+
<div class="flex items-center justify-end gap-1">
|
|
223
|
+
<button
|
|
224
|
+
class="btn btn-ghost btn-xs"
|
|
225
|
+
title="Copy short URL"
|
|
226
|
+
onclick={() => copyUrl(link)}
|
|
227
|
+
>
|
|
228
|
+
{#if copiedId === link.id}
|
|
229
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
230
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
|
231
|
+
</svg>
|
|
232
|
+
{:else}
|
|
233
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
234
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
235
|
+
</svg>
|
|
236
|
+
{/if}
|
|
237
|
+
</button>
|
|
238
|
+
{#if onToggle}
|
|
239
|
+
<button
|
|
240
|
+
class="btn btn-ghost btn-xs"
|
|
241
|
+
title={link.is_active ? 'Deactivate' : 'Activate'}
|
|
242
|
+
onclick={() => onToggle(link.id, !link.is_active)}
|
|
243
|
+
>
|
|
244
|
+
{#if link.is_active}
|
|
245
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
246
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
|
247
|
+
</svg>
|
|
248
|
+
{:else}
|
|
249
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
250
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
251
|
+
</svg>
|
|
252
|
+
{/if}
|
|
253
|
+
</button>
|
|
254
|
+
{/if}
|
|
255
|
+
{#if onDelete}
|
|
256
|
+
<button
|
|
257
|
+
class="btn btn-ghost btn-xs text-error"
|
|
258
|
+
title="Delete link"
|
|
259
|
+
onclick={() => onDelete(link.id)}
|
|
260
|
+
>
|
|
261
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
262
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
263
|
+
</svg>
|
|
264
|
+
</button>
|
|
265
|
+
{/if}
|
|
266
|
+
</div>
|
|
267
|
+
</td>
|
|
268
|
+
</tr>
|
|
269
|
+
{/each}
|
|
270
|
+
</tbody>
|
|
271
|
+
</table>
|
|
272
|
+
</div>
|
|
273
|
+
{/if}
|
|
274
|
+
</div>
|
|
@@ -165,15 +165,24 @@
|
|
|
165
165
|
<style>
|
|
166
166
|
.search-dropdown {
|
|
167
167
|
position: relative;
|
|
168
|
+
--sd-bg: var(--sd-color-bg, oklch(var(--b1)));
|
|
169
|
+
--sd-bg-hover: var(--sd-color-bg-hover, oklch(var(--b2)));
|
|
170
|
+
--sd-bg-selected: var(--sd-color-bg-selected, oklch(var(--b3)));
|
|
171
|
+
--sd-border: var(--sd-color-border, oklch(var(--bc) / 0.2));
|
|
172
|
+
--sd-text: var(--sd-color-text, oklch(var(--bc)));
|
|
173
|
+
--sd-text-muted: var(--sd-color-text-muted, oklch(var(--bc) / 0.5));
|
|
174
|
+
--sd-text-label: var(--sd-color-text-label, oklch(var(--p) / 0.7));
|
|
175
|
+
--sd-accent: var(--sd-color-accent, oklch(var(--p)));
|
|
176
|
+
--sd-success: var(--sd-color-success, oklch(var(--su)));
|
|
168
177
|
}
|
|
169
178
|
|
|
170
179
|
/* Trigger button */
|
|
171
180
|
.sd-trigger {
|
|
172
181
|
width: 100%;
|
|
173
182
|
padding: 0.25rem 0.5rem;
|
|
174
|
-
border-radius: 0.5rem;
|
|
175
|
-
font-family:
|
|
176
|
-
font-size:
|
|
183
|
+
border-radius: var(--rounded-btn, 0.5rem);
|
|
184
|
+
font-family: inherit;
|
|
185
|
+
font-size: inherit;
|
|
177
186
|
text-align: left;
|
|
178
187
|
display: flex;
|
|
179
188
|
align-items: center;
|
|
@@ -182,13 +191,12 @@
|
|
|
182
191
|
transition: background 0.15s, border-color 0.15s;
|
|
183
192
|
min-height: 2rem;
|
|
184
193
|
cursor: pointer;
|
|
185
|
-
background:
|
|
186
|
-
border: 1px solid
|
|
187
|
-
color:
|
|
194
|
+
background: var(--sd-bg);
|
|
195
|
+
border: 1px solid var(--sd-border);
|
|
196
|
+
color: var(--sd-text);
|
|
188
197
|
}
|
|
189
198
|
.sd-trigger:hover:not(:disabled) {
|
|
190
|
-
background:
|
|
191
|
-
border-color: oklch(0.30 0.02 250);
|
|
199
|
+
background: var(--sd-bg-hover);
|
|
192
200
|
}
|
|
193
201
|
.sd-trigger:disabled, .sd-disabled {
|
|
194
202
|
opacity: 0.5;
|
|
@@ -211,7 +219,7 @@
|
|
|
211
219
|
height: 0.75rem;
|
|
212
220
|
flex-shrink: 0;
|
|
213
221
|
transition: transform 0.15s;
|
|
214
|
-
color:
|
|
222
|
+
color: var(--sd-text-muted);
|
|
215
223
|
}
|
|
216
224
|
.sd-chevron-open {
|
|
217
225
|
transform: rotate(180deg);
|
|
@@ -224,17 +232,17 @@
|
|
|
224
232
|
margin-top: 0.25rem;
|
|
225
233
|
width: 100%;
|
|
226
234
|
min-width: 12rem;
|
|
227
|
-
border-radius: 0.5rem;
|
|
235
|
+
border-radius: var(--rounded-box, 0.5rem);
|
|
228
236
|
overflow: hidden;
|
|
229
|
-
box-shadow: 0 4px 24px oklch(0 0 0 / 0.
|
|
230
|
-
background:
|
|
231
|
-
border: 1px solid
|
|
237
|
+
box-shadow: 0 4px 24px oklch(0 0 0 / 0.15);
|
|
238
|
+
background: var(--sd-bg);
|
|
239
|
+
border: 1px solid var(--sd-border);
|
|
232
240
|
}
|
|
233
241
|
|
|
234
242
|
/* Search section */
|
|
235
243
|
.sd-search {
|
|
236
244
|
padding: 0.375rem 0.625rem;
|
|
237
|
-
border-bottom: 1px solid
|
|
245
|
+
border-bottom: 1px solid var(--sd-border);
|
|
238
246
|
}
|
|
239
247
|
.sd-search-inner {
|
|
240
248
|
display: flex;
|
|
@@ -245,22 +253,22 @@
|
|
|
245
253
|
width: 0.75rem;
|
|
246
254
|
height: 0.75rem;
|
|
247
255
|
flex-shrink: 0;
|
|
248
|
-
color:
|
|
256
|
+
color: var(--sd-text-muted);
|
|
249
257
|
}
|
|
250
258
|
.sd-search-input {
|
|
251
259
|
width: 100%;
|
|
252
260
|
background: transparent;
|
|
253
|
-
font-size:
|
|
254
|
-
font-family:
|
|
255
|
-
color:
|
|
261
|
+
font-size: inherit;
|
|
262
|
+
font-family: inherit;
|
|
263
|
+
color: var(--sd-text);
|
|
256
264
|
border: none;
|
|
257
265
|
outline: none;
|
|
258
266
|
}
|
|
259
267
|
.sd-search-input::placeholder {
|
|
260
|
-
color:
|
|
268
|
+
color: var(--sd-text-muted);
|
|
261
269
|
}
|
|
262
270
|
.sd-search-clear {
|
|
263
|
-
color:
|
|
271
|
+
color: var(--sd-text-muted);
|
|
264
272
|
background: none;
|
|
265
273
|
border: none;
|
|
266
274
|
cursor: pointer;
|
|
@@ -284,12 +292,12 @@
|
|
|
284
292
|
padding: 0.375rem 0.75rem 0.125rem;
|
|
285
293
|
}
|
|
286
294
|
.sd-group-label span {
|
|
287
|
-
font-size: 0.
|
|
288
|
-
font-family:
|
|
295
|
+
font-size: 0.75em;
|
|
296
|
+
font-family: inherit;
|
|
289
297
|
font-weight: 600;
|
|
290
298
|
text-transform: uppercase;
|
|
291
299
|
letter-spacing: 0.05em;
|
|
292
|
-
color:
|
|
300
|
+
color: var(--sd-text-label);
|
|
293
301
|
}
|
|
294
302
|
|
|
295
303
|
.sd-option {
|
|
@@ -299,21 +307,21 @@
|
|
|
299
307
|
align-items: center;
|
|
300
308
|
gap: 0.5rem;
|
|
301
309
|
text-align: left;
|
|
302
|
-
font-size:
|
|
303
|
-
font-family:
|
|
310
|
+
font-size: inherit;
|
|
311
|
+
font-family: inherit;
|
|
304
312
|
transition: background 0.1s;
|
|
305
313
|
cursor: pointer;
|
|
306
314
|
background: transparent;
|
|
307
315
|
border: none;
|
|
308
316
|
border-left: 2px solid transparent;
|
|
309
|
-
color:
|
|
317
|
+
color: var(--sd-text);
|
|
310
318
|
}
|
|
311
319
|
.sd-option:hover {
|
|
312
|
-
background:
|
|
320
|
+
background: var(--sd-bg-hover);
|
|
313
321
|
}
|
|
314
322
|
.sd-option-selected {
|
|
315
|
-
background:
|
|
316
|
-
border-left-color:
|
|
323
|
+
background: var(--sd-bg-selected);
|
|
324
|
+
border-left-color: var(--sd-accent);
|
|
317
325
|
}
|
|
318
326
|
|
|
319
327
|
.sd-option-icon {
|
|
@@ -328,14 +336,14 @@
|
|
|
328
336
|
height: 0.75rem;
|
|
329
337
|
flex-shrink: 0;
|
|
330
338
|
margin-left: auto;
|
|
331
|
-
color:
|
|
339
|
+
color: var(--sd-success);
|
|
332
340
|
}
|
|
333
341
|
|
|
334
342
|
.sd-empty {
|
|
335
343
|
padding: 0.75rem;
|
|
336
344
|
text-align: center;
|
|
337
|
-
font-size:
|
|
338
|
-
font-family:
|
|
339
|
-
color:
|
|
345
|
+
font-size: inherit;
|
|
346
|
+
font-family: inherit;
|
|
347
|
+
color: var(--sd-text-muted);
|
|
340
348
|
}
|
|
341
349
|
</style>
|