replace el-form

This commit is contained in:
pa
2026-01-12 22:42:54 +09:00
committed by Natsumi
parent 82bd985142
commit c814f8f60c
34 changed files with 1419 additions and 736 deletions

View File

@@ -145,7 +145,7 @@
side="top"
:content="t('dialog.group.actions.unrepresent_tooltip')">
<Button
class="rounded-full"
class="rounded-full mr-2"
variant="secondary"
size="icon-lg"
style="margin-left: 5px"
@@ -156,10 +156,9 @@
<TooltipWrapper v-else side="top" :content="t('dialog.group.actions.represent_tooltip')">
<span>
<Button
class="rounded-full"
class="rounded-full mr-2"
variant="outline"
size="icon-lg"
style="margin-left: 5px"
:disabled="groupDialog.ref.privacy === 'private'"
@click="setGroupRepresentation(groupDialog.id)">
<StarFilled />
@@ -171,10 +170,9 @@
<TooltipWrapper side="top" :content="t('dialog.group.actions.cancel_join_request_tooltip')">
<span>
<Button
class="rounded-full"
class="rounded-full mr-2"
variant="outline"
size="icon-lg"
style="margin-left: 5px"
@click="cancelGroupRequest(groupDialog.id)">
<Close />
</Button>
@@ -185,10 +183,9 @@
<TooltipWrapper side="top" :content="t('dialog.group.actions.pending_request_tooltip')">
<span>
<Button
class="rounded-full"
class="rounded-full mr-2"
variant="outline"
size="icon-lg"
style="margin-left: 5px"
@click="joinGroup(groupDialog.id)">
<Check />
</Button>
@@ -201,10 +198,9 @@
side="top"
:content="t('dialog.group.actions.request_join_tooltip')">
<Button
class="rounded-full"
class="rounded-full mr-2"
variant="outline"
size="icon-lg"
style="margin-left: 5px"
@click="joinGroup(groupDialog.id)">
<Message />
</Button>
@@ -214,12 +210,7 @@
side="top"
:content="t('dialog.group.actions.invite_required_tooltip')">
<span>
<Button
class="rounded-full"
variant="outline"
size="icon-lg"
disabled
style="margin-left: 5px">
<Button class="rounded-full mr-2" variant="outline" size="icon-lg" disabled>
<Message />
</Button>
</span>
@@ -229,10 +220,9 @@
side="top"
:content="t('dialog.group.actions.join_group_tooltip')">
<Button
class="rounded-full"
class="rounded-full mr-2"
variant="outline"
size="icon-lg"
style="margin-left: 5px"
@click="joinGroup(groupDialog.id)">
<Check />
</Button>
@@ -245,8 +235,7 @@
:variant="
groupDialog.ref.membershipStatus === 'userblocked' ? 'destructive' : 'outline'
"
size="icon-lg"
style="margin-left: 5px">
size="icon-lg">
<MoreFilled />
</Button>
</DropdownMenuTrigger>

View File

@@ -5,88 +5,105 @@
width="650px"
append-to-body>
<div v-if="groupPostEditDialog.visible">
<h3 v-text="groupPostEditDialog.groupRef.name"></h3>
<el-form :model="groupPostEditDialog" label-width="150px">
<el-form-item :label="t('dialog.group_post_edit.title')">
<InputGroupField v-model="groupPostEditDialog.title" size="sm" />
</el-form-item>
<el-form-item :label="t('dialog.group_post_edit.message')">
<InputGroupTextareaField
v-model="groupPostEditDialog.text"
:rows="4"
style="margin-top: 10px"
input-class="resize-none" />
</el-form-item>
<el-form-item>
<label v-if="!groupPostEditDialog.postId" class="inline-flex items-center gap-2">
<Checkbox v-model="groupPostEditDialog.sendNotification" />
<span>{{ t('dialog.group_post_edit.send_notification') }}</span>
</label>
</el-form-item>
<el-form-item :label="t('dialog.group_post_edit.post_visibility')">
<RadioGroup v-model="groupPostEditDialog.visibility" class="flex items-center gap-4">
<div class="flex items-center space-x-2">
<RadioGroupItem id="groupPostVisibility-public" value="public" />
<label for="groupPostVisibility-public">
{{ t('dialog.group_post_edit.visibility_public') }}
</label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem id="groupPostVisibility-group" value="group" />
<label for="groupPostVisibility-group">
{{ t('dialog.group_post_edit.visibility_group') }}
</label>
</div>
</RadioGroup>
</el-form-item>
<el-form-item v-if="groupPostEditDialog.visibility === 'group'" :label="t('dialog.new_instance.roles')">
<Select
multiple
:model-value="Array.isArray(groupPostEditDialog.roleIds) ? groupPostEditDialog.roleIds : []"
@update:modelValue="handleRoleIdsChange">
<SelectTrigger size="sm" class="w-full">
<SelectValue>
<span class="truncate">
{{ selectedRoleSummary || t('dialog.new_instance.role_placeholder') }}
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="role in groupPostEditDialog.groupRef?.roles ?? []"
:key="role.id"
:value="role.id">
{{ role.name }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</el-form-item>
<el-form-item :label="t('dialog.group_post_edit.image')">
<template v-if="gallerySelectDialog.selectedFileId">
<div style="display: inline-block; flex: none; margin-right: 5px">
<img
:src="gallerySelectDialog.selectedImageUrl"
style="flex: none; width: 60px; height: 60px; border-radius: 4px; object-fit: cover"
@click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)"
loading="lazy" />
<Button
size="sm"
variant="outline"
style="vertical-align: top"
@click="clearImageGallerySelect">
{{ t('dialog.invite_message.clear_selected_image') }}
<FieldGroup class="gap-4">
<Field>
<FieldLabel>{{ t('dialog.group_post_edit.title') }}</FieldLabel>
<FieldContent>
<InputGroupField v-model="groupPostEditDialog.title" size="sm" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.group_post_edit.message') }}</FieldLabel>
<FieldContent>
<InputGroupTextareaField
v-model="groupPostEditDialog.text"
:rows="4"
style="margin-top: 10px"
input-class="resize-none" />
</FieldContent>
</Field>
<Field v-if="!groupPostEditDialog.postId">
<FieldLabel class="sr-only">{{ t('dialog.group_post_edit.send_notification') }}</FieldLabel>
<FieldContent>
<label class="inline-flex items-center gap-2">
<Checkbox v-model="groupPostEditDialog.sendNotification" />
<span>{{ t('dialog.group_post_edit.send_notification') }}</span>
</label>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.group_post_edit.post_visibility') }}</FieldLabel>
<FieldContent>
<RadioGroup v-model="groupPostEditDialog.visibility" class="flex items-center gap-4">
<div class="flex items-center space-x-2">
<RadioGroupItem id="groupPostVisibility-public" value="public" />
<label for="groupPostVisibility-public">
{{ t('dialog.group_post_edit.visibility_public') }}
</label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem id="groupPostVisibility-group" value="group" />
<label for="groupPostVisibility-group">
{{ t('dialog.group_post_edit.visibility_group') }}
</label>
</div>
</RadioGroup>
</FieldContent>
</Field>
<Field v-if="groupPostEditDialog.visibility === 'group'">
<FieldLabel>{{ t('dialog.new_instance.roles') }}</FieldLabel>
<FieldContent>
<Select
multiple
:model-value="Array.isArray(groupPostEditDialog.roleIds) ? groupPostEditDialog.roleIds : []"
@update:modelValue="handleRoleIdsChange">
<SelectTrigger size="sm" class="w-full">
<SelectValue>
<span class="truncate">
{{ selectedRoleSummary || t('dialog.new_instance.role_placeholder') }}
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="role in groupPostEditDialog.groupRef?.roles ?? []"
:key="role.id"
:value="role.id">
{{ role.name }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.group_post_edit.image') }}</FieldLabel>
<FieldContent>
<template v-if="gallerySelectDialog.selectedFileId">
<div style="display: inline-block; flex: none; margin-right: 5px">
<img
:src="gallerySelectDialog.selectedImageUrl"
style="flex: none; width: 60px; height: 60px; border-radius: 4px; object-fit: cover"
@click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)"
loading="lazy" />
<Button
size="sm"
variant="outline"
style="vertical-align: top"
@click="clearImageGallerySelect">
{{ t('dialog.invite_message.clear_selected_image') }}
</Button>
</div>
</template>
<template v-else>
<Button size="sm" variant="outline" @click="showGallerySelectDialog">
{{ t('dialog.invite_message.select_image') }}
</Button>
</div>
</template>
<template v-else>
<Button size="sm" variant="outline" @click="showGallerySelectDialog">
{{ t('dialog.invite_message.select_image') }}
</Button>
</template>
</el-form-item>
</el-form>
</template>
</FieldContent>
</Field>
</FieldGroup>
</div>
<template #footer>
<div class="flex gap-2">
@@ -109,6 +126,7 @@
</template>
<script setup>
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
import { InputGroupField, InputGroupTextareaField } from '@/components/ui/input-group';
import { computed, ref } from 'vue';
import { Button } from '@/components/ui/button';

View File

@@ -1,63 +1,68 @@
<template>
<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')">
<InputGroupField
v-model="launchDialog.url"
size="sm"
style="width: 230px"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
<Button
class="rounded-full ml-1"
size="icon-sm"
variant="ghost"
@click="copyInstanceMessage(launchDialog.url)"
><Copy
/></Button>
</TooltipWrapper>
</el-form-item>
<el-form-item v-if="launchDialog.shortUrl">
<template #label>
<div class="flex items-center">
<FieldGroup class="gap-4">
<Field>
<FieldLabel>{{ t('dialog.launch.url') }}</FieldLabel>
<FieldContent class="flex-row items-center gap-2">
<InputGroupField
v-model="launchDialog.url"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
<Button
class="rounded-full"
size="icon-sm"
variant="ghost"
@click="copyInstanceMessage(launchDialog.url)"
><Copy
/></Button>
</TooltipWrapper>
</FieldContent>
</Field>
<Field v-if="launchDialog.shortUrl">
<FieldLabel>
<span class="flex items-center gap-1">
<span>{{ t('dialog.launch.short_url') }}</span>
<TooltipWrapper side="top" :content="t('dialog.launch.short_url_notice')">
<el-icon style="display: inline-block; margin-left: 5px"><Warning /></el-icon>
<el-icon><Warning /></el-icon>
</TooltipWrapper>
</div>
</template>
<InputGroupField
v-model="launchDialog.shortUrl"
size="sm"
style="width: 230px"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
<Button
class="rounded-full ml-1"
size="icon-sm"
variant="ghost"
@click="copyInstanceMessage(launchDialog.shortUrl)"
><Copy
/></Button>
</TooltipWrapper>
</el-form-item>
<el-form-item :label="t('dialog.launch.location')">
<InputGroupField
v-model="launchDialog.location"
size="sm"
style="width: 230px"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
<Button
class="rounded-full ml-1"
size="icon-sm"
variant="ghost"
@click="copyInstanceMessage(launchDialog.location)"
><Copy
/></Button>
</TooltipWrapper>
</el-form-item>
</el-form>
</span>
</FieldLabel>
<FieldContent class="flex-row items-center gap-2">
<InputGroupField
v-model="launchDialog.shortUrl"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
<Button
class="rounded-full"
size="icon-sm"
variant="ghost"
@click="copyInstanceMessage(launchDialog.shortUrl)"
><Copy
/></Button>
</TooltipWrapper>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.launch.location') }}</FieldLabel>
<FieldContent class="flex-row items-center gap-2">
<InputGroupField
v-model="launchDialog.location"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
<TooltipWrapper side="right" :content="t('dialog.launch.copy_tooltip')">
<Button
class="rounded-full"
size="icon-sm"
variant="ghost"
@click="copyInstanceMessage(launchDialog.location)"
><Copy
/></Button>
</TooltipWrapper>
</FieldContent>
</Field>
</FieldGroup>
<template #footer>
<div class="flex justify-end">
<Button
@@ -126,6 +131,7 @@
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
import { Button } from '@/components/ui/button';
import { ButtonGroup } from '@/components/ui/button-group';
import { Copy } from 'lucide-vue-next';

View File

@@ -7,361 +7,428 @@
append-to-body>
<el-tabs v-model="newInstanceDialog.selectedTab" @tab-click="newInstanceTabClick">
<el-tab-pane name="Normal" :label="t('dialog.new_instance.normal')">
<el-form :model="newInstanceDialog" label-width="150px">
<el-form-item :label="t('dialog.new_instance.access_type')">
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.accessType"
@update:model-value="
(value) => {
newInstanceDialog.accessType = value;
buildInstance();
}
">
<ToggleGroupItem value="public">{{
t('dialog.new_instance.access_type_public')
}}</ToggleGroupItem>
<ToggleGroupItem value="group">{{
t('dialog.new_instance.access_type_group')
}}</ToggleGroupItem>
<ToggleGroupItem value="friends+">{{
t('dialog.new_instance.access_type_friend_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="friends">{{
t('dialog.new_instance.access_type_friend')
}}</ToggleGroupItem>
<ToggleGroupItem value="invite+">{{
t('dialog.new_instance.access_type_invite_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="invite">{{
t('dialog.new_instance.access_type_invite')
}}</ToggleGroupItem>
</ToggleGroup>
</el-form-item>
<el-form-item
v-if="newInstanceDialog.accessType === 'group'"
:label="t('dialog.new_instance.group_access_type')">
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.groupAccessType"
@update:model-value="
(value) => {
newInstanceDialog.groupAccessType = value;
buildInstance();
}
">
<ToggleGroupItem
value="members"
<FieldGroup class="gap-4">
<Field>
<FieldLabel>{{ t('dialog.new_instance.access_type') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.accessType"
@update:model-value="
(value) => {
newInstanceDialog.accessType = value;
buildInstance();
}
">
<ToggleGroupItem value="public">{{
t('dialog.new_instance.access_type_public')
}}</ToggleGroupItem>
<ToggleGroupItem value="group">{{
t('dialog.new_instance.access_type_group')
}}</ToggleGroupItem>
<ToggleGroupItem value="friends+">{{
t('dialog.new_instance.access_type_friend_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="friends">{{
t('dialog.new_instance.access_type_friend')
}}</ToggleGroupItem>
<ToggleGroupItem value="invite+">{{
t('dialog.new_instance.access_type_invite_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="invite">{{
t('dialog.new_instance.access_type_invite')
}}</ToggleGroupItem>
</ToggleGroup>
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.group_access_type') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.groupAccessType"
@update:model-value="
(value) => {
newInstanceDialog.groupAccessType = value;
buildInstance();
}
">
<ToggleGroupItem
value="members"
:disabled="
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-open-create')
"
>{{ t('dialog.new_instance.group_access_type_members') }}</ToggleGroupItem
>
<ToggleGroupItem
value="plus"
:disabled="
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-plus-create')
"
>{{ t('dialog.new_instance.group_access_type_plus') }}</ToggleGroupItem
>
<ToggleGroupItem
value="public"
:disabled="
!hasGroupPermission(
newInstanceDialog.groupRef,
'group-instance-public-create'
) || newInstanceDialog.groupRef.privacy === 'private'
"
>{{ t('dialog.new_instance.group_access_type_public') }}</ToggleGroupItem
>
</ToggleGroup>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.region') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.region"
@update:model-value="
(value) => {
newInstanceDialog.region = value;
buildInstance();
}
">
<ToggleGroupItem value="US West">{{
t('dialog.new_instance.region_usw')
}}</ToggleGroupItem>
<ToggleGroupItem value="US East">{{
t('dialog.new_instance.region_use')
}}</ToggleGroupItem>
<ToggleGroupItem value="Europe">{{
t('dialog.new_instance.region_eu')
}}</ToggleGroupItem>
<ToggleGroupItem value="Japan">{{
t('dialog.new_instance.region_jp')
}}</ToggleGroupItem>
</ToggleGroup>
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.queueEnabled') }}</FieldLabel>
<FieldContent>
<Checkbox v-model="newInstanceDialog.queueEnabled" @update:modelValue="buildInstance" />
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.ageGate') }}</FieldLabel>
<FieldContent>
<Checkbox
v-model="newInstanceDialog.ageGate"
:disabled="
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-open-create')
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-age-gated-create')
"
>{{ t('dialog.new_instance.group_access_type_members') }}</ToggleGroupItem
>
<ToggleGroupItem
value="plus"
:disabled="
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-plus-create')
"
>{{ t('dialog.new_instance.group_access_type_plus') }}</ToggleGroupItem
>
<ToggleGroupItem
value="public"
:disabled="
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-public-create') ||
newInstanceDialog.groupRef.privacy === 'private'
"
>{{ t('dialog.new_instance.group_access_type_public') }}</ToggleGroupItem
>
</ToggleGroup>
</el-form-item>
<el-form-item :label="t('dialog.new_instance.region')">
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.region"
@update:model-value="
(value) => {
newInstanceDialog.region = value;
buildInstance();
}
">
<ToggleGroupItem value="US West">{{ t('dialog.new_instance.region_usw') }}</ToggleGroupItem>
<ToggleGroupItem value="US East">{{ t('dialog.new_instance.region_use') }}</ToggleGroupItem>
<ToggleGroupItem value="Europe">{{ t('dialog.new_instance.region_eu') }}</ToggleGroupItem>
<ToggleGroupItem value="Japan">{{ t('dialog.new_instance.region_jp') }}</ToggleGroupItem>
</ToggleGroup>
</el-form-item>
<el-form-item
v-if="newInstanceDialog.accessType === 'group'"
:label="t('dialog.new_instance.queueEnabled')">
<Checkbox v-model="newInstanceDialog.queueEnabled" @update:modelValue="buildInstance" />
</el-form-item>
<el-form-item
v-if="newInstanceDialog.accessType === 'group'"
:label="t('dialog.new_instance.ageGate')">
<Checkbox
v-model="newInstanceDialog.ageGate"
:disabled="
!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-age-gated-create')
"
@update:modelValue="buildInstance" />
</el-form-item>
<el-form-item :label="t('dialog.new_instance.display_name')">
<InputGroupField
:disabled="!isLocalUserVrcPlusSupporter"
v-model="newInstanceDialog.displayName"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
@change="buildInstance" />
</el-form-item>
<el-form-item
v-if="newInstanceDialog.accessType === 'group'"
:label="t('dialog.new_instance.group_id')">
<VirtualCombobox
v-model="newInstanceDialog.groupId"
:groups="normalGroupPickerGroups"
:placeholder="t('dialog.new_instance.group_placeholder')"
:search-placeholder="t('dialog.new_instance.group_placeholder')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true"
@change="buildInstance">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<div class="avatar">
<img :src="item.iconUrl" loading="lazy" />
@update:modelValue="buildInstance" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.display_name') }}</FieldLabel>
<FieldContent>
<InputGroupField
:disabled="!isLocalUserVrcPlusSupporter"
v-model="newInstanceDialog.displayName"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
@change="buildInstance" />
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.group_id') }}</FieldLabel>
<FieldContent>
<VirtualCombobox
v-model="newInstanceDialog.groupId"
:groups="normalGroupPickerGroups"
:placeholder="t('dialog.new_instance.group_placeholder')"
:search-placeholder="t('dialog.new_instance.group_placeholder')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true"
@change="buildInstance">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<div class="avatar">
<img :src="item.iconUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="item.label"></span>
</div>
<CheckIcon
:class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
<div class="detail">
<span class="name" v-text="item.label"></span>
</div>
<CheckIcon :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
</template>
</VirtualCombobox>
</el-form-item>
<el-form-item
</template>
</VirtualCombobox>
</FieldContent>
</Field>
<Field
v-if="
newInstanceDialog.accessType === 'group' && newInstanceDialog.groupAccessType === 'members'
"
:label="t('dialog.new_instance.roles')">
<Select
multiple
:model-value="Array.isArray(newInstanceDialog.roleIds) ? newInstanceDialog.roleIds : []"
@update:modelValue="handleRoleIdsChange">
<SelectTrigger size="sm" class="w-full">
<SelectValue>
<span class="truncate">
{{ selectedRoleSummary || t('dialog.new_instance.role_placeholder') }}
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="role in newInstanceDialog.selectedGroupRoles"
:key="role.id"
:value="role.id">
{{ role.name }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</el-form-item>
class="items-start">
<FieldLabel>{{ t('dialog.new_instance.roles') }}</FieldLabel>
<FieldContent>
<Select
multiple
:model-value="Array.isArray(newInstanceDialog.roleIds) ? newInstanceDialog.roleIds : []"
@update:modelValue="handleRoleIdsChange">
<SelectTrigger size="sm" class="w-full">
<SelectValue>
<span class="truncate">
{{ selectedRoleSummary || t('dialog.new_instance.role_placeholder') }}
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="role in newInstanceDialog.selectedGroupRoles"
:key="role.id"
:value="role.id">
{{ role.name }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FieldContent>
</Field>
<template v-if="newInstanceDialog.instanceCreated">
<el-form-item :label="t('dialog.new_instance.location')">
<InputGroupField
v-model="newInstanceDialog.location"
size="sm"
readonly
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
</el-form-item>
<el-form-item :label="t('dialog.new_instance.url')">
<InputGroupField v-model="newInstanceDialog.url" size="sm" readonly />
</el-form-item>
<Field>
<FieldLabel>{{ t('dialog.new_instance.location') }}</FieldLabel>
<FieldContent>
<InputGroupField
v-model="newInstanceDialog.location"
size="sm"
readonly
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.url') }}</FieldLabel>
<FieldContent>
<InputGroupField v-model="newInstanceDialog.url" size="sm" readonly />
</FieldContent>
</Field>
</template>
</el-form>
</FieldGroup>
</el-tab-pane>
<el-tab-pane name="Legacy" :label="t('dialog.new_instance.legacy')">
<el-form :model="newInstanceDialog" label-width="150px">
<el-form-item :label="t('dialog.new_instance.access_type')">
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.accessType"
@update:model-value="
(value) => {
newInstanceDialog.accessType = value;
buildLegacyInstance();
}
">
<ToggleGroupItem value="public">{{
t('dialog.new_instance.access_type_public')
}}</ToggleGroupItem>
<ToggleGroupItem value="group">{{
t('dialog.new_instance.access_type_group')
}}</ToggleGroupItem>
<ToggleGroupItem value="friends+">{{
t('dialog.new_instance.access_type_friend_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="friends">{{
t('dialog.new_instance.access_type_friend')
}}</ToggleGroupItem>
<ToggleGroupItem value="invite+">{{
t('dialog.new_instance.access_type_invite_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="invite">{{
t('dialog.new_instance.access_type_invite')
}}</ToggleGroupItem>
</ToggleGroup>
</el-form-item>
<el-form-item
v-if="newInstanceDialog.accessType === 'group'"
:label="t('dialog.new_instance.group_access_type')">
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.groupAccessType"
@update:model-value="
(value) => {
newInstanceDialog.groupAccessType = value;
buildLegacyInstance();
}
">
<ToggleGroupItem value="members">{{
t('dialog.new_instance.group_access_type_members')
}}</ToggleGroupItem>
<ToggleGroupItem value="plus">{{
t('dialog.new_instance.group_access_type_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="public">{{
t('dialog.new_instance.group_access_type_public')
}}</ToggleGroupItem>
</ToggleGroup>
</el-form-item>
<el-form-item :label="t('dialog.new_instance.region')">
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.region"
@update:model-value="
(value) => {
newInstanceDialog.region = value;
buildLegacyInstance();
}
">
<ToggleGroupItem value="US West">{{ t('dialog.new_instance.region_usw') }}</ToggleGroupItem>
<ToggleGroupItem value="US East">{{ t('dialog.new_instance.region_use') }}</ToggleGroupItem>
<ToggleGroupItem value="Europe">{{ t('dialog.new_instance.region_eu') }}</ToggleGroupItem>
<ToggleGroupItem value="Japan">{{ t('dialog.new_instance.region_jp') }}</ToggleGroupItem>
</ToggleGroup>
</el-form-item>
<el-form-item
v-if="newInstanceDialog.accessType === 'group'"
:label="t('dialog.new_instance.ageGate')">
<Checkbox v-model="newInstanceDialog.ageGate" @update:modelValue="buildInstance" />
</el-form-item>
<el-form-item :label="t('dialog.new_instance.world_id')">
<InputGroupField
v-model="newInstanceDialog.worldId"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
@change="buildLegacyInstance" />
</el-form-item>
<el-form-item :label="t('dialog.new_instance.instance_id')">
<InputGroupField
v-model="newInstanceDialog.instanceName"
:placeholder="t('dialog.new_instance.instance_id_placeholder')"
size="sm"
@change="buildLegacyInstance" />
</el-form-item>
<el-form-item
<FieldGroup class="gap-4">
<Field>
<FieldLabel>{{ t('dialog.new_instance.access_type') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.accessType"
@update:model-value="
(value) => {
newInstanceDialog.accessType = value;
buildLegacyInstance();
}
">
<ToggleGroupItem value="public">{{
t('dialog.new_instance.access_type_public')
}}</ToggleGroupItem>
<ToggleGroupItem value="group">{{
t('dialog.new_instance.access_type_group')
}}</ToggleGroupItem>
<ToggleGroupItem value="friends+">{{
t('dialog.new_instance.access_type_friend_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="friends">{{
t('dialog.new_instance.access_type_friend')
}}</ToggleGroupItem>
<ToggleGroupItem value="invite+">{{
t('dialog.new_instance.access_type_invite_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="invite">{{
t('dialog.new_instance.access_type_invite')
}}</ToggleGroupItem>
</ToggleGroup>
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.group_access_type') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.groupAccessType"
@update:model-value="
(value) => {
newInstanceDialog.groupAccessType = value;
buildLegacyInstance();
}
">
<ToggleGroupItem value="members">{{
t('dialog.new_instance.group_access_type_members')
}}</ToggleGroupItem>
<ToggleGroupItem value="plus">{{
t('dialog.new_instance.group_access_type_plus')
}}</ToggleGroupItem>
<ToggleGroupItem value="public">{{
t('dialog.new_instance.group_access_type_public')
}}</ToggleGroupItem>
</ToggleGroup>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.region') }}</FieldLabel>
<FieldContent>
<ToggleGroup
type="single"
required
variant="outline"
size="sm"
:model-value="newInstanceDialog.region"
@update:model-value="
(value) => {
newInstanceDialog.region = value;
buildLegacyInstance();
}
">
<ToggleGroupItem value="US West">{{
t('dialog.new_instance.region_usw')
}}</ToggleGroupItem>
<ToggleGroupItem value="US East">{{
t('dialog.new_instance.region_use')
}}</ToggleGroupItem>
<ToggleGroupItem value="Europe">{{
t('dialog.new_instance.region_eu')
}}</ToggleGroupItem>
<ToggleGroupItem value="Japan">{{
t('dialog.new_instance.region_jp')
}}</ToggleGroupItem>
</ToggleGroup>
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.ageGate') }}</FieldLabel>
<FieldContent>
<Checkbox v-model="newInstanceDialog.ageGate" @update:modelValue="buildInstance" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.world_id') }}</FieldLabel>
<FieldContent>
<InputGroupField
v-model="newInstanceDialog.worldId"
size="sm"
@click="$event.target.tagName === 'INPUT' && $event.target.select()"
@change="buildLegacyInstance" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.instance_id') }}</FieldLabel>
<FieldContent>
<InputGroupField
v-model="newInstanceDialog.instanceName"
:placeholder="t('dialog.new_instance.instance_id_placeholder')"
size="sm"
@change="buildLegacyInstance" />
</FieldContent>
</Field>
<Field
v-if="
newInstanceDialog.selectedTab === 'Legacy' &&
newInstanceDialog.accessType !== 'public' &&
newInstanceDialog.accessType !== 'group'
"
:label="t('dialog.new_instance.instance_creator')">
<VirtualCombobox
v-model="newInstanceDialog.userId"
:groups="creatorPickerGroups"
:placeholder="t('dialog.new_instance.instance_creator_placeholder')"
:search-placeholder="t('dialog.new_instance.instance_creator_placeholder')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true"
@change="buildLegacyInstance">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<template v-if="item.user">
<div class="avatar" :class="userStatusClass(item.user)">
<img :src="userImage(item.user)" loading="lazy" />
class="items-start">
<FieldLabel>{{ t('dialog.new_instance.instance_creator') }}</FieldLabel>
<FieldContent>
<VirtualCombobox
v-model="newInstanceDialog.userId"
:groups="creatorPickerGroups"
:placeholder="t('dialog.new_instance.instance_creator_placeholder')"
:search-placeholder="t('dialog.new_instance.instance_creator_placeholder')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true"
@change="buildLegacyInstance">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<template v-if="item.user">
<div class="avatar" :class="userStatusClass(item.user)">
<img :src="userImage(item.user)" loading="lazy" />
</div>
<div class="detail">
<span
class="name"
:style="{ color: item.user.$userColour }"
v-text="item.user.displayName"></span>
</div>
</template>
<template v-else>
<span v-text="item.label"></span>
</template>
<CheckIcon
:class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
</template>
</VirtualCombobox>
</FieldContent>
</Field>
<Field v-if="newInstanceDialog.accessType === 'group'">
<FieldLabel>{{ t('dialog.new_instance.group_id') }}</FieldLabel>
<FieldContent>
<VirtualCombobox
v-model="newInstanceDialog.groupId"
:groups="legacyGroupPickerGroups"
:placeholder="t('dialog.new_instance.group_placeholder')"
:search-placeholder="t('dialog.new_instance.group_placeholder')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true"
@change="buildLegacyInstance">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<div class="avatar">
<img :src="item.iconUrl" loading="lazy" />
</div>
<div class="detail">
<span
class="name"
:style="{ color: item.user.$userColour }"
v-text="item.user.displayName"></span>
<span class="name" v-text="item.label"></span>
</div>
</template>
<template v-else>
<span v-text="item.label"></span>
</template>
<CheckIcon :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
</template>
</VirtualCombobox>
</el-form-item>
<el-form-item
v-if="newInstanceDialog.accessType === 'group'"
:label="t('dialog.new_instance.group_id')">
<VirtualCombobox
v-model="newInstanceDialog.groupId"
:groups="legacyGroupPickerGroups"
:placeholder="t('dialog.new_instance.group_placeholder')"
:search-placeholder="t('dialog.new_instance.group_placeholder')"
:clearable="true"
:close-on-select="true"
:deselect-on-reselect="true"
@change="buildLegacyInstance">
<template #item="{ item, selected }">
<div class="x-friend-item flex w-full items-center">
<div class="avatar">
<img :src="item.iconUrl" loading="lazy" />
<CheckIcon
:class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
<div class="detail">
<span class="name" v-text="item.label"></span>
</div>
<CheckIcon :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
</template>
</VirtualCombobox>
</el-form-item>
<el-form-item :label="t('dialog.new_instance.location')">
<InputGroupField
v-model="newInstanceDialog.location"
size="sm"
readonly
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
</el-form-item>
<el-form-item :label="t('dialog.new_instance.url')">
<InputGroupField v-model="newInstanceDialog.url" size="sm" readonly />
</el-form-item>
</el-form>
</template>
</VirtualCombobox>
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.location') }}</FieldLabel>
<FieldContent>
<InputGroupField
v-model="newInstanceDialog.location"
size="sm"
readonly
@click="$event.target.tagName === 'INPUT' && $event.target.select()" />
</FieldContent>
</Field>
<Field>
<FieldLabel>{{ t('dialog.new_instance.url') }}</FieldLabel>
<FieldContent>
<InputGroupField v-model="newInstanceDialog.url" size="sm" readonly />
</FieldContent>
</Field>
</FieldGroup>
</el-tab-pane>
</el-tabs>
<template v-if="newInstanceDialog.selectedTab === 'Normal'" #footer>
@@ -441,11 +508,12 @@
</template>
<script setup>
import { Field, FieldContent, FieldGroup, FieldLabel } from '@/components/ui/field';
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 { Checkbox } from '@/components/ui/checkbox';
import { InputGroupField } from '@/components/ui/input-group';
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';

View File

@@ -766,8 +766,8 @@
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 { InputGroupTextareaField } from '@/components/ui/input-group';
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';

View File

@@ -0,0 +1,20 @@
<script setup>
import { cn } from '@/lib/utils';
import { fieldVariants } from '.';
const props = defineProps({
class: { type: null, required: false },
orientation: { type: null, required: false }
});
</script>
<template>
<div
role="group"
data-slot="field"
:data-orientation="orientation"
:class="cn(fieldVariants({ orientation }), props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<div
data-slot="field-content"
:class="cn('group/field-content flex flex-1 flex-col gap-1.5 leading-snug', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,22 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<p
data-slot="field-description"
:class="
cn(
'text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5',
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
props.class
)
">
<slot />
</p>
</template>

View File

@@ -0,0 +1,48 @@
<script setup>
import { cn } from '@/lib/utils';
import { computed } from 'vue';
const props = defineProps({
class: { type: null, required: false },
errors: { type: Array, required: false }
});
const content = computed(() => {
if (!props.errors || props.errors.length === 0) return null;
const uniqueErrors = [
...new Map(
props.errors.filter(Boolean).map((error) => {
const message = typeof error === 'string' ? error : error?.message;
return [message, error];
})
).values()
];
if (uniqueErrors.length === 1 && uniqueErrors[0]) {
return typeof uniqueErrors[0] === 'string' ? uniqueErrors[0] : uniqueErrors[0].message;
}
return uniqueErrors.map((error) => (typeof error === 'string' ? error : error?.message));
});
</script>
<template>
<div
v-if="$slots.default || content"
role="alert"
data-slot="field-error"
:class="cn('text-destructive text-sm font-normal', props.class)">
<slot v-if="$slots.default" />
<template v-else-if="typeof content === 'string'">
{{ content }}
</template>
<ul v-else-if="Array.isArray(content)" class="ml-4 flex list-disc flex-col gap-1">
<li v-for="(error, index) in content" :key="index">
{{ error }}
</li>
</ul>
</div>
</template>

View File

@@ -0,0 +1,20 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<div
data-slot="field-group"
:class="
cn(
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4',
props.class
)
">
<slot />
</div>
</template>

View File

@@ -0,0 +1,23 @@
<script setup>
import { Label } from '@/components/ui/label';
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<Label
data-slot="field-label"
:class="
cn(
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4',
'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
props.class
)
">
<slot />
</Label>
</template>

View File

@@ -0,0 +1,17 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false },
variant: { type: String, required: false }
});
</script>
<template>
<legend
data-slot="field-legend"
:data-variant="variant"
:class="cn('mb-3 font-medium', 'data-[variant=legend]:text-base', 'data-[variant=label]:text-sm', props.class)">
<slot />
</legend>
</template>

View File

@@ -0,0 +1,23 @@
<script setup>
import { Separator } from '@/components/ui/separator';
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<div
data-slot="field-separator"
:data-content="!!$slots.default"
:class="cn('relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2', props.class)">
<Separator class="absolute inset-0 top-1/2" />
<span
v-if="$slots.default"
class="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
data-slot="field-separator-content">
<slot />
</span>
</div>
</template>

View File

@@ -0,0 +1,21 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<fieldset
data-slot="field-set"
:class="
cn(
'flex flex-col gap-6',
'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
props.class
)
">
<slot />
</fieldset>
</template>

View File

@@ -0,0 +1,20 @@
<script setup>
import { cn } from '@/lib/utils';
const props = defineProps({
class: { type: null, required: false }
});
</script>
<template>
<div
data-slot="field-label"
:class="
cn(
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
props.class
)
">
<slot />
</div>
</template>

View File

@@ -0,0 +1,36 @@
import { cva } from 'class-variance-authority';
export const fieldVariants = cva(
'group/field flex w-full gap-3 data-[invalid=true]:text-destructive',
{
variants: {
orientation: {
vertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'],
horizontal: [
'flex-row items-center',
'[&>[data-slot=field-label]]:flex-auto',
'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px'
],
responsive: [
'flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto',
'@md/field-group:[&>[data-slot=field-label]]:flex-auto',
'@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px'
]
}
},
defaultVariants: {
orientation: 'vertical'
}
}
);
export { default as Field } from './Field.vue';
export { default as FieldContent } from './FieldContent.vue';
export { default as FieldDescription } from './FieldDescription.vue';
export { default as FieldError } from './FieldError.vue';
export { default as FieldGroup } from './FieldGroup.vue';
export { default as FieldLabel } from './FieldLabel.vue';
export { default as FieldLegend } from './FieldLegend.vue';
export { default as FieldSeparator } from './FieldSeparator.vue';
export { default as FieldSet } from './FieldSet.vue';
export { default as FieldTitle } from './FieldTitle.vue';

View File

@@ -0,0 +1,17 @@
<script setup>
import { Slot } from 'reka-ui';
import { useFormField } from './useFormField';
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
</script>
<template>
<Slot
:id="formItemId"
data-slot="form-control"
:aria-describedby="!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`"
:aria-invalid="!!error">
<slot />
</Slot>
</template>

View File

@@ -0,0 +1,17 @@
<script setup>
import { cn } from '@/lib/utils';
import { useFormField } from './useFormField';
const props = defineProps({
class: { type: null, required: false }
});
const { formDescriptionId } = useFormField();
</script>
<template>
<p :id="formDescriptionId" data-slot="form-description" :class="cn('text-muted-foreground text-sm', props.class)">
<slot />
</p>
</template>

View File

@@ -0,0 +1,20 @@
<script setup>
import { cn } from '@/lib/utils';
import { provide } from 'vue';
import { useId } from 'reka-ui';
import { FORM_ITEM_INJECTION_KEY } from './injectionKeys';
const props = defineProps({
class: { type: null, required: false }
});
const id = useId();
provide(FORM_ITEM_INJECTION_KEY, id);
</script>
<template>
<div data-slot="form-item" :class="cn('grid gap-2', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,25 @@
<script setup>
import { Label } from '@/components/ui/label';
import { cn } from '@/lib/utils';
import { useFormField } from './useFormField';
const props = defineProps({
for: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const { error, formItemId } = useFormField();
</script>
<template>
<Label
data-slot="form-label"
:data-error="!!error"
:class="cn('data-[error=true]:text-destructive', props.class)"
:for="formItemId">
<slot />
</Label>
</template>

View File

@@ -0,0 +1,22 @@
<script setup>
import { ErrorMessage } from 'vee-validate';
import { cn } from '@/lib/utils';
import { toValue } from 'vue';
import { useFormField } from './useFormField';
const props = defineProps({
class: { type: null, required: false }
});
const { name, formMessageId } = useFormField();
</script>
<template>
<ErrorMessage
:id="formMessageId"
data-slot="form-message"
as="p"
:name="toValue(name)"
:class="cn('text-destructive text-sm', props.class)" />
</template>

View File

@@ -0,0 +1,11 @@
export { default as FormControl } from './FormControl.vue';
export { default as FormDescription } from './FormDescription.vue';
export { default as FormItem } from './FormItem.vue';
export { default as FormLabel } from './FormLabel.vue';
export { default as FormMessage } from './FormMessage.vue';
export { FORM_ITEM_INJECTION_KEY } from './injectionKeys';
export {
Form,
Field as FormField,
FieldArray as FormFieldArray
} from 'vee-validate';

View File

@@ -0,0 +1 @@
export const FORM_ITEM_INJECTION_KEY = Symbol();

View File

@@ -0,0 +1,31 @@
import { computed, inject } from 'vue';
import { FieldContextKey } from 'vee-validate';
import { FORM_ITEM_INJECTION_KEY } from './injectionKeys';
export function useFormField() {
const fieldContext = inject(FieldContextKey);
const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY);
if (!fieldContext)
throw new Error('useFormField should be used within <FormField>');
const { name, errorMessage: error, meta } = fieldContext;
const id = fieldItemContext;
const fieldState = {
valid: computed(() => meta.valid),
isDirty: computed(() => meta.dirty),
isTouched: computed(() => meta.touched),
error
};
return {
id,
name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState
};
}

View File

@@ -13,6 +13,7 @@
<template>
<Button
type="button"
:data-size="props.size"
:variant="props.variant"
:class="cn(inputGroupButtonVariants({ size: props.size }), props.class)">

View File

@@ -0,0 +1,28 @@
<script setup>
import { Label } from 'reka-ui';
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
const props = defineProps({
for: { type: String, required: false },
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<Label
data-slot="label"
v-bind="delegatedProps"
:class="
cn(
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
props.class
)
">
<slot />
</Label>
</template>

View File

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