mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 12:05:53 +00:00
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:
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
+32
-36
@@ -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"
|
||||
/>
|
||||
|
||||
+2
-2
@@ -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
-11
@@ -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'
|
||||
|
||||
+3
-3
@@ -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()
|
||||
|
||||
+3
-3
@@ -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'
|
||||
|
||||
+2
-2
@@ -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 })"
|
||||
|
||||
+3
-6
@@ -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'
|
||||
|
||||
+2
-2
@@ -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'
|
||||
|
||||
+2
-2
@@ -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'
|
||||
|
||||
+2
-2
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1988,9 +1988,6 @@
|
||||
"icon-select.select": {
|
||||
"defaultMessage": "Seleccionar icono"
|
||||
},
|
||||
"input.search": {
|
||||
"defaultMessage": "Buscar"
|
||||
},
|
||||
"input.search-version.placeholder": {
|
||||
"defaultMessage": "Buscar versión..."
|
||||
},
|
||||
|
||||
@@ -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..."
|
||||
},
|
||||
|
||||
@@ -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..."
|
||||
},
|
||||
|
||||
@@ -1934,12 +1934,6 @@
|
||||
"icon-select.select": {
|
||||
"defaultMessage": "Выбрать иконку"
|
||||
},
|
||||
"input.clear": {
|
||||
"defaultMessage": "Очистить ввод"
|
||||
},
|
||||
"input.search": {
|
||||
"defaultMessage": "Поиск"
|
||||
},
|
||||
"input.search-version.placeholder": {
|
||||
"defaultMessage": "Поиск версии..."
|
||||
},
|
||||
|
||||
@@ -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..."
|
||||
},
|
||||
|
||||
@@ -1958,9 +1958,6 @@
|
||||
"icon-select.select": {
|
||||
"defaultMessage": "Simge seç"
|
||||
},
|
||||
"input.search": {
|
||||
"defaultMessage": "Ara"
|
||||
},
|
||||
"input.search-version.placeholder": {
|
||||
"defaultMessage": "Sürüm ara..."
|
||||
},
|
||||
|
||||
@@ -1904,9 +1904,6 @@
|
||||
"icon-select.select": {
|
||||
"defaultMessage": "Вибрати значок"
|
||||
},
|
||||
"input.search": {
|
||||
"defaultMessage": "Пошук"
|
||||
},
|
||||
"input.search-version.placeholder": {
|
||||
"defaultMessage": "Знайти версію…"
|
||||
},
|
||||
|
||||
@@ -2018,12 +2018,6 @@
|
||||
"icon-select.select": {
|
||||
"defaultMessage": "选择图标"
|
||||
},
|
||||
"input.clear": {
|
||||
"defaultMessage": "清除输入"
|
||||
},
|
||||
"input.search": {
|
||||
"defaultMessage": "搜索"
|
||||
},
|
||||
"input.search-version.placeholder": {
|
||||
"defaultMessage": "查找版本……"
|
||||
},
|
||||
@@ -6765,4 +6759,3 @@
|
||||
"defaultMessage": "类型"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+50
-156
@@ -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>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
Reference in New Issue
Block a user