From 68395951dc5b7c932fcdd7a5b96503281dc5940e Mon Sep 17 00:00:00 2001 From: Bastian Masanek Date: Thu, 6 Nov 2025 10:25:35 +0100 Subject: [PATCH] Refactor Select Component Variants and Styles for Improved Consistency - Updated SelectTrigger and SelectItem components to use variant props for error handling, enhancing visual feedback. - Refactored styles in SelectContent and SelectItem for better alignment and responsiveness. - Added new variant handling in the SelectTrigger component to streamline class management. - Updated styleguide.vue to include examples of the Select component with error and disabled states for better documentation. --- app/components/Checkout/AddressForm.vue | 10 +- app/components/Checkout/CheckoutForm.vue | 4 +- app/components/navigation/AreaTabs.vue | 2 +- app/components/ui/select/SelectContent.vue | 4 +- app/components/ui/select/SelectItem.vue | 4 +- app/components/ui/select/SelectTrigger.vue | 12 +- app/components/ui/select/index.ts | 26 +++ app/pages/internal/styleguide.vue | 180 +++++++++++++++++++-- 8 files changed, 211 insertions(+), 31 deletions(-) diff --git a/app/components/Checkout/AddressForm.vue b/app/components/Checkout/AddressForm.vue index 4b5336d..7c94efd 100644 --- a/app/components/Checkout/AddressForm.vue +++ b/app/components/Checkout/AddressForm.vue @@ -113,10 +113,7 @@ const parseDateFromDisplay = (displayDate: string) => { > @@ -284,10 +281,7 @@ const parseDateFromDisplay = (displayDate: string) => { > diff --git a/app/components/Checkout/CheckoutForm.vue b/app/components/Checkout/CheckoutForm.vue index ab4444c..1af0f08 100644 --- a/app/components/Checkout/CheckoutForm.vue +++ b/app/components/Checkout/CheckoutForm.vue @@ -147,7 +147,7 @@ function getError(field: string): string {
- + diff --git a/app/components/navigation/AreaTabs.vue b/app/components/navigation/AreaTabs.vue index 8293d70..59ae64f 100644 --- a/app/components/navigation/AreaTabs.vue +++ b/app/components/navigation/AreaTabs.vue @@ -67,7 +67,7 @@ const areas: ProductArea[] = [ }, { id: 'labs', - label: 'Labore', + label: 'Kurse', icon: FlaskConical, enabled: false, visible: true, diff --git a/app/components/ui/select/SelectContent.vue b/app/components/ui/select/SelectContent.vue index af0f3a2..5cec299 100644 --- a/app/components/ui/select/SelectContent.vue +++ b/app/components/ui/select/SelectContent.vue @@ -24,7 +24,7 @@ const delegatedProps = computed(() => { v-bind="delegatedProps" :class=" cn( - 'relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', + 'relative z-50 min-w-[8rem] overflow-hidden rounded-2xl border border-white/10 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-xl text-zinc-800 dark:text-zinc-100 shadow-glass data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', props.position === 'popper' && 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', props.class @@ -33,7 +33,7 @@ const delegatedProps = computed(() => { > diff --git a/app/components/ui/select/SelectItem.vue b/app/components/ui/select/SelectItem.vue index 31504f9..257b147 100644 --- a/app/components/ui/select/SelectItem.vue +++ b/app/components/ui/select/SelectItem.vue @@ -23,12 +23,12 @@ const delegatedProps = computed(() => { v-bind="delegatedProps" :class=" cn( - 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', + 'relative flex w-full cursor-default select-none items-center rounded-xl gap-2 py-2.5 pl-8 pr-3 text-sm outline-none focus:outline-none focus-visible:outline-none transition-all duration-200 hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', props.class ) " > - + diff --git a/app/components/ui/select/SelectTrigger.vue b/app/components/ui/select/SelectTrigger.vue index c3af393..e37bcc5 100644 --- a/app/components/ui/select/SelectTrigger.vue +++ b/app/components/ui/select/SelectTrigger.vue @@ -3,16 +3,22 @@ import { type HTMLAttributes, computed } from 'vue' import { SelectTrigger as SelectTriggerPrimitive } from 'reka-ui' import { ChevronDown } from 'lucide-vue-next' import { cn } from '@/lib/utils' +import { selectTriggerVariants, type SelectTriggerVariants } from '.' interface SelectTriggerProps { disabled?: boolean + variant?: SelectTriggerVariants['variant'] + size?: SelectTriggerVariants['size'] class?: HTMLAttributes['class'] } -const props = defineProps() +const props = withDefaults(defineProps(), { + variant: 'default', + size: 'default', +}) const delegatedProps = computed(() => { - const { class: _, ...delegated } = props + const { class: _, variant: __, size: ___, ...delegated } = props return delegated }) @@ -22,7 +28,7 @@ const delegatedProps = computed(() => { v-bind="delegatedProps" :class=" cn( - 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', + selectTriggerVariants({ variant: props.variant, size: props.size }), props.class ) " diff --git a/app/components/ui/select/index.ts b/app/components/ui/select/index.ts index df5bfd5..d1dec94 100644 --- a/app/components/ui/select/index.ts +++ b/app/components/ui/select/index.ts @@ -1,5 +1,31 @@ +import type { VariantProps } from 'class-variance-authority' +import { cva } from 'class-variance-authority' + export { default as Select } from './Select.vue' export { default as SelectTrigger } from './SelectTrigger.vue' export { default as SelectValue } from './SelectValue.vue' export { default as SelectContent } from './SelectContent.vue' export { default as SelectItem } from './SelectItem.vue' + +export const selectTriggerVariants = cva( + 'flex w-full items-center justify-between rounded-xl border border-white/20 bg-white/10 px-4 py-3 text-base text-white ring-offset-transparent placeholder:text-white/50 transition-all duration-300 hover:bg-white/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1', + { + variants: { + variant: { + default: '', + error: 'border-warning/50 focus-visible:ring-warning/50', + }, + size: { + default: 'h-12', + sm: 'h-10 text-sm px-3 py-2', + lg: 'h-14 text-lg px-5 py-4', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +) + +export type SelectTriggerVariants = VariantProps diff --git a/app/pages/internal/styleguide.vue b/app/pages/internal/styleguide.vue index c8d7c39..802601e 100644 --- a/app/pages/internal/styleguide.vue +++ b/app/pages/internal/styleguide.vue @@ -5,8 +5,10 @@ * Protected by Basic Auth via server/middleware/internal-auth.ts */ +import { ref } from 'vue' import { AlertCircle, CheckCircle } from 'lucide-vue-next' import RoleSwitcher from '@/components/navigation/RoleSwitcher.vue' +import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select' definePageMeta({ layout: 'styleguide', @@ -26,6 +28,12 @@ const copyCode = async (code: string) => { console.error('Failed to copy:', err) } } + +// Select component examples state +const selectBasic = ref('') +const selectError = ref('') +const selectDisabled = ref('disabled-option') +const selectCountry = ref('DE')