@pantoia/ui 0.1.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 (67) hide show
  1. package/accordion/AGENTS.md +60 -0
  2. package/accordion/index.module.css +10 -0
  3. package/accordion/index.tsx +91 -0
  4. package/alert-dialog/AGENTS.md +78 -0
  5. package/alert-dialog/index.tsx +119 -0
  6. package/badge/AGENTS.md +35 -0
  7. package/badge/index.tsx +34 -0
  8. package/button/AGENTS.md +39 -0
  9. package/button/index.tsx +98 -0
  10. package/callout/AGENTS.md +42 -0
  11. package/callout/index.tsx +70 -0
  12. package/checkbox/AGENTS.md +54 -0
  13. package/checkbox/index.tsx +151 -0
  14. package/code-block/AGENTS.md +60 -0
  15. package/code-block/index.module.css +46 -0
  16. package/code-block/index.tsx +127 -0
  17. package/combobox/AGENTS.md +150 -0
  18. package/combobox/index.tsx +420 -0
  19. package/command-palette/AGENTS.md +44 -0
  20. package/command-palette/index.tsx +108 -0
  21. package/copy-button/AGENTS.md +43 -0
  22. package/copy-button/index.tsx +119 -0
  23. package/dialog/AGENTS.md +81 -0
  24. package/dialog/index.tsx +108 -0
  25. package/dist/index.js +189 -0
  26. package/drawer/AGENTS.md +66 -0
  27. package/drawer/index.module.css +17 -0
  28. package/drawer/index.tsx +198 -0
  29. package/dropdown-menu/AGENTS.md +133 -0
  30. package/dropdown-menu/index.tsx +223 -0
  31. package/field/AGENTS.md +96 -0
  32. package/field/index.tsx +149 -0
  33. package/input/AGENTS.md +35 -0
  34. package/input/index.tsx +36 -0
  35. package/kbd/AGENTS.md +17 -0
  36. package/kbd/index.tsx +13 -0
  37. package/label/AGENTS.md +21 -0
  38. package/label/index.tsx +13 -0
  39. package/number-field/AGENTS.md +53 -0
  40. package/number-field/index.tsx +227 -0
  41. package/package.json +73 -0
  42. package/popover/AGENTS.md +64 -0
  43. package/popover/index.tsx +66 -0
  44. package/press.css +37 -0
  45. package/radio-group/AGENTS.md +81 -0
  46. package/radio-group/index.tsx +139 -0
  47. package/registry.json +726 -0
  48. package/select/AGENTS.md +90 -0
  49. package/select/index.tsx +169 -0
  50. package/styles.css +347 -0
  51. package/switch/AGENTS.md +37 -0
  52. package/switch/index.tsx +42 -0
  53. package/table/AGENTS.md +82 -0
  54. package/table/index.tsx +120 -0
  55. package/tabs/AGENTS.md +60 -0
  56. package/tabs/index.module.css +14 -0
  57. package/tabs/index.tsx +105 -0
  58. package/test-utils.ts +10 -0
  59. package/textarea/AGENTS.md +35 -0
  60. package/textarea/index.tsx +37 -0
  61. package/toast/AGENTS.md +42 -0
  62. package/toast/index.tsx +71 -0
  63. package/toggle/AGENTS.md +37 -0
  64. package/toggle/index.tsx +36 -0
  65. package/tooltip/AGENTS.md +50 -0
  66. package/tooltip/index.tsx +57 -0
  67. package/tv.ts +23 -0
@@ -0,0 +1,60 @@
1
+ # Accordion
2
+
3
+ ```
4
+ 받기 npx @pantoia/ui add accordion
5
+ 층 ui
6
+ 파일 accordion/index.tsx · accordion/index.module.css · accordion/AGENTS.md
7
+ ```
8
+
9
+ ## 부위
10
+
11
+ | 이름 | props |
12
+ |---|---|
13
+ | `Accordion` | `AccordionProps` |
14
+ | `AccordionItem` | — |
15
+ | `AccordionTrigger` | — |
16
+ | `AccordionContent` | — |
17
+
18
+ ### `AccordionItem`
19
+
20
+ | 이름 | 타입 | 필수 |
21
+ |---|---|---|
22
+ | `value` | `string` | 예 |
23
+ | `disabled` | `boolean` | |
24
+ | `className` | `string` | |
25
+ | `children` | `ReactNode` | |
26
+
27
+ ### `AccordionTrigger`
28
+
29
+ | 이름 | 타입 | 필수 |
30
+ |---|---|---|
31
+ | `className` | `string` | |
32
+ | `children` | `ReactNode` | |
33
+
34
+ ### `AccordionContent`
35
+
36
+ | 이름 | 타입 | 필수 |
37
+ |---|---|---|
38
+ | `className` | `string` | |
39
+ | `children` | `ReactNode` | |
40
+
41
+ ## props
42
+
43
+ ### `AccordionProps`
44
+
45
+ | 이름 | 타입 | 필수 |
46
+ |---|---|---|
47
+ | `value` | `string \| string[]` | |
48
+ | `defaultValue` | `string \| string[]` | |
49
+ | `onValueChange` | `(value: string & string[]) => void` | |
50
+ | `multiple` | `boolean` | |
51
+ | `className` | `string` | |
52
+ | `children` | `ReactNode` | |
53
+
54
+ ## 함께 받는 것
55
+
56
+ `tv`
57
+
58
+ ## npm 의존
59
+
60
+ `lucide-react` · `radix-ui`
@@ -0,0 +1,10 @@
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; } }
3
+
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); }
7
+
8
+ @media (prefers-reduced-motion: reduce) {
9
+ .panel { animation: none; }
10
+ }
@@ -0,0 +1,91 @@
1
+ 'use client'
2
+
3
+ import { ChevronDown } from 'lucide-react'
4
+ import { Accordion as AccordionPrimitive } from 'radix-ui'
5
+ import type { ReactNode } from 'react'
6
+
7
+ import { tv } from '../tv'
8
+
9
+ import s from './index.module.css'
10
+
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
+ export function Accordion({ value, defaultValue, onValueChange, multiple, className, children }: AccordionProps) {
21
+ const shared = {
22
+ className: tv({ base: 'rounded-panel border border-line overflow-hidden divide-y divide-line' })({ className }),
23
+ children,
24
+ }
25
+
26
+ return multiple
27
+ ? (
28
+ <AccordionPrimitive.Root
29
+ type="multiple"
30
+ value={value as string[] | undefined}
31
+ defaultValue={defaultValue as string[] | undefined}
32
+ onValueChange={onValueChange}
33
+ {...shared}
34
+ />
35
+ )
36
+ : (
37
+ <AccordionPrimitive.Root
38
+ type="single"
39
+ collapsible
40
+ value={value as string | undefined}
41
+ defaultValue={defaultValue as string | undefined}
42
+ onValueChange={onValueChange}
43
+ {...shared}
44
+ />
45
+ )
46
+ }
47
+
48
+ export function AccordionItem({ value, disabled, className, children }: {
49
+ value: string
50
+ disabled?: boolean
51
+ className?: string
52
+ children?: ReactNode
53
+ }) {
54
+ return (
55
+ <AccordionPrimitive.Item value={value} disabled={disabled} className={className}>
56
+ {children}
57
+ </AccordionPrimitive.Item>
58
+ )
59
+ }
60
+
61
+ export function AccordionTrigger({ className, children }: { className?: string, children?: ReactNode }) {
62
+ return (
63
+ <AccordionPrimitive.Header>
64
+ <AccordionPrimitive.Trigger
65
+ className={tv({ base: 'text-label text-fg flex h-11 w-full items-center'
66
+ + ' gap-stack border-b border-transparent px-4 transition-state'
67
+ + ' press open:border-line enabled:hover:bg-hover'
68
+ + ' disabled:text-fg-disabled disabled:cursor-not-allowed'
69
+ + ' focus-visible:outline-2 focus-visible:-outline-offset-2'
70
+ + ' focus-visible:outline-ring' })({ className })}
71
+ >
72
+ <span className="flex min-w-0 grow items-center gap-tight">{children}</span>
73
+
74
+ <ChevronDown
75
+ className="text-fg-subtle size-4 shrink-0 transition-transform open:rotate-180
76
+ motion-reduce:transition-none"
77
+ />
78
+ </AccordionPrimitive.Trigger>
79
+ </AccordionPrimitive.Header>
80
+ )
81
+ }
82
+
83
+ export function AccordionContent({ className, children }: { className?: string, children?: ReactNode }) {
84
+ return (
85
+ <AccordionPrimitive.Content className={s.panel}>
86
+ <div className={tv({ base: 'bg-well text-caption text-fg-muted px-4 py-3' })({ className })}>
87
+ {children}
88
+ </div>
89
+ </AccordionPrimitive.Content>
90
+ )
91
+ }
@@ -0,0 +1,78 @@
1
+ # AlertDialog
2
+
3
+ ```
4
+ 받기 npx @pantoia/ui add alert-dialog
5
+ 층 ui
6
+ 파일 alert-dialog/index.tsx · alert-dialog/AGENTS.md
7
+ ```
8
+
9
+ ## 부위
10
+
11
+ | 이름 | props |
12
+ |---|---|
13
+ | `AlertDialog` | `AlertDialogProps` |
14
+ | `AlertDialogTrigger` | — |
15
+ | `AlertDialogContent` | `ComponentProps<typeof AlertDialogPrimitive.Content>` |
16
+ | `AlertDialogTitle` | `ComponentProps<typeof AlertDialogPrimitive.Title>` |
17
+ | `AlertDialogDescription` | `ComponentProps<typeof AlertDialogPrimitive.Description>` |
18
+ | `AlertDialogHeader` | `AlertDialogHeaderProps` |
19
+ | `AlertDialogFooter` | — |
20
+ | `AlertDialogAction` | — |
21
+ | `AlertDialogCancel` | — |
22
+
23
+ ### `AlertDialogTrigger`
24
+
25
+ | 이름 | 타입 | 필수 |
26
+ |---|---|---|
27
+ | `children` | `ReactNode` | 예 |
28
+
29
+ ### `AlertDialogFooter`
30
+
31
+ | 이름 | 타입 | 필수 |
32
+ |---|---|---|
33
+ | `className` | `string` | |
34
+ | `children` | `ReactNode` | |
35
+
36
+ ### `AlertDialogAction`
37
+
38
+ | 이름 | 타입 | 필수 |
39
+ |---|---|---|
40
+ | `color` | `'neutral' \| 'primary' \| 'danger'` | |
41
+ | `onClick` | `() => void` | |
42
+ | `className` | `string` | |
43
+ | `children` | `ReactNode` | |
44
+
45
+ ### `AlertDialogCancel`
46
+
47
+ | 이름 | 타입 | 필수 |
48
+ |---|---|---|
49
+ | `className` | `string` | |
50
+ | `children` | `ReactNode` | |
51
+
52
+ ## props
53
+
54
+ ### `AlertDialogProps`
55
+
56
+ | 이름 | 타입 | 필수 |
57
+ |---|---|---|
58
+ | `open` | `boolean` | |
59
+ | `defaultOpen` | `boolean` | |
60
+ | `onOpenChange` | `(open: boolean) => void` | |
61
+ | `children` | `ReactNode` | |
62
+
63
+ ### `AlertDialogHeaderProps`
64
+
65
+ | 이름 | 타입 | 필수 |
66
+ |---|---|---|
67
+ | `lead` | `ReactNode` | |
68
+ | `trail` | `ReactNode` | |
69
+ | `className` | `string` | |
70
+ | `children` | `ReactNode` | |
71
+
72
+ ## 함께 받는 것
73
+
74
+ `button` · `tv`
75
+
76
+ ## npm 의존
77
+
78
+ `radix-ui`
@@ -0,0 +1,119 @@
1
+ 'use client'
2
+
3
+ import { AlertDialog as AlertDialogPrimitive } from 'radix-ui'
4
+ import type { ComponentProps, ReactNode } from 'react'
5
+
6
+ import { Button } from '../button'
7
+ import { tv } from '../tv'
8
+
9
+ export interface AlertDialogProps {
10
+ open?: boolean
11
+ defaultOpen?: boolean
12
+ onOpenChange?: (open: boolean) => void
13
+ children?: ReactNode
14
+ }
15
+
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
+ )
22
+ }
23
+
24
+ export function AlertDialogTrigger({ children }: { children: ReactNode }) {
25
+ return <AlertDialogPrimitive.Trigger asChild>{children}</AlertDialogPrimitive.Trigger>
26
+ }
27
+
28
+ export function AlertDialogContent({ className, children, ...rest }: ComponentProps<typeof AlertDialogPrimitive.Content>) {
29
+ return (
30
+ <AlertDialogPrimitive.Portal>
31
+ <AlertDialogPrimitive.Overlay className="bg-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
32
+
33
+ <AlertDialogPrimitive.Content
34
+ {...rest}
35
+ className={tv({ base: 'bg-float text-fg z-overlay fixed left-1/2 top-1/2'
36
+ + ' w-full max-w-112 -translate-x-1/2 -translate-y-1/2'
37
+ + ' rounded-r20 p-6 flex flex-col gap-stack overflow-y-auto'
38
+ + ' open:animate-fade-in closed:animate-fade-out'
39
+ + ' focus-visible:outline-none' })({ className })}
40
+ >
41
+ {children}
42
+ </AlertDialogPrimitive.Content>
43
+ </AlertDialogPrimitive.Portal>
44
+ )
45
+ }
46
+
47
+ export function AlertDialogTitle({ className, ...rest }: ComponentProps<typeof AlertDialogPrimitive.Title>) {
48
+ return <AlertDialogPrimitive.Title {...rest} className={tv({ base: 'text-section' })({ className })} />
49
+ }
50
+
51
+ export function AlertDialogDescription({ className, ...rest }: ComponentProps<typeof AlertDialogPrimitive.Description>) {
52
+ return (
53
+ <AlertDialogPrimitive.Description
54
+ {...rest}
55
+ className={tv({ base: 'text-prose text-fg-muted' })({ className })}
56
+ />
57
+ )
58
+ }
59
+
60
+ export const alertDialogHeader = tv({
61
+ slots: {
62
+ root: 'flex items-start gap-stack',
63
+ lead: 'shrink-0',
64
+ body: 'flex min-w-0 grow flex-col gap-tight',
65
+ trail: 'shrink-0',
66
+ },
67
+ })
68
+
69
+ export interface AlertDialogHeaderProps {
70
+ lead?: ReactNode
71
+ trail?: ReactNode
72
+ className?: string
73
+ children?: ReactNode
74
+ }
75
+
76
+ export function AlertDialogHeader({ lead, trail, className, children }: AlertDialogHeaderProps) {
77
+ const ui = alertDialogHeader()
78
+
79
+ return (
80
+ <div className={ui.root({ className })}>
81
+ {lead && <div className={ui.lead()}>{lead}</div>}
82
+ <div className={ui.body()}>{children}</div>
83
+ {trail && <div className={ui.trail()}>{trail}</div>}
84
+ </div>
85
+ )
86
+ }
87
+
88
+ export function AlertDialogFooter({ className, children }: { className?: string, children?: ReactNode }) {
89
+ return (
90
+ <div className={tv({ base: 'flex justify-end gap-2' })({ className })}>
91
+ {children}
92
+ </div>
93
+ )
94
+ }
95
+
96
+ export function AlertDialogAction({ color = 'neutral', onClick, className, children }: {
97
+ color?: 'neutral' | 'primary' | 'danger'
98
+ onClick?: () => void
99
+ className?: string
100
+ children?: ReactNode
101
+ }) {
102
+ return (
103
+ <AlertDialogPrimitive.Action asChild>
104
+ <Button emphasis="solid" color={color} onClick={onClick} className={className}>
105
+ {children}
106
+ </Button>
107
+ </AlertDialogPrimitive.Action>
108
+ )
109
+ }
110
+
111
+ export function AlertDialogCancel({ className, children }: { className?: string, children?: ReactNode }) {
112
+ return (
113
+ <AlertDialogPrimitive.Cancel asChild>
114
+ <Button color="neutral" emphasis="outline" className={className}>
115
+ {children}
116
+ </Button>
117
+ </AlertDialogPrimitive.Cancel>
118
+ )
119
+ }
@@ -0,0 +1,35 @@
1
+ # Badge
2
+
3
+ ```
4
+ 받기 npx @pantoia/ui add badge
5
+ 층 ui
6
+ 파일 badge/index.tsx · badge/AGENTS.md
7
+ ```
8
+
9
+ ## 부위
10
+
11
+ | 이름 | props |
12
+ |---|---|
13
+ | `Badge` | `BadgeProps` |
14
+
15
+ ## props
16
+
17
+ ### `BadgeProps`
18
+
19
+ `Omit<HTMLAttributes<HTMLSpanElement>, 'color'>` 를 확장한다.
20
+
21
+ | 이름 | 타입 | 필수 |
22
+ |---|---|---|
23
+ | `color` | `'neutral' \| 'primary' \| 'info' \| 'success' \| 'warning' \| 'danger'` | |
24
+ | `shape` | `'square' \| 'pill'` | |
25
+
26
+ ## 변형 — `badge`
27
+
28
+ | 축 | 값 | 기본 |
29
+ |---|---|---|
30
+ | `color` | `neutral` · `primary` · `info` · `success` · `warning` · `danger` | `neutral` |
31
+ | `shape` | `square` · `pill` | `square` |
32
+
33
+ ## 함께 받는 것
34
+
35
+ `tv`
@@ -0,0 +1,34 @@
1
+ import type { HTMLAttributes } from 'react'
2
+
3
+ import { tv } from '../tv'
4
+
5
+ export const badge = tv({
6
+ base: 'inline-flex items-center gap-tight px-2 py-0.5 text-micro'
7
+ + ' [&_svg]:size-3',
8
+
9
+ variants: {
10
+ color: {
11
+ neutral: 'bg-well text-fg-muted',
12
+ primary: 'bg-primary-weak text-primary-on-weak',
13
+ info: 'bg-info-weak text-info-on-weak',
14
+ success: 'bg-success-weak text-success-on-weak',
15
+ warning: 'bg-warning-weak text-warning-on-weak',
16
+ danger: 'bg-danger-weak text-danger-on-weak',
17
+ },
18
+ shape: {
19
+ square: 'rounded-mark',
20
+ pill: 'rounded-full',
21
+ },
22
+ },
23
+
24
+ defaultVariants: { color: 'neutral', shape: 'square' },
25
+ })
26
+
27
+ export interface BadgeProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'color'> {
28
+ color?: 'neutral' | 'primary' | 'info' | 'success' | 'warning' | 'danger'
29
+ shape?: 'square' | 'pill'
30
+ }
31
+
32
+ export function Badge({ color, shape, className, ...rest }: BadgeProps) {
33
+ return <span {...rest} className={badge({ color, shape, className })} />
34
+ }
@@ -0,0 +1,39 @@
1
+ # Button
2
+
3
+ ```
4
+ 받기 npx @pantoia/ui add button
5
+ 층 ui
6
+ 파일 button/index.tsx · button/AGENTS.md
7
+ ```
8
+
9
+ ## 부위
10
+
11
+ | 이름 | props |
12
+ |---|---|
13
+ | `Button` | `ButtonProps` |
14
+
15
+ ## props
16
+
17
+ ### `ButtonProps`
18
+
19
+ `Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'color'>` 를 확장한다.
20
+
21
+ | 이름 | 타입 | 필수 |
22
+ |---|---|---|
23
+ | `color` | `'primary' \| 'neutral' \| 'danger'` | 예 |
24
+ | `emphasis` | `'solid' \| 'weak' \| 'outline' \| 'ghost'` | 예 |
25
+ | `size` | `'sm' \| 'md' \| 'lg'` | |
26
+ | `shape` | `'rect' \| 'square' \| 'pill' \| 'circle'` | |
27
+
28
+ ## 변형 — `button`
29
+
30
+ | 축 | 값 | 기본 |
31
+ |---|---|---|
32
+ | `color` | `primary` · `neutral` · `danger` | — |
33
+ | `emphasis` | `solid` · `weak` · `outline` · `ghost` | — |
34
+ | `size` | `sm` · `md` · `lg` | `md` |
35
+ | `shape` | `rect` · `square` · `pill` · `circle` | `rect` |
36
+
37
+ ## 함께 받는 것
38
+
39
+ `tv`
@@ -0,0 +1,98 @@
1
+ import type { ButtonHTMLAttributes } from 'react'
2
+
3
+ import { tv } from '../tv'
4
+
5
+ export const button = tv({
6
+ base: 'inline-flex shrink-0 items-center justify-center gap-tight whitespace-nowrap'
7
+ + ' rounded-r8 text-label'
8
+ + ' transition-state press press-shrink'
9
+ + ' focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
10
+ + ' disabled:text-fg-disabled disabled:cursor-not-allowed',
11
+
12
+ variants: {
13
+ color: { primary: '', neutral: '', danger: '' },
14
+ emphasis: { solid: '', weak: '', outline: '', ghost: '' },
15
+ size: {
16
+ sm: 'h-8 [&_svg]:size-4',
17
+ md: 'h-9 [&_svg]:size-4',
18
+ lg: 'h-10 text-t16 font-medium [&_svg]:size-5',
19
+ },
20
+ shape: {
21
+ rect: '',
22
+ square: '',
23
+ pill: 'rounded-full',
24
+ circle: 'rounded-full',
25
+ },
26
+ },
27
+
28
+ compoundVariants: [
29
+ { color: 'primary', emphasis: 'solid',
30
+ class: 'bg-primary-solid text-fg-on-solid'
31
+ + ' enabled:hover:bg-primary-solid-hover'
32
+ + ' disabled:bg-primary-solid/20' },
33
+ { color: 'primary', emphasis: 'weak',
34
+ class: 'bg-primary-weak text-primary-on-weak disabled:text-primary-on-weak/40'
35
+ + ' enabled:hover:bg-primary-solid/10' },
36
+ { color: 'primary', emphasis: 'outline',
37
+ class: 'text-primary-on-weak disabled:text-primary-on-weak/40'
38
+ + ' ring-1 ring-inset ring-primary-stroke disabled:ring-line'
39
+ + ' enabled:hover:bg-primary-solid/10' },
40
+ { color: 'primary', emphasis: 'ghost',
41
+ class: 'text-primary-on-weak disabled:text-primary-on-weak/40'
42
+ + ' enabled:hover:bg-primary-solid/10' },
43
+
44
+ { color: 'neutral', emphasis: 'solid',
45
+ class: 'bg-neutral-solid text-fg-on-control'
46
+ + ' enabled:hover:bg-neutral-solid-hover'
47
+ + ' disabled:bg-neutral-solid/20' },
48
+ { color: 'neutral', emphasis: 'weak',
49
+ class: 'bg-well text-fg'
50
+ + ' enabled:hover:bg-fg-muted/10' },
51
+ { color: 'neutral', emphasis: 'outline',
52
+ class: 'text-fg ring-1 ring-inset ring-line-strong disabled:ring-line'
53
+ + ' enabled:hover:bg-hover' },
54
+ { color: 'neutral', emphasis: 'ghost',
55
+ class: 'text-fg enabled:hover:bg-hover' },
56
+
57
+ { color: 'danger', emphasis: 'solid',
58
+ class: 'bg-danger-solid text-fg-on-solid'
59
+ + ' enabled:hover:bg-danger-solid-hover'
60
+ + ' disabled:bg-danger-solid/20' },
61
+ { color: 'danger', emphasis: 'weak',
62
+ class: 'bg-danger-weak text-danger-on-weak disabled:text-danger-on-weak/40'
63
+ + ' enabled:hover:bg-danger/10' },
64
+ { color: 'danger', emphasis: 'outline',
65
+ class: 'text-danger-on-weak disabled:text-danger-on-weak/40'
66
+ + ' ring-1 ring-inset ring-danger-stroke disabled:ring-line'
67
+ + ' enabled:hover:bg-danger/10' },
68
+ { color: 'danger', emphasis: 'ghost',
69
+ class: 'text-danger-on-weak disabled:text-danger-on-weak/40'
70
+ + ' enabled:hover:bg-danger/10' },
71
+
72
+ { shape: ['rect', 'pill'], size: 'sm', class: 'px-3' },
73
+ { shape: ['rect', 'pill'], size: 'md', class: 'px-3.5' },
74
+ { shape: ['rect', 'pill'], size: 'lg', class: 'px-4' },
75
+ { shape: ['square', 'circle'], size: 'sm', class: 'w-8' },
76
+ { shape: ['square', 'circle'], size: 'md', class: 'w-9' },
77
+ { shape: ['square', 'circle'], size: 'lg', class: 'w-10' },
78
+ ],
79
+
80
+ defaultVariants: { size: 'md', shape: 'rect' },
81
+ })
82
+
83
+ export interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'color'> {
84
+ color: 'primary' | 'neutral' | 'danger'
85
+ emphasis: 'solid' | 'weak' | 'outline' | 'ghost'
86
+ size?: 'sm' | 'md' | 'lg'
87
+ shape?: 'rect' | 'square' | 'pill' | 'circle'
88
+ }
89
+
90
+ export function Button({ color, emphasis, size, shape, className, ...rest }: ButtonProps) {
91
+ return (
92
+ <button
93
+ type="button"
94
+ {...rest}
95
+ className={button({ color, emphasis, size, shape, className })}
96
+ />
97
+ )
98
+ }
@@ -0,0 +1,42 @@
1
+ # Callout
2
+
3
+ ```
4
+ 받기 npx @pantoia/ui add callout
5
+ 층 ui
6
+ 파일 callout/index.tsx · callout/AGENTS.md
7
+ ```
8
+
9
+ ## 부위
10
+
11
+ | 이름 | props |
12
+ |---|---|
13
+ | `Callout` | `CalloutProps` |
14
+
15
+ ## props
16
+
17
+ ### `CalloutProps`
18
+
19
+ | 이름 | 타입 | 필수 |
20
+ |---|---|---|
21
+ | `color` | `'info' \| 'success' \| 'warning' \| 'danger'` | |
22
+ | `live` | `boolean` | |
23
+ | `icon` | `ReactNode` | |
24
+ | `title` | `ReactNode` | |
25
+ | `className` | `string` | |
26
+ | `children` | `ReactNode` | |
27
+
28
+ ## 변형 — `callout`
29
+
30
+ 슬롯 `root` · `icon` · `title` · `body` · `text`
31
+
32
+ | 축 | 값 | 기본 |
33
+ |---|---|---|
34
+ | `color` | `info` · `success` · `warning` · `danger` | `info` |
35
+
36
+ ## 함께 받는 것
37
+
38
+ `tv`
39
+
40
+ ## npm 의존
41
+
42
+ `lucide-react`
@@ -0,0 +1,70 @@
1
+ import { CircleCheck, CircleX, Info, TriangleAlert } from 'lucide-react'
2
+ import type { ReactNode } from 'react'
3
+
4
+ import { tv } from '../tv'
5
+
6
+ export const callout = tv({
7
+ slots: {
8
+ root: 'flex gap-stack rounded-panel border p-4 text-prose',
9
+ icon: 'flex shrink-0 mt-0.5 [&_svg]:size-5',
10
+ title: 'font-bold',
11
+ body: 'flex min-w-0 flex-col gap-tight',
12
+ text: 'flex flex-col gap-2',
13
+ },
14
+
15
+ variants: {
16
+ color: {
17
+ info: {
18
+ root: 'bg-info-weak text-info-on-weak border-info-stroke',
19
+ icon: 'text-info',
20
+ },
21
+ success: {
22
+ root: 'bg-success-weak text-success-on-weak border-success-stroke',
23
+ icon: 'text-success',
24
+ },
25
+ warning: {
26
+ root: 'bg-warning-weak text-warning-on-weak border-warning-stroke',
27
+ icon: 'text-warning',
28
+ },
29
+ danger: {
30
+ root: 'bg-danger-weak text-danger-on-weak border-danger-stroke',
31
+ icon: 'text-danger',
32
+ },
33
+ },
34
+ },
35
+
36
+ defaultVariants: { color: 'info' },
37
+ })
38
+
39
+ const ICONS = {
40
+ info: Info,
41
+ success: CircleCheck,
42
+ warning: TriangleAlert,
43
+ danger: CircleX,
44
+ } as const
45
+
46
+ export interface CalloutProps {
47
+ color?: 'info' | 'success' | 'warning' | 'danger'
48
+ live?: boolean
49
+ icon?: ReactNode
50
+ title?: ReactNode
51
+ className?: string
52
+ children?: ReactNode
53
+ }
54
+
55
+ export function Callout({ color, live, icon, title, className, children }: CalloutProps) {
56
+ const ui = callout({ color })
57
+ const Fallback = ICONS[color ?? 'info']
58
+
59
+ return (
60
+ <div className={ui.root({ className })} role={live ? 'alert' : 'note'}>
61
+ <span className={ui.icon()}>
62
+ {icon ?? <Fallback />}
63
+ </span>
64
+ <div className={ui.body()}>
65
+ {title && <span className={ui.title()}>{title}</span>}
66
+ <div className={ui.text()}>{children}</div>
67
+ </div>
68
+ </div>
69
+ )
70
+ }