@pantoia/ui 0.3.0 → 0.5.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 (63) hide show
  1. package/README.md +46 -0
  2. package/accordion/index.module.css +4 -4
  3. package/accordion/index.tsx +21 -51
  4. package/alert-dialog/index.tsx +36 -42
  5. package/badge/index.tsx +1 -1
  6. package/button/index.tsx +31 -30
  7. package/callout/index.tsx +4 -4
  8. package/checkbox/index.tsx +20 -85
  9. package/combobox/index.tsx +122 -316
  10. package/command-palette/index.tsx +51 -32
  11. package/dialog/index.tsx +28 -24
  12. package/dist/index.js +5 -5
  13. package/drawer/index.module.css +2 -2
  14. package/drawer/index.tsx +52 -130
  15. package/dropdown-menu/index.tsx +87 -95
  16. package/field/index.tsx +27 -86
  17. package/input/index.tsx +7 -3
  18. package/kbd/index.tsx +2 -2
  19. package/label/index.tsx +6 -8
  20. package/number-field/index.tsx +122 -173
  21. package/package.json +19 -11
  22. package/popover/index.tsx +61 -32
  23. package/radio-group/index.tsx +35 -38
  24. package/registry.json +64 -154
  25. package/select/index.tsx +141 -70
  26. package/switch/index.tsx +13 -6
  27. package/table/index.tsx +1 -2
  28. package/tabs/index.module.css +3 -3
  29. package/tabs/index.tsx +22 -76
  30. package/textarea/index.tsx +19 -7
  31. package/toast/index.module.css +187 -0
  32. package/toast/index.tsx +141 -46
  33. package/toggle/index.tsx +4 -3
  34. package/tokens.css +7 -1
  35. package/tooltip/index.tsx +61 -30
  36. package/utilities.css +11 -6
  37. package/accordion/AGENTS.md +0 -60
  38. package/alert-dialog/AGENTS.md +0 -78
  39. package/badge/AGENTS.md +0 -35
  40. package/button/AGENTS.md +0 -39
  41. package/callout/AGENTS.md +0 -37
  42. package/checkbox/AGENTS.md +0 -54
  43. package/combobox/AGENTS.md +0 -150
  44. package/command-palette/AGENTS.md +0 -41
  45. package/dialog/AGENTS.md +0 -81
  46. package/drawer/AGENTS.md +0 -66
  47. package/dropdown-menu/AGENTS.md +0 -133
  48. package/field/AGENTS.md +0 -92
  49. package/input/AGENTS.md +0 -35
  50. package/kbd/AGENTS.md +0 -17
  51. package/label/AGENTS.md +0 -21
  52. package/number-field/AGENTS.md +0 -49
  53. package/popover/AGENTS.md +0 -64
  54. package/radio-group/AGENTS.md +0 -81
  55. package/select/AGENTS.md +0 -90
  56. package/switch/AGENTS.md +0 -37
  57. package/table/AGENTS.md +0 -82
  58. package/tabs/AGENTS.md +0 -61
  59. package/test-utils.ts +0 -10
  60. package/textarea/AGENTS.md +0 -35
  61. package/toast/AGENTS.md +0 -34
  62. package/toggle/AGENTS.md +0 -37
  63. package/tooltip/AGENTS.md +0 -50
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # Pantoia UI
2
+
3
+ React 디자인 시스템 패키지. 문서는 `design.pantoia.com`
4
+
5
+ ## 사용
6
+
7
+ ```
8
+ npx @pantoia/ui init components.json을 만든다
9
+ npx @pantoia/ui add button components/ui/button/에 소스를 복사한다
10
+ npx @pantoia/ui list 컴포넌트 목록
11
+ npx @pantoia/ui diff 복사한 소스와 배포본의 차이
12
+ ```
13
+
14
+ `init`이 전역 CSS에 아래 줄을 추가한다.
15
+
16
+ ```css
17
+ @import "@pantoia/ui/tailwind.css";
18
+ ```
19
+
20
+ ## 개발
21
+
22
+ 호스트에는 Docker만 있으면 된다.
23
+
24
+ ```
25
+ git clone git@github.com:pantoia/pantoia-ui.git
26
+ cd pantoia-ui
27
+ docker compose up -d dev
28
+ ```
29
+
30
+ 편집기는 `.devcontainer/devcontainer.json`으로 컨테이너 안에서 연다.
31
+
32
+ ```
33
+ docker compose up -d dev 실행
34
+ docker compose down 종료
35
+ docker compose exec dev npm run verify typecheck, check, lint, test
36
+ docker compose exec dev npm pack tarball
37
+ ```
38
+
39
+ ## 구조
40
+
41
+ ```
42
+ <이름>/ 컴포넌트
43
+ cli/ CLI
44
+ docs/decisions/ 결정 기록
45
+ .claude/rules/ 일하는 규칙
46
+ ```
@@ -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">