refactor: migrate all input fields to StyledInput (#5306)

* feat: StyledInput component

* migrate: auth pages to styledInput

* migrate: search/filter inputs

* migrate: dashboard inputs

* migrate: app frontend

* migrate: search related inputs

* migrate: all of app-frontend

* fix: missing inputs on app-frontend

* migrate: frontend

* feat: multiline

* migrate: textareas

* fix: storybook use text-primary

* fix: lint

* fix: merge conflict

* feat: cleanup
This commit is contained in:
Calum H.
2026-02-09 14:57:31 +00:00
committed by GitHub
parent 90438a1ad5
commit 37eac92329
108 changed files with 1670 additions and 1479 deletions

View File

@@ -7,23 +7,17 @@
</span>
<span class="text-secondary mb-2">{{ formatMessage(messages.createUserDescription) }}</span>
</label>
<div v-if="showUserField" class="mb-4">
<div class="iconified-input">
<UserIcon aria-hidden="true" />
<input
id="create-affiliate-user-input"
v-model="affiliateUsername"
class="card-shadow"
autocomplete="off"
spellcheck="false"
type="text"
:placeholder="formatMessage(messages.createUserPlaceholder)"
/>
<Button v-if="affiliateUsername" class="r-btn" @click="() => (affiliateUsername = '')">
<XIcon />
</Button>
</div>
</div>
<StyledInput
v-if="showUserField"
id="create-affiliate-user-input"
v-model="affiliateUsername"
:icon="UserIcon"
autocomplete="off"
type="text"
:placeholder="formatMessage(messages.createUserPlaceholder)"
clearable
wrapper-class="mb-4"
/>
<label class="contents" for="create-affiliate-title-input">
<span class="text-lg font-semibold text-contrast mb-1">
{{ formatMessage(messages.createTitleLabel) }}
@@ -33,22 +27,24 @@
}}</span>
</label>
<div class="flex items-center gap-2">
<div class="iconified-input">
<AutoBrandIcon :keyword="affiliateLinkTitle" aria-hidden="true">
<div class="relative inline-flex items-center flex-1">
<AutoBrandIcon
:keyword="affiliateLinkTitle"
aria-hidden="true"
class="absolute left-3 h-5 w-5 z-[1] pointer-events-none text-secondary"
>
<AffiliateIcon />
</AutoBrandIcon>
<input
<StyledInput
id="create-affiliate-title-input"
v-model="affiliateLinkTitle"
class="card-shadow"
autocomplete="off"
spellcheck="false"
type="text"
:placeholder="formatMessage(messages.createTitlePlaceholder)"
clearable
wrapper-class="w-full"
input-class="pl-10"
/>
<Button v-if="affiliateLinkTitle" class="r-btn" @click="() => (affiliateLinkTitle = '')">
<XIcon />
</Button>
</div>
<ButtonStyled color="brand">
<button :disabled="creatingLink || !canCreate" @click="createAffiliateLink">
@@ -63,12 +59,12 @@
</template>
<script lang="ts"></script>
<script setup lang="ts">
import { AffiliateIcon, PlusIcon, SpinnerIcon, UserIcon, XIcon } from '@modrinth/assets'
import { AffiliateIcon, PlusIcon, SpinnerIcon, UserIcon } from '@modrinth/assets'
import { computed, ref, useTemplateRef } from 'vue'
import { defineMessages, useVIntl } from '../../composables/i18n'
import { commonMessages } from '../../utils/common-messages'
import { AutoBrandIcon, Button, ButtonStyled, NewModal } from '../index'
import { AutoBrandIcon, ButtonStyled, NewModal, StyledInput } from '../index'
export type CreateAffiliateProps = { sourceName: string; username?: string }
const props = withDefaults(

View File

@@ -57,18 +57,17 @@
@keydown="handleDropdownKeydown"
>
<div v-if="searchable" class="p-4">
<div class="iconified-input w-full border-surface-5 border-[1px] border-solid rounded-xl">
<SearchIcon aria-hidden="true" />
<input
ref="searchInputRef"
v-model="searchQuery"
type="text"
:placeholder="searchPlaceholder"
class=""
@keydown.stop="handleSearchKeydown"
@input="emit('searchInput', searchQuery)"
/>
</div>
<StyledInput
ref="searchInputRef"
v-model="searchQuery"
:icon="SearchIcon"
type="text"
:placeholder="searchPlaceholder"
wrapper-class="w-full"
input-class="!border !border-solid !border-surface-5"
@keydown.stop="handleSearchKeydown"
@input="emit('searchInput', searchQuery)"
/>
</div>
<div v-if="searchable && filteredOptions.length > 0" class="h-px bg-surface-5"></div>
@@ -134,6 +133,8 @@ import {
watch,
} from 'vue'
import StyledInput from './StyledInput.vue'
export interface ComboboxOption<T> {
value: T
label: string

View File

@@ -18,22 +18,21 @@
<slot />
<DropdownIcon class="h-5 w-5 text-secondary" />
<template #menu>
<div v-if="search" class="iconified-input mb-2 w-full">
<label for="search-input" hidden>Search...</label>
<SearchIcon aria-hidden="true" />
<input
id="search-input"
ref="searchInput"
v-model="searchQuery"
placeholder="Search..."
type="text"
@keydown.enter="
() => {
toggleOption(filteredOptions[0])
}
"
/>
</div>
<StyledInput
v-if="search"
id="search-input"
ref="searchInput"
v-model="searchQuery"
:icon="SearchIcon"
placeholder="Search..."
type="text"
wrapper-class="mb-2 w-full"
@keydown.enter="
() => {
toggleOption(filteredOptions[0])
}
"
/>
<ScrollablePanel v-if="search">
<Button
v-for="(option, index) in filteredOptions"
@@ -75,7 +74,7 @@
import { CheckIcon, DropdownIcon, SearchIcon } from '@modrinth/assets'
import { computed, ref } from 'vue'
import { Button, ButtonStyled, PopoutMenu } from '../index'
import { Button, ButtonStyled, PopoutMenu, StyledInput } from '../index'
import ScrollablePanel from './ScrollablePanel.vue'
type Option = string | number | object

View File

@@ -4,29 +4,28 @@
<label class="label" for="insert-link-label">
<span class="label__title">Label</span>
</label>
<div class="iconified-input">
<AlignLeftIcon />
<input id="insert-link-label" v-model="linkText" type="text" placeholder="Enter label..." />
<Button class="r-btn" @click="() => (linkText = '')">
<XIcon />
</Button>
</div>
<StyledInput
id="insert-link-label"
v-model="linkText"
:icon="AlignLeftIcon"
type="text"
placeholder="Enter label..."
clearable
wrapper-class="w-full"
/>
<label class="label" for="insert-link-url">
<span class="label__title">URL<span class="required">*</span></span>
</label>
<div class="iconified-input">
<LinkIcon />
<input
id="insert-link-url"
v-model="linkUrl"
type="text"
placeholder="Enter the link's URL..."
@input="validateURL"
/>
<Button class="r-btn" @click="() => (linkUrl = '')">
<XIcon />
</Button>
</div>
<StyledInput
id="insert-link-url"
v-model="linkUrl"
:icon="LinkIcon"
type="text"
placeholder="Enter the link's URL..."
clearable
wrapper-class="w-full"
@input="validateURL"
/>
<template v-if="linkValidationErrorMessage">
<span class="label">
<span class="label__title">Error</span>
@@ -68,18 +67,15 @@
Describe the image completely as you would to someone who could not see the image.
</span>
</label>
<div class="iconified-input">
<AlignLeftIcon />
<input
id="insert-image-alt"
v-model="linkText"
type="text"
placeholder="Describe the image..."
/>
<Button class="r-btn" @click="() => (linkText = '')">
<XIcon />
</Button>
</div>
<StyledInput
id="insert-image-alt"
v-model="linkText"
:icon="AlignLeftIcon"
type="text"
placeholder="Describe the image..."
clearable
wrapper-class="w-full"
/>
<label class="label" for="insert-link-url">
<span class="label__title">URL<span class="required">*</span></span>
</label>
@@ -101,19 +97,17 @@
<UploadIcon />
</FileInput>
</div>
<div v-if="!props.onImageUpload || imageUploadOption === 'link'" class="iconified-input">
<ImageIcon />
<input
id="insert-link-url"
v-model="linkUrl"
type="text"
placeholder="Enter the image URL..."
@input="validateURL"
/>
<Button class="r-btn" @click="() => (linkUrl = '')">
<XIcon />
</Button>
</div>
<StyledInput
v-if="!props.onImageUpload || imageUploadOption === 'link'"
id="insert-link-url"
v-model="linkUrl"
:icon="ImageIcon"
type="text"
placeholder="Enter the image URL..."
clearable
wrapper-class="w-full"
@input="validateURL"
/>
<template v-if="linkValidationErrorMessage">
<span class="label">
<span class="label__title">Error</span>
@@ -154,19 +148,16 @@
<span class="label__title">YouTube video URL<span class="required">*</span></span>
<span class="label__description"> Enter a valid link to a YouTube video. </span>
</label>
<div class="iconified-input">
<YouTubeIcon />
<input
id="insert-video-url"
v-model="linkUrl"
type="text"
placeholder="Enter YouTube video URL"
@input="validateURL"
/>
<Button class="r-btn" @click="() => (linkUrl = '')">
<XIcon />
</Button>
</div>
<StyledInput
id="insert-video-url"
v-model="linkUrl"
:icon="YouTubeIcon"
type="text"
placeholder="Enter YouTube video URL"
clearable
wrapper-class="w-full"
@input="validateURL"
/>
<template v-if="linkValidationErrorMessage">
<span class="label">
<span class="label__title">Error</span>
@@ -202,7 +193,7 @@
</div>
</div>
</NewModal>
<div class="resizable-textarea-wrapper">
<div class="block grow w-full">
<div class="editor-action-row">
<div class="editor-actions">
<template
@@ -300,6 +291,7 @@ import NewModal from '../modal/NewModal.vue'
import Button from './Button.vue'
import Chips from './Chips.vue'
import FileInput from './FileInput.vue'
import StyledInput from './StyledInput.vue'
import Toggle from './Toggle.vue'
const props = withDefaults(
@@ -894,11 +886,6 @@ function openVideoModal() {
}
}
.resizable-textarea-wrapper textarea {
min-height: 10rem;
width: 100%;
}
.info-blurb {
display: flex;
align-items: center;
@@ -928,10 +915,6 @@ function openVideoModal() {
.modal-insert {
padding: var(--gap-lg);
.iconified-input {
width: 100%;
}
.label {
margin-block: var(--gap-lg) var(--gap-sm);
display: block;

View File

@@ -0,0 +1,181 @@
<template>
<div
class="relative"
:class="[
wrapperClass,
multiline ? 'flex' : 'inline-flex',
{ 'opacity-50 cursor-not-allowed': disabled },
!multiline && variant === 'outlined' ? 'items-stretch' : 'items-center',
]"
>
<!-- Left icon (filled variant, single-line only) -->
<component
:is="icon"
v-if="icon && variant === 'filled' && !multiline"
class="absolute left-3 h-5 w-5 z-[1] pointer-events-none transition-colors"
:class="[isFocused ? 'opacity-100 text-contrast' : 'opacity-60 text-secondary']"
aria-hidden="true"
/>
<!-- Multiline textarea -->
<textarea
v-if="multiline"
ref="inputRef"
:id="id"
:value="model"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:name="name"
:autocomplete="autocomplete"
:maxlength="maxlength"
:rows="rows"
class="w-full text-primary placeholder:text-secondary focus:text-contrast font-medium transition-[shadow,color] appearance-none shadow-none focus:ring-4 focus:ring-brand-shadow bg-surface-4 border-none rounded-xl"
:class="[
inputClass,
'pl-3 pr-3 py-2 text-base',
error ? 'outline outline-2 outline-red bg-warning-bg' : 'outline-none',
disabled ? 'cursor-not-allowed' : '',
resizeClass,
]"
@input="onInput"
@focus="isFocused = true"
@blur="isFocused = false"
/>
<!-- Single-line input -->
<input
v-else
ref="inputRef"
:id="id"
:type="type"
:value="model"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:name="name"
:autocomplete="autocomplete"
:inputmode="inputmode"
:maxlength="maxlength"
:min="min"
:max="max"
:step="step"
class="w-full text-primary placeholder:text-secondary focus:text-contrast font-medium transition-[shadow,color] appearance-none shadow-none focus:ring-4 focus:ring-brand-shadow"
:class="[
inputClass,
variant === 'filled' && icon ? 'pl-10' : 'pl-3',
clearable && model && variant === 'filled' ? 'pr-8' : 'pr-3',
size === 'small' ? 'h-8 py-1.5 text-sm' : 'h-9 py-2 text-base',
error ? 'outline outline-2 outline-red bg-warning-bg' : 'outline-none',
disabled ? 'cursor-not-allowed' : '',
variant === 'outlined'
? 'bg-transparent border border-solid border-button-bg rounded-l-xl border-r-0'
: 'bg-surface-4 border-none rounded-xl',
]"
@input="onInput"
@focus="isFocused = true"
@blur="isFocused = false"
/>
<!-- Clear button (right side, filled variant, single-line only) -->
<button
v-if="!multiline && clearable && model && !disabled && !readonly && variant === 'filled'"
type="button"
class="absolute right-0.5 z-[1] p-2 bg-transparent border-none text-secondary hover:text-contrast transition-colors cursor-pointer select-none"
aria-label="Clear input"
@click="clear"
>
<XIcon class="h-5 w-5" />
</button>
<!-- Right icon button (outlined variant, single-line only) -->
<button
v-if="!multiline && variant === 'outlined'"
type="button"
class="flex items-center justify-center px-2 bg-transparent border border-solid border-button-bg rounded-r-xl text-secondary hover:text-contrast transition-colors shrink-0"
:aria-label="clearable && model ? 'Clear input' : 'Search'"
:tabindex="clearable && model ? undefined : -1"
@click="clearable && model ? clear() : undefined"
>
<XIcon v-if="clearable && model" class="h-4 w-4" />
<component :is="icon" v-else-if="icon" class="h-4 w-4" />
<SearchIcon v-else class="h-4 w-4" />
</button>
<!-- Custom rightside slot -->
<slot name="right" />
</div>
</template>
<script setup lang="ts">
import { SearchIcon, XIcon } from '@modrinth/assets'
import { type Component, computed, ref } from 'vue'
const model = defineModel<string | number | undefined>()
const props = withDefaults(
defineProps<{
icon?: Component
type?: 'text' | 'email' | 'password' | 'number' | 'url' | 'search' | 'date' | 'datetime-local'
placeholder?: string
id?: string
name?: string
autocomplete?: string
inputmode?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'
maxlength?: number
min?: number
max?: number
step?: number
disabled?: boolean
readonly?: boolean
error?: boolean
size?: 'standard' | 'small'
variant?: 'filled' | 'outlined'
clearable?: boolean
multiline?: boolean
rows?: number
resize?: 'none' | 'vertical' | 'both'
inputClass?: string
wrapperClass?: string
}>(),
{
type: 'text',
size: 'standard',
variant: 'filled',
disabled: false,
readonly: false,
error: false,
clearable: false,
multiline: false,
rows: 3,
resize: 'none',
},
)
const emit = defineEmits<{
clear: []
}>()
const inputRef = ref<HTMLInputElement | HTMLTextAreaElement>()
const isFocused = ref(false)
const resizeClass = computed(
() => ({ none: 'resize-none', vertical: 'resize-y', both: 'resize' })[props.resize ?? 'none'],
)
defineExpose({ focus: () => inputRef.value?.focus() })
function onInput(event: Event) {
const target = event.target as HTMLInputElement | HTMLTextAreaElement
model.value =
props.type === 'number' && !props.multiline
? target.value === ''
? undefined
: Number(target.value)
: target.value
}
function clear() {
model.value = props.type === 'number' && !props.multiline ? undefined : ''
emit('clear')
}
</script>

View File

@@ -57,6 +57,7 @@ export { default as SettingsLabel } from './SettingsLabel.vue'
export { default as SimpleBadge } from './SimpleBadge.vue'
export { default as Slider } from './Slider.vue'
export { default as SmartClickable } from './SmartClickable.vue'
export { default as StyledInput } from './StyledInput.vue'
export type { TableColumn } from './Table.vue'
export { default as Table } from './Table.vue'
export { default as TagItem } from './TagItem.vue'

View File

@@ -60,7 +60,7 @@
<div v-if="!existingSubscription">
<p class="my-2 text-lg font-bold">Configure your server</p>
<div class="flex flex-col gap-4">
<input v-model="serverName" placeholder="Server name" class="input" maxlength="48" />
<StyledInput v-model="serverName" placeholder="Server name" :maxlength="48" />
<!-- <DropdownSelect
v-model="serverLoader"
v-tooltip="'Select the mod loader for your server'"
@@ -136,7 +136,7 @@
>
<div class="flex flex-col w-full gap-2">
<div class="font-semibold">Shared CPUs</div>
<input :value="sharedCpus" disabled class="input w-full" />
<StyledInput :model-value="sharedCpus" disabled wrapper-class="w-full" />
</div>
<div class="flex flex-col w-full gap-2">
<div class="font-semibold flex items-center gap-1">
@@ -148,14 +148,18 @@
class="h-4 w-4text-secondary opacity-60"
/>
</div>
<input :value="mutatedProduct.metadata.cpu" disabled class="input w-full" />
<StyledInput
:model-value="mutatedProduct.metadata.cpu"
disabled
wrapper-class="w-full"
/>
</div>
<div class="flex flex-col w-full gap-2">
<div class="font-semibold">Storage</div>
<input
<StyledInput
v-model="customServerConfig.storageGbFormatted"
disabled
class="input w-full"
wrapper-class="w-full"
/>
</div>
</div>
@@ -551,6 +555,7 @@ import { paymentMethodMessages } from '../../utils/common-messages'
import Admonition from '../base/Admonition.vue'
import Checkbox from '../base/Checkbox.vue'
import Slider from '../base/Slider.vue'
import StyledInput from '../base/StyledInput.vue'
import AnimatedLogo from '../brand/AnimatedLogo.vue'
import NewModal from '../modal/NewModal.vue'
import LoaderIcon from '../servers/icons/LoaderIcon.vue'

View File

@@ -9,15 +9,13 @@
<div class="flex h-[550px] border-solid border-transparent border-[1px] border-b-surface-4">
<div class="w-[300px] flex flex-col relative">
<div class="p-4 pb-2">
<div class="iconified-input w-full border-solid border-[1px] border-surface-4 rounded-xl">
<SearchIcon class="transition-colors" />
<input
v-model="searchQuery"
type="text"
:placeholder="formatMessage(messages.searchVersionPlaceholder)"
class="!bg-transparent rounded-xl transition-colors"
/>
</div>
<StyledInput
v-model="searchQuery"
:icon="SearchIcon"
type="text"
:placeholder="formatMessage(messages.searchVersionPlaceholder)"
wrapper-class="w-full"
/>
</div>
<div class="flex-1 overflow-y-auto px-4 pb-16">
@@ -186,6 +184,7 @@ import { defineMessages, useVIntl } from '../../../composables/i18n'
import { commonMessages } from '../../../utils/common-messages'
import Avatar from '../../base/Avatar.vue'
import ButtonStyled from '../../base/ButtonStyled.vue'
import StyledInput from '../../base/StyledInput.vue'
import NewModal from '../../modal/NewModal.vue'
const { formatMessage } = useVIntl()

View File

@@ -23,13 +23,12 @@
<span class="italic font-bold">{{ confirmationText }}</span> below:
</span>
</label>
<input
<StyledInput
v-if="hasToType"
id="confirmation"
v-model="confirmation_typed"
type="text"
placeholder="Type here..."
class="max-w-[20rem]"
wrapper-class="max-w-[20rem]"
/>
<div class="flex gap-2">
<ButtonStyled :color="danger ? 'red' : 'brand'">
@@ -55,6 +54,7 @@ import { renderString } from '@modrinth/utils'
import { computed, ref } from 'vue'
import ButtonStyled from '../base/ButtonStyled.vue'
import StyledInput from '../base/StyledInput.vue'
import NewModal from './NewModal.vue'
const props = defineProps({

View File

@@ -2,7 +2,6 @@
import {
ClipboardCopyIcon,
GlobeIcon,
LinkIcon,
MailIcon,
MastodonIcon,
RedditIcon,
@@ -12,7 +11,7 @@ import {
import QrcodeVue from 'qrcode.vue'
import { computed, nextTick, ref } from 'vue'
import { Button, Modal } from '../index'
import { Button, Modal, StyledInput } from '../index'
const props = defineProps({
header: {
@@ -142,26 +141,27 @@ defineExpose({
<ClipboardCopyIcon aria-hidden="true" />
</Button>
</div>
<div v-else class="resizable-textarea-wrapper">
<textarea v-model="content" />
<Button
v-tooltip="'Copy Text'"
icon-only
aria-label="Copy Text"
class="copy-button transparent"
@click="copyText"
>
<ClipboardCopyIcon aria-hidden="true" />
</Button>
</div>
<div class="all-buttons">
<div v-if="link" class="iconified-input">
<LinkIcon />
<input type="text" :value="url" readonly />
<Button v-tooltip="'Copy Text'" aria-label="Copy Text" class="r-btn" @click="copyText">
<StyledInput v-else v-model="content" multiline resize="vertical" wrapper-class="h-full">
<template #right>
<Button
v-tooltip="'Copy Text'"
icon-only
aria-label="Copy Text"
class="copy-button transparent"
@click="copyText"
>
<ClipboardCopyIcon aria-hidden="true" />
</Button>
</div>
</template>
</StyledInput>
<div class="all-buttons">
<StyledInput v-if="link" type="text" :model-value="url" readonly wrapper-class="w-full">
<template #right>
<Button v-tooltip="'Copy Text'" aria-label="Copy Text" class="r-btn" @click="copyText">
<ClipboardCopyIcon aria-hidden="true" />
</Button>
</template>
</StyledInput>
<div class="button-row">
<Button v-if="canShare" v-tooltip="'Share'" aria-label="Share" icon-only @click="share">
<ShareIcon aria-hidden="true" />
@@ -236,14 +236,6 @@ defineExpose({
justify-content: center;
}
.iconified-input {
width: 100%;
input {
flex-basis: auto;
}
}
.button-row {
display: flex;
flex-direction: row;
@@ -295,19 +287,4 @@ defineExpose({
transition: none !important;
}
}
.resizable-textarea-wrapper {
position: relative;
height: 100%;
textarea {
width: 100%;
margin: 0;
}
.btn {
opacity: 1;
margin: 0;
}
}
</style>

View File

@@ -65,25 +65,18 @@
</template>
<template v-else #default>
<slot name="prefix" />
<div v-if="filterType.searchable" class="iconified-input mx-2 my-1 !flex">
<SearchIcon aria-hidden="true" />
<input
:id="`search-${filterType.id}`"
v-model="query"
class="!min-h-9 text-sm"
type="text"
:placeholder="formatMessage(messages.searchPlaceholder)"
autocomplete="off"
/>
<Button
v-if="query"
class="r-btn"
:aria-label="formatMessage(messages.clearSearchAriaLabel)"
@click="() => (query = '')"
>
<XIcon aria-hidden="true" />
</Button>
</div>
<StyledInput
v-if="filterType.searchable"
:id="`search-${filterType.id}`"
v-model="query"
:icon="SearchIcon"
type="text"
:placeholder="formatMessage(messages.searchPlaceholder)"
autocomplete="off"
clearable
size="small"
wrapper-class="mx-2 my-1 w-[calc(100%-1rem)]"
/>
<ScrollablePanel :class="{ 'h-[16rem]': scrollable }" :disable-scrolling="!scrollable">
<div :class="innerPanelClass ? innerPanelClass : ''" class="flex flex-col gap-1">
@@ -165,21 +158,14 @@
</template>
<script setup lang="ts">
import {
BanIcon,
DropdownIcon,
LockOpenIcon,
SearchIcon,
UpdatedIcon,
XIcon,
} from '@modrinth/assets'
import { BanIcon, DropdownIcon, LockOpenIcon, SearchIcon, UpdatedIcon } from '@modrinth/assets'
import { computed, ref } from 'vue'
import { defineMessages, useVIntl } from '../../composables/i18n'
import type { FilterOption, FilterType, FilterValue } from '../../utils/search'
import Accordion from '../base/Accordion.vue'
import ButtonStyled from '../base/ButtonStyled.vue'
import { Button, Checkbox, ScrollablePanel } from '../index'
import { Checkbox, ScrollablePanel, StyledInput } from '../index'
import SearchFilterOption from './SearchFilterOption.vue'
const { formatMessage } = useVIntl()

View File

@@ -4,14 +4,13 @@
<label for="backup-name-input">
<span class="text-lg font-semibold text-contrast">Name</span>
</label>
<input
<StyledInput
id="backup-name-input"
ref="input"
v-model="backupName"
type="text"
class="w-full rounded-lg bg-bg-input p-4"
:placeholder="`Backup #${newBackupAmount}`"
maxlength="48"
:maxlength="48"
wrapper-class="w-full"
/>
<Transition
enter-active-class="transition-all duration-300 ease-out"
@@ -76,6 +75,7 @@ import {
injectNotificationManager,
} from '../../../providers'
import ButtonStyled from '../../base/ButtonStyled.vue'
import StyledInput from '../../base/StyledInput.vue'
import NewModal from '../../modal/NewModal.vue'
const { addNotification } = injectNotificationManager()

View File

@@ -4,14 +4,13 @@
<label for="backup-name-input">
<span class="text-lg font-semibold text-contrast"> Name </span>
</label>
<input
<StyledInput
id="backup-name-input"
ref="input"
v-model="backupName"
type="text"
class="bg-bg-input w-full rounded-lg p-4"
:placeholder="`Backup #${backupNumber}`"
maxlength="48"
:maxlength="48"
wrapper-class="w-full"
/>
<div v-if="nameExists" class="flex items-center gap-1">
<IssuesIcon class="hidden text-orange sm:block" />
@@ -56,6 +55,7 @@ import {
injectNotificationManager,
} from '../../../providers'
import ButtonStyled from '../../base/ButtonStyled.vue'
import StyledInput from '../../base/StyledInput.vue'
import NewModal from '../../modal/NewModal.vue'
const { addNotification } = injectNotificationManager()

View File

@@ -68,19 +68,17 @@
</nav>
<div v-if="!isEditing" class="flex flex-shrink-0 items-center gap-2">
<div class="iconified-input w-full sm:w-[280px]">
<SearchIcon aria-hidden="true" class="!text-secondary" />
<input
id="search-folder"
:value="searchQuery"
type="search"
name="search"
autocomplete="off"
class="h-10 w-full rounded-[14px] border-0 bg-surface-4 text-sm"
placeholder="Search files"
@input="$emit('update:searchQuery', ($event.target as HTMLInputElement).value)"
/>
</div>
<StyledInput
id="search-folder"
:model-value="searchQuery"
:icon="SearchIcon"
type="search"
name="search"
autocomplete="off"
placeholder="Search files"
wrapper-class="w-full sm:w-[280px]"
@update:model-value="$emit('update:searchQuery', $event)"
/>
<ButtonStyled type="outlined">
<OverflowMenu
@@ -168,7 +166,7 @@ import {
ShareIcon,
UploadIcon,
} from '@modrinth/assets'
import { Button, ButtonStyled, OverflowMenu } from '@modrinth/ui'
import { Button, ButtonStyled, OverflowMenu, StyledInput } from '@modrinth/ui'
import { computed } from 'vue'
import TeleportOverflowMenu from './explorer/TeleportOverflowMenu.vue'

View File

@@ -3,14 +3,11 @@
<form class="flex flex-col gap-4 md:w-[600px]" @submit.prevent="handleSubmit">
<div class="flex flex-col gap-2">
<div class="font-semibold text-contrast">Name</div>
<input
<StyledInput
ref="createInput"
v-model="itemName"
autofocus
type="text"
class="bg-bg-input w-full rounded-lg p-4"
:placeholder="`e.g. ${type === 'file' ? 'config.yml' : 'plugins'}`"
required
wrapper-class="w-full"
/>
<div v-if="submitted && error" class="text-red">{{ error }}</div>
</div>
@@ -34,7 +31,7 @@
<script setup lang="ts">
import { PlusIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, NewModal } from '@modrinth/ui'
import { ButtonStyled, NewModal, StyledInput } from '@modrinth/ui'
import { computed, nextTick, ref } from 'vue'
const props = defineProps<{

View File

@@ -2,14 +2,11 @@
<NewModal ref="modal" :header="`Moving ${item?.name}`">
<form class="flex flex-col gap-4 md:w-[600px]" @submit.prevent="handleSubmit">
<div class="flex flex-col gap-2">
<input
<StyledInput
ref="destinationInput"
v-model="destination"
autofocus
type="text"
class="bg-bg-input w-full rounded-lg p-4"
placeholder="e.g. /mods/modname"
required
wrapper-class="w-full"
/>
</div>
<div class="flex items-center gap-2 text-nowrap">
@@ -38,7 +35,7 @@
<script setup lang="ts">
import { ArrowBigUpDashIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, NewModal } from '@modrinth/ui'
import { ButtonStyled, NewModal, StyledInput } from '@modrinth/ui'
import { computed, nextTick, ref } from 'vue'
const destinationInput = ref<HTMLInputElement | null>(null)

View File

@@ -3,14 +3,7 @@
<form class="flex flex-col gap-4 md:w-[600px]" @submit.prevent="handleSubmit">
<div class="flex flex-col gap-2">
<div class="font-semibold text-contrast">Name</div>
<input
ref="renameInput"
v-model="itemName"
autofocus
type="text"
class="bg-bg-input w-full rounded-lg p-4"
required
/>
<StyledInput ref="renameInput" v-model="itemName" wrapper-class="w-full" />
<div v-if="submitted && error" class="text-red">{{ error }}</div>
</div>
<div class="flex justify-start gap-4">
@@ -33,7 +26,7 @@
<script setup lang="ts">
import { EditIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, NewModal } from '@modrinth/ui'
import { ButtonStyled, NewModal, StyledInput } from '@modrinth/ui'
import { computed, nextTick, ref } from 'vue'
const props = defineProps<{

View File

@@ -5,6 +5,7 @@ import { computed, ref, watchSyncEffect } from 'vue'
import { defineMessages, type LocaleDefinition, useVIntl } from '../../composables/i18n'
import { isModifierKeyDown } from '../../utils/events'
import StyledInput from '../base/StyledInput.vue'
const { formatMessage } = useVIntl()
@@ -168,16 +169,16 @@ function getCategoryName(category: Category): string {
<template>
<div class="flex flex-col gap-4">
<div v-if="$locales.length > 1" class="iconified-input w-full -mb-4">
<SearchIcon />
<input
<div v-if="$locales.length > 1" class="-mb-4">
<StyledInput
id="language-search"
v-model="$query"
:icon="SearchIcon"
name="language"
type="search"
:placeholder="formatMessage(messages.searchFieldPlaceholder)"
class="input-text-inherit"
:disabled="isChangingLocale()"
wrapper-class="w-full"
@keydown="onSearchKeydown"
/>