Add Field component system and migrate UI

This commit is contained in:
Gregor Vostrak
2026-02-10 12:22:53 +01:00
parent 1ecb332458
commit fd012e7c69
71 changed files with 1023 additions and 741 deletions

View File

@@ -7,7 +7,7 @@ import type { CreateClientBody } from '@/packages/api/src';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { useFocus } from '@vueuse/core';
import { useClientsStore } from '@/utils/useClients';
import InputLabel from '../../../packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '@/packages/ui/src/field';
const { createClient } = useClientsStore();
const show = defineModel('show', { default: false });
@@ -37,19 +37,19 @@ useFocus(clientNameInput, { initialValue: true });
<template #content>
<div class="flex items-center space-x-4">
<div class="col-span-6 sm:col-span-4 flex-1">
<InputLabel for="clientName" value="Client Name" />
<Field class="col-span-6 sm:col-span-4 flex-1">
<FieldLabel for="clientName">Client Name</FieldLabel>
<TextInput
id="clientName"
ref="clientNameInput"
v-model="client.name"
type="text"
placeholder="Client Name"
class="mt-1 block w-full"
class="block w-full"
required
autocomplete="clientName"
@keydown.enter="submit" />
</div>
</Field>
</div>
</template>
<template #footer>

View File

@@ -14,7 +14,7 @@ import {
ComboboxViewport,
} from 'radix-vue';
import Dropdown from '@/packages/ui/src/Input/Dropdown.vue';
import { Button } from '@/Components/ui/button';
import { Button } from '@/packages/ui/src/Buttons';
const { members } = useMembersQuery();
@@ -77,7 +77,6 @@ function selectMember(member: Member) {
:disabled="disabled"
type="button"
variant="input"
size="input"
class="w-full justify-between text-start font-normal">
<div class="flex items-center gap-3 truncate">
<UserIcon class="w-4 text-text-secondary shrink-0" />
@@ -92,7 +91,7 @@ function selectMember(member: Member) {
<template #content>
<ComboboxRoot
v-model:search-term="searchValue"
:open="open"
v-model:open="open"
class="relative"
:filter-function="(val: string[]) => val">
<ComboboxAnchor>

View File

@@ -9,8 +9,7 @@ import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import Checkbox from '@/packages/ui/src/Input/Checkbox.vue';
import { useNotificationsStore } from '@/utils/notification';
import { getCurrentOrganizationId } from '@/utils/useUser';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
const props = defineProps<{
show: boolean;
@@ -113,25 +112,21 @@ const close = () => {
},
}">
<template #default="{ field }">
<div class="flex flex-col">
<div class="flex items-center space-x-3 text-sm">
<Checkbox
:id="field.name"
:name="field.name"
:checked="field.state.value"
@update:checked="field.handleChange"
@blur="field.handleBlur" />
<InputLabel
:for="field.name"
class="font-medium text-text-primary">
I understand that this will permanently delete all data
related to this member
</InputLabel>
</div>
<InputError
class="pl-7 pt-2"
:message="field.state.meta.errors[0]" />
</div>
<Field orientation="horizontal">
<Checkbox
:id="field.name"
:name="field.name"
:checked="field.state.value"
@update:checked="field.handleChange"
@blur="field.handleBlur" />
<FieldLabel :for="field.name" class="font-medium text-text-primary">
I understand that this will permanently delete all data related
to this member
</FieldLabel>
<FieldError v-if="field.state.meta.errors[0]" class="pl-7 pt-2">
{{ field.state.meta.errors[0] }}
</FieldError>
</Field>
</template>
</form.Field>
</div>

View File

@@ -6,7 +6,7 @@ import type { Member, UpdateMemberBody } from '@/packages/api/src';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { type MemberBillableKey, useMembersStore } from '@/utils/useMembers';
import BillableRateInput from '@/packages/ui/src/Input/BillableRateInput.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '@/packages/ui/src/field';
import MemberBillableRateModal from '@/Components/Common/Member/MemberBillableRateModal.vue';
import MemberBillableSelect from '@/Components/Common/Member/MemberBillableSelect.vue';
import { onMounted, watch } from 'vue';
@@ -121,28 +121,24 @@ const roleDescription = computed(() => {
<template #content>
<div class="pb-5 pt-2 divide-y divide-border-secondary">
<div class="pb-5 flex space-x-6">
<div>
<InputLabel for="role" value="Role" />
<MemberRoleSelect
v-model="memberBody.role"
class="mt-2"
name="role"></MemberRoleSelect>
</div>
<Field>
<FieldLabel for="role">Role</FieldLabel>
<MemberRoleSelect v-model="memberBody.role" name="role"></MemberRoleSelect>
</Field>
<div class="flex-1 text-xs flex items-center pt-6">
<p>{{ roleDescription }}</p>
</div>
</div>
<div class="flex items-center space-x-4 pt-5">
<div class="col-span-6 sm:col-span-4 flex-1 flex space-x-5">
<div>
<InputLabel for="billableType" value="Billable" />
<Field>
<FieldLabel for="billableType">Billable</FieldLabel>
<MemberBillableSelect
v-model="billableRateSelect"
class="mt-2"
name="billableType"></MemberBillableSelect>
</div>
<div v-if="billableRateSelect === 'custom-rate'" class="flex-1">
<InputLabel for="memberBillableRate" value="Billable Rate" />
</Field>
<Field v-if="billableRateSelect === 'custom-rate'" class="flex-1">
<FieldLabel for="memberBillableRate">Billable Rate</FieldLabel>
<BillableRateInput
v-model="memberBody.billable_rate"
focus
@@ -150,7 +146,7 @@ const roleDescription = computed(() => {
:currency="getOrganizationCurrencyString()"
name="memberBillableRate"
@keydown.enter="saveWithChecks()"></BillableRateInput>
</div>
</Field>
</div>
</div>
</div>

View File

@@ -5,8 +5,7 @@ import DialogModal from '@/packages/ui/src/DialogModal.vue';
import { ref } from 'vue';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import { useFocus } from '@vueuse/core';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
import type { Role } from '@/types/jetstream';
import { Link, useForm } from '@inertiajs/vue3';
import { getCurrentOrganizationId } from '@/utils/useUser';
@@ -111,8 +110,8 @@ useFocus(clientNameInput, { initialValue: true });
</div>
</div>
<div v-else class="space-y-4">
<div class="col-span-6 sm:col-span-4 flex-1">
<InputLabel for="email" value="Email" />
<Field class="col-span-6 sm:col-span-4 flex-1">
<FieldLabel for="email">Email</FieldLabel>
<TextInput
id="email"
ref="memberEmailInput"
@@ -120,16 +119,16 @@ useFocus(clientNameInput, { initialValue: true });
name="email"
type="text"
placeholder="Member Email"
class="mt-1 block w-full"
class="block w-full"
required
autocomplete="memberName"
@keydown.enter="submit" />
<InputError :message="errors.email" class="mt-2" />
</div>
<FieldError v-if="errors.email">{{ errors.email }}</FieldError>
</Field>
<div v-if="availableRoles.length > 0">
<InputLabel for="roles" value="Role" />
<InputError :message="errors.role" class="mt-2" />
<Field v-if="availableRoles.length > 0">
<FieldLabel for="roles">Role</FieldLabel>
<FieldError v-if="errors.role">{{ errors.role }}</FieldError>
<div
class="relative z-0 mt-1 border border-card-border rounded-lg bg-card-background cursor-pointer">
@@ -182,7 +181,7 @@ useFocus(clientNameInput, { initialValue: true });
</div>
</button>
</div>
</div>
</Field>
</div>
</template>
<template #footer>

View File

@@ -123,7 +123,7 @@ function updateValue(project: Project) {
<UseFocusTrap v-if="open" :options="{ immediate: true, allowOutsideClick: true }">
<ComboboxRoot
v-model:search-term="searchValue"
:open="open"
v-model:open="open"
:model-value="currentProject"
class="relative"
@update:model-value="updateValue">

View File

@@ -14,7 +14,7 @@ import { useClientsQuery } from '@/utils/useClientsQuery';
import ProjectColorSelector from '@/packages/ui/src/Project/ProjectColorSelector.vue';
import { UserCircleIcon } from '@heroicons/vue/20/solid';
import EstimatedTimeSection from '@/packages/ui/src/EstimatedTimeSection.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '@/packages/ui/src/field';
import ProjectBillableRateModal from '@/packages/ui/src/Project/ProjectBillableRateModal.vue';
import { getOrganizationCurrencyString } from '@/utils/money';
import ProjectEditBillableSection from '@/packages/ui/src/Project/ProjectEditBillableSection.vue';
@@ -82,34 +82,28 @@ async function submitBillableRate() {
<template #content>
<div class="sm:flex items-center space-y-2 sm:space-y-0 sm:space-x-5">
<div class="flex-1 flex items-center">
<Field class="flex-1 flex items-center">
<div class="text-center">
<InputLabel for="color" value="Color" />
<ProjectColorSelector
v-model="project.color"
class="mt-1"></ProjectColorSelector>
<FieldLabel for="color">Color</FieldLabel>
<ProjectColorSelector v-model="project.color"></ProjectColorSelector>
</div>
</div>
<div class="w-full">
<InputLabel for="projectName" value="Project name" />
</Field>
<Field class="w-full">
<FieldLabel for="projectName">Project name</FieldLabel>
<TextInput
id="projectName"
ref="projectNameInput"
v-model="project.name"
type="text"
placeholder="Project Name"
class="mt-1 block w-full"
class="block w-full"
required
autocomplete="projectName"
@keydown.enter="submit()" />
</div>
<div class="">
<InputLabel for="client" value="Client" />
<ClientDropdown
v-model="project.client_id"
:create-client
:clients="clients"
class="mt-1">
</Field>
<Field>
<FieldLabel for="client">Client</FieldLabel>
<ClientDropdown v-model="project.client_id" :create-client :clients="clients">
<template #trigger>
<Badge
class="bg-input-background cursor-pointer hover:bg-tertiary"
@@ -123,7 +117,7 @@ async function submitBillableRate() {
</Badge>
</template>
</ClientDropdown>
</div>
</Field>
</div>
<div>
<div>

View File

@@ -9,7 +9,7 @@ import { useProjectMembersStore } from '@/utils/useProjectMembers';
import BillableRateInput from '@/packages/ui/src/Input/BillableRateInput.vue';
import { UserIcon } from '@heroicons/vue/24/solid';
import ProjectMemberBillableRateModal from '@/Components/Common/ProjectMember/ProjectMemberBillableRateModal.vue';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '@/packages/ui/src/field';
import { getOrganizationCurrencyString } from '@/utils/money';
const { updateProjectMember } = useProjectMembersStore();
@@ -82,14 +82,14 @@ useFocus(projectNameInput, { initialValue: true });
<UserIcon class="w-4 text-text-secondary"></UserIcon>
<span>{{ props.name }}</span>
</div>
<div class="col-span-3 sm:col-span-1 flex-1">
<InputLabel for="billable_rate" class="mb-2" value="Billable Rate"></InputLabel>
<Field class="col-span-3 sm:col-span-1 flex-1">
<FieldLabel for="billable_rate">Billable Rate</FieldLabel>
<BillableRateInput
v-model="projectMemberBody.billable_rate"
:currency="getOrganizationCurrencyString()"
name="billable_rate"
@keydown.enter="submit"></BillableRateInput>
</div>
</Field>
</div>
</template>
<template #footer>

View File

@@ -4,7 +4,7 @@ import SecondaryButton from '../../../packages/ui/src/Buttons/SecondaryButton.vu
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import { ref } from 'vue';
import PrimaryButton from '../../../packages/ui/src/Buttons/PrimaryButton.vue';
import InputLabel from '../../../packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '@/packages/ui/src/field';
import type { CreateReportBody, CreateReportBodyProperties } from '@/packages/api/src';
import { useMutation } from '@tanstack/vue-query';
import { getCurrentOrganizationId } from '@/utils/useUser';
@@ -82,31 +82,33 @@ async function submit() {
<template #content>
<div class="items-center space-y-4 w-full">
<div class="w-full">
<InputLabel for="name" value="Name" />
<TextInput id="name" v-model="report.name" class="mt-1.5 w-full"></TextInput>
</div>
<div>
<InputLabel for="description" value="Description" />
<Field class="w-full">
<FieldLabel for="name">Name</FieldLabel>
<TextInput id="name" v-model="report.name" class="w-full"></TextInput>
</Field>
<Field>
<FieldLabel for="description">Description</FieldLabel>
<TextInput
id="description"
v-model="report.description"
class="mt-1.5 w-full"></TextInput>
</div>
<InputLabel value="Visibility" />
<div class="flex items-center space-x-12">
<div class="flex items-center space-x-3 px-2 py-3">
<Checkbox id="is_public" v-model:checked="report.is_public"></Checkbox>
<InputLabel for="is_public" value="Public" />
class="w-full"></TextInput>
</Field>
<Field>
<FieldLabel>Visibility</FieldLabel>
<div class="flex items-center space-x-12">
<Field orientation="horizontal" class="px-2 py-3">
<Checkbox id="is_public" v-model:checked="report.is_public"></Checkbox>
<FieldLabel for="is_public">Public</FieldLabel>
</Field>
<Field v-if="report.is_public" class="flex-row items-center space-x-4">
<div>
<FieldLabel for="public_until">Expires at</FieldLabel>
<div class="text-text-tertiary font-medium">(optional)</div>
</div>
<DatePicker v-model="report.public_until"></DatePicker>
</Field>
</div>
<div v-if="report.is_public" class="flex items-center space-x-4">
<div>
<InputLabel for="public_until" value="Expires at" />
<div class="text-text-tertiary font-medium">(optional)</div>
</div>
<DatePicker v-model="report.public_until"></DatePicker>
</div>
</div>
</Field>
</div>
</template>
<template #footer>

View File

@@ -4,7 +4,7 @@ import SecondaryButton from '../../../packages/ui/src/Buttons/SecondaryButton.vu
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import { computed, ref, watch } from 'vue';
import PrimaryButton from '../../../packages/ui/src/Buttons/PrimaryButton.vue';
import InputLabel from '../../../packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '@/packages/ui/src/field';
import type { UpdateReportBody } from '@/packages/api/src';
import { useMutation, useQueryClient } from '@tanstack/vue-query';
import { getCurrentOrganizationId } from '@/utils/useUser';
@@ -105,28 +105,30 @@ async function submit() {
<template #content>
<div class="items-center space-y-4 w-full">
<div class="w-full">
<InputLabel for="name" value="Name" />
<TextInput id="name" v-model="report.name" class="mt-1.5 w-full"></TextInput>
</div>
<div>
<InputLabel for="description" value="Description" />
<Field class="w-full">
<FieldLabel for="name">Name</FieldLabel>
<TextInput id="name" v-model="report.name" class="w-full"></TextInput>
</Field>
<Field>
<FieldLabel for="description">Description</FieldLabel>
<TextInput
id="description"
v-model="report.description"
class="mt-1.5 w-full"></TextInput>
</div>
<InputLabel value="Visibility" />
<div class="flex items-center space-x-12">
<div class="flex items-center space-x-2 px-2 py-3">
<Checkbox id="is_public" v-model:checked="report.is_public"></Checkbox>
<InputLabel for="is_public" value="Public" />
class="w-full"></TextInput>
</Field>
<Field>
<FieldLabel>Visibility</FieldLabel>
<div class="flex items-center space-x-12">
<Field orientation="horizontal" class="px-2 py-3">
<Checkbox id="is_public" v-model:checked="report.is_public"></Checkbox>
<FieldLabel for="is_public">Public</FieldLabel>
</Field>
<Field v-if="report.is_public" orientation="horizontal">
<FieldLabel for="public_until">Expires at</FieldLabel>
<DatePicker v-model="localPublicUntil"></DatePicker>
</Field>
</div>
<div v-if="report.is_public" class="flex items-center space-x-4">
<InputLabel for="public_until" value="Expires at" />
<DatePicker v-model="localPublicUntil"></DatePicker>
</div>
</div>
</Field>
</div>
</template>
<template #footer>

View File

@@ -9,7 +9,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/Components/ui/select';
import InputLabel from '@/packages/ui/src/Input/InputLabel.vue';
import { Field, FieldLabel } from '@/packages/ui/src/field';
import {
NumberField,
NumberFieldInput,
@@ -162,7 +162,7 @@ const iconClass = computed(() => {
>Rounding is a premium feature. Upgrade to unlock this feature.</span
>
<Link href="/billing">
<Button size="sm" variant="input" class="items-center space-x-1">
<Button size="sm" variant="outline" class="items-center space-x-1">
<CreditCardIcon class="w-3.5 h-3.5 text-text-tertiary mr-1" />
Go to Billing
</Button>
@@ -170,14 +170,14 @@ const iconClass = computed(() => {
</div>
<div v-else class="space-y-4">
<div>
<div class="flex items-center justify-between">
<InputLabel for="enable-rounding" value="Enable Rounding" />
<Field orientation="horizontal" class="justify-between">
<FieldLabel for="enable-rounding">Enable Rounding</FieldLabel>
<Switch
id="enable-rounding"
:model-value="enabled"
class="data-[state=checked]:bg-accent-500"
@update:model-value="updateEnabled" />
</div>
</Field>
<div
class="mb-3 pb-2 pt-1 text-xs text-muted-foreground border-b border-border-secondary text-text-tertiary">
Rounding is applied to each individual time entry, not to the accumulated
@@ -185,15 +185,15 @@ const iconClass = computed(() => {
</div>
</div>
<div>
<InputLabel for="rounding-type" value="Rounding Type" class="mb-2" />
<Field>
<FieldLabel for="rounding-type">Rounding Type</FieldLabel>
<Select
:model-value="type"
:disabled="!enabled"
@update:model-value="(value) => updateType(value as TimeEntryRoundingType)">
<SelectTrigger
id="rounding-type"
size="small"
size="sm"
class="w-full"
:disabled="!enabled">
<SelectValue placeholder="Select rounding type" />
@@ -204,16 +204,16 @@ const iconClass = computed(() => {
<SelectItem value="nearest">Round Nearest</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<InputLabel for="minutes-interval" value="Minutes Interval" class="mb-2" />
</Field>
<Field>
<FieldLabel for="minutes-interval">Minutes Interval</FieldLabel>
<Select
:model-value="selectedInterval"
:disabled="!enabled"
@update:model-value="(value) => handleIntervalChange(value as string)">
<SelectTrigger
id="minutes-interval"
size="small"
size="sm"
class="w-full"
:disabled="!enabled">
<SelectValue placeholder="Select interval" />
@@ -232,7 +232,6 @@ const iconClass = computed(() => {
<NumberField
id="custom-minutes"
:model-value="customMinutes"
size="small"
:min="1"
:max="1440"
:disabled="!enabled"
@@ -247,7 +246,7 @@ const iconClass = computed(() => {
</NumberFieldContent>
</NumberField>
</div>
</div>
</Field>
</div>
</PopoverContent>
</Popover>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, reactive, nextTick } from 'vue';
import DialogModal from '@/packages/ui/src/DialogModal.vue';
import InputError from '@/packages/ui/src/Input/InputError.vue';
import { Field, FieldError } from '@/packages/ui/src/field';
import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
@@ -87,18 +87,18 @@ const closeModal = () => {
<template #content>
{{ content }}
<div class="mt-4">
<Field class="mt-4">
<TextInput
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-3/4"
class="block w-3/4"
placeholder="Password"
autocomplete="current-password"
@keyup.enter="confirmPassword" />
<InputError :message="form.error" class="mt-2" />
</div>
<FieldError v-if="form.error">{{ form.error }}</FieldError>
</Field>
</template>
<template #footer>

View File

@@ -1,25 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils';
import { Primitive, type PrimitiveProps } from 'reka-ui';
import { type ButtonVariants, buttonVariants } from '.';
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant'];
size?: ButtonVariants['size'];
class?: HTMLAttributes['class'];
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)">
<slot />
</Primitive>
</template>

View File

@@ -1,36 +0,0 @@
import { cva, type VariantProps } from 'class-variance-authority';
export { default as Button } from './Button.vue';
export const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border shadow-xs hover:text-text-primary bg-card-background dark:bg-transparent border-input dark:border-input hover:bg-white/5',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
input: 'border-input-border border bg-input-background text-text-primary focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent shadow-sm',
},
size: {
default: 'h-9 px-4 py-2',
xs: 'h-7 rounded px-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
input: 'h-[42px] px-3 py-2 text-base',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
export type ButtonVariants = VariantProps<typeof buttonVariants>;

View File

@@ -42,7 +42,6 @@ const organization = inject<ComputedRef<Organization>>('organization');
<PopoverTrigger as-child>
<Button
variant="input"
size="input"
:class="[
'w-full justify-start text-left font-normal',
!model && 'text-muted-foreground',

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
import type { LabelProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { reactiveOmit } from '@vueuse/core';
import { Label } from 'reka-ui';
import { cn } from '@/lib/utils';
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<Label
v-bind="delegatedProps"
:class="
cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
props.class
)
">
<slot />
</Label>
</template>

View File

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

View File

@@ -13,7 +13,7 @@ const props = defineProps<{
data-slot="input"
:class="
cn(
'flex h-[42px] w-full rounded-md border border-input-border bg-input-background px-3 py-2.5 text-sm text-center shadow-sm transition-colors placeholder:text-muted-foreground focus:border-input-border focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
'flex h-9 w-full rounded-md border border-input-border bg-input-background px-3 py-1 text-sm text-center shadow-sm transition-colors placeholder:text-muted-foreground focus:border-input-border focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
props.class
)
" />

View File

@@ -7,7 +7,7 @@ import { computed, type HTMLAttributes } from 'vue';
const props = withDefaults(
defineProps<
SelectTriggerProps & {
size?: 'small';
size?: 'default' | 'sm' | 'lg';
class?: HTMLAttributes['class'];
showChevron?: boolean;
variant?: 'default' | 'outline';
@@ -18,11 +18,19 @@ const props = withDefaults(
showChevron: true,
variant: 'default',
active: false,
size: 'default',
}
);
const delegatedProps = computed(() => {
const { class: _, showChevron: __, variant: ___, active: ____, ...delegated } = props;
const {
class: _,
showChevron: __,
variant: ___,
active: ____,
size: _____,
...delegated
} = props;
return delegated;
});
@@ -30,7 +38,14 @@ const delegatedProps = computed(() => {
const forwardedProps = useForwardProps(delegatedProps);
const sizeClasses = computed(() => {
return props.size === 'small' ? 'h-8 text-xs' : 'h-[42px] w-full text-sm';
switch (props.size) {
case 'sm':
return 'h-8 px-3 text-xs';
case 'lg':
return 'h-10 px-4 text-sm';
default:
return 'h-9 px-3 text-sm';
}
});
const variantClasses = computed(() => {
@@ -49,7 +64,7 @@ const variantClasses = computed(() => {
v-bind="forwardedProps"
:class="
cn(
'flex items-center justify-between gap-3 whitespace-nowrap rounded-md px-3 py-2.5 data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start font-medium transition-colors',
'flex items-center justify-between gap-3 whitespace-nowrap rounded-md data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start font-medium transition-colors',
sizeClasses,
variantClasses,
props.class