@pantoia/ui 0.2.0 → 0.4.0

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.
Files changed (66) hide show
  1. package/accordion/index.module.css +4 -4
  2. package/accordion/index.tsx +30 -58
  3. package/alert-dialog/index.tsx +38 -44
  4. package/badge/index.tsx +2 -2
  5. package/button/index.tsx +31 -30
  6. package/callout/index.tsx +29 -8
  7. package/checkbox/index.tsx +24 -88
  8. package/combobox/index.tsx +133 -323
  9. package/command-palette/index.tsx +55 -34
  10. package/dialog/index.tsx +30 -26
  11. package/dist/index.js +155 -27
  12. package/drawer/index.module.css +2 -2
  13. package/drawer/index.tsx +54 -132
  14. package/dropdown-menu/index.tsx +95 -100
  15. package/field/index.tsx +43 -94
  16. package/input/index.tsx +13 -9
  17. package/kbd/index.tsx +2 -2
  18. package/label/index.tsx +7 -9
  19. package/number-field/index.tsx +129 -176
  20. package/package.json +12 -15
  21. package/palette.css +114 -0
  22. package/popover/index.tsx +62 -33
  23. package/radio-group/index.tsx +36 -39
  24. package/registry.json +65 -169
  25. package/select/index.tsx +154 -76
  26. package/switch/index.tsx +13 -6
  27. package/table/index.tsx +5 -6
  28. package/tabs/index.module.css +5 -3
  29. package/tabs/index.tsx +26 -77
  30. package/tailwind.css +3 -0
  31. package/textarea/index.tsx +25 -13
  32. package/toast/index.module.css +187 -0
  33. package/toast/index.tsx +141 -46
  34. package/toggle/index.tsx +8 -7
  35. package/{styles.css → tokens.css} +82 -303
  36. package/tooltip/index.tsx +62 -31
  37. package/tv.ts +0 -1
  38. package/utilities.css +124 -0
  39. package/accordion/AGENTS.md +0 -60
  40. package/alert-dialog/AGENTS.md +0 -78
  41. package/badge/AGENTS.md +0 -35
  42. package/button/AGENTS.md +0 -39
  43. package/callout/AGENTS.md +0 -41
  44. package/checkbox/AGENTS.md +0 -54
  45. package/combobox/AGENTS.md +0 -150
  46. package/command-palette/AGENTS.md +0 -45
  47. package/dialog/AGENTS.md +0 -81
  48. package/drawer/AGENTS.md +0 -66
  49. package/dropdown-menu/AGENTS.md +0 -133
  50. package/field/AGENTS.md +0 -96
  51. package/input/AGENTS.md +0 -35
  52. package/kbd/AGENTS.md +0 -17
  53. package/label/AGENTS.md +0 -21
  54. package/number-field/AGENTS.md +0 -53
  55. package/popover/AGENTS.md +0 -64
  56. package/press.css +0 -37
  57. package/radio-group/AGENTS.md +0 -81
  58. package/select/AGENTS.md +0 -90
  59. package/switch/AGENTS.md +0 -37
  60. package/table/AGENTS.md +0 -82
  61. package/tabs/AGENTS.md +0 -60
  62. package/test-utils.ts +0 -10
  63. package/textarea/AGENTS.md +0 -35
  64. package/toast/AGENTS.md +0 -34
  65. package/toggle/AGENTS.md +0 -37
  66. package/tooltip/AGENTS.md +0 -50
package/select/index.tsx CHANGED
@@ -1,24 +1,24 @@
1
1
  'use client'
2
2
 
3
- import { Check, ChevronDown, ChevronUp } from 'lucide-react'
4
- import { Select as SelectPrimitive } from 'radix-ui'
3
+ import { Select as SelectPrimitive } from '@base-ui/react/select'
4
+ import { Children, isValidElement, useEffect, useRef, useState } from 'react'
5
5
  import type { ComponentProps, ReactNode } from 'react'
6
6
 
7
7
  import { tv } from '../tv'
8
8
 
9
9
  export const selectTrigger = tv({
10
10
  base: 'flex w-full items-center justify-between gap-tight rounded-r8'
11
- + ' text-value text-foreground'
11
+ + ' text-value text-fg'
12
12
  + ' transition-state'
13
13
  + ' invalid:ring-2 invalid:ring-line-danger'
14
14
  + ' focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-line-focus-ring'
15
- + ' disabled:bg-bg-neutral-disabled disabled:text-foreground-disabled disabled:cursor-not-allowed'
16
- + ' unset:text-foreground-placeholder',
15
+ + ' disabled:bg-bg-neutral-disabled disabled:text-fg-disabled disabled:cursor-not-allowed'
16
+ + ' unset:text-fg-placeholder',
17
17
 
18
18
  variants: {
19
19
  surface: {
20
20
  bare: 'bg-transparent ring-1 ring-inset ring-line-strong',
21
- well: 'bg-background-well',
21
+ well: 'bg-bg-well',
22
22
  },
23
23
  size: {
24
24
  sm: 'h-8 px-3',
@@ -30,20 +30,53 @@ export const selectTrigger = tv({
30
30
  defaultVariants: { surface: 'bare', size: 'md' },
31
31
  })
32
32
 
33
- export interface SelectProps extends Omit<ComponentProps<typeof SelectPrimitive.Trigger>, 'value' | 'defaultValue' | 'dir'> {
34
- value?: string
35
- defaultValue?: string
36
- onValueChange?: (value: string) => void
37
- open?: boolean
38
- defaultOpen?: boolean
39
- onOpenChange?: (open: boolean) => void
40
- placeholder?: string
41
- surface?: 'bare' | 'well'
42
- size?: 'sm' | 'md' | 'lg'
43
- arrow?: boolean
44
- children?: ReactNode
33
+ const selectArrow = tv({
34
+ base: 'data-uncentered:invisible'
35
+ + ' side-top:top-full'
36
+ + ' side-bottom:bottom-full side-bottom:rotate-180'
37
+ + ' side-right:left-0 side-right:origin-top-left side-right:rotate-90 side-right:-translate-y-1/2'
38
+ + ' side-left:right-0 side-left:origin-top-right side-left:-rotate-90 side-left:-translate-y-1/2',
39
+ })
40
+
41
+ type ItemElementProps = { value?: string, children?: ReactNode }
42
+
43
+ function findItemText(children: ReactNode): { label: ReactNode } | undefined {
44
+ let found: { label: ReactNode } | undefined
45
+ Children.forEach(children, child => {
46
+ if (found || !isValidElement<ItemElementProps>(child)) return
47
+ found = child.type === SelectItemText
48
+ ? { label: child.props.children }
49
+ : findItemText(child.props.children)
50
+ })
51
+ return found
45
52
  }
46
53
 
54
+ function collectItems(children: ReactNode, items: Record<string, ReactNode>) {
55
+ Children.forEach(children, child => {
56
+ if (!isValidElement<ItemElementProps>(child)) return
57
+ if (child.type === SelectItem && child.props.value !== undefined) {
58
+ const text = findItemText(child.props.children)
59
+ if (text) items[child.props.value] = text.label
60
+ return
61
+ }
62
+ collectItems(child.props.children, items)
63
+ })
64
+ return items
65
+ }
66
+
67
+ export type SelectProps = Omit<ComponentProps<typeof SelectPrimitive.Trigger>, 'className' | 'value' | 'defaultValue'>
68
+ & Pick<SelectPrimitive.Root.Props<string>, 'open' | 'defaultOpen' | 'onOpenChange' | 'items' | 'name' | 'required' | 'readOnly' | 'modal' | 'form'>
69
+ & {
70
+ value?: string
71
+ defaultValue?: string
72
+ onValueChange?: (value: string, eventDetails: SelectPrimitive.Root.ChangeEventDetails) => void
73
+ placeholder?: string
74
+ surface?: 'bare' | 'well'
75
+ size?: 'sm' | 'md' | 'lg'
76
+ arrow?: boolean
77
+ className?: string
78
+ }
79
+
47
80
  export function Select({
48
81
  value,
49
82
  defaultValue,
@@ -51,6 +84,12 @@ export function Select({
51
84
  open,
52
85
  defaultOpen,
53
86
  onOpenChange,
87
+ items,
88
+ name,
89
+ required,
90
+ readOnly,
91
+ modal,
92
+ form,
54
93
  placeholder,
55
94
  surface,
56
95
  size,
@@ -59,53 +98,91 @@ export function Select({
59
98
  children,
60
99
  ...rest
61
100
  }: SelectProps) {
101
+ const [shown, setShown] = useState(defaultOpen ?? false)
102
+ const popup = useRef<HTMLDivElement>(null)
103
+ const isOpen = open ?? shown
104
+
105
+ useEffect(() => {
106
+ if (!isOpen) return
107
+
108
+ const frame = requestAnimationFrame(() => {
109
+ const list = popup.current
110
+ if (!list || list.querySelector('[role="option"][aria-selected="true"]')) return
111
+ list.querySelector<HTMLElement>('[role="option"]:not([data-disabled])')?.focus({ preventScroll: true })
112
+ })
113
+
114
+ return () => cancelAnimationFrame(frame)
115
+ }, [isOpen])
116
+
62
117
  return (
63
- <SelectPrimitive.Root
118
+ <SelectPrimitive.Root<string>
64
119
  value={value}
65
120
  defaultValue={defaultValue}
66
- onValueChange={onValueChange}
121
+ onValueChange={(next, eventDetails) => onValueChange?.(next ?? '', eventDetails)}
67
122
  open={open}
68
123
  defaultOpen={defaultOpen}
69
- onOpenChange={onOpenChange}
124
+ onOpenChange={(next, eventDetails) => {
125
+ setShown(next)
126
+ onOpenChange?.(next, eventDetails)
127
+ }}
128
+ items={items ?? collectItems(children, {})}
129
+ name={name}
130
+ required={required}
131
+ readOnly={readOnly}
132
+ modal={modal}
133
+ form={form}
70
134
  >
71
135
  <SelectPrimitive.Trigger {...rest} className={selectTrigger({ surface, size, className })}>
72
136
  <span className="min-w-0 truncate">
73
137
  <SelectPrimitive.Value placeholder={placeholder} />
74
138
  </span>
75
- <SelectPrimitive.Icon className="text-foreground-subtle">
76
- <ChevronDown className="size-4" />
139
+ <SelectPrimitive.Icon className="text-fg-subtle">
140
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4 transition-transform in-data-popup-open:rotate-180 motion-reduce:transition-none">
141
+ <path d="m6 9 6 6 6-6" />
142
+ </svg>
77
143
  </SelectPrimitive.Icon>
78
144
  </SelectPrimitive.Trigger>
79
145
 
80
146
  <SelectPrimitive.Portal>
81
- <SelectPrimitive.Content
82
- className="float text-foreground rounded-float z-overlay
83
- min-w-(--radix-select-trigger-width) py-1
84
- open:animate-enter closed:animate-exit"
85
- position="popper"
86
- sideOffset={8}
147
+ <SelectPrimitive.Positioner
148
+ alignItemWithTrigger={false}
149
+ align="start"
150
+ sideOffset={arrow ? 14 : 8}
87
151
  arrowPadding={16}
152
+ className="z-overlay min-w-max will-change-transform"
88
153
  >
89
- <SelectPrimitive.ScrollUpButton className="text-foreground-subtle flex h-6 cursor-default items-center justify-center">
90
- <ChevronUp className="size-4" />
91
- </SelectPrimitive.ScrollUpButton>
92
-
93
- <SelectPrimitive.Viewport className="flex flex-col gap-px px-1">
94
- {children}
95
- </SelectPrimitive.Viewport>
96
-
97
- <SelectPrimitive.ScrollDownButton className="text-foreground-subtle flex h-6 cursor-default items-center justify-center">
98
- <ChevronDown className="size-4" />
99
- </SelectPrimitive.ScrollDownButton>
100
-
101
- {arrow && (
102
- <SelectPrimitive.Arrow asChild width={12} height={6}>
103
- <svg viewBox="0 0 12 6" preserveAspectRatio="none" className="fill-background-panel -translate-y-px">
104
- <path d="M0 0L4.58579 4.58579C5.36683 5.36683 6.63316 5.36684 7.41421 4.58579L12 0" className="stroke-line stroke-1" />
154
+ <SelectPrimitive.Popup
155
+ ref={popup}
156
+ className="float text-fg rounded-r12 z-overlay
157
+ flex flex-col outline-none
158
+ min-w-(--anchor-width) py-1
159
+ open:animate-enter closed:animate-exit"
160
+ >
161
+ <SelectPrimitive.ScrollUpArrow className="text-fg-subtle flex h-6 cursor-default items-center justify-center">
162
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4">
163
+ <path d="m18 15-6-6-6 6" />
105
164
  </svg>
106
- </SelectPrimitive.Arrow>
107
- )}
108
- </SelectPrimitive.Content>
165
+ </SelectPrimitive.ScrollUpArrow>
166
+
167
+ <SelectPrimitive.List className="relative flex flex-1 flex-col gap-px overflow-x-hidden overflow-y-auto px-1">
168
+ {children}
169
+ </SelectPrimitive.List>
170
+
171
+ <SelectPrimitive.ScrollDownArrow className="text-fg-subtle flex h-6 cursor-default items-center justify-center">
172
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4">
173
+ <path d="m6 9 6 6 6-6" />
174
+ </svg>
175
+ </SelectPrimitive.ScrollDownArrow>
176
+
177
+ {arrow && (
178
+ <SelectPrimitive.Arrow className={selectArrow()}>
179
+ <svg width={12} height={6} viewBox="0 0 12 6" preserveAspectRatio="none" className="block fill-bg-panel -translate-y-px">
180
+ <path d="M0 0L4.58579 4.58579C5.36683 5.36683 6.63316 5.36684 7.41421 4.58579L12 0" className="stroke-line stroke-1" />
181
+ </svg>
182
+ </SelectPrimitive.Arrow>
183
+ )}
184
+ </SelectPrimitive.Popup>
185
+ </SelectPrimitive.Positioner>
109
186
  </SelectPrimitive.Portal>
110
187
  </SelectPrimitive.Root>
111
188
  )
@@ -113,17 +190,18 @@ export function Select({
113
190
 
114
191
  const selectGroup = tv({ base: 'flex flex-col gap-px' })
115
192
 
116
- export function SelectGroup({ label, className, children }: {
193
+ export type SelectGroupProps = Omit<ComponentProps<typeof SelectPrimitive.Group>, 'className'> & {
117
194
  label?: string
118
195
  className?: string
119
- children?: ReactNode
120
- }) {
196
+ }
197
+
198
+ export function SelectGroup({ label, className, children, ...rest }: SelectGroupProps) {
121
199
  return (
122
- <SelectPrimitive.Group className={selectGroup({ className })}>
200
+ <SelectPrimitive.Group {...rest} className={selectGroup({ className })}>
123
201
  {label && (
124
- <SelectPrimitive.Label className="text-caption text-foreground-subtle px-2 pb-1 pt-2">
202
+ <SelectPrimitive.GroupLabel className="text-caption text-fg-subtle px-2 pb-1 pt-2">
125
203
  {label}
126
- </SelectPrimitive.Label>
204
+ </SelectPrimitive.GroupLabel>
127
205
  )}
128
206
  {children}
129
207
  </SelectPrimitive.Group>
@@ -134,43 +212,39 @@ const selectItem = tv({
134
212
  base: 'flex min-h-9 cursor-default items-center justify-between gap-stack'
135
213
  + ' rounded-r8 px-2 py-1 transition-state'
136
214
  + ' data-highlighted:bg-bg-neutral-hover data-highlighted:outline-none'
137
- + ' data-disabled:text-foreground-disabled data-disabled:pointer-events-none',
215
+ + ' data-disabled:text-fg-disabled data-disabled:pointer-events-none',
138
216
  })
139
217
 
140
- export function SelectItem({ value, disabled, className, children }: {
218
+ export type SelectItemProps = Omit<ComponentProps<typeof SelectPrimitive.Item>, 'className' | 'value'> & {
141
219
  value: string
142
- disabled?: boolean
143
220
  className?: string
144
- children?: ReactNode
145
- }) {
221
+ }
222
+
223
+ export function SelectItem({ className, children, ...rest }: SelectItemProps) {
146
224
  return (
147
- <SelectPrimitive.Item
148
- value={value}
149
- disabled={disabled}
150
- className={selectItem({ className })}
151
- >
225
+ <SelectPrimitive.Item {...rest} className={selectItem({ className })}>
152
226
  <span className="flex min-w-0 flex-col">
153
227
  {children}
154
228
  </span>
155
229
 
156
- <SelectPrimitive.ItemIndicator className="text-foreground-muted flex shrink-0">
157
- <Check className="size-4" />
230
+ <SelectPrimitive.ItemIndicator className="text-fg-muted flex shrink-0">
231
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4">
232
+ <path d="M20 6 9 17l-5-5" />
233
+ </svg>
158
234
  </SelectPrimitive.ItemIndicator>
159
235
  </SelectPrimitive.Item>
160
236
  )
161
237
  }
162
238
 
163
- const selectItemText = tv({ base: 'text-label text-foreground' })
239
+ export type SelectItemTextProps = Omit<ComponentProps<typeof SelectPrimitive.ItemText>, 'className'> & {
240
+ className?: string
241
+ }
164
242
 
165
- export function SelectItemText({ className, children }: { className?: string, children?: ReactNode }) {
166
- return (
167
- <SelectPrimitive.ItemText className={selectItemText({ className })}>
168
- {children}
169
- </SelectPrimitive.ItemText>
170
- )
243
+ export function SelectItemText({ className: _className, render = <span />, ...rest }: SelectItemTextProps) {
244
+ return <SelectPrimitive.ItemText {...rest} render={render} />
171
245
  }
172
246
 
173
- const selectItemDescription = tv({ base: 'text-caption text-foreground-subtle' })
247
+ const selectItemDescription = tv({ base: 'text-caption text-fg-subtle' })
174
248
 
175
249
  export function SelectItemDescription({ className, children }: { className?: string, children?: ReactNode }) {
176
250
  return (
@@ -182,6 +256,10 @@ export function SelectItemDescription({ className, children }: { className?: str
182
256
 
183
257
  const selectSeparator = tv({ base: 'bg-line -mx-1 my-1 h-px' })
184
258
 
185
- export function SelectSeparator({ className }: { className?: string }) {
186
- return <SelectPrimitive.Separator className={selectSeparator({ className })} />
259
+ export type SelectSeparatorProps = Omit<ComponentProps<typeof SelectPrimitive.Separator>, 'className'> & {
260
+ className?: string
261
+ }
262
+
263
+ export function SelectSeparator({ className, ...rest }: SelectSeparatorProps) {
264
+ return <SelectPrimitive.Separator {...rest} className={selectSeparator({ className })} />
187
265
  }
package/switch/index.tsx CHANGED
@@ -1,13 +1,13 @@
1
1
  'use client'
2
2
 
3
- import { Switch as SwitchPrimitive } from 'radix-ui'
3
+ import { Switch as SwitchPrimitive } from '@base-ui/react/switch'
4
4
  import type { ComponentProps } from 'react'
5
5
 
6
6
  import { tv } from '../tv'
7
7
 
8
8
  export const uiSwitch = tv({
9
9
  base: 'h-6 w-10 shrink-0 rounded-full p-0.5 transition-state press press-shrink'
10
- + ' bg-background-well ring-1 ring-inset ring-line-strong'
10
+ + ' bg-bg-neutral-track ring-1 ring-inset ring-line-strong'
11
11
  + ' invalid:ring-2 invalid:ring-line-danger'
12
12
  + ' focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-line-focus-ring'
13
13
  + ' disabled:bg-bg-neutral-disabled disabled:cursor-not-allowed'
@@ -26,15 +26,22 @@ export const uiSwitch = tv({
26
26
  defaultVariants: { color: 'neutral' },
27
27
  })
28
28
 
29
- export interface SwitchProps extends ComponentProps<typeof SwitchPrimitive.Root> {
29
+ export type SwitchProps = Omit<ComponentProps<typeof SwitchPrimitive.Root>, 'className'> & {
30
30
  color?: 'neutral' | 'info' | 'success' | 'warning' | 'danger'
31
+ className?: string
31
32
  }
32
33
 
33
- export function Switch({ color, className, ...rest }: SwitchProps) {
34
+ export function Switch({
35
+ color,
36
+ className,
37
+ nativeButton = true,
38
+ render = <button type="button" />,
39
+ ...rest
40
+ }: SwitchProps) {
34
41
  return (
35
- <SwitchPrimitive.Root {...rest} className={uiSwitch({ color, className })}>
42
+ <SwitchPrimitive.Root {...rest} nativeButton={nativeButton} render={render} className={uiSwitch({ color, className })}>
36
43
  <SwitchPrimitive.Thumb
37
- className="bg-bg-neutral-knob block size-5 rounded-full checked:translate-x-4
44
+ className="bg-bg-neutral-knob-off checked:bg-bg-neutral-knob block size-5 rounded-full checked:translate-x-4
38
45
  transition-transform ease-state motion-reduce:transition-none"
39
46
  />
40
47
  </SwitchPrimitive.Root>
package/table/index.tsx CHANGED
@@ -20,7 +20,7 @@ export interface TableProps {
20
20
  children?: ReactNode
21
21
  }
22
22
 
23
- const tableFrame = tv({ base: 'frame rounded-panel overflow-hidden' })
23
+ const tableFrame = tv({ base: 'frame rounded-r12 overflow-hidden' })
24
24
 
25
25
  export function Table({ label, fixed = false, className, children }: TableProps) {
26
26
  const [box, setBox] = useState<HTMLDivElement | null>(null)
@@ -59,7 +59,7 @@ export function Table({ label, fixed = false, className, children }: TableProps)
59
59
  )
60
60
  }
61
61
 
62
- const tableHeader = tv({ base: 'bg-background-well border-b border-line' })
62
+ const tableHeader = tv({ base: 'bg-bg-well border-b border-line' })
63
63
 
64
64
  export function TableHeader({ className, children }: { className?: string, children?: ReactNode }) {
65
65
  return (
@@ -71,7 +71,7 @@ export function TableHeader({ className, children }: { className?: string, child
71
71
  )
72
72
  }
73
73
 
74
- const tableHeadCell = tv({ base: 'text-caption text-foreground-muted h-9 px-4 text-left whitespace-nowrap' })
74
+ const tableHeadCell = tv({ base: 'text-caption text-fg-muted h-9 px-4 text-left whitespace-nowrap' })
75
75
 
76
76
  export function TableHeadCell({ className, children }: { className?: string, children?: ReactNode }) {
77
77
  return (
@@ -89,8 +89,7 @@ export function TableBody({ className, children }: { className?: string, childre
89
89
  }
90
90
 
91
91
  const tableRow = tv({
92
- base: 'border-b border-line last:border-b-0 transition-state'
93
- + ' hover:bg-bg-neutral-hover',
92
+ base: 'border-b border-line last:border-b-0',
94
93
  })
95
94
 
96
95
  export function TableRow({ className, children }: { className?: string, children?: ReactNode }) {
@@ -104,7 +103,7 @@ export function TableRow({ className, children }: { className?: string, children
104
103
  }
105
104
 
106
105
  export const tableCell = tv({
107
- base: 'text-label text-foreground h-11 px-4',
106
+ base: 'text-label text-fg h-11 px-4',
108
107
 
109
108
  variants: {
110
109
  mono: { true: 'font-mono', false: '' },
@@ -2,13 +2,15 @@
2
2
  position: absolute;
3
3
  bottom: 0;
4
4
  height: 2px;
5
- background: var(--color-foreground);
6
- width: var(--tabs-indicator-size);
7
- translate: var(--tabs-indicator-position) 0;
5
+ background: var(--color-fg);
6
+ width: var(--active-tab-width);
7
+ translate: var(--active-tab-left) 0;
8
8
  transition: translate var(--dur-state) var(--ease-state),
9
9
  width var(--dur-state) var(--ease-state);
10
10
  }
11
11
 
12
+ .content[data-hidden] { display: none; }
13
+
12
14
  @media (prefers-reduced-motion: reduce) {
13
15
  .underline { transition: none; }
14
16
  }
package/tabs/index.tsx CHANGED
@@ -1,113 +1,62 @@
1
1
  'use client'
2
2
 
3
- import { Tabs as TabsPrimitive } from 'radix-ui'
4
- import { useEffect, useRef, type ReactNode } from 'react'
3
+ import { Tabs as TabsPrimitive } from '@base-ui/react/tabs'
4
+ import type { ComponentProps } from 'react'
5
5
 
6
6
  import { tv } from '../tv'
7
7
 
8
8
  import s from './index.module.css'
9
9
 
10
- const ACTIVE = '[data-state="active"]'
11
-
12
- export interface TabsProps {
13
- value?: string
14
- defaultValue?: string
15
- onValueChange?: (value: string) => void
16
- className?: string
17
- children?: ReactNode
18
- }
10
+ export type TabsProps = Omit<ComponentProps<typeof TabsPrimitive.Root>, 'className'> & { className?: string }
19
11
 
20
12
  const tabs = tv({ base: 'flex flex-col gap-stack' })
21
13
 
22
- export function Tabs({ value, defaultValue, onValueChange, className, children }: TabsProps) {
23
- return (
24
- <TabsPrimitive.Root
25
- value={value}
26
- defaultValue={defaultValue}
27
- onValueChange={onValueChange}
28
- className={tabs({ className })}
29
- >
30
- {children}
31
- </TabsPrimitive.Root>
32
- )
14
+ export function Tabs({ className, ...rest }: TabsProps) {
15
+ return <TabsPrimitive.Root {...rest} className={tabs({ className })} />
33
16
  }
34
17
 
35
18
  const tabsList = tv({ base: 'border-line scrollbar-thin scrollbar-thumb-line-strong scrollbar-track-transparent overflow-x-auto border-b' })
36
19
 
37
- export function TabsList({ className, children }: { className?: string, children?: ReactNode }) {
38
- const list = useRef<HTMLDivElement>(null)
39
- const bar = useRef<HTMLSpanElement>(null)
40
-
41
- useEffect(() => {
42
- const root = list.current
43
- const mark = bar.current
44
- if (!root || !mark) return
45
-
46
- const measure = () => {
47
- const active = root.querySelector<HTMLElement>(ACTIVE)
48
- mark.style.setProperty('--tabs-indicator-size', `${active?.offsetWidth ?? 0}px`)
49
- mark.style.setProperty('--tabs-indicator-position', `${active?.offsetLeft ?? 0}px`)
50
- }
51
-
52
- measure()
53
-
54
- const attrs = new MutationObserver(measure)
55
- attrs.observe(root, { attributes: true, subtree: true, attributeFilter: ['data-state'] })
56
-
57
- const box = new ResizeObserver(measure)
58
- box.observe(root)
59
-
60
- return () => { attrs.disconnect(); box.disconnect() }
61
- }, [])
20
+ export type TabsListProps = Omit<ComponentProps<typeof TabsPrimitive.List>, 'className'> & { className?: string }
62
21
 
22
+ export function TabsList({ className, children, ...rest }: TabsListProps) {
63
23
  return (
64
- <div
65
- className={tabsList({ className })}
66
- >
67
- <TabsPrimitive.List ref={list} className="relative flex w-max min-w-full">
24
+ <div className={tabsList({ className })}>
25
+ <TabsPrimitive.List {...rest} className="relative flex w-max min-w-full">
68
26
  {children}
69
27
 
70
- <span ref={bar} className={s.underline} />
28
+ <TabsPrimitive.Indicator className={s.underline} />
71
29
  </TabsPrimitive.List>
72
30
  </div>
73
31
  )
74
32
  }
75
33
 
76
34
  const tabsTrigger = tv({
77
- base: 'text-label text-foreground-muted inline-flex h-9 items-center gap-tight px-3 transition-state'
35
+ base: 'text-label text-fg-muted inline-flex h-9 items-center gap-tight px-3 transition-state'
78
36
  + ' [&_svg]:size-4'
79
- + ' press-shrink enabled:hover:text-foreground'
80
- + ' disabled:text-foreground-disabled disabled:cursor-not-allowed'
81
- + ' aria-selected:text-foreground'
37
+ + ' press-shrink not-data-disabled:cursor-pointer not-data-disabled:hover:text-fg'
38
+ + ' data-disabled:text-fg-disabled data-disabled:cursor-not-allowed'
39
+ + ' aria-selected:text-fg'
82
40
  + ' focus-visible:outline-2 focus-visible:-outline-offset-2'
83
41
  + ' focus-visible:outline-line-focus-ring',
84
42
  })
85
43
 
86
- export function TabsTrigger({ value, disabled, className, children }: {
44
+ export type TabsTriggerProps = Omit<ComponentProps<typeof TabsPrimitive.Tab>, 'className' | 'value'> & {
87
45
  value: string
88
- disabled?: boolean
89
46
  className?: string
90
- children?: ReactNode
91
- }) {
92
- return (
93
- <TabsPrimitive.Trigger
94
- value={value}
95
- disabled={disabled}
96
- className={tabsTrigger({ className })}
97
- >
98
- {children}
99
- </TabsPrimitive.Trigger>
100
- )
101
47
  }
102
48
 
103
- export function TabsContent({ value, className, children }: {
49
+ export function TabsTrigger({ className, ...rest }: TabsTriggerProps) {
50
+ return <TabsPrimitive.Tab {...rest} className={tabsTrigger({ className })} />
51
+ }
52
+
53
+ const tabsContent = tv({ base: s.content })
54
+
55
+ export type TabsContentProps = Omit<ComponentProps<typeof TabsPrimitive.Panel>, 'className' | 'value'> & {
104
56
  value: string
105
57
  className?: string
106
- children?: ReactNode
107
- }) {
108
- return (
109
- <TabsPrimitive.Content value={value} className={className}>
110
- {children}
111
- </TabsPrimitive.Content>
112
- )
58
+ }
59
+
60
+ export function TabsContent({ className, ...rest }: TabsContentProps) {
61
+ return <TabsPrimitive.Panel {...rest} className={tabsContent({ className })} />
113
62
  }
package/tailwind.css ADDED
@@ -0,0 +1,3 @@
1
+ @import "./palette.css";
2
+ @import "./tokens.css";
3
+ @import "./utilities.css";
@@ -1,21 +1,24 @@
1
- import type { TextareaHTMLAttributes } from 'react'
1
+ 'use client'
2
+
3
+ import { Field as FieldPrimitive } from '@base-ui/react/field'
4
+ import type { ComponentProps } from 'react'
2
5
 
3
6
  import { tv } from '../tv'
4
7
 
5
8
  export const textarea = tv({
6
- base: 'w-full rounded-r8 text-value text-foreground'
7
- + ' placeholder:text-foreground-placeholder'
9
+ base: 'w-full rounded-r8 text-value text-fg'
10
+ + ' placeholder:text-fg-placeholder'
8
11
  + ' invalid:ring-2 invalid:ring-line-danger'
9
- + ' read-only:text-foreground-muted'
12
+ + ' read-only:text-fg-muted'
10
13
  + ' resize-y'
11
14
  + ' transition-state'
12
15
  + ' focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-line-focus-ring'
13
- + ' disabled:bg-bg-neutral-disabled disabled:text-foreground-disabled disabled:cursor-not-allowed',
16
+ + ' disabled:bg-bg-neutral-disabled disabled:text-fg-disabled disabled:cursor-not-allowed',
14
17
 
15
18
  variants: {
16
19
  surface: {
17
- bare: 'bg-transparent ring-1 ring-inset ring-line-strong read-only:bg-background-well',
18
- well: 'bg-background-well',
20
+ bare: 'bg-transparent ring-1 ring-inset ring-line-strong read-only:bg-bg-well',
21
+ well: 'bg-bg-well',
19
22
  },
20
23
  size: {
21
24
  sm: 'p-2',
@@ -27,11 +30,20 @@ export const textarea = tv({
27
30
  defaultVariants: { surface: 'bare', size: 'md' },
28
31
  })
29
32
 
30
- export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
31
- surface?: 'bare' | 'well'
32
- size?: 'sm' | 'md' | 'lg'
33
- }
33
+ export type TextareaProps = Omit<ComponentProps<'textarea'>, 'className' | 'value' | 'defaultValue'>
34
+ & Pick<FieldPrimitive.Control.Props, 'value' | 'defaultValue' | 'onValueChange'>
35
+ & {
36
+ surface?: 'bare' | 'well'
37
+ size?: 'sm' | 'md' | 'lg'
38
+ className?: string
39
+ }
34
40
 
35
- export function Textarea({ surface, size, rows = 3, className, ...rest }: TextareaProps) {
36
- return <textarea {...rest} rows={rows} className={textarea({ surface, size, className })} />
41
+ export function Textarea({ surface, size, rows = 3, cols, wrap, className, ...rest }: TextareaProps) {
42
+ return (
43
+ <FieldPrimitive.Control
44
+ {...(rest as FieldPrimitive.Control.Props)}
45
+ render={<textarea rows={rows} cols={cols} wrap={wrap} />}
46
+ className={textarea({ surface, size, className })}
47
+ />
48
+ )
37
49
  }