Skip to content

Commit 5469c62

Browse files
committed
refactor: consolidate frontend controls
1 parent 16a2370 commit 5469c62

61 files changed

Lines changed: 496 additions & 433 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/AvatarUpload.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script>
2+
import FileInput from './FileInput.svelte';
23
/** Reusable image-avatar upload control with availability/MIME checks and
34
* fallback tile. Categories require matching backend attachment support. */
45
import { Camera, Trash2, Package } from '@lucide/svelte';
@@ -112,8 +113,7 @@
112113
113114
{#if showUpload && attachmentStatus.enabled}
114115
<div class="p-4 rounded border" style="border-color: var(--ds-border); background-color: var(--ds-surface-raised);">
115-
<input
116-
type="file"
116+
<FileInput
117117
accept="image/*"
118118
onchange={(e) => handleFiles(e.currentTarget.files)}
119119
disabled={uploading}

frontend/src/lib/components/BackgroundImageSelector.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script>
2+
import FileInput from './FileInput.svelte';
23
import { Upload, Trash2 } from '@lucide/svelte';
34
import { backgroundCategories, getPresetsByCategory } from '../utils/backgroundImages.js';
45
import Button from './Button.svelte';
@@ -119,8 +120,7 @@
119120

120121
{#if showUploadSection && attachmentStatus.enabled}
121122
<div class="mt-3 p-4 rounded-lg border" style="border-color: var(--ds-border); background-color: var(--ds-surface);">
122-
<input
123-
type="file"
123+
<FileInput
124124
accept="image/*"
125125
onchange={handleFileChange}
126126
disabled={uploading}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<script>
2+
let {
3+
id = undefined,
4+
name = undefined,
5+
accept = undefined,
6+
multiple = false,
7+
disabled = false,
8+
tabindex = undefined,
9+
ariaHidden = undefined,
10+
class: className = '',
11+
style = '',
12+
dataTestid = undefined,
13+
inputRef = $bindable(null),
14+
onchange = undefined,
15+
} = $props();
16+
export { className as class };
17+
</script>
18+
19+
<input
20+
{id}
21+
{name}
22+
type="file"
23+
{accept}
24+
{multiple}
25+
{disabled}
26+
{tabindex}
27+
aria-hidden={ariaHidden}
28+
bind:this={inputRef}
29+
data-testid={dataTestid}
30+
class={className}
31+
{style}
32+
{onchange}
33+
/>

frontend/src/lib/components/Input.svelte

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,23 @@
1818
* pattern?: string,
1919
* title?: string,
2020
* autocomplete?: string,
21+
* enterkeyhint?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send',
2122
* readonly?: boolean,
2223
* minlength?: number,
2324
* maxlength?: number,
25+
* list?: string,
2426
* class?: string,
27+
* style?: string,
2528
* dataTestid?: string,
29+
* dataAutofocus?: boolean,
2630
* ariaLabel?: string,
2731
* ariaDescribedby?: string,
2832
* inputRef?: any,
2933
* oninput?: (e?: any) => void,
3034
* onchange?: (e?: any) => void,
3135
* onfocus?: (e?: any) => void,
3236
* onblur?: (e?: any) => void,
37+
* onclick?: (e?: any) => void,
3338
* onkeydown?: (e?: any) => void,
3439
* onkeyup?: (e?: any) => void,
3540
* }}
@@ -50,11 +55,15 @@
5055
pattern = undefined,
5156
title = undefined,
5257
autocomplete = undefined,
58+
enterkeyhint = undefined,
5359
readonly = false,
5460
minlength = undefined,
5561
maxlength = undefined,
62+
list = undefined,
5663
class: className = '',
64+
style = '',
5765
dataTestid = undefined,
66+
dataAutofocus = false,
5867
ariaLabel = undefined,
5968
ariaDescribedby = undefined,
6069
// Optional ref binding for parent components that need the raw input element
@@ -64,6 +73,7 @@
6473
onchange = undefined,
6574
onfocus = undefined,
6675
onblur = undefined,
76+
onclick = undefined,
6777
onkeydown = undefined,
6878
onkeyup = undefined
6979
} = $props();
@@ -94,12 +104,15 @@
94104
{pattern}
95105
{title}
96106
autocomplete={autocompleteValue}
107+
{enterkeyhint}
97108
{readonly}
98109
{minlength}
99110
{maxlength}
111+
{list}
100112
bind:value
101113
bind:this={inputRef}
102114
data-testid={dataTestid}
115+
data-autofocus={dataAutofocus || undefined}
103116
aria-label={ariaLabel}
104117
aria-describedby={ariaDescribedby}
105118
{placeholder}
@@ -110,11 +123,12 @@
110123
{max}
111124
{step}
112125
class={allClasses}
113-
style="background-color: var(--ds-background-input); border-color: var(--ds-border); color: var(--ds-text);"
126+
style="background-color: var(--ds-background-input); border-color: var(--ds-border); color: var(--ds-text); {style}"
114127
{oninput}
115128
{onchange}
116129
{onfocus}
117130
{onblur}
131+
{onclick}
118132
{onkeydown}
119133
{onkeyup}
120134
/>

frontend/src/lib/components/LogoUploader.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script>
2+
import FileInput from './FileInput.svelte';
23
import { Upload, Trash2, Image } from '@lucide/svelte';
34
import Button from './Button.svelte';
45
import Label from './Label.svelte';
@@ -57,8 +58,7 @@
5758
<!-- Upload button and input -->
5859
<div class="flex items-center gap-3">
5960
<label class="cursor-pointer">
60-
<input
61-
type="file"
61+
<FileInput
6262
accept="image/png,image/jpeg,image/svg+xml,image/gif,image/webp"
6363
onchange={handleFileChange}
6464
disabled={uploading || !attachmentStatus.enabled}

frontend/src/lib/components/NativeSelect.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
id = undefined,
1313
class: className = '',
1414
dataTestid = undefined,
15+
ariaLabel = undefined,
1516
onchange = undefined,
1617
} = $props();
1718
@@ -45,6 +46,7 @@
4546
{required}
4647
multiple
4748
data-testid={dataTestid}
49+
aria-label={ariaLabel}
4850
class={classes}
4951
style="background-color: var(--ds-background-input); border-color: var(--ds-border); color: var(--ds-text);"
5052
onchange={handleChange}
@@ -60,6 +62,7 @@
6062
{disabled}
6163
{required}
6264
data-testid={dataTestid}
65+
aria-label={ariaLabel}
6366
class={classes}
6467
style="background-color: var(--ds-background-input); border-color: var(--ds-border); color: var(--ds-text);"
6568
onchange={handleChange}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<script>
2+
let {
3+
id = undefined,
4+
name = undefined,
5+
value = undefined,
6+
groupValue = $bindable(null),
7+
checked = false,
8+
disabled = false,
9+
class: className = '',
10+
dataTestid = undefined,
11+
onchange = undefined,
12+
} = $props();
13+
export { className as class };
14+
15+
const isGrouped = $derived(groupValue !== null);
16+
const isChecked = $derived(isGrouped ? groupValue === value : checked);
17+
18+
function handleChange(event) {
19+
if (!event.currentTarget.checked) return;
20+
if (isGrouped) groupValue = value;
21+
onchange?.(value, event);
22+
}
23+
</script>
24+
25+
<input
26+
{id}
27+
type="radio"
28+
{name}
29+
{value}
30+
{disabled}
31+
checked={isChecked}
32+
onchange={handleChange}
33+
data-testid={dataTestid}
34+
class="h-4 w-4 accent-[var(--ds-interactive)] {className}"
35+
/>

frontend/src/lib/components/Textarea.svelte

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@
44
placeholder = '',
55
disabled = false,
66
required = false,
7+
autofocus = false,
78
spellcheck = undefined,
89
rows = 3,
910
size = 'medium',
1011
id = undefined,
1112
class: className = '',
1213
style = '',
1314
'data-testid': dataTestid = undefined,
15+
textareaRef = $bindable(null),
1416
// Svelte 5 event handlers
1517
oninput = undefined,
1618
onchange = undefined,
1719
onfocus = undefined,
1820
onblur = undefined,
19-
onkeydown = undefined
21+
onkeydown = undefined,
22+
onclick = undefined,
23+
...restProps
2024
} = $props();
2125
export { className as class };
2226
@@ -35,12 +39,16 @@
3539
].filter(Boolean).join(' '));
3640
</script>
3741

42+
<!-- svelte-ignore a11y_autofocus -->
3843
<textarea
44+
{...restProps}
3945
{id}
4046
bind:value
47+
bind:this={textareaRef}
4148
{placeholder}
4249
{disabled}
4350
{required}
51+
{autofocus}
4452
{spellcheck}
4553
{rows}
4654
class={allClasses}
@@ -51,4 +59,5 @@
5159
{onfocus}
5260
{onblur}
5361
{onkeydown}
62+
{onclick}
5463
></textarea>

frontend/src/lib/dialogs/CompleteIterationDialog.svelte

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script>
22
import { CheckCircle, X } from '@lucide/svelte';
33
import Button from '../components/Button.svelte';
4+
import Radio from '../components/Radio.svelte';
45
import ModalBackdrop from '../components/ModalBackdrop.svelte';
56
import ItemPicker from '../pickers/ItemPicker.svelte';
67
import { t } from '../stores/i18n.svelte.js';
@@ -135,11 +136,10 @@
135136
136137
<!-- Radio: Move to backlog -->
137138
<label class="flex items-center gap-2 cursor-pointer">
138-
<input
139-
type="radio"
139+
<Radio
140140
name="move-target"
141141
value="backlog"
142-
bind:group={moveTarget}
142+
bind:groupValue={moveTarget}
143143
class="accent-[var(--ds-interactive)]"
144144
/>
145145
<span class="text-sm" style="color: var(--ds-text);">
@@ -149,11 +149,10 @@
149149
150150
<!-- Radio: Move to another iteration -->
151151
<label class="flex items-center gap-2 cursor-pointer">
152-
<input
153-
type="radio"
152+
<Radio
154153
name="move-target"
155154
value="iteration"
156-
bind:group={moveTarget}
155+
bind:groupValue={moveTarget}
157156
class="accent-[var(--ds-interactive)]"
158157
/>
159158
<span class="text-sm" style="color: var(--ds-text);">

frontend/src/lib/dialogs/DeleteItemDialog.svelte

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import AlertBox from '../components/AlertBox.svelte';
77
import FormField from '../components/FormField.svelte';
88
import Input from '../components/Input.svelte';
9+
import Radio from '../components/Radio.svelte';
910
import ItemPicker from '../pickers/ItemPicker.svelte';
1011
import { t } from '../stores/i18n.svelte.js';
1112
import { api } from '../api';
@@ -225,11 +226,10 @@
225226
class="flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors {selectedMode === 'deleteAll' ? 'border-red-500' : ''}"
226227
style="border-color: {selectedMode === 'deleteAll' ? 'var(--ds-border-danger)' : 'var(--ds-border)'}; background-color: {selectedMode === 'deleteAll' ? 'var(--ds-background-danger)' : 'transparent'};"
227228
>
228-
<input
229-
type="radio"
229+
<Radio
230230
name="deleteMode"
231231
value="deleteAll"
232-
bind:group={selectedMode}
232+
bind:groupValue={selectedMode}
233233
disabled={loading}
234234
class="mt-1"
235235
/>
@@ -251,11 +251,10 @@
251251
class="flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors"
252252
style="border-color: {selectedMode === 'reparent' ? 'var(--ds-border-selected)' : 'var(--ds-border)'}; background-color: {selectedMode === 'reparent' ? 'var(--ds-background-selected)' : 'transparent'};"
253253
>
254-
<input
255-
type="radio"
254+
<Radio
256255
name="deleteMode"
257256
value="reparent"
258-
bind:group={selectedMode}
257+
bind:groupValue={selectedMode}
259258
disabled={loading}
260259
class="mt-1"
261260
/>

0 commit comments

Comments
 (0)