@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
@@ -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,99 +1,71 @@
1
1
  'use client'
2
2
 
3
- import { ChevronDown } from 'lucide-react'
4
- import { Accordion as AccordionPrimitive } from 'radix-ui'
5
- import type { ReactNode } from 'react'
3
+ import { Accordion as AccordionPrimitive } from '@base-ui/react/accordion'
4
+ import type { ComponentProps } from 'react'
6
5
 
7
6
  import { tv } from '../tv'
8
7
 
9
8
  import s from './index.module.css'
10
9
 
11
- export interface AccordionProps {
12
- value?: string | string[]
13
- defaultValue?: string | string[]
14
- onValueChange?: (value: string & string[]) => void
15
- multiple?: boolean
16
- className?: string
17
- children?: ReactNode
18
- }
19
-
20
- const accordion = tv({ base: 'frame rounded-panel overflow-hidden divide-y divide-line' })
10
+ export type AccordionProps = Omit<AccordionPrimitive.Root.Props<string>, 'className'> & { className?: string }
21
11
 
22
- export function Accordion({ value, defaultValue, onValueChange, multiple, className, children }: AccordionProps) {
23
- const shared = {
24
- className: accordion({ className }),
25
- children,
26
- }
12
+ const accordion = tv({ base: 'frame rounded-r12 overflow-hidden divide-y divide-line' })
27
13
 
28
- return multiple
29
- ? (
30
- <AccordionPrimitive.Root
31
- type="multiple"
32
- value={value as string[] | undefined}
33
- defaultValue={defaultValue as string[] | undefined}
34
- onValueChange={onValueChange}
35
- {...shared}
36
- />
37
- )
38
- : (
39
- <AccordionPrimitive.Root
40
- type="single"
41
- collapsible
42
- value={value as string | undefined}
43
- defaultValue={defaultValue as string | undefined}
44
- onValueChange={onValueChange}
45
- {...shared}
46
- />
47
- )
14
+ export function Accordion({ className, ...rest }: AccordionProps) {
15
+ return <AccordionPrimitive.Root<string> {...rest} className={accordion({ className })} />
48
16
  }
49
17
 
50
- export function AccordionItem({ value, disabled, className, children }: {
18
+ export type AccordionItemProps = Omit<ComponentProps<typeof AccordionPrimitive.Item>, 'className' | 'value'> & {
51
19
  value: string
52
- disabled?: boolean
53
20
  className?: string
54
- children?: ReactNode
55
- }) {
56
- return (
57
- <AccordionPrimitive.Item value={value} disabled={disabled} className={className}>
58
- {children}
59
- </AccordionPrimitive.Item>
60
- )
21
+ }
22
+
23
+ export function AccordionItem(props: AccordionItemProps) {
24
+ return <AccordionPrimitive.Item {...props} />
61
25
  }
62
26
 
63
27
  const accordionTrigger = tv({
64
- base: 'text-label text-foreground flex h-11 w-full items-center'
28
+ base: 'text-label text-fg flex h-11 w-full items-center'
65
29
  + ' gap-stack border-b border-transparent px-4 transition-state'
66
- + ' press open:border-line enabled:hover:bg-bg-neutral-hover'
67
- + ' disabled:text-foreground-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'
68
32
  + ' focus-visible:outline-2 focus-visible:-outline-offset-2'
69
33
  + ' focus-visible:outline-line-focus-ring',
70
34
  })
71
35
 
72
- 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) {
73
39
  return (
74
40
  <AccordionPrimitive.Header>
75
41
  <AccordionPrimitive.Trigger
42
+ {...rest}
76
43
  className={accordionTrigger({ className })}
77
44
  >
78
45
  <span className="flex min-w-0 grow items-center gap-tight">{children}</span>
79
46
 
80
- <ChevronDown
81
- className="text-foreground-subtle size-4 shrink-0 transition-transform open:rotate-180
47
+ <svg
48
+ viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"
49
+ className="text-fg-subtle size-4 shrink-0 transition-transform open:rotate-180
82
50
  motion-reduce:transition-none"
83
- />
51
+ >
52
+ <path d="m6 9 6 6 6-6" />
53
+ </svg>
84
54
  </AccordionPrimitive.Trigger>
85
55
  </AccordionPrimitive.Header>
86
56
  )
87
57
  }
88
58
 
89
- const accordionContent = tv({ base: 'bg-background-well text-caption text-foreground-muted px-4 py-3' })
59
+ const accordionContent = tv({ base: 'bg-bg-well text-caption text-fg-muted px-4 py-3' })
60
+
61
+ export type AccordionContentProps = Omit<ComponentProps<typeof AccordionPrimitive.Panel>, 'className'> & { className?: string }
90
62
 
91
- export function AccordionContent({ className, children }: { className?: string, children?: ReactNode }) {
63
+ export function AccordionContent({ className, children, ...rest }: AccordionContentProps) {
92
64
  return (
93
- <AccordionPrimitive.Content className={s.panel}>
65
+ <AccordionPrimitive.Panel {...rest} className={s.panel}>
94
66
  <div className={accordionContent({ className })}>
95
67
  {children}
96
68
  </div>
97
- </AccordionPrimitive.Content>
69
+ </AccordionPrimitive.Panel>
98
70
  )
99
71
  }
@@ -1,62 +1,67 @@
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({
29
- base: 'sheet text-foreground z-overlay fixed left-1/2 top-1/2'
22
+ base: 'sheet text-fg z-overlay fixed left-1/2 top-1/2'
30
23
  + ' w-full max-w-112 -translate-x-1/2 -translate-y-1/2'
31
24
  + ' rounded-r20 p-6 flex flex-col gap-stack overflow-y-auto'
32
25
  + ' open:animate-fade-in closed:animate-fade-out'
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-background-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
- const alertDialogDescription = tv({ base: 'text-prose text-foreground-muted' })
58
+ const alertDialogDescription = tv({ base: 'text-prose text-fg-muted' })
59
+
60
+ export type AlertDialogDescriptionProps = Omit<ComponentProps<typeof AlertDialogPrimitive.Description>, 'className'> & {
61
+ className?: string
62
+ }
58
63
 
59
- export function AlertDialogDescription({ className, ...rest }: ComponentProps<typeof AlertDialogPrimitive.Description>) {
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-background-well text-foreground-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',
@@ -16,7 +16,7 @@ export const badge = tv({
16
16
  danger: 'bg-bg-danger-weak text-fg-danger-contrast',
17
17
  },
18
18
  shape: {
19
- square: 'rounded-mark',
19
+ square: 'rounded-r4',
20
20
  pill: 'rounded-full',
21
21
  },
22
22
  },
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-foreground-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-background-well text-foreground'
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-foreground 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-foreground 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
@@ -1,11 +1,10 @@
1
- import { CircleCheck, CircleX, Info, TriangleAlert } from 'lucide-react'
2
1
  import type { ReactNode } from 'react'
3
2
 
4
3
  import { tv } from '../tv'
5
4
 
6
5
  export const callout = tv({
7
6
  slots: {
8
- root: 'flex gap-stack rounded-panel border p-4 text-prose',
7
+ root: 'flex gap-stack rounded-r12 border p-4 text-prose',
9
8
  icon: 'flex shrink-0 mt-0.5 [&_svg]:size-5',
10
9
  title: 'font-bold',
11
10
  body: 'flex min-w-0 flex-1 flex-col gap-tight',
@@ -37,10 +36,33 @@ export const callout = tv({
37
36
  })
38
37
 
39
38
  const ICONS = {
40
- info: Info,
41
- success: CircleCheck,
42
- warning: TriangleAlert,
43
- danger: CircleX,
39
+ info: (
40
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-5">
41
+ <circle cx="12" cy="12" r="10" />
42
+ <path d="M12 16v-4" />
43
+ <path d="M12 8h.01" />
44
+ </svg>
45
+ ),
46
+ success: (
47
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-5">
48
+ <circle cx="12" cy="12" r="10" />
49
+ <path d="m16 9-5.5 5.5L8 12" />
50
+ </svg>
51
+ ),
52
+ warning: (
53
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-5">
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
+ <path d="M12 9v4" />
56
+ <path d="M12 17h.01" />
57
+ </svg>
58
+ ),
59
+ danger: (
60
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-5">
61
+ <circle cx="12" cy="12" r="10" />
62
+ <path d="m15 9-6 6" />
63
+ <path d="m9 9 6 6" />
64
+ </svg>
65
+ ),
44
66
  } as const
45
67
 
46
68
  export interface CalloutProps {
@@ -53,12 +75,11 @@ export interface CalloutProps {
53
75
 
54
76
  export function Callout({ color, icon, title, className, children }: CalloutProps) {
55
77
  const ui = callout({ color })
56
- const Fallback = ICONS[color ?? 'info']
57
78
 
58
79
  return (
59
80
  <div className={ui.root({ className })} role="alert">
60
81
  <span className={ui.icon()}>
61
- {icon ?? <Fallback />}
82
+ {icon ?? ICONS[color ?? 'info']}
62
83
  </span>
63
84
  <div className={ui.body()}>
64
85
  {title && <span className={ui.title()}>{title}</span>}
@@ -1,21 +1,13 @@
1
1
  'use client'
2
2
 
3
- import { Check, Minus } from 'lucide-react'
4
- import { Checkbox as CheckboxPrimitive } from 'radix-ui'
5
- import {
6
- createContext,
7
- use,
8
- useRef,
9
- useState,
10
- type ComponentProps,
11
- type KeyboardEvent as ReactKeyboardEvent,
12
- type ReactNode,
13
- } 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'
14
6
 
15
7
  import { tv } from '../tv'
16
8
 
17
9
  export const checkbox = tv({
18
- base: 'size-5 shrink-0 rounded-mark inline-flex items-center justify-center'
10
+ base: 'size-5 shrink-0 rounded-r4 inline-flex items-center justify-center'
19
11
  + ' bg-transparent ring-1 ring-inset ring-line-strong'
20
12
  + ' transition-state press press-shrink'
21
13
  + ' checked:ring-2'
@@ -37,52 +29,25 @@ export const checkbox = tv({
37
29
  defaultVariants: { color: 'neutral' },
38
30
  })
39
31
 
40
- interface GroupContext {
41
- value: string[]
42
- toggle: (item: string, next: boolean) => void
43
- disabled?: boolean
44
- }
45
-
46
- const Group = createContext<GroupContext | null>(null)
47
-
48
32
  const ITEMS = '[role="checkbox"]:not([disabled])'
49
33
 
50
- export interface CheckboxGroupProps {
51
- value?: string[]
52
- defaultValue?: string[]
53
- onValueChange?: (value: string[]) => void
54
- disabled?: boolean
55
- className?: string
56
- children?: ReactNode
57
- }
34
+ export type CheckboxGroupProps = Omit<ComponentProps<typeof CheckboxGroupPrimitive>, 'className'> & { className?: string }
35
+
36
+ type GroupKeyEvent = Parameters<NonNullable<CheckboxGroupProps['onKeyDown']>>[0]
58
37
 
59
38
  const checkboxGroup = tv({ base: 'flex flex-col gap-3' })
60
39
 
61
- export function CheckboxGroup({
62
- value,
63
- defaultValue,
64
- onValueChange,
65
- disabled,
66
- className,
67
- children,
68
- }: CheckboxGroupProps) {
69
- const [own, setOwn] = useState<string[]>(defaultValue ?? [])
70
- const list = value ?? own
71
- const root = useRef<HTMLDivElement>(null)
72
-
73
- const toggle = (item: string, next: boolean) => {
74
- const after = next ? [...list, item] : list.filter(one => one !== item)
75
- if (value === undefined) setOwn(after)
76
- onValueChange?.(after)
77
- }
40
+ export function CheckboxGroup({ className, onKeyDown, ...rest }: CheckboxGroupProps) {
41
+ const onKeys = (e: GroupKeyEvent) => {
42
+ onKeyDown?.(e)
43
+ if (e.defaultPrevented) return
78
44
 
79
- const onKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {
80
45
  const step = e.key === 'ArrowDown' || e.key === 'ArrowRight'
81
46
  ? 1
82
47
  : e.key === 'ArrowUp' || e.key === 'ArrowLeft' ? -1 : 0
83
- if (step === 0 || !root.current) return
48
+ if (step === 0) return
84
49
 
85
- const items = [...root.current.querySelectorAll<HTMLElement>(ITEMS)]
50
+ const items = [...e.currentTarget.querySelectorAll<HTMLElement>(ITEMS)]
86
51
  const at = items.indexOf(document.activeElement as HTMLElement)
87
52
  if (at === -1) return
88
53
 
@@ -90,63 +55,34 @@ export function CheckboxGroup({
90
55
  items[(at + step + items.length) % items.length]!.focus()
91
56
  }
92
57
 
93
- return (
94
- <Group value={{ value: list, toggle, disabled }}>
95
- <div
96
- ref={root}
97
- role="group"
98
- onKeyDown={onKeyDown}
99
- className={checkboxGroup({ className })}
100
- >
101
- {children}
102
- </div>
103
- </Group>
104
- )
58
+ return <CheckboxGroupPrimitive {...rest} onKeyDown={onKeys} className={checkboxGroup({ className })} />
105
59
  }
106
60
 
107
- export interface CheckboxProps extends Omit<ComponentProps<typeof CheckboxPrimitive.Root>, 'checked' | 'onCheckedChange' | 'value'> {
108
- value?: string
109
- checked?: boolean
110
- onCheckedChange?: (checked: boolean) => void
111
- indeterminate?: boolean
112
- onIndeterminateChange?: (indeterminate: boolean) => void
61
+ export type CheckboxProps = Omit<ComponentProps<typeof CheckboxPrimitive.Root>, 'className'> & {
113
62
  color?: 'neutral' | 'info' | 'success' | 'warning' | 'danger'
63
+ className?: string
114
64
  }
115
65
 
116
66
  export function Checkbox({
117
- value,
118
- checked,
119
- onCheckedChange,
120
67
  indeterminate,
121
- onIndeterminateChange,
122
68
  color,
123
69
  className,
124
- disabled,
70
+ nativeButton = true,
71
+ render = <button type="button" />,
125
72
  ...rest
126
73
  }: CheckboxProps) {
127
- const group = use(Group)
128
- const item = value ?? 'on'
129
-
130
- const on = group ? group.value.includes(item) : checked ?? false
131
- const state = indeterminate ? 'indeterminate' as const : on
132
-
133
- const change = (next: boolean | 'indeterminate') => {
134
- if (typeof next !== 'boolean') return
135
- if (group) group.toggle(item, next)
136
- else onCheckedChange?.(next)
137
- if (indeterminate) onIndeterminateChange?.(false)
138
- }
139
-
140
74
  return (
141
75
  <CheckboxPrimitive.Root
142
76
  {...rest}
143
- checked={state}
144
- disabled={disabled ?? group?.disabled}
145
- onCheckedChange={change}
77
+ nativeButton={nativeButton}
78
+ render={render}
79
+ indeterminate={indeterminate}
146
80
  className={checkbox({ color, className })}
147
81
  >
148
82
  <CheckboxPrimitive.Indicator className="text-fg-neutral-solid-contrast flex">
149
- {indeterminate ? <Minus className="size-4" /> : <Check className="size-4" />}
83
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4">
84
+ {indeterminate ? <path d="M5 12h14" /> : <path d="M20 6 9 17l-5-5" />}
85
+ </svg>
150
86
  </CheckboxPrimitive.Indicator>
151
87
  </CheckboxPrimitive.Root>
152
88
  )