Skip to content

Commit de2f5e3

Browse files
alexdlndanielroe
andauthored
feat: add input component (#1173)
Co-authored-by: Daniel Roe <daniel@roe.dev>
1 parent 840f3d4 commit de2f5e3

File tree

18 files changed

+343
-92
lines changed

18 files changed

+343
-92
lines changed

app/components/AppHeader.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,12 @@ onKeyStroke(
122122

123123
<!-- Center: Search bar + nav items -->
124124
<div
125-
class="flex-1 flex items-center justify-center md:gap-6"
126-
:class="{ 'hidden sm:flex': !isSearchExpanded }"
125+
class="flex-1 flex items-center md:gap-6"
126+
:class="{
127+
'hidden sm:flex': !isSearchExpanded,
128+
'justify-end': isOnHomePage,
129+
'justify-center': !isOnHomePage,
130+
}"
127131
>
128132
<!-- Search bar (hidden on mobile unless expanded) -->
129133
<HeaderSearchBox

app/components/Compare/PackageSelector.vue

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,17 +139,16 @@ function handleBlur() {
139139

140140
<!-- Add package input -->
141141
<div v-if="packages.length < maxPackages" class="relative">
142-
<div class="relative group">
142+
<div class="relative group flex items-center">
143143
<label for="package-search" class="sr-only">
144144
{{ $t('compare.selector.search_label') }}
145145
</label>
146146
<span
147-
class="absolute inset-y-0 start-3 flex items-center text-fg-subtle pointer-events-none group-focus-within:text-accent"
148-
aria-hidden="true"
147+
class="absolute inset-is-3 text-fg-subtle font-mono text-md pointer-events-none transition-colors duration-200 motion-reduce:transition-none [.group:hover:not(:focus-within)_&]:text-fg/80 group-focus-within:text-accent z-1"
149148
>
150-
<span class="i-carbon:search w-4 h-4" />
149+
/
151150
</span>
152-
<input
151+
<InputBase
153152
id="package-search"
154153
v-model="inputValue"
155154
type="text"
@@ -158,7 +157,9 @@ function handleBlur() {
158157
? $t('compare.selector.search_first')
159158
: $t('compare.selector.search_add')
160159
"
161-
class="w-full bg-bg-subtle border border-border rounded-lg ps-10 pe-4 py-2.5 font-mono text-sm text-fg placeholder:text-fg-subtle motion-reduce:transition-none duration-200 focus:border-accent focus-visible:(outline-2 outline-accent/70)"
160+
no-correct
161+
size="medium"
162+
class="w-full min-w-25 ps-7"
162163
aria-autocomplete="list"
163164
@focus="isInputFocused = true"
164165
@blur="handleBlur"

app/components/Filter/Panel.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,13 +239,15 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
239239
</button>
240240
</div>
241241
</div>
242-
<input
242+
<InputBase
243243
id="filter-search"
244244
type="text"
245245
:value="filters.text"
246246
:placeholder="searchPlaceholder"
247247
autocomplete="off"
248-
class="w-full bg-bg-subtle border border-border rounded-md px-4 py-3 font-mono text-sm text-fg placeholder:text-fg-subtle transition-[border-color,outline-color] duration-300 hover:border-fg-subtle outline-2 outline-transparent focus:border-accent focus-visible:(outline-2 outline-accent/70)"
248+
class="w-full min-w-25"
249+
size="medium"
250+
no-correct
249251
@input="handleTextInput"
250252
/>
251253
</div>

app/components/Header/AuthModal.client.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,15 @@ watch(handleInput, newHandleInput => {
8989
>
9090
{{ $t('auth.modal.handle_label') }}
9191
</label>
92-
<input
92+
<InputBase
9393
id="handle-input"
9494
v-model="handleInput"
9595
type="text"
9696
name="handle"
9797
:placeholder="$t('auth.modal.handle_placeholder')"
98-
v-bind="noCorrect"
99-
class="w-full px-3 py-2 font-mono text-sm bg-bg-subtle border border-border rounded-md text-fg placeholder:text-fg-subtle transition-colors duration-200 hover:border-fg-subtle focus:border-accent focus-visible:(outline-2 outline-accent/70)"
98+
no-correct
99+
class="w-full"
100+
size="medium"
100101
/>
101102
<p v-if="errorMessage" class="text-red-500 text-xs mt-1" role="alert">
102103
{{ errorMessage }}

app/components/Header/ConnectorModal.vue

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,14 +161,15 @@ function handleDisconnect() {
161161
>
162162
{{ $t('connector.modal.token_label') }}
163163
</label>
164-
<input
164+
<InputBase
165165
id="connector-token"
166166
v-model="tokenInput"
167167
type="password"
168168
name="connector-token"
169169
:placeholder="$t('connector.modal.token_placeholder')"
170-
v-bind="noCorrect"
171-
class="w-full px-3 py-2 font-mono text-sm bg-bg-subtle border border-border rounded-md text-fg placeholder:text-fg-subtle transition-colors duration-200 hover:border-fg-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:outline-accent/70"
170+
no-correct
171+
class="w-full"
172+
size="medium"
172173
/>
173174
</div>
174175

@@ -183,14 +184,15 @@ function handleDisconnect() {
183184
>
184185
{{ $t('connector.modal.port_label') }}
185186
</label>
186-
<input
187+
<InputBase
187188
id="connector-port"
188189
v-model="portInput"
189190
type="text"
190191
name="connector-port"
191192
inputmode="numeric"
192193
autocomplete="off"
193-
class="w-full px-3 py-2 font-mono text-sm bg-bg-subtle border border-border rounded-md text-fg transition-colors duration-200 hover:border-fg-subtle focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:outline-accent/70"
194+
class="w-full"
195+
size="medium"
194196
/>
195197
</div>
196198
</details>

app/components/Header/SearchBox.vue

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,6 @@ watch(
7070
},
7171
)
7272
73-
function handleSearchBlur() {
74-
isSearchFocused.value = false
75-
emit('blur')
76-
}
77-
function handleSearchFocus() {
78-
isSearchFocused.value = true
79-
emit('focus')
80-
}
81-
8273
function handleSubmit() {
8374
if (pagesWithLocalFilter.has(route.name as string)) {
8475
router.push({
@@ -114,17 +105,18 @@ defineExpose({ focus })
114105
/
115106
</span>
116107

117-
<input
108+
<InputBase
118109
id="header-search"
119110
ref="inputRef"
120111
v-model="searchQuery"
121112
type="search"
122113
name="q"
123114
:placeholder="$t('search.placeholder')"
124-
v-bind="noCorrect"
125-
class="w-full min-w-25 bg-bg-subtle border border-border rounded-md ps-7 pe-3 py-1.5 font-mono text-sm text-fg placeholder:text-fg-subtle transition-[border-color,outline-color] duration-300 hover:border-fg-subtle outline-2 outline-transparent focus:border-accent focus-visible:(outline-2 outline-accent/70)"
126-
@focus="handleSearchFocus"
127-
@blur="handleSearchBlur"
115+
no-correct
116+
class="w-full min-w-25 ps-7"
117+
@focus="isSearchFocused = true"
118+
@blur="isSearchFocused = false"
119+
size="small"
128120
/>
129121
<button type="submit" class="sr-only">{{ $t('search.button') }}</button>
130122
</div>

app/components/Input/Base.vue

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<script setup lang="ts">
2+
import { noCorrect } from '~/utils/input'
3+
4+
const model = defineModel<string>({ default: '' })
5+
6+
const props = withDefaults(
7+
defineProps<{
8+
disabled?: boolean
9+
size?: 'small' | 'medium' | 'large'
10+
noCorrect?: boolean
11+
}>(),
12+
{
13+
size: 'medium',
14+
noCorrect: true,
15+
},
16+
)
17+
18+
const emit = defineEmits<{
19+
focus: [event: FocusEvent]
20+
blur: [event: FocusEvent]
21+
}>()
22+
23+
const el = useTemplateRef('el')
24+
25+
defineExpose({
26+
focus: () => el.value?.focus(),
27+
blur: () => el.value?.blur(),
28+
})
29+
</script>
30+
31+
<template>
32+
<input
33+
ref="el"
34+
v-model="model"
35+
v-bind="props.noCorrect ? noCorrect : undefined"
36+
@focus="emit('focus', $event)"
37+
@blur="emit('blur', $event)"
38+
class="bg-bg-subtle border border-border font-mono text-fg placeholder:text-fg-subtle transition-[border-color,outline-color] duration-300 hover:border-fg-subtle outline-2 outline-transparent outline-offset-2 focus:border-accent focus-visible:outline-accent/70 disabled:(opacity-50 cursor-not-allowed)"
39+
:class="{
40+
'text-xs leading-[1.2] px-2 py-2 rounded-md': size === 'small',
41+
'text-sm leading-none px-3 py-2.5 rounded-lg': size === 'medium',
42+
'text-base leading-none px-6 py-3.5 h-14 rounded-xl': size === 'large',
43+
}"
44+
:disabled="
45+
/** Catching Vue render-bug of invalid `disabled=false` attribute in the final HTML */
46+
disabled ? true : undefined
47+
"
48+
/>
49+
</template>

app/components/Org/MembersPanel.vue

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -331,14 +331,15 @@ watch(lastExecutionTime, () => {
331331
aria-hidden="true"
332332
/>
333333
<label for="members-search" class="sr-only">{{ $t('org.members.filter_label') }}</label>
334-
<input
334+
<InputBase
335335
id="members-search"
336336
v-model="searchQuery"
337337
type="search"
338338
name="members-search"
339339
:placeholder="$t('org.members.filter_placeholder')"
340-
v-bind="noCorrect"
341-
class="w-full ps-7 pe-2 py-1.5 font-mono text-sm bg-bg-subtle border border-border rounded text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:border-accent focus-visible:(outline-2 outline-accent/70)"
340+
no-correct
341+
class="w-full min-w-25 ps-7"
342+
size="small"
342343
/>
343344
</div>
344345
<div
@@ -516,14 +517,15 @@ watch(lastExecutionTime, () => {
516517
<label for="new-member-username" class="sr-only">{{
517518
$t('org.members.username_label')
518519
}}</label>
519-
<input
520+
<InputBase
520521
id="new-member-username"
521522
v-model="newUsername"
522523
type="text"
523524
name="new-member-username"
524525
:placeholder="$t('org.members.username_placeholder')"
525-
v-bind="noCorrect"
526-
class="w-full px-2 py-1.5 font-mono text-sm bg-bg border border-border rounded text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:border-border-hover focus-visible:outline-accent/70"
526+
no-correct
527+
class="w-full min-w-25"
528+
size="small"
527529
/>
528530
<div class="flex items-center gap-2">
529531
<label for="new-member-role" class="sr-only">{{ $t('org.members.role_label') }}</label>
@@ -553,7 +555,7 @@ watch(lastExecutionTime, () => {
553555
<button
554556
type="submit"
555557
:disabled="!newUsername.trim() || isAddingMember"
556-
class="px-3 py-1.5 font-mono text-xs text-bg bg-fg rounded transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-accent/70"
558+
class="px-3 py-2 font-mono text-xs text-bg bg-fg rounded transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-accent/70"
557559
>
558560
{{ isAddingMember ? '…' : $t('org.members.add_button') }}
559561
</button>

app/components/Org/OperationsQueue.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ watch(isExecuting, executing => {
242242
</div>
243243
<form class="flex items-center gap-2" @submit.prevent="handleRetryWithOtp">
244244
<label for="otp-input" class="sr-only">{{ $t('operations.queue.otp_label') }}</label>
245-
<input
245+
<InputBase
246246
id="otp-input"
247247
v-model="otpInput"
248248
type="text"
@@ -252,12 +252,13 @@ watch(isExecuting, executing => {
252252
:placeholder="$t('operations.queue.otp_placeholder')"
253253
autocomplete="one-time-code"
254254
spellcheck="false"
255-
class="flex-1 px-3 py-1.5 font-mono text-sm bg-bg border border-border rounded text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:border-border-hover focus-visible:outline-accent/70"
255+
class="flex-1 min-w-25"
256+
size="small"
256257
/>
257258
<button
258259
type="submit"
259260
:disabled="!otpInput.trim() || isExecuting"
260-
class="px-3 py-1.5 font-mono text-xs text-bg bg-amber-500 rounded transition-all duration-200 hover:bg-amber-400 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/50"
261+
class="px-3 py-2 font-mono text-xs text-bg bg-amber-500 rounded transition-all duration-200 hover:bg-amber-400 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/50"
261262
>
262263
{{ isExecuting ? $t('operations.queue.retrying') : $t('operations.queue.retry_otp') }}
263264
</button>

app/components/Org/TeamsPanel.vue

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -287,14 +287,15 @@ watch(lastExecutionTime, () => {
287287
aria-hidden="true"
288288
/>
289289
<label for="teams-search" class="sr-only">{{ $t('org.teams.filter_label') }}</label>
290-
<input
290+
<InputBase
291291
id="teams-search"
292292
v-model="searchQuery"
293293
type="search"
294294
name="teams-search"
295295
:placeholder="$t('org.teams.filter_placeholder')"
296-
v-bind="noCorrect"
297-
class="w-full ps-7 pe-2 py-1.5 font-mono text-sm bg-bg-subtle border border-border rounded text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:border-accent focus-visible:(outline-2 outline-accent/70)"
296+
no-correct
297+
class="w-full min-w-25 ps-7"
298+
size="medium"
298299
/>
299300
</div>
300301
<div
@@ -425,7 +426,7 @@ watch(lastExecutionTime, () => {
425426
>
426427
~{{ user }}
427428
</NuxtLink>
428-
<span class="font-mono text-sm text-fg">{{ teamName }}</span>
429+
<span class="font-mono text-sm text-fg mx-2">{{ teamName }}</span>
429430
<button
430431
type="button"
431432
class="p-1 text-fg-subtle hover:text-red-400 transition-colors duration-200 rounded focus-visible:outline-accent/70"
@@ -446,14 +447,15 @@ watch(lastExecutionTime, () => {
446447
<label :for="`add-user-${teamName}`" class="sr-only">{{
447448
$t('org.teams.username_to_add', { team: teamName })
448449
}}</label>
449-
<input
450+
<InputBase
450451
:id="`add-user-${teamName}`"
451452
v-model="newUserUsername"
452453
type="text"
453454
:name="`add-user-${teamName}`"
454455
:placeholder="$t('org.teams.username_placeholder')"
455-
v-bind="noCorrect"
456-
class="flex-1 px-2 py-1 font-mono text-sm bg-bg-subtle border border-border rounded text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:border-border-hover focus-visible:outline-accent/70"
456+
no-correct
457+
class="flex-1 min-w-25"
458+
size="medium"
457459
/>
458460
<button
459461
type="submit"
@@ -497,25 +499,26 @@ watch(lastExecutionTime, () => {
497499
<form class="flex items-center gap-2" @submit.prevent="handleCreateTeam">
498500
<div class="flex-1 flex items-center">
499501
<span
500-
class="px-2 py-1.5 font-mono text-sm text-fg-subtle bg-bg border border-ie-0 border-border rounded-is"
502+
class="px-2 py-3 leading-none font-mono text-sm text-fg-subtle bg-bg border border-ie-0 border-border rounded-is"
501503
>
502504
{{ orgName }}:
503505
</span>
504506
<label for="new-team-name" class="sr-only">{{ $t('org.teams.team_name_label') }}</label>
505-
<input
507+
<InputBase
506508
id="new-team-name"
507509
v-model="newTeamName"
508510
type="text"
509511
name="new-team-name"
510512
:placeholder="$t('org.teams.team_name_placeholder')"
511-
v-bind="noCorrect"
512-
class="flex-1 px-2 py-1.5 font-mono text-sm bg-bg border border-border rounded-ie text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:border-border-hover focus-visible:outline-accent/70"
513+
no-correct
514+
class="flex-1 min-w-25 rounded-is-none"
515+
size="medium"
513516
/>
514517
</div>
515518
<button
516519
type="submit"
517520
:disabled="!newTeamName.trim() || isCreatingTeam"
518-
class="px-3 py-1.5 font-mono text-xs text-bg bg-fg rounded transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-accent/70"
521+
class="px-3 py-2 font-mono text-xs text-bg bg-fg rounded transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-accent/70"
519522
>
520523
{{ isCreatingTeam ? '…' : $t('org.teams.create_button') }}
521524
</button>

0 commit comments

Comments
 (0)