Files
Modrinth-plus/packages/ui/src/components/base/StyledInput.vue
Prospector e13a89dd72 External projects moderator database (#5692)
* Begin external projects moderator database frontend

* add copy link button

* begin project page permissions settings

* MEL database backend routes

* include filename in external files

* Hook up frontend external license page to backend

* more work on user-facing external projects stuff

* put user-facing stuff behind feature flag

* prepr

* clippy

---------

Co-authored-by: aecsocket <aecsocket@tutanota.com>
2026-05-04 16:31:37 +00:00

185 lines
5.3 KiB
Vue

<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"
:id="id"
ref="inputRef"
: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
:id="id"
ref="inputRef"
: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',
{
'placeholder:text-sm': type === 'search',
},
]"
@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>