@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,198 @@
1
+ 'use client'
2
+
3
+ import { Dialog } from 'radix-ui'
4
+ import {
5
+ createContext,
6
+ use,
7
+ useRef,
8
+ useState,
9
+ type ComponentProps,
10
+ type PointerEvent as ReactPointerEvent,
11
+ type ReactNode,
12
+ type RefObject,
13
+ } from 'react'
14
+
15
+ import { tv } from '../tv'
16
+
17
+ import s from './index.module.css'
18
+
19
+ type Side = 'left' | 'right'
20
+
21
+ const DrawerContext = createContext<{ side: Side, close: () => void }>({
22
+ side: 'left',
23
+ close: () => {},
24
+ })
25
+
26
+ const SWIPE_THRESHOLD = 40
27
+ const SWIPE_VELOCITY = 0.3
28
+ const MIN_DRAG = 1
29
+ const IGNORE = 'button,a,input,select,textarea,label,[role="button"]'
30
+
31
+ const MOVEMENT = '--drawer-swipe-movement-x'
32
+
33
+ type Drag = { id: number, from: number, at: number, dx: number, v: number, on: boolean }
34
+
35
+ function useSwipeDismiss(el: RefObject<HTMLDivElement | null>, side: Side, close: () => void) {
36
+ const drag = useRef<Drag | null>(null)
37
+
38
+ const onPointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {
39
+ if (e.button !== 0) return
40
+ if ((e.target as Element).closest(IGNORE)) return
41
+
42
+ drag.current = { id: e.pointerId, from: e.clientX, at: performance.now(), dx: 0, v: 0, on: false }
43
+ }
44
+
45
+ const onPointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {
46
+ const d = drag.current
47
+ if (!d || d.id !== e.pointerId || !el.current) return
48
+
49
+ const raw = e.clientX - d.from
50
+ const dx = side === 'left' ? Math.min(0, raw) : Math.max(0, raw)
51
+ const now = performance.now()
52
+ const span = now - d.at
53
+
54
+ if (span > 0) d.v = (dx - d.dx) / span
55
+ d.dx = dx
56
+ d.at = now
57
+
58
+ if (!d.on && Math.abs(dx) >= MIN_DRAG) {
59
+ d.on = true
60
+ el.current.setAttribute('data-swiping', '')
61
+ el.current.setPointerCapture(e.pointerId)
62
+ }
63
+
64
+ if (d.on) el.current.style.setProperty(MOVEMENT, `${dx}px`)
65
+ }
66
+
67
+ const onPointerUp = (e: ReactPointerEvent<HTMLDivElement>) => {
68
+ const d = drag.current
69
+ if (!d || d.id !== e.pointerId) return
70
+ drag.current = null
71
+
72
+ if (!d.on) return
73
+ el.current?.removeAttribute('data-swiping')
74
+
75
+ const gone = side === 'left' ? -d.dx : d.dx
76
+ const speed = side === 'left' ? -d.v : d.v
77
+
78
+ if (gone >= SWIPE_THRESHOLD || speed > SWIPE_VELOCITY) close()
79
+ else el.current?.style.removeProperty(MOVEMENT)
80
+ }
81
+
82
+ return { onPointerDown, onPointerMove, onPointerUp, onPointerCancel: onPointerUp }
83
+ }
84
+
85
+ export interface DrawerProps {
86
+ side?: Side
87
+ open?: boolean
88
+ defaultOpen?: boolean
89
+ onOpenChange?: (open: boolean) => void
90
+ children: ReactNode
91
+ }
92
+
93
+ export function Drawer({ side = 'left', open, defaultOpen = false, onOpenChange, children }: DrawerProps) {
94
+ const [own, setOwn] = useState(defaultOpen)
95
+ const isOpen = open ?? own
96
+
97
+ const set = (next: boolean) => {
98
+ if (open === undefined) setOwn(next)
99
+ onOpenChange?.(next)
100
+ }
101
+
102
+ return (
103
+ <DrawerContext value={{ side, close: () => set(false) }}>
104
+ <Dialog.Root open={isOpen} onOpenChange={set} modal>
105
+ {children}
106
+ </Dialog.Root>
107
+ </DrawerContext>
108
+ )
109
+ }
110
+
111
+ export function DrawerTrigger({ children }: { children: ReactNode }) {
112
+ return <Dialog.Trigger asChild>{children}</Dialog.Trigger>
113
+ }
114
+
115
+ export function DrawerClose({ children }: { children: ReactNode }) {
116
+ return <Dialog.Close asChild>{children}</Dialog.Close>
117
+ }
118
+
119
+ export const drawerContent = tv({
120
+ base: 'bg-float text-fg z-overlay fixed inset-y-0'
121
+ + ' flex w-full flex-col gap-stack overflow-y-auto p-4 sm:w-120'
122
+ + ' focus-visible:outline-none',
123
+ variants: {
124
+ side: { left: 'left-0', right: 'right-0' },
125
+ },
126
+ })
127
+
128
+ export function DrawerContent({
129
+ className,
130
+ style,
131
+ children,
132
+ ...rest
133
+ }: ComponentProps<typeof Dialog.Content>) {
134
+ const { side, close } = use(DrawerContext)
135
+ const el = useRef<HTMLDivElement>(null)
136
+ const swipe = useSwipeDismiss(el, side, close)
137
+
138
+ return (
139
+ <Dialog.Portal>
140
+ <Dialog.Overlay className="bg-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
141
+
142
+ <Dialog.Content
143
+ {...rest}
144
+ ref={el}
145
+ style={{ touchAction: 'pan-y', ...style }}
146
+ onPointerDown={swipe.onPointerDown}
147
+ onPointerMove={swipe.onPointerMove}
148
+ onPointerUp={swipe.onPointerUp}
149
+ onPointerCancel={swipe.onPointerCancel}
150
+ className={[
151
+ s.panel,
152
+ side === 'right' ? s['panel-right'] : '',
153
+ drawerContent({ side, className }),
154
+ ].filter(Boolean).join(' ')}
155
+ >
156
+ {children}
157
+ </Dialog.Content>
158
+ </Dialog.Portal>
159
+ )
160
+ }
161
+
162
+ export const drawerHeader = tv({
163
+ slots: {
164
+ root: 'flex items-start gap-stack',
165
+ lead: 'shrink-0',
166
+ body: 'flex min-w-0 grow flex-col gap-tight',
167
+ trail: 'shrink-0',
168
+ },
169
+ })
170
+
171
+ export interface DrawerHeaderProps {
172
+ lead?: ReactNode
173
+ trail?: ReactNode
174
+ className?: string
175
+ children?: ReactNode
176
+ }
177
+
178
+ export function DrawerHeader({ lead, trail, className, children }: DrawerHeaderProps) {
179
+ const ui = drawerHeader()
180
+
181
+ return (
182
+ <div className={ui.root({ className })}>
183
+ {lead && <div className={ui.lead()}>{lead}</div>}
184
+ <div className={ui.body()}>{children}</div>
185
+ {trail && <div className={ui.trail()}>{trail}</div>}
186
+ </div>
187
+ )
188
+ }
189
+
190
+ export function DrawerTitle({ className, ...rest }: ComponentProps<typeof Dialog.Title>) {
191
+ return <Dialog.Title {...rest} className={tv({ base: 'text-section min-w-0' })({ className })} />
192
+ }
193
+
194
+ export function DrawerDescription({ className, ...rest }: ComponentProps<typeof Dialog.Description>) {
195
+ return (
196
+ <Dialog.Description {...rest} className={tv({ base: 'text-prose text-fg-muted' })({ className })} />
197
+ )
198
+ }
@@ -0,0 +1,133 @@
1
+ # DropdownMenu
2
+
3
+ ```
4
+ 받기 npx @pantoia/ui add dropdown-menu
5
+ 층 ui
6
+ 파일 dropdown-menu/index.tsx · dropdown-menu/AGENTS.md
7
+ ```
8
+
9
+ ## 부위
10
+
11
+ | 이름 | props |
12
+ |---|---|
13
+ | `DropdownMenu` | `DropdownMenuProps` |
14
+ | `DropdownMenuTrigger` | — |
15
+ | `DropdownMenuContent` | `DropdownMenuContentProps` |
16
+ | `DropdownMenuGroup` | — |
17
+ | `DropdownMenuItem` | `DropdownMenuItemProps` |
18
+ | `DropdownMenuCheckboxItem` | — |
19
+ | `DropdownMenuRadioGroup` | — |
20
+ | `DropdownMenuRadioItem` | — |
21
+ | `DropdownMenuSeparator` | — |
22
+ | `DropdownMenuSub` | — |
23
+ | `DropdownMenuSubTrigger` | — |
24
+ | `DropdownMenuSubContent` | `ComponentProps<typeof MenuPrimitive.SubContent>` |
25
+
26
+ ### `DropdownMenuTrigger`
27
+
28
+ | 이름 | 타입 | 필수 |
29
+ |---|---|---|
30
+ | `children` | `ReactNode` | 예 |
31
+
32
+ ### `DropdownMenuGroup`
33
+
34
+ | 이름 | 타입 | 필수 |
35
+ |---|---|---|
36
+ | `label` | `string` | |
37
+ | `className` | `string` | |
38
+ | `children` | `ReactNode` | |
39
+
40
+ ### `DropdownMenuCheckboxItem`
41
+
42
+ | 이름 | 타입 | 필수 |
43
+ |---|---|---|
44
+ | `checked` | `boolean` | |
45
+ | `onCheckedChange` | `(checked: boolean) => void` | |
46
+ | `disabled` | `boolean` | |
47
+ | `className` | `string` | |
48
+ | `children` | `ReactNode` | |
49
+
50
+ ### `DropdownMenuRadioGroup`
51
+
52
+ | 이름 | 타입 | 필수 |
53
+ |---|---|---|
54
+ | `value` | `string` | |
55
+ | `onValueChange` | `(value: string) => void` | |
56
+ | `children` | `ReactNode` | |
57
+
58
+ ### `DropdownMenuRadioItem`
59
+
60
+ | 이름 | 타입 | 필수 |
61
+ |---|---|---|
62
+ | `value` | `string` | 예 |
63
+ | `disabled` | `boolean` | |
64
+ | `className` | `string` | |
65
+ | `children` | `ReactNode` | |
66
+
67
+ ### `DropdownMenuSeparator`
68
+
69
+ | 이름 | 타입 | 필수 |
70
+ |---|---|---|
71
+ | `className` | `string` | |
72
+
73
+ ### `DropdownMenuSub`
74
+
75
+ | 이름 | 타입 | 필수 |
76
+ |---|---|---|
77
+ | `open` | `boolean` | |
78
+ | `defaultOpen` | `boolean` | |
79
+ | `onOpenChange` | `(open: boolean) => void` | |
80
+ | `children` | `ReactNode` | |
81
+
82
+ ### `DropdownMenuSubTrigger`
83
+
84
+ | 이름 | 타입 | 필수 |
85
+ |---|---|---|
86
+ | `disabled` | `boolean` | |
87
+ | `lead` | `ReactNode` | |
88
+ | `className` | `string` | |
89
+ | `children` | `ReactNode` | |
90
+
91
+ ## props
92
+
93
+ ### `DropdownMenuProps`
94
+
95
+ | 이름 | 타입 | 필수 |
96
+ |---|---|---|
97
+ | `open` | `boolean` | |
98
+ | `defaultOpen` | `boolean` | |
99
+ | `onOpenChange` | `(open: boolean) => void` | |
100
+ | `children` | `ReactNode` | |
101
+
102
+ ### `DropdownMenuContentProps`
103
+
104
+ `Omit<ComponentProps<typeof MenuPrimitive.Content>, 'align'>` 를 확장한다.
105
+
106
+ | 이름 | 타입 | 필수 |
107
+ |---|---|---|
108
+ | `align` | `'start' \| 'center' \| 'end'` | |
109
+ | `arrow` | `boolean` | |
110
+
111
+ ### `DropdownMenuItemProps`
112
+
113
+ `Omit<ComponentProps<typeof MenuPrimitive.Item>, 'color'>` 를 확장한다.
114
+
115
+ | 이름 | 타입 | 필수 |
116
+ |---|---|---|
117
+ | `color` | `'neutral' \| 'danger'` | |
118
+ | `lead` | `ReactNode` | |
119
+ | `trail` | `ReactNode` | |
120
+
121
+ ## 변형 — `menuItem`
122
+
123
+ | 축 | 값 | 기본 |
124
+ |---|---|---|
125
+ | `color` | `neutral` · `danger` | `neutral` |
126
+
127
+ ## 함께 받는 것
128
+
129
+ `tv`
130
+
131
+ ## npm 의존
132
+
133
+ `lucide-react` · `radix-ui`
@@ -0,0 +1,223 @@
1
+ 'use client'
2
+
3
+ import { Check, ChevronRight } from 'lucide-react'
4
+ import { DropdownMenu as MenuPrimitive } from 'radix-ui'
5
+ import type { ComponentProps, ReactNode } from 'react'
6
+
7
+ import { tv } from '../tv'
8
+
9
+ export const menuItem = tv({
10
+ base: 'flex h-9 cursor-default items-center gap-2 rounded-r8 px-2 menu-gutter:ps-8'
11
+ + ' [&_svg]:size-4 [&_svg]:shrink-0'
12
+ + ' text-label transition-state'
13
+ + ' data-highlighted:outline-none data-disabled:pointer-events-none',
14
+
15
+ variants: {
16
+ color: {
17
+ neutral: 'text-fg data-highlighted:bg-hover data-disabled:text-fg-disabled',
18
+ danger: 'text-danger data-highlighted:bg-danger-weak data-disabled:text-danger/40',
19
+ },
20
+ },
21
+
22
+ defaultVariants: { color: 'neutral' },
23
+ })
24
+
25
+ const PANEL = 'menu bg-float text-fg rounded-float shadow-s3 z-overlay'
26
+ + ' flex min-w-44 flex-col gap-px p-1'
27
+ + ' open:animate-enter closed:animate-exit'
28
+ + ' focus-visible:outline-none'
29
+
30
+ export interface DropdownMenuProps {
31
+ open?: boolean
32
+ defaultOpen?: boolean
33
+ onOpenChange?: (open: boolean) => void
34
+ children?: ReactNode
35
+ }
36
+
37
+ export function DropdownMenu({ open, defaultOpen, onOpenChange, children }: DropdownMenuProps) {
38
+ return (
39
+ <MenuPrimitive.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
40
+ {children}
41
+ </MenuPrimitive.Root>
42
+ )
43
+ }
44
+
45
+ export function DropdownMenuTrigger({ children }: { children: ReactNode }) {
46
+ return <MenuPrimitive.Trigger asChild>{children}</MenuPrimitive.Trigger>
47
+ }
48
+
49
+ export interface DropdownMenuContentProps extends Omit<ComponentProps<typeof MenuPrimitive.Content>, 'align'> {
50
+ align?: 'start' | 'center' | 'end'
51
+ arrow?: boolean
52
+ }
53
+
54
+ export function DropdownMenuContent({
55
+ align = 'start',
56
+ arrow = true,
57
+ className,
58
+ children,
59
+ ...rest
60
+ }: DropdownMenuContentProps) {
61
+ return (
62
+ <MenuPrimitive.Portal>
63
+ <MenuPrimitive.Content
64
+ {...rest}
65
+ align={align}
66
+ sideOffset={8}
67
+ arrowPadding={16}
68
+ className={tv({ base: PANEL })({ className })}
69
+ >
70
+ {children}
71
+
72
+ {arrow && <MenuPrimitive.Arrow className="fill-float" width={12} height={6} />}
73
+ </MenuPrimitive.Content>
74
+ </MenuPrimitive.Portal>
75
+ )
76
+ }
77
+
78
+ export function DropdownMenuGroup({ label, className, children }: {
79
+ label?: string
80
+ className?: string
81
+ children?: ReactNode
82
+ }) {
83
+ return (
84
+ <MenuPrimitive.Group className={tv({ base: 'flex flex-col gap-px' })({ className })}>
85
+ {label && (
86
+ <MenuPrimitive.Label className="text-caption text-fg-subtle px-2 pb-1 pt-2">
87
+ {label}
88
+ </MenuPrimitive.Label>
89
+ )}
90
+ {children}
91
+ </MenuPrimitive.Group>
92
+ )
93
+ }
94
+
95
+ export interface DropdownMenuItemProps extends Omit<ComponentProps<typeof MenuPrimitive.Item>, 'color'> {
96
+ color?: 'neutral' | 'danger'
97
+ lead?: ReactNode
98
+ trail?: ReactNode
99
+ }
100
+
101
+ export function DropdownMenuItem({ color, lead, trail, className, children, ...rest }: DropdownMenuItemProps) {
102
+ return (
103
+ <MenuPrimitive.Item {...rest} className={menuItem({ color, className })}>
104
+ {lead && (
105
+ <span className="menu-lead grid size-4 shrink-0 place-items-center">
106
+ {lead}
107
+ </span>
108
+ )}
109
+
110
+ <span className="min-w-0 grow">{children}</span>
111
+ {trail}
112
+ </MenuPrimitive.Item>
113
+ )
114
+ }
115
+
116
+ export function DropdownMenuCheckboxItem({ checked, onCheckedChange, disabled, className, children }: {
117
+ checked?: boolean
118
+ onCheckedChange?: (checked: boolean) => void
119
+ disabled?: boolean
120
+ className?: string
121
+ children?: ReactNode
122
+ }) {
123
+ return (
124
+ <MenuPrimitive.CheckboxItem
125
+ checked={checked}
126
+ onCheckedChange={onCheckedChange}
127
+ disabled={disabled}
128
+ className={menuItem({ className })}
129
+ onSelect={e => e.preventDefault()}
130
+ >
131
+ <span className="menu-lead grid size-4 shrink-0 place-items-center">
132
+ <MenuPrimitive.ItemIndicator>
133
+ <Check className="size-4" />
134
+ </MenuPrimitive.ItemIndicator>
135
+ </span>
136
+
137
+ <span className="min-w-0 grow">{children}</span>
138
+ </MenuPrimitive.CheckboxItem>
139
+ )
140
+ }
141
+
142
+ export function DropdownMenuRadioGroup({ value, onValueChange, children }: {
143
+ value?: string
144
+ onValueChange?: (value: string) => void
145
+ children?: ReactNode
146
+ }) {
147
+ return (
148
+ <MenuPrimitive.RadioGroup value={value} onValueChange={onValueChange}>
149
+ {children}
150
+ </MenuPrimitive.RadioGroup>
151
+ )
152
+ }
153
+
154
+ export function DropdownMenuRadioItem({ value, disabled, className, children }: {
155
+ value: string
156
+ disabled?: boolean
157
+ className?: string
158
+ children?: ReactNode
159
+ }) {
160
+ return (
161
+ <MenuPrimitive.RadioItem value={value} disabled={disabled} className={menuItem({ className })}>
162
+ <span className="menu-lead grid size-4 shrink-0 place-items-center">
163
+ <MenuPrimitive.ItemIndicator>
164
+ <span className="bg-control-on in-disabled:bg-control-on-disabled size-2 rounded-full" />
165
+ </MenuPrimitive.ItemIndicator>
166
+ </span>
167
+
168
+ <span className="min-w-0 grow">{children}</span>
169
+ </MenuPrimitive.RadioItem>
170
+ )
171
+ }
172
+
173
+ export function DropdownMenuSeparator({ className }: { className?: string }) {
174
+ return <MenuPrimitive.Separator className={tv({ base: 'bg-line -mx-1 my-1 h-px' })({ className })} />
175
+ }
176
+
177
+ export function DropdownMenuSub({ open, defaultOpen, onOpenChange, children }: {
178
+ open?: boolean
179
+ defaultOpen?: boolean
180
+ onOpenChange?: (open: boolean) => void
181
+ children?: ReactNode
182
+ }) {
183
+ return (
184
+ <MenuPrimitive.Sub open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
185
+ {children}
186
+ </MenuPrimitive.Sub>
187
+ )
188
+ }
189
+
190
+ export function DropdownMenuSubTrigger({ disabled, lead, className, children }: {
191
+ disabled?: boolean
192
+ lead?: ReactNode
193
+ className?: string
194
+ children?: ReactNode
195
+ }) {
196
+ return (
197
+ <MenuPrimitive.SubTrigger disabled={disabled} className={menuItem({ className })}>
198
+ {lead && (
199
+ <span className="menu-lead grid size-4 shrink-0 place-items-center">
200
+ {lead}
201
+ </span>
202
+ )}
203
+
204
+ <span className="min-w-0 grow">{children}</span>
205
+
206
+ <ChevronRight className="text-fg-subtle size-4 shrink-0" />
207
+ </MenuPrimitive.SubTrigger>
208
+ )
209
+ }
210
+
211
+ export function DropdownMenuSubContent({ className, children, ...rest }: ComponentProps<typeof MenuPrimitive.SubContent>) {
212
+ return (
213
+ <MenuPrimitive.Portal>
214
+ <MenuPrimitive.SubContent
215
+ {...rest}
216
+ sideOffset={4}
217
+ className={tv({ base: PANEL })({ className })}
218
+ >
219
+ {children}
220
+ </MenuPrimitive.SubContent>
221
+ </MenuPrimitive.Portal>
222
+ )
223
+ }
@@ -0,0 +1,96 @@
1
+ # Field
2
+
3
+ ```
4
+ 받기 npx @pantoia/ui add field
5
+ 층 ui
6
+ 파일 field/index.tsx · field/AGENTS.md
7
+ ```
8
+
9
+ ## 부위
10
+
11
+ | 이름 | props |
12
+ |---|---|
13
+ | `Field` | `FieldProps` |
14
+ | `FieldLabel` | — |
15
+ | `FieldHelp` | — |
16
+ | `FieldError` | `FieldErrorProps` |
17
+ | `FieldPending` | — |
18
+ | `FieldSuccess` | — |
19
+
20
+ ### `FieldLabel`
21
+
22
+ | 이름 | 타입 | 필수 |
23
+ |---|---|---|
24
+ | `className` | `string` | |
25
+ | `children` | `ReactNode` | |
26
+
27
+ ### `FieldHelp`
28
+
29
+ | 이름 | 타입 | 필수 |
30
+ |---|---|---|
31
+ | `className` | `string` | |
32
+ | `children` | `ReactNode` | |
33
+
34
+ ### `FieldPending`
35
+
36
+ | 이름 | 타입 | 필수 |
37
+ |---|---|---|
38
+ | `className` | `string` | |
39
+ | `children` | `ReactNode` | |
40
+
41
+ ### `FieldSuccess`
42
+
43
+ | 이름 | 타입 | 필수 |
44
+ |---|---|---|
45
+ | `className` | `string` | |
46
+ | `children` | `ReactNode` | |
47
+
48
+ ## props
49
+
50
+ ### `FieldContext`
51
+
52
+ | 이름 | 타입 | 필수 |
53
+ |---|---|---|
54
+ | `id` | `string` | 예 |
55
+ | `labelId` | `string` | 예 |
56
+ | `messageId` | `string` | 예 |
57
+
58
+ ### `FieldControl`
59
+
60
+ | 이름 | 타입 | 필수 |
61
+ |---|---|---|
62
+ | `id` | `string` | 예 |
63
+ | `aria-labelledby` | `string` | 예 |
64
+ | `aria-invalid` | `'true' \| undefined` | 예 |
65
+ | `aria-describedby` | `string` | 예 |
66
+
67
+ ### `FieldProps`
68
+
69
+ | 이름 | 타입 | 필수 |
70
+ |---|---|---|
71
+ | `invalid` | `boolean` | |
72
+ | `layout` | `'stack' \| 'inline' \| 'split'` | |
73
+ | `className` | `string` | |
74
+ | `children` | `ReactNode \| ((args: { control: FieldControl }) => ReactNode)` | |
75
+
76
+ ### `FieldErrorProps`
77
+
78
+ | 이름 | 타입 | 필수 |
79
+ |---|---|---|
80
+ | `items` | `string \| string[]` | |
81
+ | `className` | `string` | |
82
+ | `children` | `ReactNode` | |
83
+
84
+ ## 변형 — `field`
85
+
86
+ | 축 | 값 | 기본 |
87
+ |---|---|---|
88
+ | `layout` | `stack` · `inline` · `split` | `stack` |
89
+
90
+ ## 함께 받는 것
91
+
92
+ `label` · `tv`
93
+
94
+ ## npm 의존
95
+
96
+ `lucide-react`