Extract shared surface, button, and select styles
This commit is contained in:
parent
bdb95a9d31
commit
30cbba22aa
|
|
@ -11,6 +11,8 @@ import {
|
|||
SelectViewport
|
||||
} from 'reka-ui'
|
||||
|
||||
import { selectContent, selectItem, selectTrigger } from '@/components/ui/select'
|
||||
|
||||
defineProps<{
|
||||
options: { value: T; label: string }[]
|
||||
placeholder?: string
|
||||
|
|
@ -23,7 +25,7 @@ const modelValue = defineModel<T>({ required: true })
|
|||
<SelectRoot v-model="modelValue">
|
||||
<SelectTrigger
|
||||
data-test-id="app-select-trigger"
|
||||
class="flex min-w-0 flex-1 cursor-pointer items-center justify-between rounded border border-border bg-input px-1.5 py-1 text-xs text-surface outline-none hover:bg-hover"
|
||||
:class="selectTrigger({ class: 'min-w-0 flex-1 rounded px-1.5 py-1 text-xs' })"
|
||||
>
|
||||
<SelectValue :placeholder="placeholder" />
|
||||
<icon-lucide-chevron-down class="ml-1 size-3 shrink-0 text-muted" />
|
||||
|
|
@ -32,14 +34,14 @@ const modelValue = defineModel<T>({ required: true })
|
|||
<SelectContent
|
||||
position="popper"
|
||||
:side-offset="2"
|
||||
class="z-50 max-h-56 min-w-[var(--reka-select-trigger-width)] overflow-hidden rounded-md border border-border bg-panel shadow-lg"
|
||||
:class="selectContent({ class: 'max-h-56 min-w-[var(--reka-select-trigger-width)]' })"
|
||||
>
|
||||
<SelectViewport class="p-0.5">
|
||||
<SelectItem
|
||||
v-for="opt in options"
|
||||
:key="String(opt.value)"
|
||||
:value="opt.value"
|
||||
class="relative flex cursor-pointer items-center rounded py-1.5 pl-6 pr-2 text-xs text-surface outline-none data-[highlighted]:bg-hover"
|
||||
:class="selectItem({ class: 'rounded py-1.5 pl-6 pr-2 text-xs' })"
|
||||
>
|
||||
<SelectItemIndicator class="absolute left-1.5 inline-flex items-center justify-center">
|
||||
<icon-lucide-check class="size-3 text-accent" />
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import {
|
|||
useFilter
|
||||
} from 'reka-ui'
|
||||
|
||||
import { selectContent, selectItem, selectTrigger } from '@/components/ui/select'
|
||||
import { panelSurface } from '@/components/ui/surface'
|
||||
import { listFamilies } from '@/engine/fonts'
|
||||
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
|
|
@ -51,7 +53,7 @@ function onSelect(val: string) {
|
|||
<PopoverAnchor>
|
||||
<button
|
||||
data-test-id="font-picker-trigger"
|
||||
class="flex w-full cursor-pointer items-center justify-between rounded border border-border bg-input px-2 py-1 text-xs text-surface hover:bg-hover"
|
||||
:class="selectTrigger({ class: 'w-full rounded px-2 py-1 text-xs' })"
|
||||
@click="open = !open"
|
||||
>
|
||||
<span class="truncate">{{ modelValue }}</span>
|
||||
|
|
@ -61,7 +63,7 @@ function onSelect(val: string) {
|
|||
<PopoverContent
|
||||
:side-offset="2"
|
||||
align="start"
|
||||
class="z-50 flex min-w-56 w-[var(--reka-popper-anchor-width)] flex-col overflow-hidden rounded-md border border-border bg-panel shadow-lg"
|
||||
:class="panelSurface({ radius: 'md', padding: 'none', class: 'z-50 flex min-w-56 w-[var(--reka-popper-anchor-width)] flex-col overflow-hidden' })"
|
||||
@open-auto-focus.prevent
|
||||
>
|
||||
<ListboxRoot :model-value="modelValue" @update:model-value="onSelect">
|
||||
|
|
@ -89,7 +91,7 @@ function onSelect(val: string) {
|
|||
<ListboxItem
|
||||
:value="option"
|
||||
data-test-id="font-picker-item"
|
||||
class="flex w-full cursor-pointer items-center gap-2 px-2 py-2 text-sm text-surface outline-none data-[highlighted]:bg-hover"
|
||||
:class="selectItem({ class: 'w-full gap-2 px-2 py-2 text-sm' })"
|
||||
:style="{ fontFamily: `'${option}', sans-serif` }"
|
||||
>
|
||||
<icon-lucide-check v-if="option === modelValue" class="size-3 shrink-0 text-accent" />
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import {
|
|||
} from 'reka-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { uiButton } from '@/components/ui/button'
|
||||
import { selectContent, selectItem, selectTrigger } from '@/components/ui/select'
|
||||
import { MODELS, useAIChat } from '@/composables/use-chat'
|
||||
|
||||
const { modelId } = useAIChat()
|
||||
|
|
@ -52,7 +54,7 @@ function handleSubmit(e: Event) {
|
|||
<SelectRoot v-model="modelId">
|
||||
<SelectTrigger
|
||||
data-test-id="chat-model-selector"
|
||||
class="flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] text-muted hover:bg-hover hover:text-surface"
|
||||
:class="selectTrigger({ class: 'gap-1 rounded border-none bg-transparent px-1.5 py-0.5 text-[10px] text-muted' })"
|
||||
>
|
||||
<icon-lucide-bot class="size-3" />
|
||||
{{ selectedModelName }}
|
||||
|
|
@ -63,14 +65,14 @@ function handleSubmit(e: Event) {
|
|||
position="popper"
|
||||
side="top"
|
||||
:side-offset="4"
|
||||
class="z-50 max-h-60 overflow-y-auto rounded-lg border border-border bg-panel p-1 shadow-lg"
|
||||
:class="selectContent({ radius: 'lg', padding: 'md', class: 'max-h-60 overflow-y-auto' })"
|
||||
>
|
||||
<SelectViewport>
|
||||
<SelectItem
|
||||
v-for="model in MODELS"
|
||||
:key="model.id"
|
||||
:value="model.id"
|
||||
class="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-[11px] text-surface outline-none data-[highlighted]:bg-hover"
|
||||
:class="selectItem({ class: 'gap-2 rounded px-2 py-1.5 text-[11px]' })"
|
||||
>
|
||||
<SelectItemText class="flex-1">{{ model.name }}</SelectItemText>
|
||||
<span
|
||||
|
|
@ -101,7 +103,7 @@ function handleSubmit(e: Event) {
|
|||
<button
|
||||
type="button"
|
||||
data-test-id="chat-stop-button"
|
||||
class="shrink-0 rounded border border-border px-2 py-1.5 text-xs text-muted hover:bg-hover"
|
||||
:class="uiButton({ tone: 'ghost', shape: 'rounded', size: 'sm', class: 'shrink-0 border border-border px-2 py-1.5' })"
|
||||
@click="emit('stop')"
|
||||
>
|
||||
<icon-lucide-square class="size-3" />
|
||||
|
|
@ -122,7 +124,7 @@ function handleSubmit(e: Event) {
|
|||
<button
|
||||
type="submit"
|
||||
data-test-id="chat-send-button"
|
||||
class="shrink-0 rounded bg-accent px-2.5 py-1.5 text-xs font-medium text-white hover:bg-accent/90"
|
||||
:class="uiButton({ tone: 'accent', shape: 'rounded', size: 'sm', class: 'shrink-0 px-2.5 py-1.5 font-medium' })"
|
||||
:disabled="!input.trim()"
|
||||
>
|
||||
<icon-lucide-send class="size-3" />
|
||||
|
|
|
|||
60
src/components/ui/button.ts
Normal file
60
src/components/ui/button.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { tv } from 'tailwind-variants'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
const button = tv({
|
||||
base: 'inline-flex items-center justify-center transition-colors select-none',
|
||||
variants: {
|
||||
tone: {
|
||||
ghost: 'bg-transparent text-muted hover:bg-hover hover:text-surface',
|
||||
accent: 'bg-accent text-white hover:bg-accent/90',
|
||||
panel: 'bg-panel/70 text-surface backdrop-blur-xl',
|
||||
panelAccent: 'bg-panel/70 text-accent border-accent/20 backdrop-blur-xl'
|
||||
},
|
||||
shape: {
|
||||
square: 'rounded',
|
||||
rounded: 'rounded-md',
|
||||
pill: 'rounded-full'
|
||||
},
|
||||
size: {
|
||||
sm: 'h-7 px-2 text-xs',
|
||||
md: 'h-8 px-3 text-xs',
|
||||
icon: 'size-8',
|
||||
iconSm: 'size-7'
|
||||
},
|
||||
bordered: {
|
||||
true: 'border border-white/10',
|
||||
false: ''
|
||||
},
|
||||
active: {
|
||||
true: '',
|
||||
false: ''
|
||||
}
|
||||
},
|
||||
compoundVariants: [
|
||||
{ tone: 'accent', size: 'icon', class: 'px-0' },
|
||||
{ tone: 'ghost', size: 'icon', class: 'px-0' },
|
||||
{ tone: 'panel', size: 'icon', class: 'shadow-md' },
|
||||
{ tone: 'panel', size: 'iconSm', class: 'shadow-sm' },
|
||||
{ tone: 'panelAccent', size: 'icon', class: 'shadow-md' },
|
||||
{ tone: 'panel', bordered: true, class: 'border border-white/10' },
|
||||
{ tone: 'panelAccent', bordered: true, class: 'border' }
|
||||
],
|
||||
defaultVariants: {
|
||||
tone: 'ghost',
|
||||
shape: 'rounded',
|
||||
size: 'sm',
|
||||
bordered: false,
|
||||
active: false
|
||||
}
|
||||
})
|
||||
|
||||
export function uiButton(options?: {
|
||||
tone?: 'ghost' | 'accent' | 'panel' | 'panelAccent'
|
||||
shape?: 'square' | 'rounded' | 'pill'
|
||||
size?: 'sm' | 'md' | 'icon' | 'iconSm'
|
||||
bordered?: boolean
|
||||
active?: boolean
|
||||
class?: string
|
||||
}) {
|
||||
return twMerge(button(options), options?.class)
|
||||
}
|
||||
51
src/components/ui/select.ts
Normal file
51
src/components/ui/select.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { tv } from 'tailwind-variants'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
const trigger = tv({
|
||||
base: 'flex items-center justify-between border border-border bg-input text-surface outline-none hover:bg-hover'
|
||||
})
|
||||
|
||||
const content = tv({
|
||||
base: 'z-50 overflow-hidden border border-border bg-panel',
|
||||
variants: {
|
||||
radius: {
|
||||
md: 'rounded-md',
|
||||
lg: 'rounded-lg'
|
||||
},
|
||||
elevation: {
|
||||
lg: 'shadow-lg',
|
||||
xl: 'shadow-xl'
|
||||
},
|
||||
padding: {
|
||||
none: '',
|
||||
sm: 'p-0.5',
|
||||
md: 'p-1'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
radius: 'md',
|
||||
elevation: 'lg',
|
||||
padding: 'sm'
|
||||
}
|
||||
})
|
||||
|
||||
const item = tv({
|
||||
base: 'relative flex cursor-pointer items-center text-surface outline-none data-[highlighted]:bg-hover'
|
||||
})
|
||||
|
||||
export function selectTrigger(options?: { class?: string }) {
|
||||
return twMerge(trigger(), options?.class)
|
||||
}
|
||||
|
||||
export function selectContent(options?: {
|
||||
radius?: 'md' | 'lg'
|
||||
elevation?: 'lg' | 'xl'
|
||||
padding?: 'none' | 'sm' | 'md'
|
||||
class?: string
|
||||
}) {
|
||||
return twMerge(content(options), options?.class)
|
||||
}
|
||||
|
||||
export function selectItem(options?: { class?: string }) {
|
||||
return twMerge(item(), options?.class)
|
||||
}
|
||||
39
src/components/ui/surface.ts
Normal file
39
src/components/ui/surface.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { tv } from 'tailwind-variants'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
const surface = tv({
|
||||
base: 'border border-border bg-panel',
|
||||
variants: {
|
||||
elevation: {
|
||||
md: 'shadow-md',
|
||||
lg: 'shadow-lg',
|
||||
xl: 'shadow-xl',
|
||||
overlay: 'shadow-[0_8px_30px_rgb(0_0_0/0.4)]'
|
||||
},
|
||||
radius: {
|
||||
md: 'rounded-md',
|
||||
lg: 'rounded-lg',
|
||||
xl: 'rounded-xl'
|
||||
},
|
||||
padding: {
|
||||
none: '',
|
||||
sm: 'p-1',
|
||||
md: 'p-2',
|
||||
lg: 'p-3'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
elevation: 'lg',
|
||||
radius: 'lg',
|
||||
padding: 'sm'
|
||||
}
|
||||
})
|
||||
|
||||
export function panelSurface(options?: {
|
||||
elevation?: 'md' | 'lg' | 'xl' | 'overlay'
|
||||
radius?: 'md' | 'lg' | 'xl'
|
||||
padding?: 'none' | 'sm' | 'md' | 'lg'
|
||||
class?: string
|
||||
}) {
|
||||
return twMerge(surface(options), options?.class)
|
||||
}
|
||||
Loading…
Reference in a new issue