@meith/ui 0.35.1 → 0.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meith/ui",
3
- "version": "0.35.1",
3
+ "version": "0.36.1",
4
4
  "description": "Meith's shared UI primitives: buttons, menus and the class utilities themes build with.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -17,6 +17,8 @@
17
17
  "./dialog": "./src/dialog.tsx",
18
18
  "./input-otp": "./src/input-otp.tsx",
19
19
  "./menu": "./src/menu.tsx",
20
+ "./nav-tabs-enhancer": "./src/nav-tabs-enhancer.tsx",
21
+ "./password-input": "./src/password-input.tsx",
20
22
  "./popover": "./src/popover.tsx",
21
23
  "./tabs": "./src/tabs.tsx",
22
24
  "./toast": "./src/toast.tsx",
package/src/card.tsx CHANGED
@@ -1,4 +1,5 @@
1
1
  import { cn } from './utils'
2
+ import { surfaceVariants } from './variants'
2
3
 
3
4
  type CardElement = 'section' | 'article' | 'div'
4
5
 
@@ -12,10 +13,7 @@ function Card({ as = 'section', className, ...props }: CardProps) {
12
13
  return (
13
14
  <Component
14
15
  data-slot="card"
15
- className={cn(
16
- 'overflow-hidden rounded-lg border border-border bg-card text-card-foreground shadow-elevation',
17
- className,
18
- )}
16
+ className={cn(surfaceVariants(), '@container/card overflow-hidden', className)}
19
17
  {...props}
20
18
  />
21
19
  )
@@ -26,7 +24,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
26
24
  <div
27
25
  data-slot="card-header"
28
26
  className={cn(
29
- 'flex flex-wrap items-center justify-between gap-x-4 gap-y-1 border-b border-border bg-surface px-4 py-2.5',
27
+ 'flex flex-wrap items-center justify-between gap-x-4 gap-y-1 border-b border-border bg-surface/60 px-5 py-3',
30
28
  className,
31
29
  )}
32
30
  {...props}
@@ -48,7 +46,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<'p'>) {
48
46
  return (
49
47
  <p
50
48
  data-slot="card-description"
51
- className={cn('text-sm text-muted-foreground', className)}
49
+ className={cn('text-sm leading-relaxed text-muted-foreground', className)}
52
50
  {...props}
53
51
  />
54
52
  )
@@ -65,7 +63,7 @@ function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
65
63
  }
66
64
 
67
65
  function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
68
- return <div data-slot="card-content" className={cn('px-4 py-3.5', className)} {...props} />
66
+ return <div data-slot="card-content" className={cn('px-5 py-4', className)} {...props} />
69
67
  }
70
68
 
71
69
  function CardRows({ className, ...props }: React.ComponentProps<'ul'>) {
@@ -77,7 +75,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
77
75
  <div
78
76
  data-slot="card-footer"
79
77
  className={cn(
80
- 'flex flex-wrap items-center gap-3 border-t border-border px-4 py-2.5 text-xs text-muted-foreground',
78
+ 'flex flex-wrap items-center gap-3 border-t border-border px-5 py-3 text-xs text-muted-foreground',
81
79
  className,
82
80
  )}
83
81
  {...props}
@@ -26,7 +26,7 @@ function Disclosure({
26
26
  >
27
27
  <summary
28
28
  className={cn(
29
- 'flex cursor-default list-none items-center gap-2 px-4 py-2.5 text-sm font-medium select-none',
29
+ 'flex cursor-pointer list-none items-center gap-2 min-h-11 px-4 py-3 text-sm font-medium select-none',
30
30
  'transition-colors hover:bg-muted',
31
31
  '[&::-webkit-details-marker]:hidden [&::marker]:content-none',
32
32
  )}
@@ -44,7 +44,7 @@ function Disclosure({
44
44
  <path d="m4.5 3 3 3-3 3" />
45
45
  </svg>
46
46
 
47
- <span className="min-w-0 flex-1 truncate">{summary}</span>
47
+ <span className="min-w-0 flex-1 [overflow-wrap:anywhere]">{summary}</span>
48
48
 
49
49
  {aside !== undefined && (
50
50
  <span className="shrink-0 text-xs font-normal text-muted-foreground">{aside}</span>
package/src/empty.tsx CHANGED
@@ -17,7 +17,7 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<'p'>) {
17
17
  return (
18
18
  <p
19
19
  data-slot="empty-title"
20
- className={cn('text-sm font-medium text-foreground', className)}
20
+ className={cn('text-base font-semibold tracking-tight text-foreground', className)}
21
21
  {...props}
22
22
  />
23
23
  )
@@ -27,7 +27,7 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<'p'>) {
27
27
  return (
28
28
  <p
29
29
  data-slot="empty-description"
30
- className={cn('max-w-prose text-sm text-muted-foreground', className)}
30
+ className={cn('max-w-md text-sm leading-relaxed text-muted-foreground', className)}
31
31
  {...props}
32
32
  />
33
33
  )
package/src/field.tsx CHANGED
@@ -1,19 +1,12 @@
1
1
  import { cn } from './utils'
2
-
3
- const CONTROL = [
4
- 'w-full rounded-md border border-input bg-card px-3 text-sm text-foreground',
5
- 'transition-[border-color,box-shadow] duration-100',
6
- 'placeholder:text-muted-foreground',
7
- 'disabled:cursor-not-allowed disabled:opacity-60',
8
- 'aria-invalid:border-destructive',
9
- ].join(' ')
2
+ import { controlVariants } from './variants'
10
3
 
11
4
  function Input({ className, type = 'text', ...props }: React.ComponentProps<'input'>) {
12
5
  return (
13
6
  <input
14
7
  data-slot="input"
15
8
  type={type}
16
- className={cn(CONTROL, 'h-9 py-1', className)}
9
+ className={cn(controlVariants(), 'py-2', className)}
17
10
  {...props}
18
11
  />
19
12
  )
@@ -23,15 +16,23 @@ function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
23
16
  return (
24
17
  <textarea
25
18
  data-slot="textarea"
26
- className={cn(CONTROL, 'min-h-32 resize-y py-2 leading-relaxed', className)}
19
+ className={cn(controlVariants(), 'h-auto min-h-36 resize-y py-3 leading-relaxed', className)}
27
20
  {...props}
28
21
  />
29
22
  )
30
23
  }
31
24
 
32
- function NativeSelect({ className, ...props }: React.ComponentProps<'select'>) {
25
+ function NativeSelect({
26
+ className,
27
+ controlSize = 'default',
28
+ ...props
29
+ }: React.ComponentProps<'select'> & { controlSize?: 'sm' | 'default' }) {
33
30
  return (
34
- <select data-slot="native-select" className={cn(CONTROL, 'h-9 pr-8', className)} {...props} />
31
+ <select
32
+ data-slot="native-select"
33
+ className={cn(controlVariants({ size: controlSize }), 'pr-8', className)}
34
+ {...props}
35
+ />
35
36
  )
36
37
  }
37
38
 
@@ -40,7 +41,7 @@ function Label({ className, ...props }: React.ComponentProps<'label'>) {
40
41
  // biome-ignore lint/a11y/noLabelWithoutControl: the Label primitive itself — its caller supplies the control it wraps
41
42
  <label
42
43
  data-slot="label"
43
- className={cn('text-sm font-medium leading-none text-foreground select-none', className)}
44
+ className={cn('text-sm font-medium leading-5 text-foreground select-none', className)}
44
45
  {...props}
45
46
  />
46
47
  )
@@ -77,7 +78,7 @@ function Field({
77
78
  const describedBy = [errorId, descriptionId].filter((value) => value !== null).join(' ')
78
79
 
79
80
  return (
80
- <div data-slot="field" className={cn('flex flex-col gap-1.5', className)} {...props}>
81
+ <div data-slot="field" className={cn('flex min-w-0 flex-col gap-2', className)} {...props}>
81
82
  <Label htmlFor={id}>{label}</Label>
82
83
 
83
84
  {children({
@@ -94,7 +95,7 @@ function Field({
94
95
  )}
95
96
 
96
97
  {descriptionId !== null && (
97
- <p id={descriptionId} className="text-xs text-muted-foreground">
98
+ <p id={descriptionId} className="text-xs leading-relaxed text-muted-foreground">
98
99
  {description}
99
100
  </p>
100
101
  )}
package/src/index.ts CHANGED
@@ -19,10 +19,31 @@ export { Disclosure } from './disclosure'
19
19
  export { Empty, EmptyAction, EmptyDescription, EmptyTitle } from './empty'
20
20
  export type { FieldProps } from './field'
21
21
  export { Field, Input, Label, NativeSelect, Textarea } from './field'
22
+ export { type NavTab, NavTabs } from './nav-tabs'
23
+ export {
24
+ NavigationDrawer,
25
+ type NavigationDrawerProps,
26
+ NavigationDrawerTrigger,
27
+ } from './navigation-drawer'
28
+ export {
29
+ PageDescription,
30
+ PageHeader,
31
+ PageHeaderActions,
32
+ PageHeaderContent,
33
+ PageTitle,
34
+ } from './page-header'
22
35
  export type { SeparatorProps } from './separator'
23
36
  export { Separator } from './separator'
24
37
  export type { TextLinkProps } from './text-link'
25
38
  export { TextLink } from './text-link'
26
39
  export { cn } from './utils'
27
- export type { BadgeVariants, ButtonVariants, TextLinkVariants } from './variants'
28
- export { badgeVariants, buttonVariants, textLinkVariants } from './variants'
40
+ export type { BadgeVariants, ButtonVariants, ControlVariants, TextLinkVariants } from './variants'
41
+ export {
42
+ badgeVariants,
43
+ buttonVariants,
44
+ controlVariants,
45
+ navTabListVariants,
46
+ navTabVariants,
47
+ surfaceVariants,
48
+ textLinkVariants,
49
+ } from './variants'
@@ -0,0 +1,64 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+
5
+ const SELECTOR = '[data-nav-tabs]'
6
+ const CURRENT = '[aria-current]:not([aria-current="false"])'
7
+
8
+ function reveal(list: HTMLElement, active: HTMLElement | null) {
9
+ if (active === null || list.clientWidth === 0) return
10
+ const bounds = list.getBoundingClientRect()
11
+ const tab = active.getBoundingClientRect()
12
+ if (tab.left >= bounds.left + 4 && tab.right <= bounds.right - 4) return
13
+ list.scrollBy({ left: tab.left - bounds.left - (list.clientWidth - tab.width) / 2 })
14
+ }
15
+
16
+ export function NavTabsEnhancer() {
17
+ useEffect(() => {
18
+ const lists = new Map<HTMLElement, HTMLElement | null>()
19
+ const resize = new ResizeObserver((entries) => {
20
+ for (const entry of entries) {
21
+ const list = entry.target.closest<HTMLElement>(SELECTOR)
22
+ if (list === null) continue
23
+ reveal(list, lists.get(list) ?? null)
24
+ }
25
+ })
26
+
27
+ function refresh() {
28
+ for (const list of lists.keys()) {
29
+ if (list.isConnected) continue
30
+ resize.unobserve(list)
31
+ const active = lists.get(list)
32
+ if (active) resize.unobserve(active)
33
+ lists.delete(list)
34
+ }
35
+ for (const list of document.querySelectorAll<HTMLElement>(SELECTOR)) {
36
+ const active = list.querySelector<HTMLElement>(CURRENT)
37
+ if (!lists.has(list)) resize.observe(list)
38
+ if (lists.get(list) !== active) {
39
+ const previous = lists.get(list)
40
+ if (previous) resize.unobserve(previous)
41
+ if (active) resize.observe(active)
42
+ reveal(list, active)
43
+ }
44
+ lists.set(list, active)
45
+ }
46
+ }
47
+
48
+ const changes = new MutationObserver(refresh)
49
+ changes.observe(document.body, {
50
+ childList: true,
51
+ subtree: true,
52
+ attributes: true,
53
+ attributeFilter: ['aria-current'],
54
+ })
55
+ refresh()
56
+
57
+ return () => {
58
+ changes.disconnect()
59
+ resize.disconnect()
60
+ }
61
+ }, [])
62
+
63
+ return null
64
+ }
@@ -0,0 +1,60 @@
1
+ import { cn } from './utils'
2
+ import { navTabListVariants, navTabVariants } from './variants'
3
+
4
+ export interface NavTab {
5
+ readonly href: string
6
+ readonly label: string
7
+ readonly isCurrent: boolean
8
+ readonly count?: number
9
+ }
10
+
11
+ export function NavTabs({
12
+ label,
13
+ tabs,
14
+ aside,
15
+ className,
16
+ }: {
17
+ label: string
18
+ tabs: readonly NavTab[]
19
+ aside?: React.ReactNode
20
+ className?: string
21
+ }) {
22
+ if (tabs.length === 0) return null
23
+
24
+ return (
25
+ <nav
26
+ aria-label={label}
27
+ className={cn('flex min-w-0 max-w-full flex-wrap items-center gap-x-3 gap-y-2', className)}
28
+ >
29
+ <ul data-nav-tabs className={navTabListVariants()}>
30
+ {tabs.map((tab) => (
31
+ <li key={tab.href} className="shrink-0">
32
+ <a
33
+ href={tab.href}
34
+ {...(tab.isCurrent ? { 'aria-current': 'page' as const } : {})}
35
+ className={navTabVariants({ active: tab.isCurrent })}
36
+ >
37
+ {tab.label}
38
+ {tab.count !== undefined && tab.count > 0 && (
39
+ <span
40
+ className={cn(
41
+ 'rounded px-1.5 text-xs font-semibold tabular-nums',
42
+ tab.isCurrent
43
+ ? 'bg-secondary text-secondary-foreground'
44
+ : 'bg-muted text-muted-foreground',
45
+ )}
46
+ >
47
+ {tab.count}
48
+ </span>
49
+ )}
50
+ </a>
51
+ </li>
52
+ ))}
53
+ </ul>
54
+
55
+ {aside !== undefined && (
56
+ <span className="shrink-0 text-xs text-muted-foreground">{aside}</span>
57
+ )}
58
+ </nav>
59
+ )
60
+ }
@@ -0,0 +1,92 @@
1
+ import { cn } from './utils'
2
+ import { buttonVariants } from './variants'
3
+
4
+ export interface NavigationDrawerProps {
5
+ readonly id: string
6
+ readonly title: string
7
+ readonly openLabel: string
8
+ readonly closeLabel: string
9
+ readonly showTrigger?: boolean
10
+ readonly children: React.ReactNode
11
+ }
12
+
13
+ export function NavigationDrawerTrigger({
14
+ target,
15
+ label,
16
+ className,
17
+ ...props
18
+ }: Omit<React.ComponentProps<'button'>, 'children'> & {
19
+ readonly target: string
20
+ readonly label: string
21
+ }) {
22
+ return (
23
+ <button
24
+ {...props}
25
+ type="button"
26
+ popoverTarget={target}
27
+ aria-label={label}
28
+ className={cn(buttonVariants({ variant: 'outline', size: 'icon' }), 'size-11', className)}
29
+ >
30
+ <svg
31
+ aria-hidden="true"
32
+ viewBox="0 0 24 24"
33
+ className="size-5"
34
+ fill="none"
35
+ stroke="currentColor"
36
+ strokeWidth="1.5"
37
+ strokeLinecap="round"
38
+ >
39
+ <path d="M4 6h16M4 12h16M4 18h16" />
40
+ </svg>
41
+ </button>
42
+ )
43
+ }
44
+
45
+ export function NavigationDrawer({
46
+ id,
47
+ title,
48
+ openLabel,
49
+ closeLabel,
50
+ showTrigger = true,
51
+ children,
52
+ }: NavigationDrawerProps) {
53
+ return (
54
+ <>
55
+ {showTrigger && <NavigationDrawerTrigger target={id} label={openLabel} />}
56
+ <div
57
+ id={id}
58
+ popover="auto"
59
+ role="dialog"
60
+ data-slot="navigation-drawer"
61
+ aria-labelledby={`${id}-title`}
62
+ className="fixed inset-y-0 end-0 start-auto m-0 h-dvh max-h-dvh w-80 max-w-[calc(100vw-3rem)] flex-col border-0 border-s border-border bg-card p-0 text-card-foreground shadow-xl backdrop:bg-black/40 [&:popover-open]:flex"
63
+ >
64
+ <div className="flex shrink-0 items-center justify-between gap-3 border-b border-border px-4 py-3">
65
+ <h2 id={`${id}-title`} className="font-heading text-base font-semibold">
66
+ {title}
67
+ </h2>
68
+ <button
69
+ type="button"
70
+ popoverTarget={id}
71
+ popoverTargetAction="hide"
72
+ aria-label={closeLabel}
73
+ className={cn(buttonVariants({ variant: 'ghost', size: 'icon' }), 'size-11')}
74
+ >
75
+ <svg
76
+ aria-hidden="true"
77
+ viewBox="0 0 24 24"
78
+ className="size-5"
79
+ fill="none"
80
+ stroke="currentColor"
81
+ strokeWidth="1.5"
82
+ strokeLinecap="round"
83
+ >
84
+ <path d="m6 6 12 12M18 6 6 18" />
85
+ </svg>
86
+ </button>
87
+ </div>
88
+ <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-3">{children}</div>
89
+ </div>
90
+ </>
91
+ )
92
+ }
@@ -0,0 +1,56 @@
1
+ import { cn } from './utils'
2
+
3
+ function PageHeader({ className, ...props }: React.ComponentProps<'header'>) {
4
+ return (
5
+ <header
6
+ data-slot="page-header"
7
+ className={cn('flex flex-wrap items-end justify-between gap-x-6 gap-y-4', className)}
8
+ {...props}
9
+ />
10
+ )
11
+ }
12
+
13
+ function PageHeaderContent({ className, ...props }: React.ComponentProps<'div'>) {
14
+ return (
15
+ <div
16
+ data-slot="page-header-content"
17
+ className={cn('flex min-w-0 flex-1 basis-64 flex-col gap-2', className)}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ function PageTitle({ className, ...props }: React.ComponentProps<'h1'>) {
24
+ return (
25
+ <h1
26
+ data-slot="page-title"
27
+ className={cn(
28
+ 'font-heading text-2xl font-semibold tracking-tight text-balance text-foreground [overflow-wrap:anywhere] sm:text-3xl',
29
+ className,
30
+ )}
31
+ {...props}
32
+ />
33
+ )
34
+ }
35
+
36
+ function PageDescription({ className, ...props }: React.ComponentProps<'p'>) {
37
+ return (
38
+ <p
39
+ data-slot="page-description"
40
+ className={cn('max-w-prose text-sm leading-relaxed text-muted-foreground', className)}
41
+ {...props}
42
+ />
43
+ )
44
+ }
45
+
46
+ function PageHeaderActions({ className, ...props }: React.ComponentProps<'div'>) {
47
+ return (
48
+ <div
49
+ data-slot="page-header-actions"
50
+ className={cn('flex max-w-full flex-wrap items-center gap-2', className)}
51
+ {...props}
52
+ />
53
+ )
54
+ }
55
+
56
+ export { PageDescription, PageHeader, PageHeaderActions, PageHeaderContent, PageTitle }
@@ -0,0 +1,61 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+
5
+ import { Input } from './field'
6
+ import { cn } from './utils'
7
+
8
+ export interface PasswordInputProps extends Omit<React.ComponentProps<'input'>, 'type'> {
9
+ readonly showLabel: string
10
+ readonly hideLabel: string
11
+ }
12
+
13
+ export function PasswordInput({
14
+ showLabel,
15
+ hideLabel,
16
+ className,
17
+ disabled,
18
+ ...props
19
+ }: PasswordInputProps) {
20
+ const [visible, setVisible] = useState(false)
21
+ const [enhanced, setEnhanced] = useState(false)
22
+
23
+ useEffect(() => setEnhanced(true), [])
24
+
25
+ return (
26
+ <span data-slot="password-input" className="relative block min-w-0">
27
+ <Input
28
+ {...props}
29
+ disabled={disabled}
30
+ type={visible ? 'text' : 'password'}
31
+ className={cn(className, 'pr-12')}
32
+ />
33
+ {enhanced && (
34
+ <button
35
+ type="button"
36
+ disabled={disabled}
37
+ aria-label={visible ? hideLabel : showLabel}
38
+ aria-controls={props.id}
39
+ title={visible ? hideLabel : showLabel}
40
+ onClick={() => setVisible((current) => !current)}
41
+ className="absolute inset-y-0 right-0 flex w-11 items-center justify-center rounded-r-md text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50"
42
+ >
43
+ <svg
44
+ aria-hidden="true"
45
+ viewBox="0 0 24 24"
46
+ className="size-4"
47
+ fill="none"
48
+ stroke="currentColor"
49
+ strokeWidth="1.5"
50
+ strokeLinecap="round"
51
+ strokeLinejoin="round"
52
+ >
53
+ <path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z" />
54
+ <circle cx="12" cy="12" r="3" />
55
+ {visible && <path d="m3 3 18 18" />}
56
+ </svg>
57
+ </button>
58
+ )}
59
+ </span>
60
+ )
61
+ }
package/src/variants.ts CHANGED
@@ -4,6 +4,7 @@ export const buttonVariants = cva(
4
4
  [
5
5
  'inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap',
6
6
  'rounded-md border text-sm font-medium leading-none',
7
+ 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',
7
8
  'transition-[color,background-color,border-color,opacity] duration-100 ease-out',
8
9
  'disabled:pointer-events-none disabled:opacity-50',
9
10
  'aria-disabled:pointer-events-none aria-disabled:opacity-50',
@@ -12,7 +13,8 @@ export const buttonVariants = cva(
12
13
  {
13
14
  variants: {
14
15
  variant: {
15
- primary: 'border-transparent bg-primary text-primary-foreground hover:bg-primary-hover',
16
+ primary:
17
+ 'border-transparent bg-primary text-primary-foreground shadow-sm hover:bg-primary-hover',
16
18
  secondary:
17
19
  'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/70',
18
20
  outline: 'border-border bg-card text-foreground hover:bg-muted',
@@ -23,10 +25,10 @@ export const buttonVariants = cva(
23
25
  link: 'h-auto border-transparent p-0 text-foreground underline underline-offset-4 decoration-border hover:decoration-current',
24
26
  },
25
27
  size: {
26
- sm: 'h-8 gap-1.5 px-2.5 text-xs',
27
- default: 'h-9 px-3.5',
28
- lg: 'h-10 px-4',
29
- icon: 'size-9 p-0',
28
+ sm: 'h-8 gap-1.5 px-2.5 text-xs pointer-coarse:min-h-11',
29
+ default: 'h-10 px-4 pointer-coarse:min-h-11',
30
+ lg: 'h-11 px-5',
31
+ icon: 'size-10 p-0 pointer-coarse:size-11',
30
32
  },
31
33
  },
32
34
  defaultVariants: { variant: 'secondary', size: 'default' },
@@ -93,3 +95,56 @@ export const textLinkVariants = cva(
93
95
  )
94
96
 
95
97
  export type TextLinkVariants = VariantProps<typeof textLinkVariants>
98
+
99
+ export const controlVariants = cva(
100
+ [
101
+ 'w-full min-w-0 rounded-md border border-input bg-card text-foreground',
102
+ 'transition-[border-color,box-shadow]',
103
+ 'duration-150',
104
+ 'placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-60',
105
+ 'focus-visible:border-ring focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',
106
+ 'aria-invalid:border-destructive aria-invalid:focus-visible:outline-destructive',
107
+ 'pointer-coarse:min-h-11 pointer-coarse:text-base',
108
+ ],
109
+ {
110
+ variants: {
111
+ size: {
112
+ sm: 'min-h-8 px-2.5 py-1 text-xs',
113
+ default: 'min-h-10 px-3 py-2 text-sm',
114
+ },
115
+ },
116
+ defaultVariants: { size: 'default' },
117
+ },
118
+ )
119
+
120
+ export type ControlVariants = VariantProps<typeof controlVariants>
121
+
122
+ export const surfaceVariants = cva(
123
+ 'min-w-0 rounded-xl border border-border bg-card text-card-foreground shadow-elevation',
124
+ {
125
+ variants: {
126
+ padded: { true: 'flex flex-col gap-4 p-5', false: '' },
127
+ },
128
+ defaultVariants: { padded: false },
129
+ },
130
+ )
131
+
132
+ export const navTabListVariants = cva(
133
+ 'inline-flex max-w-full items-center gap-1 overflow-x-auto rounded-lg border border-border bg-surface p-1',
134
+ )
135
+
136
+ export const navTabVariants = cva(
137
+ [
138
+ 'inline-flex h-9 items-center gap-1.5 rounded-md px-3 text-sm whitespace-nowrap transition-colors pointer-coarse:min-h-11',
139
+ 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',
140
+ ],
141
+ {
142
+ variants: {
143
+ active: {
144
+ true: 'bg-card font-semibold text-primary shadow-sm',
145
+ false: 'font-medium text-muted-foreground hover:bg-card/60 hover:text-foreground',
146
+ },
147
+ },
148
+ defaultVariants: { active: false },
149
+ },
150
+ )