refactor: StyledInput refactor -> Input components (#6733)

* feat: align style

* feat: new input components

* refactor: migrate to new input components

* refactor: rename component

* prepr

* chore: group inputs like buttons

* fix: lint

* feat: docs

* fix: lint

* fix: bugs

* prepr

* fix slug + adjust create server project

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Calum H.
2026-08-25 06:15:38 +00:00
committed by GitHub
co-authored by Prospector
parent 80607c1963
commit c67f980c4a
166 changed files with 1804 additions and 1261 deletions
@@ -7,7 +7,7 @@
</span>
<span class="text-secondary mb-2">{{ formatMessage(messages.createUserDescription) }}</span>
</label>
<StyledInput
<Input
v-if="showUserField"
id="create-affiliate-user-input"
v-model="affiliateUsername"
@@ -28,14 +28,7 @@
</label>
<div class="flex items-center gap-2">
<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>
<StyledInput
<Input
id="create-affiliate-title-input"
v-model="affiliateLinkTitle"
autocomplete="off"
@@ -43,8 +36,13 @@
:placeholder="formatMessage(messages.createTitlePlaceholder)"
clearable
wrapper-class="w-full"
input-class="pl-10"
/>
>
<template #leading>
<AutoBrandIcon :keyword="affiliateLinkTitle">
<AffiliateIcon />
</AutoBrandIcon>
</template>
</Input>
</div>
<Button
type="colored"
@@ -69,7 +67,7 @@ import { Button } from '#ui/components/base/buttons'
import { defineMessages, useVIntl } from '../../composables/i18n'
import { commonMessages } from '../../utils/common-messages'
import { AutoBrandIcon, NewModal, StyledInput } from '../index'
import { AutoBrandIcon, Input, NewModal } from '../index'
export type CreateAffiliateProps = { sourceName: string; username?: string }
const props = withDefaults(
@@ -22,14 +22,14 @@
ref="inputRef"
class="border-t border-solid border-b-0 border-x-0 border-surface-4 bg-surface-3 p-4"
>
<StyledInput
<Input
v-model="commandInput"
v-tooltip="disableInput ? disableInputTooltip : undefined"
:icon="TerminalSquareIcon"
:placeholder="disableInput ? disabledInputPlaceholder : 'Send a command'"
:disabled="disableInput"
wrapper-class="w-full"
input-class="!h-10"
size="medium"
@keydown.enter="submitCommand"
/>
</div>
@@ -42,7 +42,7 @@ import type { Terminal } from '@xterm/xterm'
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { IconButton } from '#ui/components/base/buttons'
import StyledInput from '#ui/components/base/StyledInput.vue'
import Input from '#ui/components/base/inputs/Input.vue'
import { useTerminal } from '#ui/composables/terminal'
const props = withDefaults(
+13 -21
View File
@@ -1,26 +1,18 @@
<template>
<div ref="containerRef" class="relative inline-block w-full">
<!-- Searchable mode: input trigger -->
<div
v-if="searchable"
class="relative w-full rounded-xl"
:class="{ 'bg-surface-4': searchInputVariant === 'surface' }"
>
<!--
Selection mirror: horizontal padding must match StyledInput (filled + left icon uses `pl-10`,
else `pl-3`) and `resolvedSearchInputClass` when the chevron is shown (`!pr-9`), or the overlay
text will not line up with the transparent input text / caret.
-->
<div v-if="searchable" class="relative w-full rounded-xl">
<!-- Selection mirror must match InputFrame's leading and trailing slot geometry. -->
<div
v-if="searchSelectionOverlayVisible"
class="pointer-events-none absolute inset-y-0 left-0 right-0 z-0 flex min-w-0 items-center gap-2 font-medium text-primary"
:class="[showSearchIcon ? 'pl-10' : 'pl-3', showChevron ? 'pr-9' : 'pr-3']"
class="pointer-events-none absolute inset-y-0 left-0 right-0 z-[2] flex min-w-0 items-center gap-2 font-medium text-primary"
:class="[showSearchIcon ? 'pl-10' : 'pl-3', showChevron ? 'pr-10' : 'pr-3']"
aria-hidden="true"
>
<span class="min-w-0 truncate">{{ searchQuery }}</span>
<slot name="search-selection-affix" :option="selectedOption" />
</div>
<StyledInput
<Input
ref="searchTriggerRef"
v-model="searchQuery"
:icon="showSearchIcon ? SearchIcon : undefined"
@@ -35,7 +27,8 @@
:spellcheck="searchSpellcheck"
:inputmode="searchInputmode"
:input-attrs="searchInputAttrs"
wrapper-class="w-full !bg-transparent"
:appearance="searchInputVariant"
wrapper-class="w-full"
:input-class="resolvedSearchInputClass"
class="relative z-[1]"
@input="handleSearchInput"
@@ -45,13 +38,13 @@
@click="handleSearchClick"
@clear="handleSearchClear"
>
<template v-if="showChevron" #right>
<template v-if="showChevron" #trailing>
<ChevronLeftIcon
class="pointer-events-none absolute right-3 top-1/2 size-5 -translate-y-1/2 text-secondary transition-transform duration-150"
class="pointer-events-none size-5 text-secondary transition-transform duration-150"
:class="isOpen ? (openDirection === 'down' ? 'rotate-90' : '-rotate-90') : '-rotate-90'"
/>
</template>
</StyledInput>
</Input>
</div>
<!-- Standard mode: button trigger -->
@@ -224,7 +217,7 @@ import type {
ButtonSize,
ButtonType,
} from './buttons/types'
import StyledInput from './StyledInput.vue'
import Input from './inputs/Input.vue'
export interface ComboboxOption<T> {
value: T
@@ -364,7 +357,7 @@ const userHasTyped = ref(false)
const focusedIndex = ref(-1)
const containerRef = ref<HTMLElement>()
const triggerRef = ref<ButtonElementHandle>()
const searchTriggerRef = ref<InstanceType<typeof StyledInput>>()
const searchTriggerRef = ref<InstanceType<typeof Input>>()
const dropdownRef = ref<HTMLElement>()
const optionsScrollbarRef = ref<HTMLElement>()
const optionsContainerRef = ref<HTMLElement>()
@@ -410,8 +403,7 @@ const searchSelectionOverlayVisible = computed(() => {
})
const resolvedSearchInputClass = computed(() => {
const parts = [props.searchInputVariant === 'button' ? '!bg-button-bg' : '!bg-transparent']
if (props.showChevron) parts.push('!pr-9')
const parts: string[] = []
if (searchSelectionOverlayVisible.value) {
parts.push('!text-transparent [caret-color:var(--color-text-primary)] selection:bg-transparent')
}
@@ -222,7 +222,7 @@
v-if="activeCategory.searchable"
class="flex justify-between border-0 border-b border-solid border-b-surface-5 py-1.5 w-full"
>
<StyledInput
<Input
v-model="categorySearchQuery"
:icon="SearchIcon"
type="text"
@@ -431,8 +431,8 @@ import { Button, type ButtonElementHandle, type ButtonSize } from '#ui/component
import { defineMessages, useVIntl } from '../../composables/i18n'
import { useVirtualScroll } from '../../composables/virtual-scroll'
import Input from './inputs/Input.vue'
import MultiSelect, { type MultiSelectItem } from './MultiSelect.vue'
import StyledInput from './StyledInput.vue'
export type DropdownFilterBarOption = {
value: string
@@ -13,7 +13,7 @@ import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { IconButton } from '#ui/components/base/buttons'
import { injectI18nDebug } from '../../composables/i18n-debug'
import StyledInput from './StyledInput.vue'
import Input from './inputs/Input.vue'
const debugContext = injectI18nDebug()
@@ -346,7 +346,7 @@ const listMaxHeight = computed(() => `${panelHeight.value - 120}px`)
<div v-if="!minimized" class="flex flex-col overflow-hidden w-full">
<!-- Search -->
<div class="px-3 py-2.5 !w-full">
<StyledInput
<Input
ref="searchInputRef"
v-model="searchQuery"
placeholder="Search keys or values..."
@@ -14,7 +14,7 @@
</template>
<template #panel>
<div :class="dropdownClass">
<StyledInput
<Input
v-if="search"
id="search-input"
ref="searchInput"
@@ -69,8 +69,8 @@ import { CheckIcon, DropdownIcon, SearchIcon } from '@modrinth/assets'
import { computed, ref } from 'vue'
import { Button, TeleportPopoutMenu } from './buttons'
import Input from './inputs/Input.vue'
import ScrollablePanel from './ScrollablePanel.vue'
import StyledInput from './StyledInput.vue'
type Option = string | number | object
@@ -4,7 +4,7 @@
<label class="label" for="insert-link-label">
<span class="label__title">{{ formatMessage(messages.linkModalLabelFieldTitle) }}</span>
</label>
<StyledInput
<Input
id="insert-link-label"
v-model="linkText"
:icon="AlignLeftIcon"
@@ -18,7 +18,7 @@
{{ formatMessage(messages.urlLabel) }}<span class="required">*</span>
</span>
</label>
<StyledInput
<Input
id="insert-link-url"
v-model="linkUrl"
:icon="LinkIcon"
@@ -76,7 +76,7 @@
{{ formatMessage(messages.imageModalDescriptionFieldDescription) }}
</span>
</label>
<StyledInput
<Input
id="insert-image-alt"
v-model="linkText"
:icon="AlignLeftIcon"
@@ -113,7 +113,7 @@
<UploadIcon />
</FileInput>
</div>
<StyledInput
<Input
v-if="!props.onImageUpload || imageUploadOption === 'link'"
id="insert-link-url"
v-model="linkUrl"
@@ -171,7 +171,7 @@
{{ formatMessage(messages.videoModalUrlFieldDescription) }}
</span>
</label>
<StyledInput
<Input
id="insert-video-url"
v-model="linkUrl"
:icon="YouTubeIcon"
@@ -250,7 +250,9 @@
</div>
</div>
</div>
<div ref="editorRef" :class="{ hide: previewMode }" />
<InputFrame :class="{ hide: previewMode }" :disabled="disabled" multiline>
<div ref="editorRef" class="min-w-0 w-full flex-1 self-stretch" />
</InputFrame>
<div v-if="!previewMode" class="info-blurb mt-2">
<div class="info-blurb">
<InfoIcon />
@@ -333,8 +335,9 @@ import { commonMessages } from '../../utils/common-messages.ts'
import NewModal from '../modal/NewModal.vue'
import Chips from './Chips.vue'
import FileInput from './FileInput.vue'
import Input from './inputs/Input.vue'
import InputFrame from './inputs/InputFrame.vue'
import IntlFormatted from './IntlFormatted.vue'
import StyledInput from './StyledInput.vue'
import Toggle from './Toggle.vue'
const { formatMessage } = useVIntl()
@@ -574,6 +577,30 @@ const resolvedPlaceholder = computed(
() => props.placeholder ?? formatMessage(messages.editorPlaceholder),
)
function createEditorTheme(disabled = props.disabled) {
return EditorView.theme({
'&': {
background: 'transparent',
width: '100%',
},
'&.cm-focused': {
outline: 'none',
},
'.cm-content': {
minHeight: props.minHeight ? `${props.minHeight}px` : '200px',
padding: '0',
caretColor: 'var(--color-contrast)',
width: '100%',
pointerEvents: disabled ? 'none' : 'auto',
},
'.cm-scroller': {
height: '100%',
maxHeight: props.maxHeight ? `${props.maxHeight}px` : 'unset',
overflow: 'auto',
},
})
}
onMounted(() => {
const updateListener = EditorView.updateListener.of((update) => {
if (update.docChanged) {
@@ -583,36 +610,7 @@ onMounted(() => {
editorThemeCompartment = new Compartment()
const theme = EditorView.theme({
// in defaults.scss there's references to .cm-content and such to inherit global styles
'&': {
borderRadius: 'var(--radius-md)',
background: 'var(--color-button-bg)',
border: '0.25rem solid transparent',
transition: 'border-color 0.1s ease-in-out',
},
'&.cm-focused': {
'box-shadow': 'inset 0 0 0 transparent, 0 0 0 0.25rem var(--color-brand-shadow)',
color: 'var(--color-contrast)',
outline: 'none',
},
'.cm-focused': {
border: 'none',
},
'.cm-content': {
minHeight: props.minHeight ? `${props.minHeight}px` : '200px',
marginBlockEnd: '0.5rem',
padding: '0.5rem',
caretColor: 'var(--color-contrast)',
width: '100%',
},
'.cm-scroller': {
borderRadius: 'var(--radius-md)',
height: '100%',
maxHeight: props.maxHeight ? `${props.maxHeight}px` : 'unset',
overflow: 'auto',
},
})
const theme = createEditorTheme()
isDisabledCompartment = new Compartment()
@@ -838,43 +836,7 @@ watch(
if (editorThemeCompartment) {
editor.dispatch({
effects: [
editorThemeCompartment.reconfigure(
EditorView.theme({
// in defaults.scss there's references to .cm-content and such to inherit global styles
'&': {
borderRadius: 'var(--radius-md)',
background: 'var(--color-button-bg)',
border: '0.25rem solid transparent',
transition: 'border-color 0.1s ease-in-out',
},
'&.cm-focused': {
'box-shadow': 'inset 0 0 0 transparent, 0 0 0 0.25rem var(--color-brand-shadow)',
color: 'var(--color-contrast)',
outline: 'none',
},
'.cm-focused': {
border: 'none',
},
'.cm-content': {
minHeight: props.minHeight ? `${props.minHeight}px` : '200px',
marginBlockEnd: '0.5rem',
padding: '0.5rem',
caretColor: 'var(--color-contrast)',
width: '100%',
opacity: newValue ? 0.6 : 1,
pointerEvents: newValue ? 'none' : 'all',
cursor: newValue ? 'not-allowed' : 'auto',
},
'.cm-scroller': {
borderRadius: 'var(--radius-md)',
height: '100%',
maxHeight: props.maxHeight ? `${props.maxHeight}px` : 'unset',
overflow: 'auto',
},
}),
),
],
effects: [editorThemeCompartment.reconfigure(createEditorTheme(newValue))],
})
}
}
@@ -127,14 +127,13 @@
v-if="searchable"
class="px-0 py-1.5 border-0 border-solid border-b border-b-surface-5 flex"
>
<StyledInput
<Input
ref="searchInputRef"
v-model="searchQuery"
:icon="SearchIcon"
type="text"
:placeholder="searchPlaceholder"
wrapper-class="grow bg-surface-4 mx-0"
input-class="ps-9 mx-1.5"
wrapper-class="grow"
@input="handleSearchInput"
@keydown="handleSearchKeydown"
/>
@@ -422,7 +421,7 @@ import type {
ButtonSize,
ButtonType,
} from './buttons/types'
import StyledInput from './StyledInput.vue'
import Input from './inputs/Input.vue'
export interface MultiSelectOption<T> {
value: T
@@ -578,7 +577,7 @@ const dropdownRef = ref<HTMLElement>()
const optionsScrollbarRef = ref<HTMLElement>()
const optionsContainerRef = ref<HTMLElement>()
const selectionActionsRef = ref<HTMLElement>()
const searchInputRef = ref<InstanceType<typeof StyledInput>>()
const searchInputRef = ref<InstanceType<typeof Input>>()
const rafId = ref<number | null>(null)
const tagsContainerRef = ref<HTMLElement>()
const optionsOverlayScrollbars = ref<OverlayScrollbarsInstance | null>(null)
@@ -32,7 +32,7 @@
class="page-number-container"
>
<form v-if="item === '-'" class="grid place-content-center" @submit.prevent="goToPage">
<StyledInput
<Input
v-if="showPageInput === index"
:ref="focusInput"
v-model="pageInput"
@@ -118,7 +118,7 @@ import { type ComponentPublicInstance, computed, ref } from 'vue'
import { defineMessages, useVIntl } from '#ui/composables/i18n.ts'
import { Button, ButtonLink, IconButton } from './buttons'
import StyledInput from './StyledInput.vue'
import Input from './inputs/Input.vue'
const emit = defineEmits<{
'switch-page': [page: number]
@@ -179,8 +179,8 @@ function switchPage(newPage: number) {
function focusInput(element: Element | ComponentPublicInstance | null) {
if (element && 'focus' in element) {
const styledInput = element as InstanceType<typeof StyledInput>
styledInput.focus()
const input = element as InstanceType<typeof Input>
input.focus()
}
}
+2 -2
View File
@@ -44,7 +44,7 @@
<span> {{ max }} {{ unit }} </span>
</div>
</div>
<StyledInput
<Input
:model-value="String(currentValue)"
type="number"
class="w-24 ml-3"
@@ -60,7 +60,7 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import StyledInput from './StyledInput.vue'
import Input from './inputs/Input.vue'
const emit = defineEmits<{ 'update:modelValue': [number] }>()
@@ -1,228 +0,0 @@
<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"
v-bind="inputAttrs"
ref="inputRef"
:value="model"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:name="name"
:autocomplete="autocomplete"
:autocorrect="autocorrect"
:autocapitalize="autocapitalize"
:spellcheck="spellcheck"
:maxlength="maxlength"
:rows="rows"
class="w-full touch-manipulation text-primary placeholder:text-secondary focus:text-contrast font-medium transition-[shadow,color] appearance-none shadow-none bg-surface-4 border-none rounded-xl"
:class="[
inputClass,
'pl-3 pr-3 py-2 text-base',
feedbackRingClass,
disabled ? 'cursor-not-allowed' : '',
resizeClass,
]"
@input="onInput"
@focus="isFocused = true"
@blur="isFocused = false"
/>
<!-- Single-line input -->
<input
v-else
:id="id"
v-bind="inputAttrs"
ref="inputRef"
:type="type"
:value="model"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:name="name"
:autocomplete="autocomplete"
:autocorrect="autocorrect"
:autocapitalize="autocapitalize"
:spellcheck="spellcheck"
:inputmode="inputmode"
:maxlength="maxlength"
:min="min"
:max="max"
:step="step"
class="w-full touch-manipulation text-primary placeholder:text-secondary focus:text-contrast font-medium transition-[shadow,color] appearance-none shadow-none"
: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',
feedbackRingClass,
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 touch-manipulation bg-transparent border-none text-secondary hover:text-contrast transition-colors cursor-pointer select-none"
:aria-label="formatMessage(messages.clearInput)"
@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 touch-manipulation 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="formatMessage(clearable && model ? messages.clearInput : messages.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'
import { defineMessages, useVIntl } from '../../composables/i18n'
const { formatMessage } = useVIntl()
const messages = defineMessages({
clearInput: { id: 'input.clear', defaultMessage: 'Clear input' },
search: { id: 'input.search', defaultMessage: 'Search' },
})
const RING = {
error: 'ring-4 ring-red focus:ring-4 focus:ring-red',
none: 'outline-none focus:ring-4 focus:ring-brand-shadow',
} as const
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
autocorrect?: 'on' | 'off'
autocapitalize?: 'none' | 'off' | 'sentences' | 'words' | 'characters'
spellcheck?: boolean
inputmode?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'
maxlength?: number
min?: number
max?: number
step?: number
clamp?: boolean
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
inputAttrs?: Record<string, string | number | boolean | undefined>
}>(),
{
type: 'text',
size: 'standard',
variant: 'filled',
clamp: false,
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 feedbackRingClass = computed(() => (props.error ? RING.error : RING.none))
const resizeClass = computed(
() => ({ none: 'resize-none', vertical: 'resize-y', both: 'resize' })[props.resize ?? 'none'],
)
defineExpose({
focus: () => inputRef.value?.focus(),
select: () => {
inputRef.value?.focus()
inputRef.value?.select()
},
setValue: (v: string) => {
if (inputRef.value) inputRef.value.value = v
},
})
function onInput(event: Event) {
const target = event.target as HTMLInputElement | HTMLTextAreaElement
if (props.type !== 'number' || props.multiline) {
model.value = target.value
return
}
if (target.value === '') {
model.value = undefined
return
}
let value = Number(target.value)
if (props.clamp) {
if (props.min !== undefined) value = Math.max(props.min, value)
if (props.max !== undefined) value = Math.min(props.max, value)
target.value = String(value)
}
model.value = value
}
function clear() {
model.value = props.type === 'number' && !props.multiline ? undefined : ''
emit('clear')
}
</script>
@@ -210,7 +210,7 @@ import {
import { defineMessages, useVIntl } from '../../composables/i18n'
import Combobox, { type ComboboxOption } from './Combobox.vue'
import DatePicker from './DatePicker.vue'
import DatePicker from './inputs/DatePicker.vue'
export type TimeFramePreset =
| 'today'
+2 -2
View File
@@ -41,7 +41,6 @@ export { default as CollapsibleRegion } from './CollapsibleRegion.vue'
export type { ComboboxOption, ComboboxSearchInputVariant } from './Combobox.vue'
export { default as Combobox } from './Combobox.vue'
export { default as CopyCode } from './CopyCode.vue'
export { default as DatePicker } from './DatePicker.vue'
export { default as DoubleIcon } from './DoubleIcon.vue'
export { default as DropArea } from './DropArea.vue'
export type { DropdownFilterBarCategory, DropdownFilterBarOption } from './DropdownFilterBar.vue'
@@ -66,6 +65,8 @@ export { default as HorizontalRule } from './HorizontalRule.vue'
export { default as I18nDebugPanel } from './I18nDebugPanel.vue'
export { default as IconSelect } from './IconSelect.vue'
export { default as InlineEditableText } from './InlineEditableText.vue'
export type { InputAppearance, InputSize } from './inputs'
export { DateInput, DatePicker, Input, InputClearButton, InputFrame, Textarea } from './inputs'
export { default as IntlFormatted } from './IntlFormatted.vue'
export { default as LoadingBar } from './LoadingBar.vue'
export { default as LoadingIndicator } from './LoadingIndicator.vue'
@@ -120,7 +121,6 @@ export { default as SmartClickable } from './SmartClickable.vue'
export type { StackedAdmonitionItem, StackedAdmonitionType } from './StackedAdmonitions.vue'
export { default as StackedAdmonitions } from './StackedAdmonitions.vue'
export { default as StatItem } from './StatItem.vue'
export { default as StyledInput } from './StyledInput.vue'
export type { SortDirection, TableColumn } from './Table.vue'
export { default as Table } from './Table.vue'
export type { TabsTab, TabsValue } from './Tabs.vue'
@@ -0,0 +1,142 @@
<template>
<InputFrame
:appearance="appearance"
:class="[wrapperClass, attrs.class]"
:disabled="disabled"
:invalid="error"
:size="size"
:style="attrs.style"
>
<input
v-bind="mergeProps(controlAttrs(), inputAttrs ?? {})"
:id="id"
ref="inputRef"
:type="type"
:value="model"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:name="name"
:autocomplete="autocomplete"
:min="min"
:max="max"
:step="step"
:aria-invalid="error || undefined"
class="min-w-0 w-full flex-1 self-stretch appearance-none border-0 bg-transparent p-0 font-medium text-primary shadow-none outline-none placeholder:text-secondary focus:text-contrast focus:ring-0 [&::-webkit-calendar-picker-indicator]:hidden"
:class="[
inputClass,
controlClass,
size === 'small' ? 'text-sm' : 'text-base',
disabled ? 'cursor-not-allowed' : '',
]"
@input="onInput"
/>
<template #trailing>
<span class="flex shrink-0 items-center gap-2">
<InputClearButton v-if="showClearButton" :label="clearLabel" @click="clear" />
<button
type="button"
class="relative flex size-5 shrink-0 touch-manipulation cursor-pointer select-none items-center justify-center border-none bg-transparent p-0 text-secondary opacity-60 transition-colors before:absolute before:-inset-2 hover:text-contrast hover:opacity-100 focus-visible:text-contrast focus-visible:opacity-100 group-focus-within/input:text-contrast group-focus-within/input:opacity-100 disabled:pointer-events-none"
:aria-label="pickerLabel"
:disabled="disabled || readonly"
@click="openPicker"
>
<CalendarIcon class="size-5" aria-hidden="true" />
</button>
</span>
</template>
</InputFrame>
</template>
<script setup lang="ts">
import { CalendarIcon } from '@modrinth/assets'
import { computed, type InputHTMLAttributes, mergeProps, ref, useAttrs } from 'vue'
import InputClearButton from './InputClearButton.vue'
import InputFrame from './InputFrame.vue'
import type { InputAppearance, InputSize } from './types'
defineOptions({ inheritAttrs: false })
const model = defineModel<string | undefined>()
const attrs = useAttrs()
const props = withDefaults(
defineProps<{
appearance?: InputAppearance
autocomplete?: string
clearLabel?: string
clearable?: boolean
controlClass?: string
disabled?: boolean
error?: boolean
id?: string
inputAttrs?: InputHTMLAttributes
inputClass?: string
max?: string
min?: string
name?: string
pickerLabel?: string
placeholder?: string
readonly?: boolean
size?: InputSize
step?: number
type?: 'date' | 'datetime-local'
wrapperClass?: string
}>(),
{
appearance: 'surface',
clearLabel: 'Clear date',
clearable: false,
disabled: false,
error: false,
pickerLabel: 'Open date picker',
readonly: false,
size: 'standard',
type: 'date',
},
)
const emit = defineEmits<{
clear: []
input: [event: Event]
}>()
const inputRef = ref<HTMLInputElement>()
const hasValue = computed(() => model.value !== undefined && model.value !== '')
const showClearButton = computed(
() => props.clearable && hasValue.value && !props.disabled && !props.readonly,
)
defineExpose({ focus: () => inputRef.value?.focus() })
function controlAttrs() {
const { class: _class, style: _style, ...rest } = attrs
return rest
}
function onInput(event: Event) {
model.value = (event.target as HTMLInputElement).value
emit('input', event)
}
function clear() {
model.value = ''
emit('clear')
}
function openPicker() {
if (props.disabled || props.readonly) return
const input = inputRef.value
if (!input) return
input.focus()
if (input.showPicker) {
input.showPicker()
} else {
input.click()
}
}
</script>
@@ -5,7 +5,7 @@
:class="[
wrapperClass,
calendarOnly ? 'calendar-only' : '',
disabled ? 'cursor-not-allowed opacity-50' : '',
disabled ? 'cursor-not-allowed' : '',
showToday ? 'show-today' : '',
props.mode === 'range' && selectedDates.length < 2 ? 'can-select-range' : '',
props.mode === 'range' && selectedDates.length === 1 ? 'is-selecting-range' : '',
@@ -14,20 +14,15 @@
rangeEndpointMoveState ? 'is-moving-range-end' : '',
]"
>
<ButtonFrame
as="span"
:class="[
calendarOnly
? '!block !h-auto !min-w-0 !bg-transparent !p-0 !shadow-none hover:!brightness-100'
: 'w-full !justify-start !p-0',
disabled ? '!cursor-not-allowed' : '',
]"
>
<CalendarIcon
v-if="showIcon && !calendarOnly"
class="pointer-events-none absolute left-3 z-[1] h-5 w-5 text-secondary opacity-60 transition-colors"
aria-hidden="true"
/>
<InputFrame v-if="!calendarOnly" class="w-full" :disabled="disabled">
<template v-if="showIcon" #leading>
<span
class="flex size-5 shrink-0 items-center justify-center text-secondary opacity-60 transition-colors group-focus-within/input:text-contrast group-focus-within/input:opacity-100"
aria-hidden="true"
>
<CalendarIcon class="size-5" />
</span>
</template>
<input
:id="id"
ref="inputRef"
@@ -37,27 +32,32 @@
:readonly="readonly"
:autocomplete="autocomplete"
:class="inputClasses"
:tabindex="calendarOnly ? -1 : undefined"
:aria-hidden="calendarOnly ? 'true' : undefined"
type="text"
/>
<button
v-if="hasClearButton"
type="button"
class="absolute right-0.5 top-px z-[1] touch-manipulation cursor-pointer select-none border-none bg-transparent p-2 text-secondary transition-colors hover:text-contrast"
aria-label="Clear date"
@click.stop="clearValue"
>
<XIcon class="h-5 w-5" aria-hidden="true" />
</button>
</ButtonFrame>
<template v-if="hasClearButton" #trailing>
<InputClearButton label="Clear date" @click.stop="clearValue" />
</template>
</InputFrame>
<input
v-else
:id="id"
ref="inputRef"
:name="name"
:disabled="disabled"
:readonly="readonly"
:autocomplete="autocomplete"
:class="inputClasses"
tabindex="-1"
aria-hidden="true"
type="text"
/>
</div>
</template>
<script setup lang="ts">
import 'flatpickr/dist/flatpickr.css'
import { CalendarIcon, XIcon } from '@modrinth/assets'
import { CalendarIcon } from '@modrinth/assets'
import chevronLeftIcon from '@modrinth/assets/icons/chevron-left.svg?raw'
import chevronRightIcon from '@modrinth/assets/icons/chevron-right.svg?raw'
import flatpickr from 'flatpickr'
@@ -65,7 +65,8 @@ import type { Instance } from 'flatpickr/dist/types/instance'
import type { Options } from 'flatpickr/dist/types/options'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import ButtonFrame from './buttons/ButtonFrame.vue'
import InputClearButton from './InputClearButton.vue'
import InputFrame from './InputFrame.vue'
type DatePickerValue = string | Date | null | undefined
type RangeEdge = 'start' | 'end'
@@ -1200,12 +1201,7 @@ const hasClearButton = computed(
const inputClasses = computed(() => [
props.calendarOnly
? 'sr-only pointer-events-none absolute h-0 w-0 opacity-0'
: 'h-full w-full touch-manipulation appearance-none bg-transparent font-semibold text-primary shadow-none transition-[shadow,color] placeholder:text-secondary focus:text-contrast focus:ring-4 focus:ring-brand-shadow !outline-0',
!props.calendarOnly && props.showIcon ? 'pl-10' : '',
!props.calendarOnly && !props.showIcon ? 'pl-3' : '',
!props.calendarOnly
? `${hasClearButton.value ? 'pr-10' : 'pr-3'} rounded-xl border-none py-2 text-base outline-none`
: '',
: 'min-w-0 w-full flex-1 self-stretch appearance-none border-0 bg-transparent p-0 font-medium text-base text-primary shadow-none outline-none placeholder:text-secondary focus:text-contrast focus:ring-0',
props.disabled && !props.calendarOnly ? 'cursor-not-allowed' : '',
props.inputClass,
])
@@ -1557,7 +1553,7 @@ defineExpose({
<style scoped>
.modrinth-date-picker :deep(.flatpickr-wrapper) {
@apply w-full;
@apply min-w-0 w-full flex-1;
}
.modrinth-date-picker :deep(.flatpickr-calendar) {
@@ -0,0 +1,171 @@
<template>
<InputFrame
:appearance="appearance"
:class="[wrapperClass, attrs.class]"
:disabled="disabled"
:invalid="error"
:size="size"
:style="attrs.style"
>
<template v-if="icon || $slots.leading" #leading>
<span
class="flex size-5 shrink-0 items-center justify-center text-secondary opacity-60 transition-colors [&>svg]:size-5 group-focus-within/input:text-contrast group-focus-within/input:opacity-100"
aria-hidden="true"
>
<slot name="leading">
<component :is="icon" />
</slot>
</span>
</template>
<div class="flex min-w-0 flex-1 self-stretch items-center" @click="inputRef?.focus()">
<div class="flex min-w-0 flex-1 items-baseline">
<span
v-if="$slots.prefix"
class="shrink-0 font-medium text-secondary opacity-60 transition-colors group-focus-within/input:text-contrast group-focus-within/input:opacity-100"
:class="size === 'small' ? 'text-sm' : 'text-base'"
>
<slot name="prefix" />
</span>
<input
v-bind="mergeProps(controlAttrs(), inputAttrs ?? {})"
:id="id"
ref="inputRef"
:type="type"
:value="model"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:name="name"
:autocomplete="autocomplete"
:autocorrect="autocorrect"
:autocapitalize="autocapitalize"
:spellcheck="spellcheck"
:inputmode="inputmode"
:maxlength="maxlength"
:min="min"
:max="max"
:step="step"
:aria-invalid="error || undefined"
class="min-w-0 w-full flex-1 appearance-none !min-h-0 !border-0 !bg-transparent !p-0 font-medium text-primary !shadow-none !outline-none placeholder:text-secondary focus:text-contrast focus:ring-0"
:class="[
inputClass,
controlClass,
size === 'small' ? 'text-sm' : 'text-base',
disabled ? 'cursor-not-allowed' : '',
]"
@input="onInput"
/>
</div>
</div>
<template v-if="showClearButton || $slots.trailing || $slots.right" #trailing>
<span class="flex shrink-0 items-center gap-2">
<InputClearButton v-if="showClearButton" :label="clearLabel" @click="clear" />
<slot name="trailing" />
<slot name="right" />
</span>
</template>
</InputFrame>
</template>
<script setup lang="ts">
import { type Component, computed, type InputHTMLAttributes, mergeProps, ref, useAttrs } from 'vue'
import InputClearButton from './InputClearButton.vue'
import InputFrame from './InputFrame.vue'
import type { InputAppearance, InputSize } from './types'
defineOptions({ inheritAttrs: false })
const model = defineModel<string | number | undefined>()
const attrs = useAttrs()
const props = withDefaults(
defineProps<{
appearance?: InputAppearance
autocomplete?: string
autocapitalize?: 'none' | 'off' | 'sentences' | 'words' | 'characters'
autocorrect?: 'on' | 'off'
clearLabel?: string
clearable?: boolean
clamp?: boolean
controlClass?: string
disabled?: boolean
error?: boolean
icon?: Component
id?: string
inputAttrs?: InputHTMLAttributes
inputClass?: string
inputmode?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'
max?: number
maxlength?: number
min?: number
name?: string
placeholder?: string
readonly?: boolean
size?: InputSize
spellcheck?: boolean
step?: number
type?: 'text' | 'email' | 'password' | 'number' | 'url' | 'search'
wrapperClass?: string
}>(),
{
appearance: 'surface',
clearLabel: 'Clear input',
clearable: false,
clamp: false,
disabled: false,
error: false,
readonly: false,
size: 'standard',
type: 'text',
},
)
const emit = defineEmits<{
clear: []
input: [event: Event]
}>()
const inputRef = ref<HTMLInputElement>()
const hasValue = computed(() => model.value !== undefined && model.value !== '')
const showClearButton = computed(
() => props.clearable && hasValue.value && !props.disabled && !props.readonly,
)
defineExpose({
focus: () => inputRef.value?.focus(),
select: () => inputRef.value?.select(),
})
function controlAttrs() {
const { class: _class, style: _style, ...rest } = attrs
return rest
}
function onInput(event: Event) {
const target = event.target as HTMLInputElement
if (props.type !== 'number') {
model.value = target.value
} else if (target.value === '') {
model.value = undefined
} else {
let value = target.valueAsNumber
if (props.clamp) {
if (props.min !== undefined) value = Math.max(props.min, value)
if (props.max !== undefined) value = Math.min(props.max, value)
target.value = String(value)
}
model.value = value
}
emit('input', event)
}
function clear() {
model.value = props.type === 'number' ? undefined : ''
emit('clear')
}
</script>
@@ -0,0 +1,30 @@
<template>
<button
type="button"
class="relative flex size-5 shrink-0 touch-manipulation cursor-pointer select-none items-center justify-center border-none bg-transparent p-0 text-secondary opacity-60 transition-colors before:absolute before:-inset-2 hover:text-contrast hover:opacity-100 focus-visible:text-contrast focus-visible:opacity-100 group-focus-within/input:text-contrast group-focus-within/input:opacity-100 disabled:pointer-events-none"
:aria-label="label"
:disabled="disabled"
@click="emit('click', $event)"
>
<XIcon class="size-5" aria-hidden="true" />
</button>
</template>
<script setup lang="ts">
import { XIcon } from '@modrinth/assets'
withDefaults(
defineProps<{
disabled?: boolean
label?: string
}>(),
{
disabled: false,
label: 'Clear input',
},
)
const emit = defineEmits<{
click: [event: MouseEvent]
}>()
</script>
@@ -0,0 +1,64 @@
<template>
<div
v-bind="$attrs"
class="group/input min-w-0 touch-manipulation border border-solid font-medium text-primary shadow-none transition-[background-color,border-color,box-shadow,color] focus-within:text-contrast focus-within:ring-4 focus-within:ring-brand-shadow"
:class="[
multiline ? 'flex w-full items-start gap-2 rounded-xl px-3 py-2' : sizeClass,
appearanceClass,
disabled ? 'cursor-not-allowed opacity-50' : '',
]"
:data-disabled="disabled || undefined"
:data-invalid="invalid || undefined"
>
<slot name="leading" />
<slot />
<slot name="trailing" />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { InputAppearance, InputSize } from './types'
defineOptions({ inheritAttrs: false })
const props = withDefaults(
defineProps<{
appearance?: InputAppearance
disabled?: boolean
invalid?: boolean
multiline?: boolean
size?: InputSize
}>(),
{
appearance: 'surface',
disabled: false,
invalid: false,
multiline: false,
size: 'standard',
},
)
const sizeClass = computed(
() =>
({
small: 'inline-flex h-8 items-center gap-1.5 rounded-xl px-3',
standard: 'inline-flex h-9 items-center gap-2 rounded-xl px-3',
medium: 'inline-flex h-10 items-center gap-2 rounded-[14px] px-4',
large: 'inline-flex h-12 items-center gap-2 rounded-[14px] px-4',
})[props.size],
)
const appearanceClass = computed(() => {
if (props.invalid) return 'border-red bg-highlight-red'
if (props.appearance === 'transparent') return 'border-transparent bg-transparent'
if (props.appearance === 'button') return 'border-transparent bg-button-bg'
return 'border-surface-5 bg-surface-4'
})
</script>
<style scoped>
:deep(input) {
border-radius: 0;
}
</style>
@@ -0,0 +1,111 @@
<template>
<InputFrame
:appearance="appearance"
:class="[wrapperClass, attrs.class]"
:disabled="disabled"
:invalid="error"
multiline
:style="attrs.style"
>
<template v-if="$slots.leading" #leading>
<span
class="flex size-5 shrink-0 items-center justify-center text-secondary opacity-60 transition-colors [&>svg]:size-5 group-focus-within/input:text-contrast group-focus-within/input:opacity-100"
aria-hidden="true"
>
<slot name="leading" />
</span>
</template>
<textarea
v-bind="mergeProps(controlAttrs(), inputAttrs ?? {})"
:id="id"
ref="inputRef"
:value="model"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:name="name"
:autocomplete="autocomplete"
:autocorrect="autocorrect"
:autocapitalize="autocapitalize"
:spellcheck="spellcheck"
:maxlength="maxlength"
:rows="rows"
:aria-invalid="error || undefined"
class="min-h-0 min-w-0 w-full flex-1 self-stretch appearance-none border-0 bg-transparent p-0 font-medium text-base text-primary shadow-none outline-none placeholder:text-secondary focus:text-contrast focus:ring-0"
:class="[inputClass, controlClass, resizeClass, disabled ? 'cursor-not-allowed' : '']"
@input="onInput"
/>
<template v-if="$slots.trailing || $slots.right" #trailing>
<span class="flex shrink-0 items-start gap-2">
<slot name="trailing" />
<slot name="right" />
</span>
</template>
</InputFrame>
</template>
<script setup lang="ts">
import { computed, mergeProps, ref, type TextareaHTMLAttributes, useAttrs } from 'vue'
import InputFrame from './InputFrame.vue'
import type { InputAppearance } from './types'
defineOptions({ inheritAttrs: false })
const model = defineModel<string | undefined>()
const attrs = useAttrs()
const props = withDefaults(
defineProps<{
appearance?: InputAppearance
autocomplete?: string
autocapitalize?: 'none' | 'off' | 'sentences' | 'words' | 'characters'
autocorrect?: 'on' | 'off'
controlClass?: string
disabled?: boolean
error?: boolean
id?: string
inputAttrs?: TextareaHTMLAttributes
inputClass?: string
maxlength?: number
name?: string
placeholder?: string
readonly?: boolean
resize?: 'none' | 'vertical' | 'both'
rows?: number
spellcheck?: boolean
wrapperClass?: string
}>(),
{
appearance: 'surface',
disabled: false,
error: false,
readonly: false,
resize: 'none',
rows: 3,
},
)
const emit = defineEmits<{
input: [event: Event]
}>()
const inputRef = ref<HTMLTextAreaElement>()
const resizeClass = computed(
() => ({ none: 'resize-none', vertical: 'resize-y', both: 'resize' })[props.resize],
)
defineExpose({ focus: () => inputRef.value?.focus() })
function controlAttrs() {
const { class: _class, style: _style, ...rest } = attrs
return rest
}
function onInput(event: Event) {
model.value = (event.target as HTMLTextAreaElement).value
emit('input', event)
}
</script>
@@ -0,0 +1,7 @@
export { default as DateInput } from './DateInput.vue'
export { default as DatePicker } from './DatePicker.vue'
export { default as Input } from './Input.vue'
export { default as InputClearButton } from './InputClearButton.vue'
export { default as InputFrame } from './InputFrame.vue'
export { default as Textarea } from './Textarea.vue'
export type { InputAppearance, InputSize } from './types'
@@ -0,0 +1,2 @@
export type InputAppearance = 'button' | 'surface' | 'transparent'
export type InputSize = 'small' | 'standard' | 'medium' | 'large'
@@ -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">
<StyledInput v-model="serverName" placeholder="Server name" :maxlength="48" />
<Input 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>
<StyledInput :model-value="sharedCpus" disabled wrapper-class="w-full" />
<Input :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,15 +148,11 @@
class="h-4 w-4text-secondary opacity-60"
/>
</div>
<StyledInput
:model-value="mutatedProduct.metadata.cpu"
disabled
wrapper-class="w-full"
/>
<Input :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>
<StyledInput
<Input
v-model="customServerConfig.storageGbFormatted"
disabled
wrapper-class="w-full"
@@ -476,8 +472,8 @@ import { paymentMethodMessages } from '../../utils/common-messages'
import Admonition from '../base/Admonition.vue'
import Checkbox from '../base/Checkbox.vue'
import Combobox from '../base/Combobox.vue'
import Input from '../base/inputs/Input.vue'
import Slider from '../base/Slider.vue'
import StyledInput from '../base/StyledInput.vue'
import TagIcon from '../base/TagIcon.vue'
import AnimatedLogo from '../brand/AnimatedLogo.vue'
import NewModal from '../modal/NewModal.vue'
@@ -10,7 +10,7 @@ import { defineMessages, useVIntl } from '../../composables/i18n'
import { injectModrinthClient } from '../../providers/api-client'
import AutoLink from '../base/AutoLink.vue'
import Avatar from '../base/Avatar.vue'
import StyledInput from '../base/StyledInput.vue'
import Input from '../base/inputs/Input.vue'
const { formatMessage } = useVIntl()
const { formatCompactNumber } = useCompactNumber()
@@ -105,11 +105,11 @@ const messages = defineMessages({
<div
class="flex flex-col bg-surface-2 border-0 border-l border-solid border-surface-4 overflow-hidden"
>
<StyledInput
<Input
v-if="(projects?.length ?? 0) > 5"
v-model="query"
:placeholder="formatMessage(messages.searchPlaceholder)"
input-class="bg-transparent !rounded-l-none !rounded-b-none m-1"
wrapper-class="m-1 !rounded-l-none !rounded-b-none !bg-transparent"
:icon="SearchIcon"
:disabled="isLoadingCollection || isLoadingProjects"
/>
@@ -3,7 +3,7 @@ import type { Labrinth } from '@modrinth/api-client'
import { CheckIcon, PlusIcon, SearchIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
import { computed, nextTick, ref } from 'vue'
import { NewModal, StyledInput } from '#ui/components'
import { Input, NewModal } from '#ui/components'
import { Button } from '#ui/components/base/buttons'
import { commonMessages } from '#ui/utils'
@@ -190,7 +190,7 @@ defineExpose({ show, hide })
<p class="text-secondary m-0">
{{ formatMessage(messages.addFilesModalDescription) }}
</p>
<StyledInput
<Input
ref="searchInputRef"
v-model="searchQuery"
type="text"
@@ -198,7 +198,7 @@ defineExpose({ show, hide })
:placeholder="formatMessage(messages.addFilesModalSearchPlaceholder)"
:icon="SearchIcon"
:disabled="pending"
input-class="h-[40px]"
size="medium"
class="sticky top-0"
clearable
/>
@@ -4,7 +4,7 @@ import { PlusIcon, SearchIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
import { useMutation } from '@tanstack/vue-query'
import { computed, ref, useTemplateRef } from 'vue'
import { Accordion, NewModal, StyledInput } from '#ui/components'
import { Accordion, Input, NewModal } from '#ui/components'
import { Button } from '#ui/components/base/buttons'
import { injectModrinthClient, injectNotificationManager } from '../../providers'
@@ -45,7 +45,7 @@ const { addNotification } = injectNotificationManager()
const modalRef = useTemplateRef<InstanceType<typeof NewModal>>('modalRef')
const searchAccordionRef = useTemplateRef<InstanceType<typeof Accordion>>('searchAccordionRef')
const filesAccordionRef = useTemplateRef<InstanceType<typeof Accordion>>('filesAccordionRef')
const searchInputRef = useTemplateRef<InstanceType<typeof StyledInput>>('searchInputRef')
const searchInputRef = useTemplateRef<InstanceType<typeof Input>>('searchInputRef')
const query = ref('')
const isLoading = ref(false)
@@ -218,7 +218,7 @@ defineExpose({ show, hide })
class="flex flex-wrap gap-2 shrink-0 p-4 bg-surface-3 border-0 border-b border-solid border-surface-5"
@submit.prevent="executeSearch"
>
<StyledInput
<Input
ref="searchInputRef"
v-model="query"
:icon="SearchIcon"
@@ -4,7 +4,7 @@ import { PlusIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
import { useMutation } from '@tanstack/vue-query'
import { computed, ref, useTemplateRef } from 'vue'
import { Accordion, Combobox, type ComboboxOption, NewModal, StyledInput } from '#ui/components'
import { Accordion, Combobox, type ComboboxOption, Input, NewModal, Textarea } from '#ui/components'
import { Button } from '#ui/components/base/buttons'
import { injectModrinthClient, injectNotificationManager } from '../../providers'
@@ -143,7 +143,7 @@ defineExpose({ show, hide })
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<label class="font-semibold text-contrast" for="add-global-title">Title</label>
<StyledInput
<Input
id="add-global-title"
v-model="title"
type="text"
@@ -152,7 +152,7 @@ defineExpose({ show, hide })
</div>
<div class="flex flex-col gap-2">
<label class="font-semibold text-contrast" for="add-global-link">Link</label>
<StyledInput
<Input
id="add-global-link"
v-model="link"
type="text"
@@ -164,12 +164,12 @@ defineExpose({ show, hide })
<label class="font-semibold text-contrast" for="add-global-flame-id">
CurseForge project ID
</label>
<StyledInput
<Input
id="add-global-flame-id"
v-model="flameProjectId"
type="text"
placeholder="1234567"
input-class="h-[40px]"
size="medium"
:disabled="createMutation.isPending.value"
/>
</div>
@@ -187,11 +187,9 @@ defineExpose({ show, hide })
</div>
<div class="flex flex-col gap-2">
<label class="font-semibold text-contrast" for="add-global-proof">Proof or notes</label>
<StyledInput
<Textarea
id="add-global-proof"
v-model="proof"
type="text"
multiline
input-class="min-h-[6rem]"
resize="vertical"
:disabled="createMutation.isPending.value"
@@ -14,7 +14,7 @@ import { builtinLicenses } from '@modrinth/utils'
import { useMutation, useQueryClient } from '@tanstack/vue-query'
import { computed, ref, watch } from 'vue'
import { Chips, Combobox, type ComboboxOption, StyledInput } from '#ui/components'
import { Chips, Combobox, type ComboboxOption, Input, Textarea } from '#ui/components'
import { FileInput } from '#ui/components/base'
import { Button, IconButton } from '#ui/components/base/buttons'
import { commonMessages } from '#ui/utils'
@@ -475,7 +475,7 @@ function cancelEditing() {
<span class="text-contrast font-semibold mt-1">
{{ formatMessage(messages.linkLabel) }}
</span>
<StyledInput
<Input
v-model="linkInput"
type="text"
class="max-w-[40rem]"
@@ -516,7 +516,7 @@ function cancelEditing() {
)
}}
</span>
<StyledInput
<Input
v-model="customLicenseInput"
type="text"
class="max-w-[40rem]"
@@ -543,11 +543,9 @@ function cancelEditing() {
formatMessage(selectedPermissionReason.notesDescription)
}}</span>
</div>
<StyledInput
<Textarea
v-model="notesInput"
type="text"
resize="both"
multiline
:rows="notesInputRows"
class="max-w-[40rem]"
:placeholder="formatMessage(messages.notesPlaceholder)"
@@ -31,7 +31,7 @@ import {
injectProjectPageContext,
} from '../../providers'
import type { QuickReply } from '../../providers/attribution-moderation'
import StyledInput from '../base/StyledInput.vue'
import Textarea from '../base/inputs/Textarea.vue'
import AddFilesToAttributionGroupModal from './AddFilesToAttributionGroupModal.vue'
import AddToExistingExternalProjectModal from './AddToExistingExternalProjectModal.vue'
import AddToGlobalPermissionsDatabaseModal from './AddToGlobalPermissionsDatabaseModal.vue'
@@ -706,9 +706,8 @@ const visibleQuickReplies = computed<OverflowMenuOption[]>(() => {
</div>
</template>
<template v-else>
<StyledInput
<Textarea
v-model="reviewReasonInput"
multiline
placeholder="Explanation of review (optional)"
class="mt-3"
/>
@@ -83,7 +83,7 @@
<!-- Instance-specific: Name field -->
<div v-if="ctx.flowType === 'instance'" class="flex flex-col gap-2">
<span class="font-semibold text-contrast">{{ formatMessage(messages.nameLabel) }}</span>
<StyledInput
<Input
v-model="ctx.instanceName.value"
:placeholder="ctx.autoInstanceName.value || formatMessage(messages.instanceNamePlaceholder)"
/>
@@ -230,8 +230,8 @@ import Avatar from '../../../base/Avatar.vue'
import Chips from '../../../base/Chips.vue'
import Collapsible from '../../../base/Collapsible.vue'
import Combobox, { type ComboboxOption } from '../../../base/Combobox.vue'
import Input from '../../../base/inputs/Input.vue'
import PaperChannelBadge from '../../../base/PaperChannelBadge.vue'
import StyledInput from '../../../base/StyledInput.vue'
import type { LoaderVersionEntry, LoaderVersionType } from '../creation-flow-context'
import { injectCreationFlowContext } from '../creation-flow-context'
import { formatLoaderLabel } from '../shared'
@@ -5,10 +5,7 @@
class="flex flex-col gap-2"
>
<span class="font-semibold text-contrast">{{ formatMessage(messages.worldNameLabel) }}</span>
<StyledInput
v-model="worldName"
:placeholder="formatMessage(messages.worldNamePlaceholder)"
/>
<Input v-model="worldName" :placeholder="formatMessage(messages.worldNamePlaceholder)" />
</div>
<div v-if="ctx.setupType.value === 'vanilla'" class="flex flex-col gap-2">
@@ -69,10 +66,7 @@
</template>
</IntlFormatted>
</span>
<StyledInput
v-model="worldSeed"
:placeholder="formatMessage(messages.worldSeedPlaceholder)"
/>
<Input v-model="worldSeed" :placeholder="formatMessage(messages.worldSeedPlaceholder)" />
<span class="text-sm text-secondary">{{ formatMessage(messages.worldSeedDescription) }}</span>
</div>
@@ -107,10 +101,9 @@
:options="generatorSettingsOptions"
:placeholder="formatMessage(messages.generatorSettingsPlaceholder)"
/>
<StyledInput
<Textarea
v-if="generatorSettingsMode === 'custom'"
v-model="generatorSettingsCustom"
multiline
:rows="4"
:placeholder="formatMessage(messages.generatorSettingsJsonPlaceholder)"
input-class="font-mono"
@@ -144,7 +137,8 @@ import { injectTags } from '../../../../providers'
import Accordion from '../../../base/Accordion.vue'
import Chips from '../../../base/Chips.vue'
import Combobox, { type ComboboxOption } from '../../../base/Combobox.vue'
import StyledInput from '../../../base/StyledInput.vue'
import Input from '../../../base/inputs/Input.vue'
import Textarea from '../../../base/inputs/Textarea.vue'
import Toggle from '../../../base/Toggle.vue'
import type { Difficulty, Gamemode, GeneratorSettingsMode } from '../creation-flow-context'
import { injectCreationFlowContext } from '../creation-flow-context'
@@ -22,7 +22,7 @@
</template>
<template v-else>
<!-- Search -->
<StyledInput
<Input
v-if="ctx.importLaunchers.value.length > 0"
v-model="ctx.importSearchQuery.value"
:icon="SearchIcon"
@@ -85,7 +85,7 @@
<IconButton label="Browse for launcher path" @click="browseForLauncherPath">
<FolderSearchIcon />
</IconButton>
<StyledInput
<Input
v-model="newLauncherPath"
:placeholder="formatMessage(messages.launcherPathPlaceholder)"
class="flex-1"
@@ -109,7 +109,7 @@ import { injectInstanceImport, injectNotificationManager } from '../../../../pro
import type { ImportableLauncher } from '../../../../providers/instance-import'
import Checkbox from '../../../base/Checkbox.vue'
import Collapsible from '../../../base/Collapsible.vue'
import StyledInput from '../../../base/StyledInput.vue'
import Input from '../../../base/inputs/Input.vue'
import { injectCreationFlowContext } from '../creation-flow-context'
const ctx = injectCreationFlowContext()
@@ -25,7 +25,7 @@
<span class="font-semibold text-contrast">{{ confirmationText }}</span> below:
</span>
</label>
<StyledInput
<Input
v-if="hasToType"
id="confirmation"
v-model="confirmation_typed"
@@ -58,7 +58,7 @@ import { computed, ref } from 'vue'
import { Button } from '#ui/components/base/buttons'
import StyledInput from '../base/StyledInput.vue'
import Input from '../base/inputs/Input.vue'
import NewModal from './NewModal.vue'
const props = defineProps({
@@ -15,7 +15,7 @@ import { computed, nextTick, ref } from 'vue'
import { ButtonLink, IconButton } from '#ui/components/base/buttons'
import { injectNotificationManager } from '#ui/providers'
import { NewModal, StyledInput } from '../index'
import { NewModal, Textarea } from '../index'
const props = defineProps({
header: {
@@ -167,13 +167,7 @@ defineExpose({
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
</IconButton>
</div>
<StyledInput
v-else
v-model="content"
multiline
resize="vertical"
wrapper-class="h-full w-[30rem]"
>
<Textarea v-else v-model="content" resize="vertical" wrapper-class="h-full w-[30rem]">
<template #right>
<IconButton
v-tooltip="'Copy Text'"
@@ -186,7 +180,7 @@ defineExpose({
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
</IconButton>
</template>
</StyledInput>
</Textarea>
<div
v-if="link || socialButtons"
:class="['flex flex-col justify-center gap-2', link ? 'w-64 max-w-full' : 'flex-grow']"
@@ -79,109 +79,131 @@
:loading="loading"
:refreshing="refreshing"
/>
<template v-if="filterType.display !== 'depends-on-project'">
<StyledInput
v-if="filterType.searchable"
:id="`search-${filterType.id}`"
v-model="query"
:icon="SearchIcon"
type="text"
:placeholder="formatMessage(messages.searchPlaceholder)"
autocomplete="off"
clearable
input-class="!bg-button-bg"
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">
<template v-if="groupedOptions">
<SearchFilterGroup
v-for="[groupName, options] in groupedOptions"
:key="`${filterType.id}-group-${groupName}`"
:group-name="groupName"
:options="options"
:supports="filterType.supports"
:included="isIncluded"
:excluded="isExcluded"
@toggle="toggleFilter"
@toggle-exclude="toggleNegativeFilter"
/>
</template>
<template v-else>
<template v-for="option in visibleOptions" :key="`${filterType.id}-${option.id}`">
<SearchFilterOption
:option="option"
:included="isIncluded(option)"
:excluded="isExcluded(option)"
<template v-else>
<div
v-if="filterType.display === 'toggle'"
:class="innerPanelClass ? innerPanelClass : ''"
class="flex flex-col gap-3"
>
<label
v-for="option in filterType.options"
:key="`${filterType.id}-toggle-${option.id}`"
class="flex cursor-pointer items-center justify-between text-secondary gap-3 font-semibold"
>
<span class="text-sm">{{ option.formatted_name ?? option.id }}</span>
<Toggle
:model-value="isExcluded(option)"
small
class="shrink-0"
@update:model-value="toggleNegativeFilter(option)"
/>
</label>
</div>
<template v-else>
<Input
v-if="filterType.searchable"
:id="`search-${filterType.id}`"
v-model="query"
:icon="SearchIcon"
type="text"
:placeholder="formatMessage(messages.searchPlaceholder)"
autocomplete="off"
clearable
size="small"
appearance="button"
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">
<template v-if="groupedOptions">
<SearchFilterGroup
v-for="[groupName, options] in groupedOptions"
:key="`${filterType.id}-group-${groupName}`"
:group-name="groupName"
:options="options"
:supports="filterType.supports"
:has-sub-options="!!option.sub_options?.length"
:expanded="isExpanded(option)"
:class="{
'mr-3': scrollable,
}"
:included="isIncluded"
:excluded="isExcluded"
@toggle="toggleFilter"
@toggle-exclude="toggleNegativeFilter"
@toggle-expand="toggleExpand(option)"
>
<slot name="option" :filter="filterType" :option="option">
<span
v-if="option.icon"
class="inline-flex items-center justify-center shrink-0 h-4 w-4"
:style="iconStyle(option)"
>
<div
v-if="typeof option.icon === 'string'"
class="h-4 w-4"
v-html="option.icon"
/>
<component :is="option.icon" v-else class="h-4 w-4" />
</span>
<span class="truncate text-sm" :style="iconStyle(option)">
{{ option.formatted_name ?? option.id }}
</span>
</slot>
</SearchFilterOption>
<div
v-if="option.sub_options?.length && isExpanded(option)"
class="ml-4 flex flex-col gap-1"
:class="{ 'mr-3': scrollable }"
>
/>
</template>
<template v-else>
<template v-for="option in visibleOptions" :key="`${filterType.id}-${option.id}`">
<SearchFilterOption
v-for="subOption in option.sub_options"
:key="`${filterType.id}-${subOption.id}`"
:option="subOption"
:included="isIncluded(subOption)"
:excluded="isExcluded(subOption)"
:option="option"
:included="isIncluded(option)"
:excluded="isExcluded(option)"
:supports="filterType.supports"
:has-sub-options="!!option.sub_options?.length"
:expanded="isExpanded(option)"
:class="{
'mr-3': scrollable,
}"
@toggle="toggleFilter"
@toggle-exclude="toggleNegativeFilter"
@toggle-expand="toggleExpand(option)"
>
<slot name="option" :filter="filterType" :option="subOption">
<span class="truncate text-sm">
{{ subOption.formatted_name ?? subOption.id }}
<slot name="option" :filter="filterType" :option="option">
<span
v-if="option.icon"
class="inline-flex items-center justify-center shrink-0 h-4 w-4"
:style="iconStyle(option)"
>
<div
v-if="typeof option.icon === 'string'"
class="h-4 w-4"
v-html="option.icon"
/>
<component :is="option.icon" v-else class="h-4 w-4" />
</span>
<span class="truncate text-sm" :style="iconStyle(option)">
{{ option.formatted_name ?? option.id }}
</span>
</slot>
</SearchFilterOption>
</div>
<div
v-if="option.sub_options?.length && isExpanded(option)"
class="ml-4 flex flex-col gap-1"
:class="{ 'mr-3': scrollable }"
>
<SearchFilterOption
v-for="subOption in option.sub_options"
:key="`${filterType.id}-${subOption.id}`"
:option="subOption"
:included="isIncluded(subOption)"
:excluded="isExcluded(subOption)"
:supports="filterType.supports"
@toggle="toggleFilter"
@toggle-exclude="toggleNegativeFilter"
>
<slot name="option" :filter="filterType" :option="subOption">
<span class="truncate text-sm">
{{ subOption.formatted_name ?? subOption.id }}
</span>
</slot>
</SearchFilterOption>
</div>
</template>
</template>
</template>
<button
v-if="filterType.display === 'expandable'"
class="flex bg-transparent text-secondary border-none cursor-pointer !w-full items-center gap-2 truncate rounded-xl px-2 py-1 text-sm font-semibold transition-all hover:text-contrast focus-visible:text-contrast active:scale-[0.98]"
@click="showMore = !showMore"
>
<DropdownIcon
class="h-4 w-4 transition-transform"
:class="{ 'rotate-180': showMore }"
/>
<span class="truncate text-sm">
{{
showMore ? formatMessage(messages.showFewer) : formatMessage(messages.showMore)
}}
</span>
</button>
</div>
</ScrollablePanel>
<button
v-if="filterType.display === 'expandable'"
class="flex bg-transparent text-secondary border-none cursor-pointer !w-full items-center gap-2 truncate rounded-xl px-2 py-1 text-sm font-semibold transition-all hover:text-contrast focus-visible:text-contrast active:scale-[0.98]"
@click="showMore = !showMore"
>
<DropdownIcon
class="h-4 w-4 transition-transform"
:class="{ 'rotate-180': showMore }"
/>
<span class="truncate text-sm">
{{
showMore ? formatMessage(messages.showFewer) : formatMessage(messages.showMore)
}}
</span>
</button>
</div>
</ScrollablePanel>
</template>
</template>
<div :class="innerPanelClass ? innerPanelClass : ''" class="empty:hidden">
<Checkbox
@@ -231,7 +253,7 @@ import {
flattenFilterOptions,
} from '../../utils/search'
import Accordion from '../base/Accordion.vue'
import { Checkbox, ScrollablePanel, StyledInput } from '../index'
import { Checkbox, Input, ScrollablePanel, Toggle } from '../index'
import SearchDependsOnFilter from './SearchDependsOnFilter.vue'
import SearchFilterGroup from './SearchFilterGroup.vue'
import SearchFilterOption from './SearchFilterOption.vue'
@@ -4,7 +4,7 @@
<label for="backup-name-input">
<span class="text-lg font-semibold text-contrast">Name</span>
</label>
<StyledInput
<Input
id="backup-name-input"
ref="input"
v-model="backupName"
@@ -80,7 +80,7 @@ import {
injectNotificationManager,
} from '../../../providers'
import { commonMessages } from '../../../utils'
import StyledInput from '../../base/StyledInput.vue'
import Input from '../../base/inputs/Input.vue'
import NewModal from '../../modal/NewModal.vue'
const { addNotification } = injectNotificationManager()
@@ -4,7 +4,7 @@
<label for="backup-name-input">
<span class="text-lg font-semibold text-contrast"> Name </span>
</label>
<StyledInput
<Input
id="backup-name-input"
ref="input"
v-model="backupName"
@@ -62,7 +62,7 @@ import {
injectNotificationManager,
} from '../../../providers'
import { commonMessages } from '../../../utils'
import StyledInput from '../../base/StyledInput.vue'
import Input from '../../base/inputs/Input.vue'
import NewModal from '../../modal/NewModal.vue'
const { addNotification } = injectNotificationManager()
@@ -58,7 +58,7 @@
</div>
<div class="flex flex-col gap-2">
<span class="font-semibold text-contrast">{{ formatMessage(messages.maxUsesLabel) }}</span>
<StyledInput
<Input
v-model="maxUses"
type="number"
:min="1"
@@ -96,8 +96,8 @@ import { defineMessages, useVIntl } from '../../../composables/i18n'
import { injectNotificationManager } from '../../../providers'
import { commonMessages } from '../../../utils/common-messages'
import Combobox, { type ComboboxOption } from '../../base/Combobox.vue'
import DatePicker from '../../base/DatePicker.vue'
import StyledInput from '../../base/StyledInput.vue'
import DatePicker from '../../base/inputs/DatePicker.vue'
import Input from '../../base/inputs/Input.vue'
import NewModal from '../../modal/NewModal.vue'
import type { InviteLinkSettings } from './types'
@@ -25,7 +25,7 @@ import {
import AutoLink from '../base/AutoLink.vue'
import Avatar from '../base/Avatar.vue'
import StyledInput from '../base/StyledInput.vue'
import Input from '../base/inputs/Input.vue'
import Table from '../base/Table.vue'
import TagItem from '../base/TagItem.vue'
import TagTagItem from '../base/TagTagItem.vue'
@@ -505,7 +505,7 @@ const authorLink = computed(() =>
</h3>
<Table :columns="contentTableColumns" :data="includedContent" row-key="id">
<template #header-actions>
<StyledInput
<Input
v-model="contentSearchQuery"
:icon="SearchIcon"
:placeholder="formatMessage(messages.searchContent)"
@@ -6,10 +6,10 @@ import { computed, ref, toValue } from 'vue'
import Admonition from '#ui/components/base/Admonition.vue'
import { Button, IconButton } from '#ui/components/base/buttons'
import Combobox, { type ComboboxOption } from '#ui/components/base/Combobox.vue'
import Input from '#ui/components/base/inputs/Input.vue'
import LoadingIndicator from '#ui/components/base/LoadingIndicator.vue'
import NavTabs from '#ui/components/base/NavTabs.vue'
import Pagination from '#ui/components/base/Pagination.vue'
import StyledInput from '#ui/components/base/StyledInput.vue'
import ProjectCard from '#ui/components/project/card/ProjectCard.vue'
import ProjectCardList from '#ui/components/project/ProjectCardList.vue'
import SearchFilterControl from '#ui/components/search/SearchFilterControl.vue'
@@ -144,7 +144,7 @@ function getProjectCardTags(result: Labrinth.Search.v3.ResultSearchProject, disp
:replace="ctx.variant === 'app'"
/>
<StyledInput
<Input
v-model="ctx.query.value"
:icon="SearchIcon"
type="text"
@@ -156,7 +156,7 @@ function getProjectCardTags(result: Labrinth.Search.v3.ResultSearchProject, disp
"
clearable
wrapper-class="w-full"
:input-class="ctx.variant === 'web' ? '!h-12' : 'h-12'"
size="large"
@clear="ctx.clearSearch()"
/>
@@ -15,12 +15,12 @@
/>
<div class="flex items-center gap-2">
<StyledInput
<Input
v-model="searchQuery"
:icon="SearchIcon"
placeholder="Search logs"
wrapper-class="flex-1"
input-class="!h-10"
size="medium"
clearable
/>
<div v-if="ctx.logSources?.value && ctx.activeLogSourceIndex" class="w-[220px]">
@@ -104,7 +104,7 @@ import { Button } from '#ui/components/base/buttons'
import type { CollapsibleAdmonitionItem } from '#ui/components/base/CollapsibleAdmonition.vue'
import CollapsibleAdmonition from '#ui/components/base/CollapsibleAdmonition.vue'
import Combobox from '#ui/components/base/Combobox.vue'
import StyledInput from '#ui/components/base/StyledInput.vue'
import Input from '#ui/components/base/inputs/Input.vue'
import NewModal from '#ui/components/modal/NewModal.vue'
import ShareModal from '#ui/components/modal/ShareModal.vue'
import { injectModrinthClient } from '#ui/providers'
@@ -18,7 +18,7 @@ import type { OverflowMenuOption } from '#ui/components/base/buttons'
import { ButtonLink } from '#ui/components/base/buttons'
import Checkbox from '#ui/components/base/Checkbox.vue'
import FilterPills from '#ui/components/base/FilterPills.vue'
import StyledInput from '#ui/components/base/StyledInput.vue'
import Input from '#ui/components/base/inputs/Input.vue'
import NewModal from '#ui/components/modal/NewModal.vue'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { injectPageContext } from '#ui/providers/page-context'
@@ -493,7 +493,7 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
</template>
<div class="flex flex-col h-[min(600px,calc(95vh-10rem))]">
<div class="flex flex-col gap-4 px-6 py-4 border-b border-solid border-0 border-surface-4">
<StyledInput
<Input
v-model="searchQuery"
:icon="SearchIcon"
:placeholder="formatMessage(messages.searchPlaceholder, { count: typeFilteredCount })"
@@ -67,7 +67,7 @@
style="height: 400px; overflow-y: auto"
>
<div class="flex items-start gap-3 px-6">
<StyledInput
<Input
v-model="searchFilter"
:icon="SearchIcon"
:placeholder="formatMessage(messages.searchPlaceholder)"
@@ -187,10 +187,7 @@
<span class="font-semibold text-contrast">
{{ formatMessage(messages.nameLabel) }}
</span>
<StyledInput
v-model="instanceName"
:placeholder="formatMessage(messages.namePlaceholder)"
/>
<Input v-model="instanceName" :placeholder="formatMessage(messages.namePlaceholder)" />
</div>
<div class="flex flex-col gap-2.5">
@@ -290,8 +287,8 @@ import Avatar from '#ui/components/base/Avatar.vue'
import { Button, IconButton } from '#ui/components/base/buttons'
import Chips from '#ui/components/base/Chips.vue'
import Combobox, { type ComboboxOption } from '#ui/components/base/Combobox.vue'
import Input from '#ui/components/base/inputs/Input.vue'
import LoadingIndicator from '#ui/components/base/LoadingIndicator.vue'
import StyledInput from '#ui/components/base/StyledInput.vue'
import NewModal from '#ui/components/modal/NewModal.vue'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { injectFilePicker } from '#ui/providers'
@@ -15,7 +15,7 @@
>
<div class="w-[300px] flex flex-col relative bg-surface-3">
<div class="p-4 pb-2">
<StyledInput
<Input
v-model="searchQuery"
:icon="SearchIcon"
type="text"
@@ -292,7 +292,7 @@ import { computed, ref, toRef } from 'vue'
import Avatar from '#ui/components/base/Avatar.vue'
import { Button } from '#ui/components/base/buttons'
import StyledInput from '#ui/components/base/StyledInput.vue'
import Input from '#ui/components/base/inputs/Input.vue'
import ConfirmModal from '#ui/components/modal/ConfirmModal.vue'
import NewModal from '#ui/components/modal/NewModal.vue'
import VersionChannelIndicator from '#ui/components/version/VersionChannelIndicator.vue'
@@ -27,7 +27,7 @@ import { Button, type OverflowMenuOption, TeleportOverflowMenu } from '#ui/compo
import DropdownFilterBar from '#ui/components/base/DropdownFilterBar.vue'
import EmptyState from '#ui/components/base/EmptyState.vue'
import FilterPills from '#ui/components/base/FilterPills.vue'
import StyledInput from '#ui/components/base/StyledInput.vue'
import Input from '#ui/components/base/inputs/Input.vue'
import { useDebugLogger } from '#ui/composables/debug-logger'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { commonMessages, formatContentTypeSentence } from '#ui/utils/common-messages'
@@ -942,13 +942,13 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
</span>
<div class="flex flex-wrap items-center gap-2">
<StyledInput
<Input
v-model="searchQuery"
:icon="SearchIcon"
type="text"
autocomplete="off"
:spellcheck="false"
input-class="!h-10"
size="medium"
wrapper-class="flex-1 min-w-0"
clearable
:placeholder="
@@ -4,15 +4,14 @@
:aria-label="formatMessage(messages.fileNavigation)"
>
<div v-if="!isEditing" class="flex items-center gap-2 @[800px]:hidden">
<StyledInput
<Input
:model-value="searchQuery"
:icon="SearchIcon"
type="search"
name="search"
autocomplete="off"
:placeholder="formatMessage(messages.searchFiles)"
class="!h-10"
input-class="!h-10"
size="medium"
wrapper-class="flex-1 min-w-0"
@update:model-value="$emit('update:searchQuery', $event)"
/>
@@ -95,7 +94,7 @@
</nav>
<div v-if="!isEditing" class="flex flex-shrink-0 items-center gap-2">
<StyledInput
<Input
id="search-folder"
:model-value="searchQuery"
:icon="SearchIcon"
@@ -103,8 +102,8 @@
name="search"
autocomplete="off"
:placeholder="formatMessage(messages.searchFiles)"
class="!h-10 hidden @[800px]:inline-flex"
input-class="!h-10"
class="hidden @[800px]:inline-flex"
size="medium"
wrapper-class="w-full sm:w-[280px]"
@update:model-value="$emit('update:searchQuery', $event)"
/>
@@ -237,7 +236,7 @@ import {
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { Button, IconButton, TeleportOverflowMenu } from '#ui/components/base/buttons'
import StyledInput from '#ui/components/base/StyledInput.vue'
import Input from '#ui/components/base/inputs/Input.vue'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { commonMessages } from '#ui/utils/common-messages'
@@ -23,7 +23,7 @@
@keydown.enter.exact.prevent.stop="emit('findNext')"
@keydown.shift.enter.prevent.stop="emit('findPrevious')"
>
<StyledInput
<Input
ref="findInputRef"
:model-value="findQuery"
type="search"
@@ -79,7 +79,7 @@
<div v-if="isReplaceOpen" class="flex items-center gap-1">
<div class="w-9 flex-shrink-0" />
<div @keydown.enter.prevent.stop="emit('replace', replaceQuery)">
<StyledInput
<Input
ref="replaceInputRef"
v-model="replaceQuery"
type="search"
@@ -116,7 +116,7 @@ import { ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, XIcon } from '@modrin
import { nextTick, ref, watch } from 'vue'
import { Button, IconButton } from '#ui/components/base/buttons'
import StyledInput from '#ui/components/base/StyledInput.vue'
import Input from '#ui/components/base/inputs/Input.vue'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { commonMessages } from '#ui/utils/common-messages'
@@ -13,7 +13,7 @@
<span class="font-semibold text-contrast">{{
formatMessage(fileValidationMessages.nameLabel)
}}</span>
<StyledInput
<Input
ref="createInput"
v-model="itemName"
:placeholder="
@@ -50,7 +50,7 @@ import { PlusIcon, XIcon } from '@modrinth/assets'
import { computed, nextTick, ref } from 'vue'
import { Button } from '#ui/components/base/buttons'
import StyledInput from '#ui/components/base/StyledInput.vue'
import Input from '#ui/components/base/inputs/Input.vue'
import NewModal from '#ui/components/modal/NewModal.vue'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { commonMessages, formatFileItemType } from '#ui/utils/common-messages'
@@ -19,7 +19,7 @@
<span class="font-semibold text-contrast">{{
formatMessage(messages.destinationPath)
}}</span>
<StyledInput
<Input
ref="destinationInput"
v-model="destination"
:placeholder="formatMessage(messages.destinationPlaceholder)"
@@ -47,7 +47,7 @@ import { RightArrowIcon, XIcon } from '@modrinth/assets'
import { nextTick, ref } from 'vue'
import { Button } from '#ui/components/base/buttons'
import StyledInput from '#ui/components/base/StyledInput.vue'
import Input from '#ui/components/base/inputs/Input.vue'
import NewModal from '#ui/components/modal/NewModal.vue'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { commonMessages, formatFileItemType } from '#ui/utils/common-messages'
@@ -7,7 +7,7 @@
<form class="space-y-6 md:min-w-[400px]" @submit.prevent="handleSubmit">
<label class="flex flex-col gap-2">
<span class="font-semibold text-contrast">{{ formatMessage(messages.newNameLabel) }}</span>
<StyledInput ref="renameInput" v-model="itemName" wrapper-class="w-full" />
<Input ref="renameInput" v-model="itemName" wrapper-class="w-full" />
<div v-if="submitted && error" class="text-sm text-red">{{ error }}</div>
</label>
</form>
@@ -31,7 +31,7 @@ import { EditIcon, XIcon } from '@modrinth/assets'
import { computed, nextTick, ref } from 'vue'
import { Button } from '#ui/components/base/buttons'
import StyledInput from '#ui/components/base/StyledInput.vue'
import Input from '#ui/components/base/inputs/Input.vue'
import NewModal from '#ui/components/modal/NewModal.vue'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { commonMessages } from '#ui/utils/common-messages'
@@ -35,7 +35,7 @@
<div v-else class="text-sm text-secondary">
{{ formatMessage(messages.zipDescription) }}
</div>
<StyledInput
<Input
v-model="url"
v-tooltip="props.disabled ? props.disabledTooltip : undefined"
:icon="LinkIcon"
@@ -104,7 +104,7 @@ import {
import { computed, nextTick, ref } from 'vue'
import { Button } from '#ui/components/base/buttons'
import StyledInput from '#ui/components/base/StyledInput.vue'
import Input from '#ui/components/base/inputs/Input.vue'
import NewModal from '#ui/components/modal/NewModal.vue'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { injectModrinthClient } from '#ui/providers/api-client'
@@ -111,11 +111,10 @@
</Button>
</div>
<div class="relative">
<StyledInput
<Textarea
id="startup-command-field"
v-model="startupCommand"
v-tooltip="advancedActionTooltip"
multiline
resize="vertical"
input-class="font-mono field-sizing-content"
:disabled="isStartupLoading || !canUseAdvancedSettings"
@@ -218,7 +217,7 @@ import {
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref, watch } from 'vue'
import { Combobox, StyledInput } from '#ui/components'
import { Combobox, Textarea } from '#ui/components'
import { Button, ButtonLink, IconButton } from '#ui/components/base/buttons'
import SaveBanner from '#ui/components/servers/SaveBanner.vue'
import { useServerPermissions } from '#ui/composables/server-permissions'
@@ -10,7 +10,7 @@
<span class="text-lg font-semibold text-contrast">Server name</span>
</label>
<div class="flex flex-col gap-2.5">
<StyledInput
<Input
id="server-name-field"
v-model="serverName"
v-tooltip="advancedActionTooltip"
@@ -150,7 +150,7 @@ import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { useStorage } from '@vueuse/core'
import { computed, ref, watch } from 'vue'
import { CopyCode, StyledInput, Toggle } from '#ui/components'
import { CopyCode, Input, Toggle } from '#ui/components'
import EditServerIcon from '#ui/components/servers/edit-server-icon/EditServerIcon.vue'
import SaveBanner from '#ui/components/servers/SaveBanner.vue'
import { useServerPermissions } from '#ui/composables/server-permissions'
@@ -5,7 +5,7 @@
<NewModal ref="editAllocationModal" header="Edit allocation" width="550px">
<form class="flex w-full flex-col gap-2" @submit.prevent="editAllocation">
<label for="edit-allocation-name" class="font-semibold text-contrast"> Name </label>
<StyledInput
<Input
id="edit-allocation-name"
ref="editAllocationInput"
v-model="editAllocationName"
@@ -77,7 +77,7 @@
<span class="text-lg font-semibold text-contrast">Allocations</span>
<div class="flex w-full flex-col items-center justify-start gap-2 sm:flex-row">
<StyledInput
<Input
v-model="createAllocationName"
v-tooltip="advancedActionTooltip"
wrapper-class="grow max-w-[400px]"
@@ -151,7 +151,7 @@
<span class="text-lg font-semibold text-contrast">DNS records</span>
</label>
<div class="flex w-full flex-col items-center justify-start gap-2 sm:flex-row">
<StyledInput
<Input
id="user-domain"
v-model="userDomain"
wrapper-class="grow max-w-[400px]"
@@ -227,7 +227,7 @@ import {
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, nextTick, ref } from 'vue'
import { ConfirmModal, NewModal, StyledInput, Table, TagItem } from '#ui/components'
import { ConfirmModal, Input, NewModal, Table, TagItem } from '#ui/components'
import type { TableColumn } from '#ui/components/base'
import { Button, IconButton } from '#ui/components/base/buttons'
import { useServerPermissions } from '#ui/composables/server-permissions'
@@ -31,7 +31,7 @@
<div class="w-full text-sm">
<label for="search-server-properties" class="sr-only"> Search server properties </label>
<StyledInput
<Input
id="search-server-properties"
v-model="searchInput"
wrapper-class="w-full"
@@ -77,7 +77,7 @@
<div v-if="isPropertyVisible('max_players')" class="flex flex-col gap-2.5 my-1">
<span class="font-semibold text-contrast">Max players</span>
<StyledInput
<Input
id="server-property-max-players"
v-tooltip="advancedActionTooltip"
:model-value="liveProperties.max_players"
@@ -91,7 +91,7 @@
<div v-if="isPropertyVisible('motd')" class="flex flex-col gap-2.5 my-1">
<span class="font-semibold text-contrast">MOTD</span>
<StyledInput
<Input
id="server-property-motd"
v-model="liveProperties.motd"
v-tooltip="advancedActionTooltip"
@@ -160,7 +160,7 @@
class="flex items-center justify-between h-10"
>
<span class="font-semibold text-contrast">Protection radius</span>
<StyledInput
<Input
id="server-property-spawn-protection-radius"
v-tooltip="advancedActionTooltip"
:model-value="liveProperties.spawn_protection"
@@ -220,7 +220,7 @@
v-else-if="getPropertyDef(key).type === 'number'"
class="w-full sm:w-[320px]"
>
<StyledInput
<Input
:id="`server-property-${key}`"
v-tooltip="advancedActionTooltip"
:model-value="liveProperties[key]"
@@ -233,7 +233,7 @@
/>
</div>
<div v-else class="flex w-full justify-end sm:w-[320px]">
<StyledInput
<Input
:id="`server-property-${key}`"
v-model="liveProperties[key]"
v-tooltip="advancedActionTooltip"
@@ -298,7 +298,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import Fuse from 'fuse.js'
import { computed, ref, watch } from 'vue'
import { Accordion, Admonition, AutoLink, Chips, StyledInput, Toggle } from '#ui/components'
import { Accordion, Admonition, AutoLink, Chips, Input, Toggle } from '#ui/components'
import SaveBanner from '#ui/components/servers/SaveBanner.vue'
import { useServerPermissions } from '#ui/composables/server-permissions'
import { injectServerSettings } from '#ui/layouts/shared/server-settings'
@@ -50,7 +50,7 @@
{{ formatMessage(commonMessages.usernameLabel) }}
</label>
<div class="flex items-center gap-2">
<StyledInput
<Input
id="admin-edit-username"
v-model="form.username"
class="w-full"
@@ -69,10 +69,9 @@
<div class="flex flex-col gap-2.5">
<label class="text-lg font-semibold text-contrast" for="admin-edit-bio">Bio</label>
<StyledInput
<Textarea
id="admin-edit-bio"
v-model="form.bio"
multiline
:error="form.bio.length > 160"
:disabled="isSaving"
/>
@@ -127,7 +126,8 @@ import { useRouter } from 'vue-router'
import Avatar from '#ui/components/base/Avatar.vue'
import { Button, FileButton } from '#ui/components/base/buttons'
import Combobox from '#ui/components/base/Combobox.vue'
import StyledInput from '#ui/components/base/StyledInput.vue'
import Input from '#ui/components/base/inputs/Input.vue'
import Textarea from '#ui/components/base/inputs/Textarea.vue'
import NewModal from '#ui/components/modal/NewModal.vue'
import { useVIntl } from '#ui/composables'
import { injectNotificationManager } from '#ui/providers'
@@ -1,12 +1,12 @@
<template>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2 md:flex-row">
<StyledInput
<Input
v-model="memberSearch"
:icon="SearchIcon"
:placeholder="formatMessage(messages.searchUsersPlaceholder, { count: members.length })"
wrapper-class="min-w-0 flex-1"
input-class="!h-10"
size="medium"
clearable
/>
<div class="flex shrink-0 items-center gap-2 flex-wrap md:flex-nowrap">
@@ -118,7 +118,7 @@ import { computed, ref, watch } from 'vue'
import { Button } from '#ui/components/base/buttons'
import Combobox, { type ComboboxOption } from '#ui/components/base/Combobox.vue'
import DropdownFilterBar from '#ui/components/base/DropdownFilterBar.vue'
import StyledInput from '#ui/components/base/StyledInput.vue'
import Input from '#ui/components/base/inputs/Input.vue'
import {
AccessTable,
apiPermissionsToAccessRole,
@@ -102,7 +102,7 @@
{{ formatMessage(messages.serversTitle) }}
</h1>
<div class="flex w-full flex-row items-center justify-end gap-2 md:mb-0">
<StyledInput
<Input
id="search"
v-model="searchInput"
:icon="SearchIcon"
@@ -241,12 +241,12 @@ import {
injectAuth,
injectModrinthClient,
injectNotificationManager,
Input,
IntlFormatted,
ModrinthServersPurchaseModal,
ResubscribeModal,
ServerListEmpty,
ServersGuestPlanModal,
StyledInput,
useServerBackupDownload,
useVIntl,
} from '@modrinth/ui'
@@ -90,7 +90,7 @@
{{ formatMessage(commonMessages.usernameLabel) }}
</h2>
<div class="flex items-center gap-2">
<StyledInput
<Input
id="username-field"
v-model="current.username"
class="w-full max-w-md"
@@ -113,12 +113,7 @@
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.bioTitle) }}
</h2>
<StyledInput
id="bio-field"
v-model="current.bio"
multiline
:error="current.bio.length > 160"
/>
<Textarea id="bio-field" v-model="current.bio" :error="current.bio.length > 160" />
<div class="text-secondary" :class="{ 'text-red': current.bio.length > 160 }">
{{ current.bio.length }}/160
</div>
@@ -138,8 +133,9 @@ import { RouterLink } from 'vue-router'
import Avatar from '#ui/components/base/Avatar.vue'
import { Button, FileButton } from '#ui/components/base/buttons'
import EmptyState from '#ui/components/base/EmptyState.vue'
import Input from '#ui/components/base/inputs/Input.vue'
import Textarea from '#ui/components/base/inputs/Textarea.vue'
import IntlFormatted from '#ui/components/base/IntlFormatted.vue'
import StyledInput from '#ui/components/base/StyledInput.vue'
import { defineMessages, useVIntl } from '#ui/composables'
import { type AuthUser, injectAuth, injectNotificationManager } from '#ui/providers'
import { commonMessages } from '#ui/utils'
@@ -4,7 +4,7 @@ import Fuse from 'fuse.js/dist/fuse.basic'
import { computed, onMounted, ref } from 'vue'
import Button from '#ui/components/base/buttons/Button.vue'
import StyledInput from '#ui/components/base/StyledInput.vue'
import Input from '#ui/components/base/inputs/Input.vue'
import {
buildLocaleMessages,
defineMessages,
@@ -249,7 +249,7 @@ function getCategoryName(category: Category): string {
<template>
<div class="flex flex-col gap-4">
<div v-if="$locales.length > 1" class="-mb-4">
<StyledInput
<Input
id="language-search"
v-model="$query"
:icon="SearchIcon"
-7
View File
@@ -2018,12 +2018,6 @@
"icon-select.select": {
"defaultMessage": "Icon wählen"
},
"input.clear": {
"defaultMessage": "Eingabe löschen"
},
"input.search": {
"defaultMessage": "Suchen"
},
"input.search-version.placeholder": {
"defaultMessage": "Version suchen..."
},
@@ -6765,4 +6759,3 @@
"defaultMessage": "Typ"
}
}
-7
View File
@@ -2018,12 +2018,6 @@
"icon-select.select": {
"defaultMessage": "Icon auswählen"
},
"input.clear": {
"defaultMessage": "Eingabe löschen"
},
"input.search": {
"defaultMessage": "Suchen"
},
"input.search-version.placeholder": {
"defaultMessage": "Version suchen..."
},
@@ -6765,4 +6759,3 @@
"defaultMessage": "Typ"
}
}
-6
View File
@@ -2018,12 +2018,6 @@
"icon-select.select": {
"defaultMessage": "Select icon"
},
"input.clear": {
"defaultMessage": "Clear input"
},
"input.search": {
"defaultMessage": "Search"
},
"input.search-version.placeholder": {
"defaultMessage": "Search version..."
},
@@ -2018,12 +2018,6 @@
"icon-select.select": {
"defaultMessage": "Seleccionar ícono"
},
"input.clear": {
"defaultMessage": "Borrar entrada"
},
"input.search": {
"defaultMessage": "Buscar"
},
"input.search-version.placeholder": {
"defaultMessage": "Buscar versión..."
},
@@ -6765,4 +6759,3 @@
"defaultMessage": "Tipo"
}
}
-3
View File
@@ -1988,9 +1988,6 @@
"icon-select.select": {
"defaultMessage": "Seleccionar icono"
},
"input.search": {
"defaultMessage": "Buscar"
},
"input.search-version.placeholder": {
"defaultMessage": "Buscar versión..."
},
-6
View File
@@ -1964,12 +1964,6 @@
"icon-select.select": {
"defaultMessage": "Scegli icona"
},
"input.clear": {
"defaultMessage": "Svuota"
},
"input.search": {
"defaultMessage": "Cerca"
},
"input.search-version.placeholder": {
"defaultMessage": "Cerca tra le versioni..."
},
-6
View File
@@ -1982,12 +1982,6 @@
"icon-select.select": {
"defaultMessage": "Wybierz ikonę"
},
"input.clear": {
"defaultMessage": "Wyczyść pole"
},
"input.search": {
"defaultMessage": "Szukaj"
},
"input.search-version.placeholder": {
"defaultMessage": "Szukaj wersji..."
},
-6
View File
@@ -1934,12 +1934,6 @@
"icon-select.select": {
"defaultMessage": "Выбрать иконку"
},
"input.clear": {
"defaultMessage": "Очистить ввод"
},
"input.search": {
"defaultMessage": "Поиск"
},
"input.search-version.placeholder": {
"defaultMessage": "Поиск версии..."
},
-6
View File
@@ -1799,12 +1799,6 @@
"icon-select.select": {
"defaultMessage": "Välj ikon"
},
"input.clear": {
"defaultMessage": "Rensa indata"
},
"input.search": {
"defaultMessage": "Sök"
},
"input.search-version.placeholder": {
"defaultMessage": "Sök efter version..."
},
-3
View File
@@ -1958,9 +1958,6 @@
"icon-select.select": {
"defaultMessage": "Simge seç"
},
"input.search": {
"defaultMessage": "Ara"
},
"input.search-version.placeholder": {
"defaultMessage": "Sürüm ara..."
},
-3
View File
@@ -1904,9 +1904,6 @@
"icon-select.select": {
"defaultMessage": "Вибрати значок"
},
"input.search": {
"defaultMessage": "Пошук"
},
"input.search-version.placeholder": {
"defaultMessage": "Знайти версію…"
},
-7
View File
@@ -2018,12 +2018,6 @@
"icon-select.select": {
"defaultMessage": "选择图标"
},
"input.clear": {
"defaultMessage": "清除输入"
},
"input.search": {
"defaultMessage": "搜索"
},
"input.search-version.placeholder": {
"defaultMessage": "查找版本……"
},
@@ -6765,4 +6759,3 @@
"defaultMessage": "类型"
}
}
-7
View File
@@ -2018,12 +2018,6 @@
"icon-select.select": {
"defaultMessage": "選擇圖示"
},
"input.clear": {
"defaultMessage": "清除輸入"
},
"input.search": {
"defaultMessage": "搜尋"
},
"input.search-version.placeholder": {
"defaultMessage": "搜尋版本..."
},
@@ -6765,4 +6759,3 @@
"defaultMessage": "類型"
}
}
@@ -0,0 +1,41 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import DateInput from '../../components/base/inputs/DateInput.vue'
const meta = {
title: 'Inputs/DateInput',
component: DateInput,
} satisfies Meta<typeof DateInput>
export default meta
type Story = StoryObj<typeof meta>
export const Date: Story = {
args: {
type: 'date',
},
}
export const DatetimeLocal: Story = {
args: {
type: 'datetime-local',
},
}
export const Clearable: Story = {
render: () => ({
components: { DateInput },
setup() {
const value = ref('2026-07-15')
return { value }
},
template: '<DateInput v-model="value" clearable />',
}),
}
export const Error: Story = {
args: {
error: true,
},
}
@@ -1,10 +1,10 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { computed, nextTick, onMounted, ref } from 'vue'
import DatePicker from '../../components/base/DatePicker.vue'
import DatePicker from '../../components/base/inputs/DatePicker.vue'
const meta = {
title: 'Base/DatePicker',
title: 'Inputs/DatePicker',
component: DatePicker,
} satisfies Meta<typeof DatePicker>
@@ -2,26 +2,22 @@ import { KeyIcon, LinkIcon, MailIcon, SearchIcon, UserIcon } from '@modrinth/ass
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import StyledInput from '../../components/base/StyledInput.vue'
import Input from '../../components/base/inputs/Input.vue'
const meta = {
title: 'Base/StyledInput',
component: StyledInput,
title: 'Inputs/Input',
component: Input,
argTypes: {
size: {
control: 'select',
options: ['standard', 'small'],
options: ['small', 'standard', 'medium', 'large'],
},
type: {
control: 'select',
options: ['text', 'email', 'password', 'number', 'url', 'search', 'date', 'datetime-local'],
},
resize: {
control: 'select',
options: ['none', 'vertical', 'both'],
options: ['text', 'email', 'password', 'number', 'url', 'search'],
},
},
} satisfies Meta<typeof StyledInput>
} satisfies Meta<typeof Input>
export default meta
type Story = StoryObj<typeof meta>
@@ -39,15 +35,32 @@ export const WithIcon: Story = {
},
}
export const WithTextPrefix: Story = {
render: () => ({
components: { Input },
setup() {
const value = ref('sodium')
return { value }
},
template: `
<Input v-model="value">
<template #prefix>
<span style="white-space: nowrap">https://modrinth.com/mod/</span>
</template>
</Input>
`,
}),
}
export const Clearable: Story = {
render: () => ({
components: { StyledInput },
components: { Input },
setup() {
const value = ref('Some text to clear')
return { value, SearchIcon }
},
template: `
<StyledInput
<Input
v-model="value"
:icon="SearchIcon"
placeholder="Search..."
@@ -59,13 +72,13 @@ export const Clearable: Story = {
export const WithIconAndClearable: Story = {
render: () => ({
components: { StyledInput },
components: { Input },
setup() {
const value = ref('Search query')
return { value, SearchIcon }
},
template: `
<StyledInput
<Input
v-model="value"
:icon="SearchIcon"
placeholder="Search..."
@@ -102,18 +115,6 @@ export const Search: Story = {
},
}
export const Date: Story = {
args: {
type: 'date',
},
}
export const DatetimeLocal: Story = {
args: {
type: 'datetime-local',
},
}
export const Small: Story = {
args: {
icon: SearchIcon,
@@ -132,13 +133,13 @@ export const Disabled: Story = {
export const Readonly: Story = {
render: () => ({
components: { StyledInput },
components: { Input },
setup() {
const value = ref('This is readonly')
return { value, UserIcon }
},
template: `
<StyledInput
<Input
v-model="value"
:icon="UserIcon"
placeholder="Readonly input"
@@ -150,13 +151,13 @@ export const Readonly: Story = {
export const Error: Story = {
render: () => ({
components: { StyledInput },
components: { Input },
setup() {
const value = ref('invalid-url')
return { value, LinkIcon }
},
template: `
<StyledInput
<Input
v-model="value"
:icon="LinkIcon"
type="url"
@@ -169,7 +170,7 @@ export const Error: Story = {
export const AllSizes: StoryObj = {
render: () => ({
components: { StyledInput },
components: { Input },
setup() {
return { SearchIcon }
},
@@ -177,7 +178,7 @@ export const AllSizes: StoryObj = {
<div style="display: flex; flex-direction: column; gap: 1rem;">
<div>
<p style="margin-bottom: 0.5rem; font-weight: 600;">Standard (36px)</p>
<StyledInput
<Input
:icon="SearchIcon"
placeholder="Standard size..."
size="standard"
@@ -185,133 +186,26 @@ export const AllSizes: StoryObj = {
</div>
<div>
<p style="margin-bottom: 0.5rem; font-weight: 600;">Small (32px)</p>
<StyledInput
<Input
:icon="SearchIcon"
placeholder="Small size..."
size="small"
/>
</div>
</div>
`,
}),
}
export const Multiline: Story = {
args: {
multiline: true,
placeholder: 'Enter a description...',
},
}
export const MultilineWithRows: Story = {
render: () => ({
components: { StyledInput },
setup() {
const value = ref('This textarea has 5 rows configured for longer content entry.')
return { value }
},
template: `
<StyledInput
v-model="value"
multiline
:rows="5"
placeholder="Enter details..."
/>
`,
}),
}
export const MultilineResizable: Story = {
args: {
multiline: true,
resize: 'vertical',
placeholder: 'Drag the bottom-right corner to resize...',
},
}
export const MultilineError: Story = {
render: () => ({
components: { StyledInput },
setup() {
const value = ref('Invalid content')
return { value }
},
template: `
<StyledInput
v-model="value"
multiline
placeholder="Enter text..."
error
/>
`,
}),
}
export const MultilineDisabled: Story = {
args: {
multiline: true,
placeholder: 'Disabled textarea',
disabled: true,
},
}
export const MultilineAllStates: StoryObj = {
render: () => ({
components: { StyledInput },
setup() {
const normalValue = ref('')
const filledValue = ref('Some content that has been entered into the textarea.')
const errorValue = ref('Invalid content')
const readonlyValue = ref('This content is readonly')
return { normalValue, filledValue, errorValue, readonlyValue }
},
template: /*html*/ `
<div style="display: flex; flex-direction: column; gap: 1rem; max-width: 400px;">
<div>
<p style="margin-bottom: 0.5rem; font-weight: 600;">Default</p>
<StyledInput
v-model="normalValue"
multiline
placeholder="Enter text..."
<p style="margin-bottom: 0.5rem; font-weight: 600;">Medium (40px)</p>
<Input
:icon="SearchIcon"
placeholder="Medium size..."
size="medium"
/>
</div>
<div>
<p style="margin-bottom: 0.5rem; font-weight: 600;">With Value</p>
<StyledInput
v-model="filledValue"
multiline
/>
</div>
<div>
<p style="margin-bottom: 0.5rem; font-weight: 600;">Error State</p>
<StyledInput
v-model="errorValue"
multiline
error
/>
</div>
<div>
<p style="margin-bottom: 0.5rem; font-weight: 600;">Disabled</p>
<StyledInput
multiline
placeholder="Disabled..."
disabled
/>
</div>
<div>
<p style="margin-bottom: 0.5rem; font-weight: 600;">Readonly</p>
<StyledInput
v-model="readonlyValue"
multiline
readonly
/>
</div>
<div>
<p style="margin-bottom: 0.5rem; font-weight: 600;">Resizable (vertical)</p>
<StyledInput
multiline
resize="vertical"
placeholder="Drag to resize..."
<p style="margin-bottom: 0.5rem; font-weight: 600;">Large (48px)</p>
<Input
:icon="SearchIcon"
placeholder="Large size..."
size="large"
/>
</div>
</div>
@@ -321,7 +215,7 @@ export const MultilineAllStates: StoryObj = {
export const AllStates: StoryObj = {
render: () => ({
components: { StyledInput },
components: { Input },
setup() {
const normalValue = ref('')
const filledValue = ref('With content')
@@ -333,7 +227,7 @@ export const AllStates: StoryObj = {
<div style="display: flex; flex-direction: column; gap: 1rem; max-width: 300px;">
<div>
<p style="margin-bottom: 0.5rem; font-weight: 600;">Default</p>
<StyledInput
<Input
v-model="normalValue"
:icon="SearchIcon"
placeholder="Enter text..."
@@ -341,7 +235,7 @@ export const AllStates: StoryObj = {
</div>
<div>
<p style="margin-bottom: 0.5rem; font-weight: 600;">With Value + Clearable</p>
<StyledInput
<Input
v-model="filledValue"
:icon="SearchIcon"
placeholder="Enter text..."
@@ -350,7 +244,7 @@ export const AllStates: StoryObj = {
</div>
<div>
<p style="margin-bottom: 0.5rem; font-weight: 600;">Error State</p>
<StyledInput
<Input
v-model="errorValue"
:icon="LinkIcon"
placeholder="Enter URL..."
@@ -359,7 +253,7 @@ export const AllStates: StoryObj = {
</div>
<div>
<p style="margin-bottom: 0.5rem; font-weight: 600;">Disabled</p>
<StyledInput
<Input
:icon="SearchIcon"
placeholder="Disabled..."
disabled
@@ -367,7 +261,7 @@ export const AllStates: StoryObj = {
</div>
<div>
<p style="margin-bottom: 0.5rem; font-weight: 600;">Readonly</p>
<StyledInput
<Input
v-model="readonlyValue"
:icon="SearchIcon"
placeholder="Readonly..."
@@ -0,0 +1,61 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import Textarea from '../../components/base/inputs/Textarea.vue'
const meta = {
title: 'Inputs/Textarea',
component: Textarea,
argTypes: {
resize: {
control: 'select',
options: ['none', 'vertical', 'both'],
},
},
} satisfies Meta<typeof Textarea>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
placeholder: 'Enter a description...',
},
}
export const WithRows: Story = {
args: {
rows: 5,
placeholder: 'Enter details...',
},
}
export const Resizable: Story = {
args: {
resize: 'vertical',
placeholder: 'Drag the bottom-right corner to resize...',
},
}
export const AllStates: StoryObj = {
render: () => ({
components: { Textarea },
setup() {
const normalValue = ref('')
const filledValue = ref('Some content that has been entered into the textarea.')
const errorValue = ref('Invalid content')
const readonlyValue = ref('This content is readonly')
return { normalValue, filledValue, errorValue, readonlyValue }
},
template: /*html*/ `
<div style="display: flex; flex-direction: column; gap: 1rem; max-width: 400px;">
<Textarea v-model="normalValue" placeholder="Enter text..." />
<Textarea v-model="filledValue" />
<Textarea v-model="errorValue" error />
<Textarea placeholder="Disabled..." disabled />
<Textarea v-model="readonlyValue" readonly />
<Textarea resize="vertical" placeholder="Drag to resize..." />
</div>
`,
}),
}