@pantoia/ui 0.3.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 (62) hide show
  1. package/accordion/index.module.css +4 -4
  2. package/accordion/index.tsx +21 -51
  3. package/alert-dialog/index.tsx +36 -42
  4. package/badge/index.tsx +1 -1
  5. package/button/index.tsx +31 -30
  6. package/callout/index.tsx +4 -4
  7. package/checkbox/index.tsx +20 -85
  8. package/combobox/index.tsx +122 -316
  9. package/command-palette/index.tsx +51 -32
  10. package/dialog/index.tsx +28 -24
  11. package/dist/index.js +5 -5
  12. package/drawer/index.module.css +2 -2
  13. package/drawer/index.tsx +52 -130
  14. package/dropdown-menu/index.tsx +87 -95
  15. package/field/index.tsx +27 -86
  16. package/input/index.tsx +7 -3
  17. package/kbd/index.tsx +2 -2
  18. package/label/index.tsx +6 -8
  19. package/number-field/index.tsx +122 -173
  20. package/package.json +2 -6
  21. package/popover/index.tsx +61 -32
  22. package/radio-group/index.tsx +35 -38
  23. package/registry.json +64 -154
  24. package/select/index.tsx +141 -70
  25. package/switch/index.tsx +13 -6
  26. package/table/index.tsx +1 -2
  27. package/tabs/index.module.css +3 -3
  28. package/tabs/index.tsx +22 -76
  29. package/textarea/index.tsx +19 -7
  30. package/toast/index.module.css +187 -0
  31. package/toast/index.tsx +141 -46
  32. package/toggle/index.tsx +4 -3
  33. package/tokens.css +7 -1
  34. package/tooltip/index.tsx +61 -30
  35. package/utilities.css +11 -6
  36. package/accordion/AGENTS.md +0 -60
  37. package/alert-dialog/AGENTS.md +0 -78
  38. package/badge/AGENTS.md +0 -35
  39. package/button/AGENTS.md +0 -39
  40. package/callout/AGENTS.md +0 -37
  41. package/checkbox/AGENTS.md +0 -54
  42. package/combobox/AGENTS.md +0 -150
  43. package/command-palette/AGENTS.md +0 -41
  44. package/dialog/AGENTS.md +0 -81
  45. package/drawer/AGENTS.md +0 -66
  46. package/dropdown-menu/AGENTS.md +0 -133
  47. package/field/AGENTS.md +0 -92
  48. package/input/AGENTS.md +0 -35
  49. package/kbd/AGENTS.md +0 -17
  50. package/label/AGENTS.md +0 -21
  51. package/number-field/AGENTS.md +0 -49
  52. package/popover/AGENTS.md +0 -64
  53. package/radio-group/AGENTS.md +0 -81
  54. package/select/AGENTS.md +0 -90
  55. package/switch/AGENTS.md +0 -37
  56. package/table/AGENTS.md +0 -82
  57. package/tabs/AGENTS.md +0 -61
  58. package/test-utils.ts +0 -10
  59. package/textarea/AGENTS.md +0 -35
  60. package/toast/AGENTS.md +0 -34
  61. package/toggle/AGENTS.md +0 -37
  62. package/tooltip/AGENTS.md +0 -50
@@ -1,9 +1,9 @@
1
- @keyframes acc-open { from { height: 0; } to { height: var(--radix-accordion-content-height); } }
2
- @keyframes acc-close { from { height: var(--radix-accordion-content-height); } to { height: 0; } }
1
+ @keyframes acc-open { from { height: 0; } to { height: var(--accordion-panel-height); } }
2
+ @keyframes acc-close { from { height: var(--accordion-panel-height); } to { height: 0; } }
3
3
 
4
4
  .panel { overflow: hidden; }
5
- .panel[data-state="open"] { animation: acc-open var(--dur-enter) var(--ease-enter); }
6
- .panel[data-state="closed"] { animation: acc-close var(--dur-exit) var(--ease-exit); }
5
+ .panel[data-open] { animation: acc-open var(--dur-enter) var(--ease-enter); }
6
+ .panel[data-closed] { animation: acc-close var(--dur-exit) var(--ease-exit); }
7
7
 
8
8
  @media (prefers-reduced-motion: reduce) {
9
9
  .panel { animation: none; }
@@ -1,77 +1,45 @@
1
1
  'use client'
2
2
 
3
- import { Accordion as AccordionPrimitive } from 'radix-ui'
4
- import type { ReactNode } from 'react'
3
+ import { Accordion as AccordionPrimitive } from '@base-ui/react/accordion'
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
- export interface AccordionProps {
11
- value?: string | string[]
12
- defaultValue?: string | string[]
13
- onValueChange?: (value: string & string[]) => void
14
- multiple?: boolean
15
- className?: string
16
- children?: ReactNode
17
- }
10
+ export type AccordionProps = Omit<AccordionPrimitive.Root.Props<string>, 'className'> & { className?: string }
18
11
 
19
12
  const accordion = tv({ base: 'frame rounded-r12 overflow-hidden divide-y divide-line' })
20
13
 
21
- export function Accordion({ value, defaultValue, onValueChange, multiple, className, children }: AccordionProps) {
22
- const shared = {
23
- className: accordion({ className }),
24
- children,
25
- }
26
-
27
- return multiple
28
- ? (
29
- <AccordionPrimitive.Root
30
- type="multiple"
31
- value={value as string[] | undefined}
32
- defaultValue={defaultValue as string[] | undefined}
33
- onValueChange={onValueChange}
34
- {...shared}
35
- />
36
- )
37
- : (
38
- <AccordionPrimitive.Root
39
- type="single"
40
- collapsible
41
- value={value as string | undefined}
42
- defaultValue={defaultValue as string | undefined}
43
- onValueChange={onValueChange}
44
- {...shared}
45
- />
46
- )
14
+ export function Accordion({ className, ...rest }: AccordionProps) {
15
+ return <AccordionPrimitive.Root<string> {...rest} className={accordion({ className })} />
47
16
  }
48
17
 
49
- export function AccordionItem({ value, disabled, className, children }: {
18
+ export type AccordionItemProps = Omit<ComponentProps<typeof AccordionPrimitive.Item>, 'className' | 'value'> & {
50
19
  value: string
51
- disabled?: boolean
52
20
  className?: string
53
- children?: ReactNode
54
- }) {
55
- return (
56
- <AccordionPrimitive.Item value={value} disabled={disabled} className={className}>
57
- {children}
58
- </AccordionPrimitive.Item>
59
- )
21
+ }
22
+
23
+ export function AccordionItem(props: AccordionItemProps) {
24
+ return <AccordionPrimitive.Item {...props} />
60
25
  }
61
26
 
62
27
  const accordionTrigger = tv({
63
28
  base: 'text-label text-fg flex h-11 w-full items-center'
64
29
  + ' gap-stack border-b border-transparent px-4 transition-state'
65
- + ' press open:border-line enabled:hover:bg-bg-neutral-hover'
66
- + ' disabled:text-fg-disabled disabled:cursor-not-allowed'
30
+ + ' press data-panel-open:border-line not-data-disabled:hover:bg-bg-neutral-hover'
31
+ + ' data-disabled:text-fg-disabled data-disabled:cursor-not-allowed'
67
32
  + ' focus-visible:outline-2 focus-visible:-outline-offset-2'
68
33
  + ' focus-visible:outline-line-focus-ring',
69
34
  })
70
35
 
71
- export function AccordionTrigger({ className, children }: { className?: string, children?: ReactNode }) {
36
+ export type AccordionTriggerProps = Omit<ComponentProps<typeof AccordionPrimitive.Trigger>, 'className'> & { className?: string }
37
+
38
+ export function AccordionTrigger({ className, children, ...rest }: AccordionTriggerProps) {
72
39
  return (
73
40
  <AccordionPrimitive.Header>
74
41
  <AccordionPrimitive.Trigger
42
+ {...rest}
75
43
  className={accordionTrigger({ className })}
76
44
  >
77
45
  <span className="flex min-w-0 grow items-center gap-tight">{children}</span>
@@ -90,12 +58,14 @@ export function AccordionTrigger({ className, children }: { className?: string,
90
58
 
91
59
  const accordionContent = tv({ base: 'bg-bg-well text-caption text-fg-muted px-4 py-3' })
92
60
 
93
- export function AccordionContent({ className, children }: { className?: string, children?: ReactNode }) {
61
+ export type AccordionContentProps = Omit<ComponentProps<typeof AccordionPrimitive.Panel>, 'className'> & { className?: string }
62
+
63
+ export function AccordionContent({ className, children, ...rest }: AccordionContentProps) {
94
64
  return (
95
- <AccordionPrimitive.Content className={s.panel}>
65
+ <AccordionPrimitive.Panel {...rest} className={s.panel}>
96
66
  <div className={accordionContent({ className })}>
97
67
  {children}
98
68
  </div>
99
- </AccordionPrimitive.Content>
69
+ </AccordionPrimitive.Panel>
100
70
  )
101
71
  }
@@ -1,28 +1,21 @@
1
1
  'use client'
2
2
 
3
- import { AlertDialog as AlertDialogPrimitive } from 'radix-ui'
3
+ import { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog'
4
4
  import type { ComponentProps, ReactNode } from 'react'
5
5
 
6
6
  import { Button } from '../button'
7
7
  import { tv } from '../tv'
8
8
 
9
- export interface AlertDialogProps {
10
- open?: boolean
11
- defaultOpen?: boolean
12
- onOpenChange?: (open: boolean) => void
13
- children?: ReactNode
14
- }
9
+ export type AlertDialogProps = ComponentProps<typeof AlertDialogPrimitive.Root>
15
10
 
16
- export function AlertDialog({ open, defaultOpen, onOpenChange, children }: AlertDialogProps) {
17
- return (
18
- <AlertDialogPrimitive.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
19
- {children}
20
- </AlertDialogPrimitive.Root>
21
- )
11
+ export function AlertDialog(props: AlertDialogProps) {
12
+ return <AlertDialogPrimitive.Root {...props} />
22
13
  }
23
14
 
24
- export function AlertDialogTrigger({ children }: { children: ReactNode }) {
25
- return <AlertDialogPrimitive.Trigger asChild>{children}</AlertDialogPrimitive.Trigger>
15
+ export type AlertDialogTriggerProps = ComponentProps<typeof AlertDialogPrimitive.Trigger>
16
+
17
+ export function AlertDialogTrigger(props: AlertDialogTriggerProps) {
18
+ return <AlertDialogPrimitive.Trigger {...props} />
26
19
  }
27
20
 
28
21
  const alertDialogContent = tv({
@@ -33,30 +26,42 @@ const alertDialogContent = tv({
33
26
  + ' focus-visible:outline-none',
34
27
  })
35
28
 
36
- export function AlertDialogContent({ className, children, ...rest }: ComponentProps<typeof AlertDialogPrimitive.Content>) {
29
+ export type AlertDialogContentProps = Omit<ComponentProps<typeof AlertDialogPrimitive.Popup>, 'className'> & {
30
+ className?: string
31
+ }
32
+
33
+ export function AlertDialogContent({ className, children, ...rest }: AlertDialogContentProps) {
37
34
  return (
38
35
  <AlertDialogPrimitive.Portal>
39
- <AlertDialogPrimitive.Overlay className="bg-bg-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
36
+ <AlertDialogPrimitive.Backdrop className="bg-bg-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
40
37
 
41
- <AlertDialogPrimitive.Content
38
+ <AlertDialogPrimitive.Popup
42
39
  {...rest}
43
40
  className={alertDialogContent({ className })}
44
41
  >
45
42
  {children}
46
- </AlertDialogPrimitive.Content>
43
+ </AlertDialogPrimitive.Popup>
47
44
  </AlertDialogPrimitive.Portal>
48
45
  )
49
46
  }
50
47
 
51
48
  const alertDialogTitle = tv({ base: 'text-section' })
52
49
 
53
- export function AlertDialogTitle({ className, ...rest }: ComponentProps<typeof AlertDialogPrimitive.Title>) {
50
+ export type AlertDialogTitleProps = Omit<ComponentProps<typeof AlertDialogPrimitive.Title>, 'className'> & {
51
+ className?: string
52
+ }
53
+
54
+ export function AlertDialogTitle({ className, ...rest }: AlertDialogTitleProps) {
54
55
  return <AlertDialogPrimitive.Title {...rest} className={alertDialogTitle({ className })} />
55
56
  }
56
57
 
57
58
  const alertDialogDescription = tv({ base: 'text-prose text-fg-muted' })
58
59
 
59
- export function AlertDialogDescription({ className, ...rest }: ComponentProps<typeof AlertDialogPrimitive.Description>) {
60
+ export type AlertDialogDescriptionProps = Omit<ComponentProps<typeof AlertDialogPrimitive.Description>, 'className'> & {
61
+ className?: string
62
+ }
63
+
64
+ export function AlertDialogDescription({ className, ...rest }: AlertDialogDescriptionProps) {
60
65
  return (
61
66
  <AlertDialogPrimitive.Description
62
67
  {...rest}
@@ -93,7 +98,7 @@ export function AlertDialogHeader({ lead, trail, className, children }: AlertDia
93
98
  )
94
99
  }
95
100
 
96
- const alertDialogFooter = tv({ base: 'flex justify-end gap-2' })
101
+ const alertDialogFooter = tv({ base: 'mt-auto flex justify-end gap-2' })
97
102
 
98
103
  export function AlertDialogFooter({ className, children }: { className?: string, children?: ReactNode }) {
99
104
  return (
@@ -103,27 +108,16 @@ export function AlertDialogFooter({ className, children }: { className?: string,
103
108
  )
104
109
  }
105
110
 
106
- export function AlertDialogAction({ color = 'neutral', onClick, className, children }: {
111
+ export type AlertDialogActionProps = Omit<ComponentProps<typeof AlertDialogPrimitive.Close>, 'color'> & {
107
112
  color?: 'neutral' | 'brand' | 'danger'
108
- onClick?: () => void
109
- className?: string
110
- children?: ReactNode
111
- }) {
112
- return (
113
- <AlertDialogPrimitive.Action asChild>
114
- <Button emphasis="solid" color={color} onClick={onClick} className={className}>
115
- {children}
116
- </Button>
117
- </AlertDialogPrimitive.Action>
118
- )
119
113
  }
120
114
 
121
- export function AlertDialogCancel({ className, children }: { className?: string, children?: ReactNode }) {
122
- return (
123
- <AlertDialogPrimitive.Cancel asChild>
124
- <Button color="neutral" emphasis="outline" className={className}>
125
- {children}
126
- </Button>
127
- </AlertDialogPrimitive.Cancel>
128
- )
115
+ export function AlertDialogAction({ color = 'neutral', render, ...rest }: AlertDialogActionProps) {
116
+ return <AlertDialogPrimitive.Close {...rest} render={render ?? <Button emphasis="solid" color={color} />} />
117
+ }
118
+
119
+ export type AlertDialogCancelProps = ComponentProps<typeof AlertDialogPrimitive.Close>
120
+
121
+ export function AlertDialogCancel({ render, ...rest }: AlertDialogCancelProps) {
122
+ return <AlertDialogPrimitive.Close {...rest} render={render ?? <Button color="neutral" emphasis="outline" />} />
129
123
  }
package/badge/index.tsx CHANGED
@@ -8,7 +8,7 @@ export const badge = tv({
8
8
 
9
9
  variants: {
10
10
  color: {
11
- neutral: 'bg-bg-well text-fg-muted',
11
+ neutral: 'bg-bg-neutral-solid/10 text-fg',
12
12
  brand: 'bg-bg-brand-weak text-fg-brand-contrast',
13
13
  info: 'bg-bg-info-weak text-fg-info-contrast',
14
14
  success: 'bg-bg-success-weak text-fg-success-contrast',
package/button/index.tsx CHANGED
@@ -1,4 +1,5 @@
1
- import type { ButtonHTMLAttributes } from 'react'
1
+ import { Button as ButtonPrimitive } from '@base-ui/react/button'
2
+ import type { ComponentProps } from 'react'
2
3
 
3
4
  import { tv } from '../tv'
4
5
 
@@ -7,7 +8,7 @@ export const button = tv({
7
8
  + ' rounded-r8 text-label'
8
9
  + ' transition-state press press-shrink'
9
10
  + ' focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-line-focus-ring'
10
- + ' disabled:text-fg-disabled disabled:cursor-not-allowed',
11
+ + ' data-disabled:text-fg-disabled data-disabled:cursor-not-allowed',
11
12
 
12
13
  variants: {
13
14
  color: { brand: '', neutral: '', danger: '' },
@@ -28,46 +29,46 @@ export const button = tv({
28
29
  compoundVariants: [
29
30
  { color: 'brand', emphasis: 'solid',
30
31
  class: 'bg-bg-brand-solid text-fg-brand-solid-contrast'
31
- + ' enabled:hover:bg-bg-brand-solid-hover'
32
- + ' disabled:bg-bg-brand-solid/20' },
32
+ + ' not-data-disabled:hover:bg-bg-brand-solid-hover'
33
+ + ' data-disabled:bg-bg-brand-solid/20' },
33
34
  { color: 'brand', emphasis: 'weak',
34
- class: 'bg-bg-brand-weak text-fg-brand-contrast disabled:text-fg-brand-contrast/40'
35
- + ' enabled:hover:bg-bg-brand-solid/10' },
35
+ class: 'bg-bg-brand-weak text-fg-brand-contrast data-disabled:text-fg-brand-contrast/40'
36
+ + ' not-data-disabled:hover:bg-bg-brand-solid/10' },
36
37
  { color: 'brand', emphasis: 'outline',
37
- class: 'text-fg-brand-contrast disabled:text-fg-brand-contrast/40'
38
- + ' ring-1 ring-inset ring-line-brand-weak disabled:ring-line'
39
- + ' enabled:hover:bg-bg-brand-solid/10' },
38
+ class: 'text-fg-brand-contrast data-disabled:text-fg-brand-contrast/40'
39
+ + ' ring-1 ring-inset ring-line-brand-weak data-disabled:ring-line'
40
+ + ' not-data-disabled:hover:bg-bg-brand-solid/10' },
40
41
  { color: 'brand', emphasis: 'ghost',
41
- class: 'text-fg-brand-contrast disabled:text-fg-brand-contrast/40'
42
- + ' enabled:hover:bg-bg-brand-solid/10' },
42
+ class: 'text-fg-brand-contrast data-disabled:text-fg-brand-contrast/40'
43
+ + ' not-data-disabled:hover:bg-bg-brand-solid/10' },
43
44
 
44
45
  { color: 'neutral', emphasis: 'solid',
45
46
  class: 'bg-bg-neutral-solid text-fg-neutral-solid-contrast'
46
- + ' enabled:hover:bg-bg-neutral-solid-hover'
47
- + ' disabled:bg-bg-neutral-solid/20' },
47
+ + ' not-data-disabled:hover:bg-bg-neutral-solid-hover'
48
+ + ' data-disabled:bg-bg-neutral-solid/20' },
48
49
  { color: 'neutral', emphasis: 'weak',
49
- class: 'bg-bg-well text-fg'
50
- + ' enabled:hover:bg-bg-neutral-solid/10' },
50
+ class: 'bg-bg-neutral-solid/8 text-fg'
51
+ + ' not-data-disabled:hover:bg-bg-neutral-solid/16' },
51
52
  { color: 'neutral', emphasis: 'outline',
52
- class: 'text-fg ring-1 ring-inset ring-line-strong disabled:ring-line'
53
- + ' enabled:hover:bg-bg-neutral-hover' },
53
+ class: 'text-fg ring-1 ring-inset ring-line-strong data-disabled:ring-line'
54
+ + ' not-data-disabled:hover:bg-bg-neutral-hover' },
54
55
  { color: 'neutral', emphasis: 'ghost',
55
- class: 'text-fg enabled:hover:bg-bg-neutral-hover' },
56
+ class: 'text-fg not-data-disabled:hover:bg-bg-neutral-hover' },
56
57
 
57
58
  { color: 'danger', emphasis: 'solid',
58
59
  class: 'bg-bg-danger-solid text-white'
59
- + ' enabled:hover:bg-bg-danger-solid-hover'
60
- + ' disabled:bg-bg-danger-solid/20' },
60
+ + ' not-data-disabled:hover:bg-bg-danger-solid-hover'
61
+ + ' data-disabled:bg-bg-danger-solid/20' },
61
62
  { color: 'danger', emphasis: 'weak',
62
- class: 'bg-bg-danger-weak text-fg-danger-contrast disabled:text-fg-danger-contrast/40'
63
- + ' enabled:hover:bg-bg-danger-solid/10' },
63
+ class: 'bg-bg-danger-weak text-fg-danger-contrast data-disabled:text-fg-danger-contrast/40'
64
+ + ' not-data-disabled:hover:bg-bg-danger-solid/10' },
64
65
  { color: 'danger', emphasis: 'outline',
65
- class: 'text-fg-danger-contrast disabled:text-fg-danger-contrast/40'
66
- + ' ring-1 ring-inset ring-line-danger-weak disabled:ring-line'
67
- + ' enabled:hover:bg-bg-danger-solid/10' },
66
+ class: 'text-fg-danger-contrast data-disabled:text-fg-danger-contrast/40'
67
+ + ' ring-1 ring-inset ring-line-danger-weak data-disabled:ring-line'
68
+ + ' not-data-disabled:hover:bg-bg-danger-solid/10' },
68
69
  { color: 'danger', emphasis: 'ghost',
69
- class: 'text-fg-danger-contrast disabled:text-fg-danger-contrast/40'
70
- + ' enabled:hover:bg-bg-danger-solid/10' },
70
+ class: 'text-fg-danger-contrast data-disabled:text-fg-danger-contrast/40'
71
+ + ' not-data-disabled:hover:bg-bg-danger-solid/10' },
71
72
 
72
73
  { shape: ['rect', 'pill'], size: 'sm', class: 'px-3' },
73
74
  { shape: ['rect', 'pill'], size: 'md', class: 'px-3.5' },
@@ -80,7 +81,8 @@ export const button = tv({
80
81
  defaultVariants: { size: 'md', shape: 'rect' },
81
82
  })
82
83
 
83
- export interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'color'> {
84
+ export interface ButtonProps extends Omit<ComponentProps<typeof ButtonPrimitive>, 'className' | 'color'> {
85
+ className?: string
84
86
  color: 'brand' | 'neutral' | 'danger'
85
87
  emphasis: 'solid' | 'weak' | 'outline' | 'ghost'
86
88
  size?: 'sm' | 'md' | 'lg'
@@ -89,8 +91,7 @@ export interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement
89
91
 
90
92
  export function Button({ color, emphasis, size, shape, className, ...rest }: ButtonProps) {
91
93
  return (
92
- <button
93
- type="button"
94
+ <ButtonPrimitive
94
95
  {...rest}
95
96
  className={button({ color, emphasis, size, shape, className })}
96
97
  />
package/callout/index.tsx CHANGED
@@ -37,27 +37,27 @@ export const callout = tv({
37
37
 
38
38
  const ICONS = {
39
39
  info: (
40
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
40
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-5">
41
41
  <circle cx="12" cy="12" r="10" />
42
42
  <path d="M12 16v-4" />
43
43
  <path d="M12 8h.01" />
44
44
  </svg>
45
45
  ),
46
46
  success: (
47
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
47
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-5">
48
48
  <circle cx="12" cy="12" r="10" />
49
49
  <path d="m16 9-5.5 5.5L8 12" />
50
50
  </svg>
51
51
  ),
52
52
  warning: (
53
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
53
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-5">
54
54
  <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
55
55
  <path d="M12 9v4" />
56
56
  <path d="M12 17h.01" />
57
57
  </svg>
58
58
  ),
59
59
  danger: (
60
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
60
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-5">
61
61
  <circle cx="12" cy="12" r="10" />
62
62
  <path d="m15 9-6 6" />
63
63
  <path d="m9 9 6 6" />
@@ -1,15 +1,8 @@
1
1
  'use client'
2
2
 
3
- import { Checkbox as CheckboxPrimitive } from 'radix-ui'
4
- import {
5
- createContext,
6
- use,
7
- useRef,
8
- useState,
9
- type ComponentProps,
10
- type KeyboardEvent as ReactKeyboardEvent,
11
- type ReactNode,
12
- } from 'react'
3
+ import { Checkbox as CheckboxPrimitive } from '@base-ui/react/checkbox'
4
+ import { CheckboxGroup as CheckboxGroupPrimitive } from '@base-ui/react/checkbox-group'
5
+ import type { ComponentProps } from 'react'
13
6
 
14
7
  import { tv } from '../tv'
15
8
 
@@ -36,52 +29,25 @@ export const checkbox = tv({
36
29
  defaultVariants: { color: 'neutral' },
37
30
  })
38
31
 
39
- interface GroupContext {
40
- value: string[]
41
- toggle: (item: string, next: boolean) => void
42
- disabled?: boolean
43
- }
44
-
45
- const Group = createContext<GroupContext | null>(null)
46
-
47
32
  const ITEMS = '[role="checkbox"]:not([disabled])'
48
33
 
49
- export interface CheckboxGroupProps {
50
- value?: string[]
51
- defaultValue?: string[]
52
- onValueChange?: (value: string[]) => void
53
- disabled?: boolean
54
- className?: string
55
- children?: ReactNode
56
- }
34
+ export type CheckboxGroupProps = Omit<ComponentProps<typeof CheckboxGroupPrimitive>, 'className'> & { className?: string }
35
+
36
+ type GroupKeyEvent = Parameters<NonNullable<CheckboxGroupProps['onKeyDown']>>[0]
57
37
 
58
38
  const checkboxGroup = tv({ base: 'flex flex-col gap-3' })
59
39
 
60
- export function CheckboxGroup({
61
- value,
62
- defaultValue,
63
- onValueChange,
64
- disabled,
65
- className,
66
- children,
67
- }: CheckboxGroupProps) {
68
- const [own, setOwn] = useState<string[]>(defaultValue ?? [])
69
- const list = value ?? own
70
- const root = useRef<HTMLDivElement>(null)
71
-
72
- const toggle = (item: string, next: boolean) => {
73
- const after = next ? [...list, item] : list.filter(one => one !== item)
74
- if (value === undefined) setOwn(after)
75
- onValueChange?.(after)
76
- }
40
+ export function CheckboxGroup({ className, onKeyDown, ...rest }: CheckboxGroupProps) {
41
+ const onKeys = (e: GroupKeyEvent) => {
42
+ onKeyDown?.(e)
43
+ if (e.defaultPrevented) return
77
44
 
78
- const onKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {
79
45
  const step = e.key === 'ArrowDown' || e.key === 'ArrowRight'
80
46
  ? 1
81
47
  : e.key === 'ArrowUp' || e.key === 'ArrowLeft' ? -1 : 0
82
- if (step === 0 || !root.current) return
48
+ if (step === 0) return
83
49
 
84
- const items = [...root.current.querySelectorAll<HTMLElement>(ITEMS)]
50
+ const items = [...e.currentTarget.querySelectorAll<HTMLElement>(ITEMS)]
85
51
  const at = items.indexOf(document.activeElement as HTMLElement)
86
52
  if (at === -1) return
87
53
 
@@ -89,59 +55,28 @@ export function CheckboxGroup({
89
55
  items[(at + step + items.length) % items.length]!.focus()
90
56
  }
91
57
 
92
- return (
93
- <Group value={{ value: list, toggle, disabled }}>
94
- <div
95
- ref={root}
96
- role="group"
97
- onKeyDown={onKeyDown}
98
- className={checkboxGroup({ className })}
99
- >
100
- {children}
101
- </div>
102
- </Group>
103
- )
58
+ return <CheckboxGroupPrimitive {...rest} onKeyDown={onKeys} className={checkboxGroup({ className })} />
104
59
  }
105
60
 
106
- export interface CheckboxProps extends Omit<ComponentProps<typeof CheckboxPrimitive.Root>, 'checked' | 'onCheckedChange' | 'value'> {
107
- value?: string
108
- checked?: boolean
109
- onCheckedChange?: (checked: boolean) => void
110
- indeterminate?: boolean
111
- onIndeterminateChange?: (indeterminate: boolean) => void
61
+ export type CheckboxProps = Omit<ComponentProps<typeof CheckboxPrimitive.Root>, 'className'> & {
112
62
  color?: 'neutral' | 'info' | 'success' | 'warning' | 'danger'
63
+ className?: string
113
64
  }
114
65
 
115
66
  export function Checkbox({
116
- value,
117
- checked,
118
- onCheckedChange,
119
67
  indeterminate,
120
- onIndeterminateChange,
121
68
  color,
122
69
  className,
123
- disabled,
70
+ nativeButton = true,
71
+ render = <button type="button" />,
124
72
  ...rest
125
73
  }: CheckboxProps) {
126
- const group = use(Group)
127
- const item = value ?? 'on'
128
-
129
- const on = group ? group.value.includes(item) : checked ?? false
130
- const state = indeterminate ? 'indeterminate' as const : on
131
-
132
- const change = (next: boolean | 'indeterminate') => {
133
- if (typeof next !== 'boolean') return
134
- if (group) group.toggle(item, next)
135
- else onCheckedChange?.(next)
136
- if (indeterminate) onIndeterminateChange?.(false)
137
- }
138
-
139
74
  return (
140
75
  <CheckboxPrimitive.Root
141
76
  {...rest}
142
- checked={state}
143
- disabled={disabled ?? group?.disabled}
144
- onCheckedChange={change}
77
+ nativeButton={nativeButton}
78
+ render={render}
79
+ indeterminate={indeterminate}
145
80
  className={checkbox({ color, className })}
146
81
  >
147
82
  <CheckboxPrimitive.Indicator className="text-fg-neutral-solid-contrast flex">