replace el-input with InputGroup

This commit is contained in:
pa
2026-01-12 20:09:58 +09:00
committed by Natsumi
parent 4749e8cb56
commit 065870a7f8
67 changed files with 707 additions and 366 deletions

View File

@@ -7,9 +7,8 @@
</PopoverTrigger>
<PopoverContent side="bottom" align="start" class="w-155">
<div class="icon-picker">
<el-input
<InputGroupSearch
v-model="search"
clearable
class="icon-picker__search"
:placeholder="t('nav_menu.icon_picker.search_placeholder')" />
<el-scrollbar v-if="filteredCategories.length" height="600px" class="icon-picker__scroll">
@@ -47,6 +46,7 @@
<script setup>
import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { InputGroupSearch } from '@/components/ui/input-group';
import { useI18n } from 'vue-i18n';
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';

View File

@@ -402,16 +402,13 @@
<div class="x-friend-item" style="width: 100%; cursor: default">
<div class="detail">
<span class="name" style="margin-bottom: 5px">{{ t('dialog.avatar.info.memo') }}</span>
<el-input
<InputGroupTextareaField
v-model="memo"
class="extra"
size="small"
type="textarea"
:rows="2"
:autosize="{ minRows: 1, maxRows: 20 }"
:placeholder="t('dialog.avatar.info.memo_placeholder')"
resize="none"
@change="onAvatarMemoChange"></el-input>
input-class="resize-none min-h-0"
@change="onAvatarMemoChange" />
</div>
</div>
<div class="x-friend-item" style="width: 100%; cursor: default">
@@ -545,6 +542,7 @@
import { computed, defineAsyncComponent, nextTick, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { ElMessageBox } from 'element-plus';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { Spinner } from '@/components/ui/spinner';
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';

View File

@@ -56,14 +56,13 @@
<div style="font-size: 12px">{{ t('dialog.set_world_tags.author_tags') }}</div>
<el-input
<InputGroupTextareaField
:model-value="setAvatarStylesDialog.authorTags"
type="textarea"
size="small"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
:rows="2"
placeholder=""
style="margin-top: 10px"
input-class="resize-none"
@update:modelValue="(v) => updateDialog({ authorTags: v })" />
</template>
@@ -80,6 +79,7 @@
<script setup>
import { Button } from '@/components/ui/button';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';
import { watch } from 'vue';

View File

@@ -29,13 +29,13 @@
<span>{{ t('dialog.set_avatar_tags.content_sex') }}</span>
</label>
<br />
<el-input
<InputGroupTextareaField
v-model="setAvatarTagsDialog.selectedTagsCsv"
size="small"
:autosize="{ minRows: 2, maxRows: 5 }"
:rows="2"
:placeholder="t('dialog.set_avatar_tags.custom_tags_placeholder')"
style="margin-top: 10px"
@input="updateInputAvatarTags"></el-input>
input-class="resize-none"
@input="updateInputAvatarTags" />
<br />
<br />
<template
@@ -101,6 +101,7 @@
<script setup>
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { Loading } from '@element-plus/icons-vue';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';

View File

@@ -120,7 +120,7 @@
destroy-on-close>
<div class="folder-editor">
<div class="folder-editor__form">
<el-input
<InputGroupField
v-model="folderEditor.data.name"
:placeholder="t('nav_menu.custom_nav.folder_name_placeholder')" />
<IconPicker v-model="folderEditor.data.icon" class="folder-editor__icon-picker" />
@@ -219,6 +219,7 @@
import { Badge } from '../ui/badge';
import { Checkbox } from '../ui/checkbox';
import { InputGroupField } from '../ui/input-group';
import { navDefinitions } from '../../shared/constants/ui.js';
import IconPicker from '../IconPicker.vue';

View File

@@ -723,10 +723,10 @@
<span style="margin-right: 10px; vertical-align: top"
>{{ t('dialog.group.posts.posts_count') }} {{ groupDialog.posts.length }}</span
>
<el-input
<InputGroupField
v-model="groupDialog.postsSearch"
clearable
size="small"
size="sm"
:placeholder="t('dialog.group.posts.search_placeholder')"
style="width: 89%; margin-bottom: 10px"
@input="updateGroupPostSearch" />
@@ -908,11 +908,11 @@
</VirtualCombobox>
</div>
</div>
<el-input
<InputGroupField
v-model="groupDialog.memberSearch"
:disabled="!hasGroupPermission(groupDialog.ref, 'group-members-manage')"
clearable
size="small"
size="sm"
:placeholder="t('dialog.group.members.search')"
style="margin-top: 10px; margin-bottom: 10px"
@input="groupMembersSearch" />
@@ -1162,6 +1162,7 @@
import { computed, nextTick, reactive, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { ElMessageBox } from 'element-plus';
import { InputGroupField } from '@/components/ui/input-group';
import { RefreshCcw } from 'lucide-vue-next';
import { Spinner } from '@/components/ui/spinner';
import { VirtualCombobox } from '@/components/ui/virtual-combobox';

View File

@@ -105,14 +105,14 @@
</DropdownMenuContent>
</DropdownMenu>
</div>
<el-input
<InputGroupField
v-model="memberSearch"
:disabled="!hasGroupPermission(groupMemberModeration.groupRef, 'group-bans-manage')"
clearable
size="small"
size="sm"
:placeholder="t('dialog.group.members.search')"
style="margin-top: 10px; margin-bottom: 10px"
@input="groupMembersSearch"></el-input>
@input="groupMembersSearch" />
<br />
<Button size="sm" variant="outline" @click="selectAllGroupMembers">{{
t('dialog.group_member_moderation.select_all')
@@ -225,12 +225,12 @@
groupBansModerationTable.data.length
}}</span>
<br />
<el-input
<InputGroupField
v-model="groupBansModerationTable.filters[0].value"
clearable
size="small"
size="sm"
:placeholder="t('dialog.group.members.search')"
style="margin-top: 10px; margin-bottom: 10px"></el-input>
style="margin-top: 10px; margin-bottom: 10px" />
<br />
<Button size="sm" variant="outline" @click="selectAllGroupBans">{{
t('dialog.group_member_moderation.select_all')
@@ -644,12 +644,12 @@
}}</Button>
</div>
</div>
<el-input
<InputGroupField
v-model="groupLogsModerationTable.filters[0].value"
clearable
size="small"
size="sm"
:placeholder="t('dialog.group.members.search')"
style="margin-top: 10px; margin-bottom: 10px"></el-input>
style="margin-top: 10px; margin-bottom: 10px" />
<br />
<DataTable v-bind="groupLogsModerationTable" style="margin-top: 10px">
<el-table-column
@@ -708,12 +708,12 @@
<br />
<span class="name">{{ t('dialog.group_member_moderation.user_id') }}</span>
<br />
<el-input
<InputGroupField
v-model="selectUserId"
size="small"
size="sm"
style="margin-top: 5px; width: 340px"
:placeholder="t('dialog.group_member_moderation.user_id_placeholder')"
clearable></el-input>
clearable />
<Button
size="sm"
variant="outline"
@@ -765,16 +765,13 @@
<br />
<br />
<span class="name">{{ t('dialog.group_member_moderation.notes') }}</span>
<el-input
<InputGroupTextareaField
v-model="note"
class="extra"
type="textarea"
:rows="2"
:autosize="{ minRows: 1, maxRows: 20 }"
:placeholder="t('dialog.group_member_moderation.note_placeholder')"
size="small"
resize="none"
style="margin-top: 5px"></el-input>
style="margin-top: 5px"
input-class="resize-none min-h-0" />
<br />
<br />
<span class="name">{{ t('dialog.group_member_moderation.selected_roles') }}</span>
@@ -870,6 +867,7 @@
<script setup>
import { ArrowDown, Loading, Refresh, Warning } from '@element-plus/icons-vue';
import { reactive, ref, watch } from 'vue';
import { InputGroupField, InputGroupTextareaField } from '@/components/ui/input-group';
import { Button } from '@/components/ui/button';
import { Spinner } from '@/components/ui/spinner';
import { Trash2 } from 'lucide-vue-next';

View File

@@ -18,14 +18,12 @@
</label>
</div>
<br />
<el-input
<InputGroupTextareaField
v-model="groupLogsExportContent"
type="textarea"
size="small"
:rows="15"
resize="none"
readonly
style="margin-top: 15px"
input-class="resize-none"
@click="handleCopyGroupLogsExportContent" />
</el-dialog>
</template>
@@ -33,6 +31,7 @@
<script setup>
import { ref, watch } from 'vue';
import { Checkbox } from '@/components/ui/checkbox';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { useI18n } from 'vue-i18n';
import { copyToClipboard } from '../../../shared/utils';

View File

@@ -8,16 +8,14 @@
<h3 v-text="groupPostEditDialog.groupRef.name"></h3>
<el-form :model="groupPostEditDialog" label-width="150px">
<el-form-item :label="t('dialog.group_post_edit.title')">
<el-input v-model="groupPostEditDialog.title" size="small"></el-input>
<InputGroupField v-model="groupPostEditDialog.title" size="sm" />
</el-form-item>
<el-form-item :label="t('dialog.group_post_edit.message')">
<el-input
<InputGroupTextareaField
v-model="groupPostEditDialog.text"
type="textarea"
:rows="4"
:autosize="{ minRows: 4, maxRows: 20 }"
style="margin-top: 10px"
resize="none"></el-input>
input-class="resize-none" />
</el-form-item>
<el-form-item>
<label v-if="!groupPostEditDialog.postId" class="inline-flex items-center gap-2">
@@ -111,6 +109,7 @@
<script setup>
import { computed, ref } from 'vue';
import { Button } from '@/components/ui/button';
import { InputGroupField, InputGroupTextareaField } from '@/components/ui/input-group';
import { Checkbox } from '@/components/ui/checkbox';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';

View File

@@ -10,13 +10,13 @@
<span>{{ t('dialog.edit_send_invite_message.description') }}</span>
</div>
<InputGroupCharCount
<InputGroupTextareaField
v-model="editAndSendInviteDialog.newMessage"
:maxlength="64"
multiline
rows="2"
:rows="2"
class="mt-2.5"
placeholder="" />
placeholder=""
show-count />
<template #footer>
<Button variant="secondary" class="mr-2" @click="cancelEditAndSendInvite">
@@ -31,7 +31,7 @@
<script setup>
import { Button } from '@/components/ui/button';
import { InputGroupCharCount } from '@/components/ui/input-group';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';

View File

@@ -2,9 +2,9 @@
<el-dialog :z-index="launchDialogIndex" v-model="isVisible" :title="t('dialog.launch.header')" width="450px">
<el-form :model="launchDialog" label-width="100px">
<el-form-item :label="t('dialog.launch.url')">
<el-input
<InputGroupField
v-model="launchDialog.url"
size="small"
size="sm"
style="width: 230px"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
@@ -26,9 +26,9 @@
</TooltipWrapper>
</div>
</template>
<el-input
<InputGroupField
v-model="launchDialog.shortUrl"
size="small"
size="sm"
style="width: 230px"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
@@ -42,9 +42,9 @@
</TooltipWrapper>
</el-form-item>
<el-form-item :label="t('dialog.launch.location')">
<el-input
<InputGroupField
v-model="launchDialog.location"
size="small"
size="sm"
style="width: 230px"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
@@ -130,6 +130,7 @@
import { ButtonGroup } from '@/components/ui/button-group';
import { Copy } from 'lucide-vue-next';
import { ElMessageBox } from 'element-plus';
import { InputGroupField } from '@/components/ui/input-group';
import { MoreHorizontal } from 'lucide-vue-next';
import { Warning } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';

View File

@@ -115,12 +115,12 @@
@update:modelValue="buildInstance" />
</el-form-item>
<el-form-item :label="t('dialog.new_instance.display_name')">
<el-input
<InputGroupField
:disabled="!isLocalUserVrcPlusSupporter"
v-model="newInstanceDialog.displayName"
size="small"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
@change="buildInstance"></el-input>
@change="buildInstance" />
</el-form-item>
<el-form-item
v-if="newInstanceDialog.accessType === 'group'"
@@ -177,14 +177,14 @@
</el-form-item>
<template v-if="newInstanceDialog.instanceCreated">
<el-form-item :label="t('dialog.new_instance.location')">
<el-input
<InputGroupField
v-model="newInstanceDialog.location"
size="small"
size="sm"
readonly
@click="$event.target.tagName === 'INPUT' && $event.target.select()"></el-input>
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
</el-form-item>
<el-form-item :label="t('dialog.new_instance.url')">
<el-input v-model="newInstanceDialog.url" size="small" readonly></el-input>
<InputGroupField v-model="newInstanceDialog.url" size="sm" readonly />
</el-form-item>
</template>
</el-form>
@@ -275,18 +275,18 @@
<Checkbox v-model="newInstanceDialog.ageGate" @update:modelValue="buildInstance" />
</el-form-item>
<el-form-item :label="t('dialog.new_instance.world_id')">
<el-input
<InputGroupField
v-model="newInstanceDialog.worldId"
size="small"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
@change="buildLegacyInstance"></el-input>
@change="buildLegacyInstance" />
</el-form-item>
<el-form-item :label="t('dialog.new_instance.instance_id')">
<el-input
<InputGroupField
v-model="newInstanceDialog.instanceName"
:placeholder="t('dialog.new_instance.instance_id_placeholder')"
size="small"
@change="buildLegacyInstance"></el-input>
size="sm"
@change="buildLegacyInstance" />
</el-form-item>
<el-form-item
v-if="
@@ -352,14 +352,14 @@
</VirtualCombobox>
</el-form-item>
<el-form-item :label="t('dialog.new_instance.location')">
<el-input
<InputGroupField
v-model="newInstanceDialog.location"
size="small"
size="sm"
readonly
@click="$event.target.tagName === 'INPUT' && $event.target.select()"></el-input>
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
</el-form-item>
<el-form-item :label="t('dialog.new_instance.url')">
<el-input v-model="newInstanceDialog.url" size="small" readonly></el-input>
<InputGroupField v-model="newInstanceDialog.url" size="sm" readonly />
</el-form-item>
</el-form>
</el-tab-pane>
@@ -443,6 +443,7 @@
<script setup>
import { computed, nextTick, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { InputGroupField } from '@/components/ui/input-group';
import { Checkbox } from '@/components/ui/checkbox';
import { Check as CheckIcon } from 'lucide-vue-next';
import { storeToRefs } from 'pinia';

View File

@@ -7,7 +7,7 @@
append-to-body>
<div style="display: flex; align-items: center; justify-content: space-between">
<span style="font-size: 14px" v-text="previousInstancesGroupDialog.groupRef.name"></span>
<el-input
<InputGroupField
v-model="previousInstancesGroupDialogTable.filters[0].value"
:placeholder="t('dialog.previous_instances.search_placeholder')"
style="width: 150px" />
@@ -70,6 +70,7 @@
<script setup>
import { computed, nextTick, reactive, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { InputGroupField } from '@/components/ui/input-group';
import { ElMessageBox } from 'element-plus';
import { useI18n } from 'vue-i18n';

View File

@@ -9,11 +9,11 @@
@close="closeDialog">
<div style="display: flex; align-items: center; justify-content: space-between">
<Location :location="location.tag" style="font-size: 14px" />
<el-input
<InputGroupField
v-model="dataTable.filters[0].value"
:placeholder="t('dialog.previous_instances.search_placeholder')"
style="width: 150px"
clearable></el-input>
clearable />
</div>
<DataTable :loading="loading" v-bind="dataTable" style="margin-top: 10px">
<el-table-column :label="t('table.previous_instances.date')" prop="created_at" sortable width="130">
@@ -64,6 +64,7 @@
import { useI18n } from 'vue-i18n';
import { compareByCreatedAt, formatDateFilter, parseLocation, timeToText } from '../../../shared/utils';
import { InputGroupField } from '../../../components/ui/input-group';
import { useGameLogStore, useInstanceStore, useUserStore } from '../../../stores';
import { database } from '../../../service/database';
import { getNextDialogIndex } from '../../../shared/utils/base/ui';

View File

@@ -7,10 +7,10 @@
append-to-body>
<div style="display: flex; align-items: center; justify-content: space-between">
<span style="font-size: 14px" v-text="previousInstancesWorldDialog.worldRef.name"></span>
<el-input
<InputGroupField
v-model="previousInstancesWorldDialogTable.filters[0].value"
:placeholder="t('dialog.previous_instances.search_placeholder')"
style="display: block; width: 150px"></el-input>
style="display: block; width: 150px" />
</div>
<DataTable :loading="loading" v-bind="previousInstancesWorldDialogTable" style="margin-top: 10px">
<el-table-column :label="t('table.previous_instances.date')" prop="created_at" sortable width="170">
@@ -74,6 +74,7 @@
<script setup>
import { computed, nextTick, reactive, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { InputGroupField } from '@/components/ui/input-group';
import { ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';

View File

@@ -6,25 +6,29 @@
width="600px"
append-to-body>
<div v-loading="bioDialog.loading">
<InputGroupCharCount
<InputGroupTextareaField
v-model="bioDialog.bio"
:maxlength="512"
multiline
rows="5"
:rows="5"
:placeholder="t('dialog.bio.bio_placeholder')"
class="mb-2.5" />
class="mb-2.5"
show-count />
<el-input
<InputGroupAction
v-for="(link, index) in bioDialog.bioLinks"
:key="index"
v-model="bioDialog.bioLinks[index]"
size="small"
maxlength="64"
show-word-limit
show-count
size="sm"
style="margin-top: 5px">
<img :src="getFaviconUrl(link)" style="width: 16px; height: 16px; vertical-align: middle" />
<Button variant="outline" @click="bioDialog.bioLinks.splice(index, 1)" />
</el-input>
<template #leading>
<img :src="getFaviconUrl(link)" style="width: 16px; height: 16px; vertical-align: middle" />
</template>
<template #actions>
<Button variant="outline" @click="bioDialog.bioLinks.splice(index, 1)" />
</template>
</InputGroupAction>
<Button
variant="outline"
@@ -46,8 +50,7 @@
<script setup>
import { Button } from '@/components/ui/button';
import { Delete } from '@element-plus/icons-vue';
import { InputGroupCharCount } from '@/components/ui/input-group';
import { InputGroupAction, InputGroupTextareaField } from '@/components/ui/input-group';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';

View File

@@ -11,26 +11,24 @@
<template v-if="!hideUserNotes || (hideUserNotes && hideUserMemos)">
<span class="name">{{ t('dialog.user.info.note') }}</span>
<br />
<InputGroupCharCount
<InputGroupTextareaField
v-model="note"
:autosize="{ minRows: 6, maxRows: 20 }"
:maxlength="256"
multiline
rows="6"
:rows="6"
:placeholder="t('dialog.user.info.note_placeholder')"
input-class="extra resize-none" />
input-class="extra resize-none"
show-count />
</template>
<template v-if="!hideUserMemos || (hideUserNotes && hideUserMemos)">
<span class="name">{{ t('dialog.user.info.memo') }}</span>
<br />
<el-input
<InputGroupTextareaField
v-model="memo"
class="extra"
type="textarea"
:rows="6"
:autosize="{ minRows: 2, maxRows: 20 }"
:placeholder="t('dialog.user.info.memo_placeholder')"
size="small"
resize="none"></el-input>
input-class="resize-none min-h-0" />
</template>
<template #footer>
<div class="dialog-footer">
@@ -44,7 +42,7 @@
<script setup>
import { ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { InputGroupCharCount } from '@/components/ui/input-group';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';

View File

@@ -7,10 +7,10 @@
append-to-body>
<div style="display: flex; align-items: center; justify-content: space-between">
<span style="font-size: 14px" v-text="previousInstancesUserDialog.userRef.displayName"></span>
<el-input
<InputGroupField
v-model="previousInstancesUserDialogTable.filters[0].value"
:placeholder="t('dialog.previous_instances.search_placeholder')"
style="display: block; width: 150px"></el-input>
style="display: block; width: 150px" />
</div>
<DataTable :loading="loading" v-bind="previousInstancesUserDialogTable" style="margin-top: 10px">
<el-table-column :label="t('table.previous_instances.date')" prop="created_at" sortable width="170">
@@ -78,6 +78,7 @@
<script setup>
import { computed, nextTick, reactive, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { InputGroupField } from '@/components/ui/input-group';
import { ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';

View File

@@ -6,12 +6,12 @@
width="600px"
append-to-body>
<div v-loading="pronounsDialog.loading">
<InputGroupCharCount
<InputGroupTextareaField
v-model="pronounsDialog.pronouns"
:maxlength="32"
multiline
rows="2"
:placeholder="t('dialog.pronouns.pronouns_placeholder')" />
:rows="2"
:placeholder="t('dialog.pronouns.pronouns_placeholder')"
show-count />
</div>
<template #footer>
<Button :disabled="pronounsDialog.loading" @click="savePronouns">
@@ -23,7 +23,7 @@
<script setup>
import { Button } from '@/components/ui/button';
import { InputGroupCharCount } from '@/components/ui/input-group';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';

View File

@@ -41,11 +41,12 @@
</SelectContent>
</Select>
<InputGroupCharCount
<InputGroupField
v-model="socialStatusDialog.statusDescription"
:placeholder="t('dialog.social_status.status_placeholder')"
:maxlength="32"
clearable
show-count
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">
@@ -88,8 +89,8 @@
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { computed, ref } from 'vue';
import { Button } from '@/components/ui/button';
import { InputGroupField } from '@/components/ui/input-group';
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

@@ -20,14 +20,12 @@
<span>{{ t('dialog.set_world_tags.enable_debugging') }}</span>
</label>
<div style="font-size: 12px; margin-top: 10px">{{ t('dialog.set_world_tags.author_tags') }}<br /></div>
<el-input
<InputGroupTextareaField
v-model="setWorldTagsDialog.authorTags"
type="textarea"
size="small"
show-word-limit
:autosize="{ minRows: 2, maxRows: 5 }"
:rows="2"
placeholder=""
style="margin-top: 10px"></el-input>
style="margin-top: 10px"
input-class="resize-none" />
<div style="font-size: 12px; margin-top: 10px">{{ t('dialog.set_world_tags.content_tags') }}<br /></div>
<label class="inline-flex items-center gap-2">
<Checkbox v-model="setWorldTagsDialog.contentHorror" />
@@ -106,6 +104,7 @@
<script setup>
import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { Checkbox } from '@/components/ui/checkbox';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';

View File

@@ -6,14 +6,16 @@
destroy-on-close
append-to-body>
<div>
<el-input
<InputGroupAction
v-for="(domain, index) in urlList"
:key="index"
v-model="urlList[index]"
size="small"
size="sm"
style="margin-top: 5px">
<Button variant="outline" @click="urlList.splice(index, 1)"></Button>
</el-input>
<template #actions>
<Button variant="outline" @click="urlList.splice(index, 1)"></Button>
</template>
</InputGroupAction>
<Button size="sm" variant="outline" style="margin-top: 5px" @click="urlList.push('')">
{{ t('dialog.allowed_video_player_domains.add_domain') }}
</Button>
@@ -29,6 +31,7 @@
<script setup>
import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { InputGroupAction } from '@/components/ui/input-group';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';

View File

@@ -437,15 +437,12 @@
<span class="name">
{{ t('dialog.world.info.memo') }}
</span>
<el-input
<InputGroupTextareaField
v-model="memo"
class="extra"
type="textarea"
:rows="2"
:autosize="{ minRows: 1, maxRows: 20 }"
:placeholder="t('dialog.world.info.memo_placeholder')"
size="small"
resize="none"
input-class="resize-none min-h-0"
@change="onWorldMemoChange" />
</div>
</div>
@@ -769,6 +766,7 @@
import { computed, defineAsyncComponent, nextTick, ref, watch } from 'vue';
import { Ellipsis, RefreshCcw, Star, Trash2 } from 'lucide-vue-next';
import { Button } from '@/components/ui/button';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';

View File

@@ -0,0 +1,47 @@
<script setup>
import { useAttrs } from 'vue';
import { useVModel } from '@vueuse/core';
import InputGroupField from './InputGroupField.vue';
defineOptions({ inheritAttrs: false });
const props = defineProps({
modelValue: { type: [String, Number], default: '' },
class: { type: null, required: false },
inputClass: { type: null, required: false },
clearable: { type: Boolean, default: false },
showPassword: { type: Boolean, default: false },
showCount: { type: Boolean, default: false },
maxlength: { type: Number, required: false },
size: { type: String, default: 'default' }
});
const emit = defineEmits(['update:modelValue']);
const attrs = useAttrs();
const modelValue = useVModel(props, 'modelValue', emit, {
passive: true,
defaultValue: props.modelValue
});
</script>
<template>
<InputGroupField
v-model="modelValue"
:class="props.class"
:input-class="props.inputClass"
:clearable="props.clearable"
:show-password="props.showPassword"
:show-count="props.showCount"
:maxlength="props.maxlength"
:size="props.size"
v-bind="attrs">
<template v-if="$slots.leading" #leading>
<slot name="leading" />
</template>
<template v-if="$slots.actions" #trailing>
<slot name="actions" />
</template>
</InputGroupField>
</template>

View File

@@ -0,0 +1,50 @@
<script setup>
import { computed, useAttrs, useSlots } from 'vue';
import { useVModel } from '@vueuse/core';
import InputGroupField from './InputGroupField.vue';
import { InputGroupText } from '.';
defineOptions({ inheritAttrs: false });
const props = defineProps({
modelValue: { type: [String, Number], default: '' },
class: { type: null, required: false },
inputClass: { type: null, required: false },
prefixText: { type: String, default: '' },
suffixText: { type: String, default: '' },
clearable: { type: Boolean, default: false },
size: { type: String, default: 'default' }
});
const emit = defineEmits(['update:modelValue']);
const attrs = useAttrs();
const slots = useSlots();
const modelValue = useVModel(props, 'modelValue', emit, {
passive: true,
defaultValue: props.modelValue
});
const hasLeading = computed(() => Boolean(props.prefixText) || Boolean(slots.leading));
const hasTrailing = computed(() => Boolean(props.suffixText) || Boolean(slots.trailing));
</script>
<template>
<InputGroupField
v-model="modelValue"
:class="props.class"
:input-class="props.inputClass"
:clearable="props.clearable"
:size="props.size"
v-bind="attrs">
<template v-if="hasLeading" #leading>
<InputGroupText v-if="props.prefixText">{{ props.prefixText }}</InputGroupText>
<slot name="leading" />
</template>
<template v-if="hasTrailing" #trailing>
<slot name="trailing" />
<InputGroupText v-if="props.suffixText">{{ props.suffixText }}</InputGroupText>
</template>
</InputGroupField>
</template>

View File

@@ -1,81 +0,0 @@
<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,124 @@
<script setup>
import { computed, ref, useAttrs } from 'vue';
import { useVModel } from '@vueuse/core';
import { Eye, EyeOff, X } from 'lucide-vue-next';
import { cn } from '@/lib/utils';
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText } from '.';
defineOptions({ inheritAttrs: false });
const props = defineProps({
modelValue: { type: [String, Number], default: '' },
class: { type: null, required: false },
inputClass: { type: null, required: false },
clearable: { type: Boolean, default: false },
showPassword: { type: Boolean, default: false },
showCount: { type: Boolean, default: false },
maxlength: { type: Number, required: false },
type: { type: String, required: false },
size: { type: String, default: 'default' }
});
const emit = defineEmits(['update:modelValue', 'input', 'change']);
const attrs = useAttrs();
const modelValue = useVModel(props, 'modelValue', emit, {
passive: true,
defaultValue: props.modelValue
});
const reveal = ref(false);
const valueLength = computed(() => String(modelValue.value ?? '').length);
const maxLength = computed(() => props.maxlength ?? attrs.maxlength);
const wrapperClass = computed(() =>
cn(props.class, attrs.class, props.size === 'sm' && 'h-8')
);
const inputClass = computed(() => cn(props.inputClass));
const inputType = computed(() => {
const rawType = props.type ?? attrs.type;
if (props.showPassword) {
return reveal.value ? 'text' : rawType || 'password';
}
return rawType;
});
const inputAttrs = computed(() => {
const {
class: _class,
style: _style,
type: _type,
maxlength: _maxlength,
onInput: _onInput,
onChange: _onChange,
...rest
} = attrs;
return {
...rest,
type: inputType.value,
maxlength: maxLength.value
};
});
const isDisabled = computed(() => Boolean(inputAttrs.value.disabled));
const showCount = computed(() => Boolean(maxLength.value) && props.showCount);
function clearValue() {
if (isDisabled.value) return;
modelValue.value = '';
emit('input', '');
emit('change', '');
}
function toggleReveal() {
if (isDisabled.value) return;
reveal.value = !reveal.value;
}
function handleInput(event) {
const value = event?.target?.value ?? '';
emit('input', value);
}
function handleChange(event) {
const value = event?.target?.value ?? '';
emit('change', value);
}
</script>
<template>
<InputGroup :class="wrapperClass" :style="attrs.style" :data-disabled="isDisabled ? 'true' : undefined">
<InputGroupAddon v-if="$slots.leading" align="inline-start">
<slot name="leading" />
</InputGroupAddon>
<InputGroupInput
v-model="modelValue"
:class="inputClass"
v-bind="inputAttrs"
@input="handleInput"
@change="handleChange" />
<InputGroupAddon v-if="$slots.trailing" align="inline-end">
<slot name="trailing" />
</InputGroupAddon>
<InputGroupAddon v-if="props.showPassword" align="inline-end">
<InputGroupButton size="icon-xs" :disabled="isDisabled" @click="toggleReveal">
<Eye v-if="!reveal" class="size-3.5" />
<EyeOff v-else class="size-3.5" />
<span class="sr-only">Toggle password</span>
</InputGroupButton>
</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 v-if="showCount && valueLength > 0" align="inline-end">
<InputGroupText class="gap-1 tabular-nums text-xs">
<span>{{ valueLength }}</span>
<span class="text-muted-foreground/70">/ {{ maxLength }}</span>
</InputGroupText>
</InputGroupAddon>
</InputGroup>
</template>

View File

@@ -0,0 +1,42 @@
<script setup>
import { useAttrs } from 'vue';
import { useVModel } from '@vueuse/core';
import { Search } from 'lucide-vue-next';
import InputGroupField from './InputGroupField.vue';
defineOptions({ inheritAttrs: false });
const props = defineProps({
modelValue: { type: [String, Number], default: '' },
class: { type: null, required: false },
inputClass: { type: null, required: false },
clearable: { type: Boolean, default: true },
size: { type: String, default: 'default' }
});
const emit = defineEmits(['update:modelValue']);
const attrs = useAttrs();
const modelValue = useVModel(props, 'modelValue', emit, {
passive: true,
defaultValue: props.modelValue
});
</script>
<template>
<InputGroupField
v-model="modelValue"
:class="props.class"
:input-class="props.inputClass"
:clearable="props.clearable"
:size="props.size"
v-bind="attrs">
<template #leading>
<Search class="size-4" />
</template>
<template v-if="$slots.trailing" #trailing>
<slot name="trailing" />
</template>
</InputGroupField>
</template>

View File

@@ -0,0 +1,144 @@
<script setup>
import { computed, nextTick, onMounted, ref, useAttrs, watch } from 'vue';
import { useVModel } from '@vueuse/core';
import { X } from 'lucide-vue-next';
import { cn } from '@/lib/utils';
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupTextarea } from '.';
defineOptions({ inheritAttrs: false });
const props = defineProps({
modelValue: { type: [String, Number], default: '' },
class: { type: null, required: false },
inputClass: { type: null, required: false },
clearable: { type: Boolean, default: false },
showCount: { type: Boolean, default: false },
maxlength: { type: Number, required: false },
autosize: { type: [Boolean, Object], default: false }
});
const emit = defineEmits(['update:modelValue', 'input', 'change']);
const attrs = useAttrs();
const modelValue = useVModel(props, 'modelValue', emit, {
passive: true,
defaultValue: props.modelValue
});
const textareaRef = ref(null);
const valueLength = computed(() => String(modelValue.value ?? '').length);
const maxLength = computed(() => props.maxlength ?? attrs.maxlength);
const wrapperClass = computed(() => cn(props.class, attrs.class));
const inputClass = computed(() => cn(props.inputClass));
const showCount = computed(() => Boolean(maxLength.value) && props.showCount);
const autosizeConfig = computed(() => {
if (!props.autosize) return null;
return typeof props.autosize === 'object' ? props.autosize : {};
});
const inputAttrs = computed(() => {
const {
class: _class,
style: _style,
maxlength: _maxlength,
onInput: _onInput,
onChange: _onChange,
...rest
} = attrs;
return {
...rest,
maxlength: maxLength.value
};
});
const isDisabled = computed(() => Boolean(inputAttrs.value.disabled));
function resolveTextareaEl() {
const instance = textareaRef.value;
if (!instance) return null;
return instance.$el ?? instance;
}
function resizeTextarea() {
if (!autosizeConfig.value) return;
const el = resolveTextareaEl();
if (!el) return;
const computedStyle = window.getComputedStyle(el);
const lineHeight = parseFloat(computedStyle.lineHeight) || 16;
const paddingTop = parseFloat(computedStyle.paddingTop) || 0;
const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0;
const minRows = autosizeConfig.value.minRows ?? Number(attrs.rows) || 1;
const maxRows = autosizeConfig.value.maxRows ?? Number.POSITIVE_INFINITY;
const minHeight = lineHeight * minRows + paddingTop + paddingBottom;
const maxHeight = lineHeight * maxRows + paddingTop + paddingBottom;
el.style.height = 'auto';
const nextHeight = Math.min(maxHeight, Math.max(el.scrollHeight, minHeight));
el.style.height = `${nextHeight}px`;
}
function clearValue() {
if (isDisabled.value) return;
modelValue.value = '';
emit('input', '');
emit('change', '');
nextTick(resizeTextarea);
}
function handleInput(event) {
const value = event?.target?.value ?? '';
emit('input', value);
resizeTextarea();
}
function handleChange(event) {
const value = event?.target?.value ?? '';
emit('change', value);
}
onMounted(() => {
if (autosizeConfig.value) {
nextTick(resizeTextarea);
}
});
watch(
() => modelValue.value,
() => {
if (autosizeConfig.value) {
nextTick(resizeTextarea);
}
}
);
</script>
<template>
<InputGroup :class="wrapperClass" :style="attrs.style" :data-disabled="isDisabled ? 'true' : undefined">
<InputGroupAddon v-if="$slots.leading" align="block-start">
<slot name="leading" />
</InputGroupAddon>
<InputGroupTextarea
ref="textareaRef"
v-model="modelValue"
:class="inputClass"
v-bind="inputAttrs"
@input="handleInput"
@change="handleChange" />
<InputGroupAddon v-if="$slots.trailing" align="block-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 v-if="showCount && valueLength > 0" align="block-end">
<InputGroupText class="gap-1 tabular-nums text-xs">
<span>{{ valueLength }}</span>
<span class="text-muted-foreground/70">/ {{ maxLength }}</span>
</InputGroupText>
</InputGroupAddon>
</InputGroup>
</template>

View File

@@ -6,7 +6,11 @@ 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 { default as InputGroupField } from './InputGroupField.vue';
export { default as InputGroupTextareaField } from './InputGroupTextareaField.vue';
export { default as InputGroupSearch } from './InputGroupSearch.vue';
export { default as InputGroupAffix } from './InputGroupAffix.vue';
export { default as InputGroupAction } from './InputGroupAction.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",