Skip to content

Commit eee4601

Browse files
Stefan ErnstStefan Ernst
authored andcommitted
Frontend improvements, keyboard handling, confirm dialog improvement
1 parent 8056df9 commit eee4601

75 files changed

Lines changed: 5990 additions & 3556 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/src/lib/components/AlertBox.svelte

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
import { AlertCircle, CircleCheck, Info, AlertTriangle } from 'lucide-svelte';
33
44
let {
5-
variant = 'error', // 'error', 'warning', 'info', 'success'
5+
variant = 'error', // 'error', 'warning', 'info', 'success', 'neutral'
66
message = '',
77
showIcon = true,
8-
class: className = ''
8+
class: className = '',
9+
children
910
} = $props();
1011
1112
const styles = $derived({
@@ -39,14 +40,20 @@
3940
const IconComponent = $derived(styles?.icon || AlertCircle);
4041
</script>
4142
42-
{#if message}
43+
{#if message || children}
4344
<div
4445
class="px-4 py-3 rounded flex items-start gap-3 {className}"
4546
style="background: var(--ds-surface-raised); border: 1px solid var(--ds-border); border-left: 4px solid {styles?.borderColor}; color: var(--ds-text);"
4647
>
4748
{#if showIcon}
4849
<IconComponent class="w-5 h-5 flex-shrink-0 mt-0.5" style="color: {styles?.iconColor};" />
4950
{/if}
50-
<span class="text-sm">{message}</span>
51+
{#if children}
52+
<div class="text-sm flex-1">
53+
{@render children()}
54+
</div>
55+
{:else}
56+
<span class="text-sm">{message}</span>
57+
{/if}
5158
</div>
5259
{/if}

frontend/src/lib/components/EmptyState.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
title = '',
1919
description = '',
2020
action = null,
21+
hasGradient = false,
2122
class: className = ''
2223
} = $props();
2324
</script>
@@ -28,5 +29,6 @@
2829
{title}
2930
{description}
3031
{action}
32+
{hasGradient}
3133
class={className}
3234
/>

frontend/src/lib/components/StateDisplay.svelte

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,33 @@
3232
action = null, // Snippet for custom action
3333
size = 'md', // For loading spinner: 'sm' | 'md' | 'lg'
3434
inline = false, // For loading: horizontal layout
35+
hasGradient = false, // Whether displayed on a gradient background
3536
class: className = ''
3637
} = $props();
3738
38-
// Computed icon color based on type
39-
const iconColor = $derived({
40-
error: 'var(--ds-icon-danger)',
41-
empty: 'var(--ds-icon-disabled)',
42-
loading: 'var(--ds-icon-subtle)'
43-
}[type] || 'var(--ds-icon-disabled)');
39+
// Computed icon color based on type and gradient
40+
const iconColor = $derived(
41+
hasGradient && type === 'empty'
42+
? 'rgba(255, 255, 255, 0.6)'
43+
: {
44+
error: 'var(--ds-icon-danger)',
45+
empty: 'var(--ds-icon-disabled)',
46+
loading: 'var(--ds-icon-subtle)'
47+
}[type] || 'var(--ds-icon-disabled)'
48+
);
49+
50+
// Text colors based on gradient
51+
const titleColor = $derived(
52+
hasGradient && type === 'empty'
53+
? 'rgba(255, 255, 255, 0.8)'
54+
: type === 'error' ? 'var(--ds-text)' : 'var(--ds-text-subtle)'
55+
);
56+
57+
const messageColor = $derived(
58+
hasGradient && type === 'empty'
59+
? 'rgba(255, 255, 255, 0.6)'
60+
: 'var(--ds-text-subtle)'
61+
);
4462
4563
// Default icons per type
4664
const defaultIcon = $derived({
@@ -89,22 +107,22 @@
89107
{#if title}
90108
<h3
91109
class="text-lg font-medium mb-1"
92-
style="color: {type === 'error' ? 'var(--ds-text)' : 'var(--ds-text-subtle)'};"
110+
style="color: {titleColor};"
93111
>
94112
{title}
95113
</h3>
96114
{:else if type === 'error'}
97-
<h3 class="text-lg font-medium mb-1" style="color: var(--ds-text);">
115+
<h3 class="text-lg font-medium mb-1" style="color: {titleColor};">
98116
{t('components.errorState.title')}
99117
</h3>
100118
{:else if type === 'empty'}
101-
<h3 class="text-lg font-medium mb-1" style="color: var(--ds-text-subtle);">
119+
<h3 class="text-lg font-medium mb-1" style="color: {titleColor};">
102120
{t('common.noData')}
103121
</h3>
104122
{/if}
105123

106124
{#if displayMessage}
107-
<p class="text-sm mb-4" style="color: var(--ds-text-subtle);">{displayMessage}</p>
125+
<p class="text-sm mb-4" style="color: {messageColor};">{displayMessage}</p>
108126
{/if}
109127

110128
{#if action}

frontend/src/lib/components/Tabs.svelte

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
<script>
2-
import { createEventDispatcher } from 'svelte';
3-
4-
const dispatch = createEventDispatcher();
5-
6-
// Props
7-
export let tabs = []; // Array of { id, label, icon?, badge?, className? }
8-
export let activeTab = ''; // Current active tab ID (bindable)
2+
let { tabs = [], activeTab = $bindable(''), onTabChange = null, children } = $props();
93
104
// Initialize activeTab to first tab if not set
11-
$: if (!activeTab && tabs.length > 0) {
12-
activeTab = tabs[0].id;
13-
}
5+
$effect(() => {
6+
if (!activeTab && tabs.length > 0) {
7+
activeTab = tabs[0].id;
8+
}
9+
});
1410
1511
function switchTab(tabId) {
1612
activeTab = tabId;
17-
dispatch('tab-change', { tab: tabId });
13+
if (onTabChange) {
14+
onTabChange({ tab: tabId });
15+
}
1816
}
1917
</script>
2018

@@ -42,6 +40,6 @@
4240

4341
<!-- Tab Content -->
4442
<div class="p-6">
45-
<slot />
43+
{@render children?.()}
4644
</div>
4745
</div>

frontend/src/lib/dialogs/ConfirmDialog.svelte

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { AlertTriangle, X, Trash2, Check } from 'lucide-svelte';
55
import Button from '../components/Button.svelte';
66
import { t } from '../stores/i18n.svelte.js';
7+
import { getShortcut, matchesShortcut } from '../utils/keyboardShortcuts.js';
78
89
const dispatch = createEventDispatcher(); // Keep for backward compatibility
910
@@ -19,20 +20,64 @@
1920
oncancel = null
2021
} = $props();
2122
23+
// Get shortcut configurations
24+
const submitShortcut = getShortcut('modal', 'submit');
25+
const cancelShortcut = getShortcut('modal', 'cancel');
26+
27+
// Focus management
28+
let backdropRef = $state(null);
29+
let previouslyFocusedElement = null;
30+
31+
// Store previously focused element when show becomes true
32+
$effect(() => {
33+
if (show && !previouslyFocusedElement) {
34+
previouslyFocusedElement = document.activeElement;
35+
}
36+
if (!show && previouslyFocusedElement) {
37+
// Restore focus when modal closes
38+
previouslyFocusedElement?.focus();
39+
previouslyFocusedElement = null;
40+
}
41+
});
42+
43+
// Focus backdrop after intro transition completes
44+
function handleIntroEnd() {
45+
backdropRef?.focus();
46+
}
47+
2248
// Use translations for defaults
2349
const resolvedTitle = $derived(title ?? t('common.areYouSure'));
2450
const resolvedMessage = $derived(message ?? t('common.confirmAction'));
2551
const resolvedConfirmText = $derived(confirmText ?? t('common.confirm'));
2652
const resolvedCancelText = $derived(cancelText ?? t('common.cancel'));
2753
28-
// Auto-close on escape key
54+
// Handle keyboard navigation using standard shortcuts
2955
function handleKeydown(event) {
30-
if (event.key === 'Escape' && show) {
56+
// Check for cancel shortcut (Escape)
57+
if (matchesShortcut(event, cancelShortcut)) {
3158
cancel();
59+
return;
60+
}
61+
62+
// Check for submit shortcut (Cmd/Ctrl+Enter)
63+
if (matchesShortcut(event, submitShortcut)) {
64+
event.preventDefault();
65+
doConfirm();
66+
return;
67+
}
68+
69+
// Enter without modifier confirms (unless on cancel button)
70+
if (event.key === 'Enter' && !event.ctrlKey && !event.metaKey) {
71+
const activeElement = document.activeElement;
72+
const isOnCancelButton = activeElement?.textContent?.trim() === resolvedCancelText;
73+
if (!isOnCancelButton) {
74+
event.preventDefault();
75+
doConfirm();
76+
}
3277
}
3378
}
3479
35-
function confirm() {
80+
function doConfirm() {
3681
dispatch('confirm'); // Keep for backward compatibility
3782
onconfirm?.(); // New Svelte 5 style
3883
show = false;
@@ -80,18 +125,20 @@
80125
let styles = $derived(getVariantStyles(variant));
81126
</script>
82127
83-
<svelte:window onkeydown={handleKeydown} />
84-
85128
{#if show}
86129
<!-- Modal backdrop -->
87130
<div
131+
bind:this={backdropRef}
88132
transition:fade={{ duration: 150 }}
89-
class="modal-backdrop fixed inset-0 z-50 flex items-center justify-center p-4"
133+
onintroend={handleIntroEnd}
134+
class="modal-backdrop fixed inset-0 z-50 flex items-center justify-center p-4 focus:outline-none"
90135
onclick={handleBackdropClick}
136+
onkeydown={handleKeydown}
91137
role="dialog"
92138
aria-modal="true"
93139
aria-labelledby="dialog-title"
94140
aria-describedby="dialog-description"
141+
tabindex="-1"
95142
>
96143
<!-- Modal content -->
97144
<div
@@ -142,14 +189,16 @@
142189
variant="default"
143190
onclick={cancel}
144191
size="small"
192+
keyboardHint="Esc"
145193
>
146194
{resolvedCancelText}
147195
</Button>
148196
149197
<Button
150198
variant={styles.buttonVariant}
151-
onclick={confirm}
199+
onclick={doConfirm}
152200
size="small"
201+
keyboardHint=""
153202
>
154203
{resolvedConfirmText}
155204
</Button>

0 commit comments

Comments
 (0)