replace some el-input with InputGroup

This commit is contained in:
pa
2026-01-11 23:00:47 +09:00
committed by Natsumi
parent 3ed266089a
commit a3212da97b
20 changed files with 381 additions and 95 deletions

View File

@@ -10,18 +10,16 @@
<span>{{ t('dialog.edit_send_invite_message.description') }}</span>
</div>
<el-input
<InputGroupCharCount
v-model="editAndSendInviteDialog.newMessage"
type="textarea"
size="small"
maxlength="64"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
placeholder=""
style="margin-top: 10px"></el-input>
:maxlength="64"
multiline
rows="2"
class="mt-2.5"
placeholder="" />
<template #footer>
<Button variant="secondary" @click="cancelEditAndSendInvite">
<Button variant="secondary" class="mr-2" @click="cancelEditAndSendInvite">
{{ t('dialog.edit_send_invite_message.cancel') }}
</Button>
<Button @click="saveEditAndSendInvite" :disabled="!editAndSendInviteDialog.newMessage">
@@ -33,6 +31,7 @@
<script setup>
import { Button } from '@/components/ui/button';
import { InputGroupCharCount } from '@/components/ui/input-group';
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';

View File

@@ -6,16 +6,13 @@
width="600px"
append-to-body>
<div v-loading="bioDialog.loading">
<el-input
<InputGroupCharCount
v-model="bioDialog.bio"
type="textarea"
size="small"
maxlength="512"
show-word-limit
:autosize="{ minRows: 5, maxRows: 20 }"
:maxlength="512"
multiline
rows="5"
:placeholder="t('dialog.bio.bio_placeholder')"
style="margin-bottom: 10px">
</el-input>
class="mb-2.5" />
<el-input
v-for="(link, index) in bioDialog.bioLinks"
@@ -48,6 +45,7 @@
<script setup>
import { Delete } from '@element-plus/icons-vue';
import { InputGroupCharCount } from '@/components/ui/input-group';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';

View File

@@ -11,17 +11,13 @@
<template v-if="!hideUserNotes || (hideUserNotes && hideUserMemos)">
<span class="name">{{ t('dialog.user.info.note') }}</span>
<br />
<el-input
<InputGroupCharCount
v-model="note"
class="extra"
type="textarea"
maxlength="256"
show-word-limit
:rows="6"
:autosize="{ minRows: 2, maxRows: 20 }"
:maxlength="256"
multiline
rows="6"
:placeholder="t('dialog.user.info.note_placeholder')"
size="small"
resize="none"></el-input>
input-class="extra resize-none" />
</template>
<template v-if="!hideUserMemos || (hideUserNotes && hideUserMemos)">
<span class="name">{{ t('dialog.user.info.memo') }}</span>
@@ -47,6 +43,7 @@
<script setup>
import { ref, watch } from 'vue';
import { InputGroupCharCount } from '@/components/ui/input-group';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';

View File

@@ -6,15 +6,12 @@
width="600px"
append-to-body>
<div v-loading="pronounsDialog.loading">
<el-input
type="textarea"
<InputGroupCharCount
v-model="pronounsDialog.pronouns"
size="small"
maxlength="32"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
:placeholder="t('dialog.pronouns.pronouns_placeholder')">
</el-input>
:maxlength="32"
multiline
rows="2"
:placeholder="t('dialog.pronouns.pronouns_placeholder')" />
</div>
<template #footer>
<el-button type="primary" :disabled="pronounsDialog.loading" @click="savePronouns">
@@ -25,6 +22,7 @@
</template>
<script setup>
import { InputGroupCharCount } from '@/components/ui/input-group';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';

View File

@@ -41,13 +41,12 @@
</SelectContent>
</Select>
<el-input
<InputGroupCharCount
v-model="socialStatusDialog.statusDescription"
:placeholder="t('dialog.social_status.status_placeholder')"
maxlength="32"
show-word-limit
:maxlength="32"
clearable
style="margin-top: 10px"></el-input>
class="mt-2.5" />
<Collapsible v-model:open="isOpen" class="mt-3 flex w-full flex-col gap-2">
<div class="flex items-center justify-between gap-4 px-4">
<h4 class="text-sm font-semibold">{{ t('dialog.social_status.history') }}</h4>
@@ -90,6 +89,7 @@
import { computed, ref } from 'vue';
import { Button } from '@/components/ui/button';
import { ChevronsUpDown } from 'lucide-vue-next';
import { InputGroupCharCount } from '@/components/ui/input-group';
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';

View File

@@ -0,0 +1,35 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<div
data-slot="input-group"
role="group"
:class="
cn(
'group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none',
'h-9 min-w-0 has-[>textarea]:h-auto',
// Variants based on alignment.
'has-[>[data-align=inline-start]]:[&>input]:pl-2',
'has-[>[data-align=inline-end]]:[&>input]:pr-2',
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
// Focus state.
'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]',
// Error state.
'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
props.class
)
">
<slot />
</div>
</template>

View File

@@ -0,0 +1,32 @@
<script setup>
import { cn } from '@/lib/utils';
import { inputGroupAddonVariants } from '.';
const props = defineProps({
align: { type: null, required: false, default: 'inline-start' },
class: { type: null, required: false }
});
function handleInputGroupAddonClick(e) {
const currentTarget = e.currentTarget;
const target = e.target;
if (target && target.closest('button')) {
return;
}
if (currentTarget && currentTarget?.parentElement) {
currentTarget.parentElement?.querySelector('input')?.focus();
}
}
</script>
<template>
<div
role="group"
data-slot="input-group-addon"
:data-align="props.align"
:class="cn(inputGroupAddonVariants({ align: props.align }), props.class)"
@click="handleInputGroupAddonClick">
<slot />
</div>
</template>

View File

@@ -0,0 +1,21 @@
<script setup>
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { inputGroupButtonVariants } from '.';
const props = defineProps({
size: { type: null, required: false, default: 'xs' },
variant: { type: null, required: false, default: 'ghost' },
class: { type: null, required: false }
});
</script>
<template>
<Button
:data-size="props.size"
:variant="props.variant"
:class="cn(inputGroupButtonVariants({ size: props.size }), props.class)">
<slot />
</Button>
</template>

View File

@@ -0,0 +1,81 @@
<script setup>
import { Hash, X } from 'lucide-vue-next';
import { computed, useAttrs } from 'vue';
import { cn } from '@/lib/utils';
import { useVModel } from '@vueuse/core';
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea
} from '.';
defineOptions({ inheritAttrs: false });
const props = defineProps({
modelValue: { type: [String, Number], default: '' },
maxlength: { type: Number, required: true },
multiline: { type: Boolean, default: false },
class: { type: null, required: false },
inputClass: { type: null, required: false },
countClass: { type: null, required: false },
iconClass: { type: null, required: false },
clearable: { type: Boolean, default: false }
});
const emit = defineEmits(['update:modelValue']);
const attrs = useAttrs();
const modelValue = useVModel(props, 'modelValue', emit, {
passive: true,
defaultValue: props.modelValue
});
const valueLength = computed(() => String(modelValue.value ?? '').length);
const remaining = computed(() => Math.max(props.maxlength - valueLength.value, 0));
const wrapperClass = computed(() => cn(props.class, attrs.class));
const inputClass = computed(() => cn(props.inputClass));
const inputAttrs = computed(() => {
const { class: _class, style: _style, ...rest } = attrs;
return rest;
});
const isDisabled = computed(() => Boolean(inputAttrs.value.disabled));
function clearValue() {
if (isDisabled.value) return;
modelValue.value = '';
}
</script>
<template>
<InputGroup :class="wrapperClass" :style="attrs.style">
<InputGroupAddon v-if="$slots.leading" align="inline-start">
<slot name="leading" />
</InputGroupAddon>
<component
:is="props.multiline ? InputGroupTextarea : InputGroupInput"
v-model="modelValue"
:maxlength="props.maxlength"
:class="inputClass"
v-bind="inputAttrs" />
<InputGroupAddon v-if="$slots.trailing" align="inline-end">
<slot name="trailing" />
</InputGroupAddon>
<InputGroupAddon v-if="props.clearable && valueLength > 0" align="inline-end">
<InputGroupButton size="icon-xs" :disabled="isDisabled" @click="clearValue">
<X class="size-3.5" />
<span class="sr-only">Clear</span>
</InputGroupButton>
</InputGroupAddon>
<InputGroupAddon :align="props.multiline ? 'block-end' : 'inline-end'" v-if="valueLength > 0">
<InputGroupText :class="cn('gap-1 tabular-nums text-xs', props.multiline && 'ml-auto', props.countClass)">
<span>{{ valueLength }}</span>
<span class="text-muted-foreground/70">/ {{ props.maxlength }}</span>
</InputGroupText>
</InputGroupAddon>
</InputGroup>
</template>

View File

@@ -0,0 +1,19 @@
<script setup>
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<Input
data-slot="input-group-control"
:class="
cn(
'flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent',
props.class
)
" />
</template>

View File

@@ -0,0 +1,19 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<span
:class="
cn(
'text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4',
props.class
)
">
<slot />
</span>
</template>

View File

@@ -0,0 +1,19 @@
<script setup>
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<Textarea
data-slot="input-group-control"
:class="
cn(
'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent',
props.class
)
" />
</template>

View File

@@ -0,0 +1,48 @@
import { cva } from 'class-variance-authority';
export { default as InputGroup } from './InputGroup.vue';
export { default as InputGroupAddon } from './InputGroupAddon.vue';
export { default as InputGroupButton } from './InputGroupButton.vue';
export { default as InputGroupInput } from './InputGroupInput.vue';
export { default as InputGroupText } from './InputGroupText.vue';
export { default as InputGroupTextarea } from './InputGroupTextarea.vue';
export { default as InputGroupCharCount } from './InputGroupCharCount.vue';
export const inputGroupAddonVariants = cva(
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
{
variants: {
align: {
'inline-start':
'order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]',
'inline-end':
'order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]',
'block-start':
'order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5',
'block-end':
'order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5'
}
},
defaultVariants: {
align: 'inline-start'
}
}
);
export const inputGroupButtonVariants = cva(
'text-sm shadow-none flex gap-2 items-center',
{
variants: {
size: {
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
sm: 'h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5',
'icon-xs':
'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0',
'icon-sm': 'size-8 p-0 has-[>svg]:p-0'
}
},
defaultVariants: {
size: 'xs'
}
}
);

View File

@@ -1,32 +1,31 @@
<script setup>
import { useVModel } from "@vueuse/core";
import { cn } from "@/lib/utils";
import { cn } from '@/lib/utils';
import { useVModel } from '@vueuse/core';
const props = defineProps({
defaultValue: { type: [String, Number], required: false },
modelValue: { type: [String, Number], required: false },
class: { type: null, required: false },
});
const props = defineProps({
defaultValue: { type: [String, Number], required: false },
modelValue: { type: [String, Number], required: false },
class: { type: null, required: false }
});
const emits = defineEmits(["update:modelValue"]);
const emits = defineEmits(['update:modelValue']);
const modelValue = useVModel(props, "modelValue", emits, {
passive: true,
defaultValue: props.defaultValue,
});
const modelValue = useVModel(props, 'modelValue', emits, {
passive: true,
defaultValue: props.defaultValue
});
</script>
<template>
<input
v-model="modelValue"
data-slot="input"
:class="
cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
props.class,
)
"
/>
<input
v-model="modelValue"
data-slot="input"
:class="
cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
props.class
)
" />
</template>

View File

@@ -0,0 +1,29 @@
<script setup>
import { cn } from '@/lib/utils';
import { useVModel } from '@vueuse/core';
const props = defineProps({
class: { type: null, required: false },
defaultValue: { type: [String, Number], required: false },
modelValue: { type: [String, Number], required: false }
});
const emits = defineEmits(['update:modelValue']);
const modelValue = useVModel(props, 'modelValue', emits, {
passive: true,
defaultValue: props.defaultValue
});
</script>
<template>
<textarea
v-model="modelValue"
data-slot="textarea"
:class="
cn(
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
props.class
)
" />
</template>

View File

@@ -0,0 +1 @@
export { default as Textarea } from './Textarea.vue';