Skip to content

Commit 86e98cc

Browse files
committed
refactor(frontend): adopt Badge and Modal primitives and finish stable test ids
Sweep the remaining screens: replace hand-rolled modal shells and backdrop keydown handling with the Modal component, inline count pills with Badge, finish the StateDisplay and TextField conversions where they share files with other changes, and wire stable data-testids.
1 parent bc54927 commit 86e98cc

21 files changed

Lines changed: 174 additions & 261 deletions

frontend/src/lib/features/agents/AgentRunnerSetup.svelte

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
<script>
22
import { onDestroy } from 'svelte';
33
import {
4-
IconCheck as Check,
5-
IconCopy as Copy,
6-
IconKey as Key,
74
IconRefresh as Refresh,
85
IconServer as Server,
96
IconX as X,
107
} from '@tabler/icons-svelte-runes';
118
import { api } from '../../api.js';
129
import AlertBox from '../../components/AlertBox.svelte';
10+
import CopyButton from '../../components/CopyButton.svelte';
1311
import Badge from '../../components/Badge.svelte';
1412
import Button from '../../components/Button.svelte';
1513
import Card from '../../components/Card.svelte';
@@ -35,7 +33,6 @@
3533
let error = $state('');
3634
let installCommand = $state('');
3735
let plaintextToken = $state('');
38-
let copied = $state('');
3936
let setupStatus = $state('idle');
4037
let runnerName = $state('');
4138
let pollTimer = null;
@@ -187,7 +184,6 @@
187184
if (!canMint) return;
188185
minting = true;
189186
error = '';
190-
copied = '';
191187
try {
192188
if (pendingTokenId) await revokePending();
193189
const instances = await api.runnerPools.listWorkspaceInstances(
@@ -226,18 +222,6 @@
226222
cancelling = false;
227223
}
228224
}
229-
230-
async function copy(value, kind) {
231-
try {
232-
await navigator.clipboard.writeText(value);
233-
copied = kind;
234-
window.setTimeout(() => {
235-
if (copied === kind) copied = '';
236-
}, 2_000);
237-
} catch {
238-
error = 'Clipboard access is unavailable. Select and copy the value manually.';
239-
}
240-
}
241225
</script>
242226
243227
<Card variant="outlined" padding="default" dataTestid="agent-runner-setup">
@@ -306,15 +290,13 @@
306290
<span class="text-sm font-medium" style="color: var(--ds-text);">
307291
Run on the runner machine
308292
</span>
309-
<Button
310-
variant="secondary"
311-
size="small"
312-
icon={copied === 'command' ? Check : Copy}
293+
<CopyButton
294+
getText={() => installCommand}
295+
label="Copy command"
296+
copiedLabel="Copied"
313297
dataTestid="agent-runner-copy-command"
314-
onclick={() => copy(installCommand, 'command')}
315-
>
316-
{copied === 'command' ? 'Copied' : 'Copy command'}
317-
</Button>
298+
onError={() => (error = 'Clipboard access is unavailable. Select and copy the value manually.')}
299+
/>
318300
</div>
319301
<code
320302
class="block max-h-40 overflow-auto whitespace-pre-wrap break-all rounded-md border p-3 text-xs"
@@ -327,15 +309,13 @@
327309
<span class="text-sm font-medium" style="color: var(--ds-text);">
328310
One-time registration token
329311
</span>
330-
<Button
331-
variant="secondary"
332-
size="small"
333-
icon={copied === 'token' ? Check : Key}
312+
<CopyButton
313+
getText={() => plaintextToken}
314+
label="Copy token"
315+
copiedLabel="Copied"
334316
dataTestid="agent-runner-copy-token"
335-
onclick={() => copy(plaintextToken, 'token')}
336-
>
337-
{copied === 'token' ? 'Copied' : 'Copy token'}
338-
</Button>
317+
onError={() => (error = 'Clipboard access is unavailable. Select and copy the value manually.')}
318+
/>
339319
</div>
340320
<code
341321
class="block overflow-auto break-all rounded-md border p-3 text-xs"

frontend/src/lib/features/channels/Channels.svelte

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script>
22
import { onMount } from 'svelte';
3+
import StateDisplay from '../../components/StateDisplay.svelte';
34
import { useEventListener } from 'runed';
45
import { t } from '../../stores/i18n.svelte.js';
56
import { IconLifebuoy, IconPlus, IconTrash, IconSettings, IconSearch, IconTag, IconPower, IconFileText } from '@tabler/icons-svelte-runes';
@@ -18,7 +19,6 @@
1819
import { errorToast, successToast } from '../../stores/toasts.svelte.js';
1920
import Lozenge from '../../components/Lozenge.svelte';
2021
import DataTable from '../../components/DataTable.svelte';
21-
import Spinner from '../../components/Spinner.svelte';
2222
import ChannelNavigation from './ChannelNavigation.svelte';
2323
import { channelTypes as channelTypeDefs, allTypesEntry, getChannelTypeIcon } from './channelTypes.js';
2424
import { channelAdminRoute } from './channelRoutes.js';
@@ -29,6 +29,7 @@
2929
import DescriptionText from '../../components/DescriptionText.svelte';
3030
import DialogFooter from '../../dialogs/DialogFooter.svelte';
3131
import { isSystemAdmin } from '../../stores/permissions.svelte.js';
32+
import TextField from '../../components/TextField.svelte';
3233
3334
// Props
3435
let { embedded = false } = $props();
@@ -558,9 +559,7 @@
558559
559560
<!-- Data Table -->
560561
{#if loading}
561-
<div class="flex items-center justify-center py-16">
562-
<Spinner />
563-
</div>
562+
<StateDisplay type="loading" />
564563
{:else if error}
565564
<div class="text-center py-16">
566565
<div class="text-ds-text-danger text-sm font-medium mb-2">{error}</div>
@@ -635,12 +634,13 @@
635634
<div class="p-6">
636635
<div class="space-y-4">
637636
<div>
638-
<Label for="channelName" required color="default" class="mb-2">Channel Name</Label>
639-
<Input
637+
<TextField
638+
label={"Channel Name"}
640639
id="channelName"
641-
bind:value={channelFormData.name}
642640
required
641+
labelColor="default"
643642
placeholder="e.g., Customer Support Portal"
643+
bind:value={channelFormData.name}
644644
/>
645645
</div>
646646

frontend/src/lib/features/channels/ManagerChannels.svelte

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<script>
22
import { onMount } from 'svelte';
3+
import StateDisplay from '../../components/StateDisplay.svelte';
34
import { IconLifebuoy } from '@tabler/icons-svelte-runes';
45
import { api } from '../../api.js';
56
import Button from '../../components/Button.svelte';
67
import DataTable from '../../components/DataTable.svelte';
78
import Lozenge from '../../components/Lozenge.svelte';
89
import SearchInput from '../../components/SearchInput.svelte';
9-
import Spinner from '../../components/Spinner.svelte';
1010
import PageHeader from '../../layout/PageHeader.svelte';
1111
import { navigate } from '../../router.js';
1212
import { t } from '../../stores/i18n.svelte.js';
@@ -114,9 +114,7 @@
114114
</Button>
115115
</div>
116116
{:else if loading}
117-
<div class="flex justify-center py-16" data-testid="manager-channels-loading">
118-
<Spinner />
119-
</div>
117+
<StateDisplay type="loading" dataTestid="manager-channels-loading" />
120118
{:else}
121119
<DataTable
122120
{columns}
Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,20 @@
11
<script>
2-
import { Copy, Check } from '@lucide/svelte';
32
import MilkdownEditor from '../../editors/LazyMilkdownEditor.svelte';
3+
import CopyButton from '../../components/CopyButton.svelte';
44
55
let { briefing = '', itemKey = '' } = $props();
6-
7-
let copied = $state(false);
8-
9-
async function copyToClipboard() {
10-
try {
11-
await navigator.clipboard.writeText(briefing);
12-
copied = true;
13-
setTimeout(() => { copied = false; }, 2000);
14-
} catch (err) {
15-
console.error('Failed to copy:', err);
16-
}
17-
}
186
</script>
197

208
<div class="space-y-3">
219
<div class="flex items-center justify-between">
2210
<span class="text-xs font-medium px-2 py-1 rounded" style="background-color: var(--ds-background-neutral); color: var(--ds-text-subtle);">
2311
{itemKey}
2412
</span>
25-
<button
26-
class="copy-btn inline-flex items-center gap-1.5 px-2 py-1 text-xs rounded transition-colors"
27-
onclick={copyToClipboard}
28-
>
29-
{#if copied}
30-
<Check class="w-3.5 h-3.5" style="color: var(--ds-icon-success);" />
31-
<span style="color: var(--ds-text-success);">Copied</span>
32-
{:else}
33-
<Copy class="w-3.5 h-3.5" />
34-
Copy
35-
{/if}
36-
</button>
13+
<CopyButton getText={() => briefing} label="Copy" copiedLabel="Copied" size="sm" title="Copy briefing" />
3714
</div>
3815

3916
<div class="prose-sm max-w-none text-sm leading-relaxed" style="color: var(--ds-text);">
4017
<MilkdownEditor content={briefing} readonly={true} showToolbar={false} compact={true} />
4118
</div>
4219
</div>
4320

44-
<style>
45-
.copy-btn {
46-
color: var(--ds-text-subtle);
47-
}
48-
49-
.copy-btn:hover {
50-
background-color: var(--ds-background-neutral-hovered);
51-
}
52-
</style>

frontend/src/lib/features/items/ItemDetail.svelte

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script>
22
import { onMount, onDestroy, untrack } from 'svelte';
3+
import { copyToClipboard } from '../../utils/clipboard.js';
34
import { useEventListener } from 'runed';
45
import { api } from '../../api.js';
56
import { navigate, currentRoute } from '../../router.js';
@@ -313,13 +314,10 @@ import NativeSelect from '../../components/NativeSelect.svelte';
313314
}
314315
315316
async function handleCopyKey() {
316-
try {
317-
const key = `${item.workspace_key || workspace?.key || 'WORK'}-${item.workspace_item_number}`;
318-
await navigator.clipboard.writeText(key);
319-
showCopySuccess(key);
320-
} catch (error) {
321-
console.error('[handleCopyKey] Failed to copy key to clipboard:', error);
322-
}
317+
const key = `${item.workspace_key || workspace?.key || 'WORK'}-${item.workspace_item_number}`;
318+
const ok = await copyToClipboard(key);
319+
if (!ok) console.error('[handleCopyKey] Failed to copy key to clipboard');
320+
else showCopySuccess(key);
323321
}
324322
325323
function showCopySuccess(key) {
@@ -1071,13 +1069,9 @@ import NativeSelect from '../../components/NativeSelect.svelte';
10711069
keywords: ['copy', 'link', 'share', 'url'],
10721070
action: async () => {
10731071
const url = `${publicBaseURL()}/workspaces/${workspaceId}/items/${itemId}`;
1074-
try {
1075-
await navigator.clipboard.writeText(url);
1076-
successToast(t('items.itemLinkCopied'));
1077-
} catch (error) {
1078-
console.error('Failed to copy to clipboard:', error);
1079-
errorToast(t('items.failedToCopyToClipboard'));
1080-
}
1072+
const ok = await copyToClipboard(url);
1073+
if (ok) successToast(t('items.itemLinkCopied'));
1074+
else errorToast(t('items.failedToCopyToClipboard'));
10811075
},
10821076
priority: COMMAND_PRIORITIES.NORMAL,
10831077
category: 'action'

frontend/src/lib/features/items/TodoList.svelte

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script>
22
import { onMount } from 'svelte';
3+
import Badge from '../../components/Badge.svelte';
34
import { slide } from 'svelte/transition';
45
import { api } from '../../api.js';
56
import { Plus, Check, X, Trash2, ChevronDown, ChevronRight } from '@lucide/svelte';
@@ -389,9 +390,7 @@
389390
{/if}
390391
</span>
391392
<span class="font-semibold text-sm" style="color: var(--ds-text);">{t('todo.myPersonalTasks')}</span>
392-
<span class="ml-auto text-xs px-2 py-0.5 rounded-full" style="background-color: var(--ds-surface-raised); color: var(--ds-text-subtle);">
393-
{personalTodos.length} {personalTodos.length === 1 ? 'item' : 'items'}
394-
</span>
393+
<Badge size="sm" class="ml-auto">{personalTodos.length} {personalTodos.length === 1 ? 'item' : 'items'}</Badge>
395394
</button>
396395
397396
{#if !personalCollapsed}
@@ -506,9 +505,7 @@
506505
{/if}
507506
</span>
508507
<span class="font-semibold text-sm" style="color: var(--ds-text);">{t('todo.assignedToMe')}</span>
509-
<span class="ml-auto text-xs px-2 py-0.5 rounded-full" style="background-color: var(--ds-surface-raised); color: var(--ds-text-subtle);">
510-
{assignedWork.length} {assignedWork.length === 1 ? 'item' : 'items'}
511-
</span>
508+
<Badge size="sm" class="ml-auto">{assignedWork.length} {assignedWork.length === 1 ? 'item' : 'items'}</Badge>
512509
</button>
513510
514511
{#if !assignedCollapsed}

frontend/src/lib/features/items/ZammadItemPanel.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script>
22
import { useEventListener } from 'runed';
3+
import Spinner from '../../components/Spinner.svelte';
34
import {
45
TicketCheck,
56
Plus,
@@ -516,7 +517,7 @@
516517
</div>
517518
518519
{#if loading}
519-
<div class="flex justify-center py-3"><Loader2 class="w-4 h-4 animate-spin" /></div>
520+
<div class="flex justify-center py-3"><Spinner size="sm" /></div>
520521
{:else if error}
521522
<p class="text-xs" style="color: var(--ds-text-danger);">{error}</p>
522523
{:else}
@@ -639,7 +640,7 @@
639640
<p class="text-sm" style="color: var(--ds-text-danger);">{editError}</p>
640641
{/if}
641642
{#if loadingEditMetadata}
642-
<div class="flex justify-center py-3"><Loader2 class="w-4 h-4 animate-spin" /></div>
643+
<div class="flex justify-center py-3"><Spinner size="sm" /></div>
643644
{:else}
644645
<FormField label={t('zammad.status')}>
645646
<NativeSelect

frontend/src/lib/features/iterations/IterationDetail.svelte

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script>
22
import { onMount } from 'svelte';
3+
import StateDisplay from '../../components/StateDisplay.svelte';
34
import { IconArrowLeft, IconCalendar, IconTarget, IconEdit, IconTrash, IconDots, IconWorld, IconBuilding, IconSparkles, IconCircleCheck } from '@tabler/icons-svelte-runes';
45
import IterationBurndown from './IterationBurndown.svelte';
56
import { api } from '../../api.js';
@@ -24,6 +25,7 @@
2425
import { aiStore } from '../../stores/aiStore.svelte.js';
2526
import { permissionStore, isSystemAdmin } from '../../stores/permissions.svelte.js';
2627
import { workspacePermissions } from '../../stores/workspacePermissions.svelte.js';
28+
import TextField from '../../components/TextField.svelte';
2729
2830
let { iterationId, workspaceId = null } = $props();
2931
@@ -275,9 +277,7 @@
275277
</div>
276278
277279
{#if loading}
278-
<div class="flex items-center justify-center py-20">
279-
<div class="animate-spin rounded-full h-8 w-8 border-b-2" style="border-color: var(--ds-text-subtle);"></div>
280-
</div>
280+
<StateDisplay type="loading" />
281281
{:else if error}
282282
<div class="text-center py-20">
283283
<p class="text-ds-text-danger">{error}</p>
@@ -401,22 +401,22 @@
401401
</div>
402402
403403
<div>
404-
<Label for="iteration-start-date" required class="mb-2">{t('iterations.startDate')}</Label>
405-
<Input
404+
<TextField
405+
label={t('iterations.startDate')}
406406
id="iteration-start-date"
407+
required
407408
type="date"
408409
bind:value={formData.start_date}
409-
required
410410
/>
411411
</div>
412412
413413
<div>
414-
<Label for="iteration-end-date" required class="mb-2">{t('iterations.endDate')}</Label>
415-
<Input
414+
<TextField
415+
label={t('iterations.endDate')}
416416
id="iteration-end-date"
417+
required
417418
type="date"
418419
bind:value={formData.end_date}
419-
required
420420
/>
421421
</div>
422422

0 commit comments

Comments
 (0)