* 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>
185 lines
5.3 KiB
Vue
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>
|