@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,420 @@
1
+ 'use client'
2
+
3
+ import { Check, ChevronDown, X } from 'lucide-react'
4
+ import { Popover as PopoverPrimitive } from 'radix-ui'
5
+ import {
6
+ createContext,
7
+ use,
8
+ useEffect,
9
+ useId,
10
+ useState,
11
+ type ComponentProps,
12
+ type InputHTMLAttributes,
13
+ type KeyboardEvent as ReactKeyboardEvent,
14
+ type ReactNode,
15
+ } from 'react'
16
+
17
+ import { tv } from '../tv'
18
+
19
+ interface ComboboxContext {
20
+ id: string
21
+ value: string | undefined
22
+ open: boolean
23
+ active: string | undefined
24
+ search: string
25
+ setSearch: (search: string) => void
26
+ setOpen: (open: boolean) => void
27
+ pick: (value: string) => void
28
+ move: (step: number) => void
29
+ setActive: (value: string | undefined) => void
30
+ itemId: (value: string) => string
31
+ count: number
32
+ ignoreFilter: boolean
33
+ see: (visible: boolean) => () => void
34
+ }
35
+
36
+ const USABLE = '[role="option"]:not([data-disabled])'
37
+
38
+ const Context = createContext<ComboboxContext | null>(null)
39
+
40
+ const useCombobox = () => {
41
+ const ctx = use(Context)
42
+ if (!ctx) throw new Error('Combobox 부품은 Combobox 안에서만 선다')
43
+ return ctx
44
+ }
45
+
46
+ export interface ComboboxProps {
47
+ value?: string
48
+ defaultValue?: string
49
+ onValueChange?: (value: string) => void
50
+ open?: boolean
51
+ defaultOpen?: boolean
52
+ onOpenChange?: (open: boolean) => void
53
+ ignoreFilter?: boolean
54
+ className?: string
55
+ children?: ReactNode
56
+ }
57
+
58
+ export function Combobox({
59
+ value,
60
+ defaultValue,
61
+ onValueChange,
62
+ open,
63
+ defaultOpen = false,
64
+ onOpenChange,
65
+ ignoreFilter = true,
66
+ className,
67
+ children,
68
+ }: ComboboxProps) {
69
+ const id = useId()
70
+ const [ownValue, setOwnValue] = useState(defaultValue)
71
+ const [ownOpen, setOwnOpen] = useState(defaultOpen)
72
+ const [active, setActive] = useState<string | undefined>(undefined)
73
+ const [search, setSearch] = useState('')
74
+ const [count, setCount] = useState(0)
75
+ const [seen, setSeen] = useState(false)
76
+
77
+ const isOpen = open ?? ownOpen
78
+ const current = value ?? ownValue
79
+
80
+ if (seen !== isOpen) {
81
+ setSeen(isOpen)
82
+ if (!isOpen) setActive(undefined)
83
+ }
84
+
85
+ const setOpen = (next: boolean) => {
86
+ if (open === undefined) setOwnOpen(next)
87
+ onOpenChange?.(next)
88
+ }
89
+
90
+ const pick = (next: string) => {
91
+ if (value === undefined) setOwnValue(next)
92
+ onValueChange?.(next)
93
+ setSearch(next)
94
+ setOpen(false)
95
+ }
96
+
97
+ const see = (visible: boolean) => {
98
+ if (!visible) return () => {}
99
+ setCount(n => n + 1)
100
+ return () => setCount(n => n - 1)
101
+ }
102
+
103
+ const move = (step: number) => {
104
+ const list = document.getElementById(`${id}-list`)
105
+ if (!list) return
106
+
107
+ const usable = [...list.querySelectorAll<HTMLElement>(USABLE)].map(el => el.dataset.value!)
108
+ if (usable.length === 0) return
109
+
110
+ const at = active === undefined ? -1 : usable.indexOf(active)
111
+ const next = at === -1
112
+ ? (step > 0 ? 0 : usable.length - 1)
113
+ : (at + step + usable.length) % usable.length
114
+
115
+ setActive(usable[next])
116
+ }
117
+
118
+ return (
119
+ <Context value={{
120
+ id,
121
+ value: current,
122
+ open: isOpen,
123
+ active,
124
+ search,
125
+ setSearch,
126
+ setOpen,
127
+ pick,
128
+ see,
129
+ move,
130
+ setActive,
131
+ itemId: (item: string) => `${id}-${item}`,
132
+ count,
133
+ ignoreFilter,
134
+ }}
135
+ >
136
+ <PopoverPrimitive.Root open={isOpen} onOpenChange={setOpen}>
137
+ <div className={className}>{children}</div>
138
+ </PopoverPrimitive.Root>
139
+ </Context>
140
+ )
141
+ }
142
+
143
+ export const comboboxAnchor = tv({
144
+ base: 'flex items-center gap-tight rounded-r8 px-3'
145
+ + ' transition-state'
146
+ + ' invalid-within:ring-2 invalid-within:ring-danger'
147
+ + ' has-disabled:bg-disabled has-disabled:cursor-not-allowed'
148
+ + ' has-focus-visible:outline-2 has-focus-visible:outline-offset-2'
149
+ + ' has-focus-visible:outline-ring',
150
+
151
+ variants: {
152
+ surface: {
153
+ bare: 'ring-1 ring-inset ring-line-strong',
154
+ well: 'bg-well',
155
+ },
156
+ size: { sm: 'h-8', md: 'h-9', lg: 'h-10 [&_input]:text-t16' },
157
+ },
158
+
159
+ defaultVariants: { surface: 'bare', size: 'md' },
160
+ })
161
+
162
+ export interface ComboboxAnchorProps {
163
+ surface?: 'bare' | 'well'
164
+ size?: 'sm' | 'md' | 'lg'
165
+ className?: string
166
+ children?: ReactNode
167
+ }
168
+
169
+ export function ComboboxAnchor({ surface, size, className, children }: ComboboxAnchorProps) {
170
+ return (
171
+ <PopoverPrimitive.Anchor className={comboboxAnchor({ surface, size, className })}>
172
+ {children}
173
+ </PopoverPrimitive.Anchor>
174
+ )
175
+ }
176
+
177
+ export interface ComboboxInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange'> {
178
+ value?: string
179
+ onValueChange?: (value: string) => void
180
+ }
181
+
182
+ export function ComboboxInput({ value, onValueChange, className, ...rest }: ComboboxInputProps) {
183
+ const ctx = useCombobox()
184
+ const text = value ?? ctx.search
185
+ const [seen, setSeen] = useState(ctx.search)
186
+
187
+ if (seen !== ctx.search) {
188
+ setSeen(ctx.search)
189
+ if (ctx.search !== text) onValueChange?.(ctx.search)
190
+ }
191
+
192
+ const change = (next: string) => {
193
+ ctx.setSearch(next)
194
+ onValueChange?.(next)
195
+ if (!ctx.open) ctx.setOpen(true)
196
+ }
197
+
198
+ const onKeyDown = (e: ReactKeyboardEvent<HTMLInputElement>) => {
199
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
200
+ e.preventDefault()
201
+ if (!ctx.open) ctx.setOpen(true)
202
+ else ctx.move(e.key === 'ArrowDown' ? 1 : -1)
203
+ return
204
+ }
205
+
206
+ if (e.key === 'Enter' && ctx.open && ctx.active !== undefined) {
207
+ e.preventDefault()
208
+ ctx.pick(ctx.active)
209
+ return
210
+ }
211
+
212
+ if (e.key === 'Escape' && ctx.open) {
213
+ e.preventDefault()
214
+ ctx.setOpen(false)
215
+ }
216
+ }
217
+
218
+ return (
219
+ <input
220
+ {...rest}
221
+ role="combobox"
222
+ aria-expanded={ctx.open}
223
+ aria-controls={ctx.open ? `${ctx.id}-list` : undefined}
224
+ aria-autocomplete="list"
225
+ aria-activedescendant={ctx.active === undefined ? undefined : ctx.itemId(ctx.active)}
226
+ value={text}
227
+ onChange={e => change(e.target.value)}
228
+ onKeyDown={onKeyDown}
229
+ className={tv({ base: 'text-value text-fg placeholder:text-fg-placeholder'
230
+ + ' w-full min-w-0 bg-transparent outline-none'
231
+ + ' disabled:text-fg-disabled disabled:cursor-not-allowed' })({ className })}
232
+ />
233
+ )
234
+ }
235
+
236
+ export function ComboboxTrigger({ label = '목록 열기', disabled, className }: {
237
+ label?: string
238
+ disabled?: boolean
239
+ className?: string
240
+ }) {
241
+ const ctx = useCombobox()
242
+
243
+ return (
244
+ <button
245
+ type="button"
246
+ tabIndex={-1}
247
+ aria-label={label}
248
+ disabled={disabled}
249
+ data-state={ctx.open ? 'open' : 'closed'}
250
+ onClick={() => ctx.setOpen(!ctx.open)}
251
+ className={tv({ base: 'text-fg-subtle grid size-5 shrink-0 place-items-center rounded-r8'
252
+ + ' transition-state press-shrink hover:text-fg'
253
+ + ' focus-visible:outline-2 focus-visible:outline-offset-2'
254
+ + ' focus-visible:outline-ring' })({ className })}
255
+ >
256
+ <ChevronDown
257
+ className="size-4 transition-transform open:rotate-180 motion-reduce:transition-none"
258
+ />
259
+ </button>
260
+ )
261
+ }
262
+
263
+ export function ComboboxCancel({ label = '지우기', className }: { label?: string, className?: string }) {
264
+ const ctx = useCombobox()
265
+
266
+ return (
267
+ <button
268
+ type="button"
269
+ tabIndex={-1}
270
+ aria-label={label}
271
+ onClick={() => ctx.setSearch('')}
272
+ className={tv({ base: 'text-fg-subtle grid size-5 shrink-0 place-items-center rounded-full'
273
+ + ' transition-state press-shrink hover:text-fg'
274
+ + ' focus-visible:outline-2 focus-visible:outline-offset-2'
275
+ + ' focus-visible:outline-ring' })({ className })}
276
+ >
277
+ <X className="size-4" />
278
+ </button>
279
+ )
280
+ }
281
+
282
+ export type ComboboxContentProps =
283
+ Omit<ComponentProps<typeof PopoverPrimitive.Content>, 'align' | 'sideOffset' | 'onOpenAutoFocus'>
284
+
285
+ export function ComboboxContent({ className, children, ...rest }: ComboboxContentProps) {
286
+ const label = rest['aria-label']
287
+
288
+ return (
289
+ <PopoverPrimitive.Portal>
290
+ <PopoverPrimitive.Content
291
+ {...rest}
292
+ align="start"
293
+ sideOffset={4}
294
+ onOpenAutoFocus={e => e.preventDefault()}
295
+ className={tv({ base: 'bg-float text-fg rounded-float shadow-s3 z-overlay'
296
+ + ' w-(--radix-popover-trigger-width) p-1'
297
+ + ' open:animate-enter closed:animate-exit' })({ className })}
298
+ >
299
+ <ComboboxViewport label={label} className="max-h-72">
300
+ {children}
301
+ </ComboboxViewport>
302
+ </PopoverPrimitive.Content>
303
+ </PopoverPrimitive.Portal>
304
+ )
305
+ }
306
+
307
+ export function ComboboxViewport({ label, className, children }: {
308
+ label?: string
309
+ className?: string
310
+ children?: ReactNode
311
+ }) {
312
+ const ctx = useCombobox()
313
+
314
+ return (
315
+ <div
316
+ id={`${ctx.id}-list`}
317
+ role={ctx.count > 0 ? 'listbox' : undefined}
318
+ aria-label={ctx.count > 0 ? label : undefined}
319
+ className={tv({ base: 'flex flex-col gap-px overflow-y-auto' })({ className })}
320
+ >
321
+ {children}
322
+ </div>
323
+ )
324
+ }
325
+
326
+ export interface ComboboxItemProps {
327
+ value: string
328
+ textValue?: string
329
+ disabled?: boolean
330
+ className?: string
331
+ children?: ReactNode
332
+ }
333
+
334
+ export function ComboboxItem({ value, textValue, disabled = false, className, children }: ComboboxItemProps) {
335
+ const ctx = useCombobox()
336
+ const shown = ctx.ignoreFilter || contains(textValue ?? value, ctx.search)
337
+
338
+ useEffect(() => ctx.see(shown), [ctx, shown])
339
+
340
+ if (!shown) return null
341
+
342
+ return (
343
+ <div
344
+ id={ctx.itemId(value)}
345
+ role="option"
346
+ data-value={value}
347
+ aria-selected={ctx.value === value}
348
+ data-highlighted={ctx.active === value ? '' : undefined}
349
+ data-disabled={disabled ? '' : undefined}
350
+ onPointerMove={() => !disabled && ctx.setActive(value)}
351
+ onClick={() => !disabled && ctx.pick(value)}
352
+ className={tv({ base: 'flex min-h-9 cursor-default items-center justify-between gap-stack'
353
+ + ' shrink-0 rounded-r8 px-2 py-1 transition-state'
354
+ + ' data-highlighted:bg-hover data-highlighted:outline-none'
355
+ + ' data-disabled:text-fg-disabled data-disabled:pointer-events-none' })({ className })}
356
+ >
357
+ <span className="flex min-w-0 flex-col gap-0.5">
358
+ {children}
359
+ </span>
360
+
361
+ {ctx.value === value && (
362
+ <span className="text-fg-muted flex shrink-0">
363
+ <Check className="size-4" />
364
+ </span>
365
+ )}
366
+ </div>
367
+ )
368
+ }
369
+
370
+ export function ComboboxItemText({ className, children }: { className?: string, children?: ReactNode }) {
371
+ return (
372
+ <span className={tv({ base: 'text-label text-fg' })({ className })}>
373
+ {children}
374
+ </span>
375
+ )
376
+ }
377
+
378
+ export function ComboboxItemDescription({ className, children }: { className?: string, children?: ReactNode }) {
379
+ return (
380
+ <span className={tv({ base: 'text-caption text-fg-subtle' })({ className })}>
381
+ {children}
382
+ </span>
383
+ )
384
+ }
385
+
386
+ export function ComboboxGroup({ label, className, children }: {
387
+ label?: string
388
+ className?: string
389
+ children?: ReactNode
390
+ }) {
391
+ return (
392
+ <div role="group" className={tv({ base: 'flex flex-col gap-px' })({ className })}>
393
+ {label && (
394
+ <span className="text-caption text-fg-subtle px-2 pb-1 pt-2">
395
+ {label}
396
+ </span>
397
+ )}
398
+ {children}
399
+ </div>
400
+ )
401
+ }
402
+
403
+ export function ComboboxEmpty({ className, children }: { className?: string, children?: ReactNode }) {
404
+ const ctx = useCombobox()
405
+ if (ctx.count > 0) return null
406
+
407
+ return (
408
+ <div role="presentation" className={tv({ base: 'text-caption text-fg-subtle px-2 py-3 text-center' })({ className })}>
409
+ {children}
410
+ </div>
411
+ )
412
+ }
413
+
414
+ export function ComboboxSeparator({ className }: { className?: string }) {
415
+ return <div className={tv({ base: 'bg-line -mx-1 my-1 h-px' })({ className })} />
416
+ }
417
+
418
+ export function contains(text: string, search: string) {
419
+ return text.normalize('NFC').toLocaleLowerCase().includes(search.normalize('NFC').toLocaleLowerCase())
420
+ }
@@ -0,0 +1,44 @@
1
+ # CommandPalette
2
+
3
+ ```
4
+ 받기 npx @pantoia/ui add command-palette
5
+ 층 derived
6
+ 파일 command-palette/index.tsx · command-palette/AGENTS.md
7
+ ```
8
+
9
+ ## 부위
10
+
11
+ | 이름 | props |
12
+ |---|---|
13
+ | `CommandPalette` | `CommandPaletteProps` |
14
+
15
+ ## props
16
+
17
+ ### `CommandPaletteItem`
18
+
19
+ | 이름 | 타입 | 필수 |
20
+ |---|---|---|
21
+ | `value` | `string` | 예 |
22
+ | `label` | `string` | 예 |
23
+ | `group` | `string` | |
24
+
25
+ ### `CommandPaletteProps`
26
+
27
+ | 이름 | 타입 | 필수 |
28
+ |---|---|---|
29
+ | `open` | `boolean` | |
30
+ | `defaultOpen` | `boolean` | |
31
+ | `onOpenChange` | `(open: boolean) => void` | |
32
+ | `items` | `CommandPaletteItem[]` | 예 |
33
+ | `title` | `string` | 예 |
34
+ | `placeholder` | `string` | 예 |
35
+ | `emptyText` | `string` | 예 |
36
+ | `onSelect` | `(value: string) => void` | |
37
+
38
+ ## 함께 받는 것
39
+
40
+ `combobox` · `dialog` · `kbd`
41
+
42
+ ## npm 의존
43
+
44
+ `lucide-react`
@@ -0,0 +1,108 @@
1
+ 'use client'
2
+
3
+ import { Search } from 'lucide-react'
4
+ import { useState } from 'react'
5
+
6
+ import {
7
+ Combobox,
8
+ ComboboxEmpty,
9
+ ComboboxInput,
10
+ ComboboxItem,
11
+ ComboboxItemDescription,
12
+ ComboboxItemText,
13
+ ComboboxViewport,
14
+ contains,
15
+ } from '../combobox'
16
+ import { Dialog, DialogContent, DialogDescription, DialogTitle } from '../dialog'
17
+ import { Kbd } from '../kbd'
18
+
19
+ export interface CommandPaletteItem {
20
+ value: string
21
+ label: string
22
+ group?: string
23
+ }
24
+
25
+ export interface CommandPaletteProps {
26
+ open?: boolean
27
+ defaultOpen?: boolean
28
+ onOpenChange?: (open: boolean) => void
29
+ items: CommandPaletteItem[]
30
+ title: string
31
+ placeholder: string
32
+ emptyText: string
33
+ onSelect?: (value: string) => void
34
+ }
35
+
36
+ export function CommandPalette({
37
+ open,
38
+ defaultOpen = false,
39
+ onOpenChange,
40
+ items,
41
+ title,
42
+ placeholder,
43
+ emptyText,
44
+ onSelect,
45
+ }: CommandPaletteProps) {
46
+ const [own, setOwn] = useState(defaultOpen)
47
+ const [search, setSearch] = useState('')
48
+ const [seen, setSeen] = useState(defaultOpen)
49
+
50
+ const isOpen = open ?? own
51
+
52
+ if (seen !== isOpen) {
53
+ setSeen(isOpen)
54
+ if (!isOpen) setSearch('')
55
+ }
56
+
57
+ const setOpen = (next: boolean) => {
58
+ if (open === undefined) setOwn(next)
59
+ onOpenChange?.(next)
60
+ }
61
+
62
+ const filtered = search ? items.filter(i => contains(i.label, search)) : items
63
+
64
+ const pick = (value: string) => {
65
+ onSelect?.(value)
66
+ setOpen(false)
67
+ }
68
+
69
+ return (
70
+ <Dialog open={isOpen} onOpenChange={setOpen}>
71
+ <DialogContent size="top">
72
+ <DialogTitle className="sr-only">{title}</DialogTitle>
73
+ <DialogDescription className="sr-only">
74
+ {placeholder}
75
+ </DialogDescription>
76
+
77
+ <Combobox open onValueChange={pick}>
78
+ <div className="border-line flex h-14 items-center gap-stack border-b px-4">
79
+ <Search className="text-fg-subtle size-5 shrink-0" />
80
+ <ComboboxInput
81
+ value={search}
82
+ onValueChange={setSearch}
83
+ placeholder={placeholder}
84
+ className="text-t16"
85
+ autoFocus
86
+ />
87
+ <Kbd>Esc</Kbd>
88
+ </div>
89
+
90
+ <ComboboxViewport className="max-h-80 gap-1 p-2">
91
+ <ComboboxEmpty className="py-6">
92
+ {emptyText}
93
+ </ComboboxEmpty>
94
+
95
+ {filtered.map(item => (
96
+ <ComboboxItem key={item.value} value={item.value}>
97
+ <ComboboxItemText>{item.label}</ComboboxItemText>
98
+ {item.group && (
99
+ <ComboboxItemDescription>{item.group}</ComboboxItemDescription>
100
+ )}
101
+ </ComboboxItem>
102
+ ))}
103
+ </ComboboxViewport>
104
+ </Combobox>
105
+ </DialogContent>
106
+ </Dialog>
107
+ )
108
+ }
@@ -0,0 +1,43 @@
1
+ # CopyButton
2
+
3
+ ```
4
+ 받기 npx @pantoia/ui add copy-button
5
+ 층 derived
6
+ 파일 copy-button/index.tsx · copy-button/AGENTS.md
7
+ ```
8
+
9
+ ## 부위
10
+
11
+ | 이름 | props |
12
+ |---|---|
13
+ | `CopyButton` | `CopyButtonProps` |
14
+
15
+ ## 도우미
16
+
17
+ `useCopy(resetAfter = 2000)`
18
+
19
+ ## props
20
+
21
+ ### `CopyState`
22
+
23
+ `'idle' | 'copied' | 'failed'`
24
+
25
+ ### `CopyButtonProps`
26
+
27
+ | 이름 | 타입 | 필수 |
28
+ |---|---|---|
29
+ | `value` | `string` | 예 |
30
+ | `color` | `'primary' \| 'neutral' \| 'danger'` | |
31
+ | `emphasis` | `'solid' \| 'weak' \| 'outline' \| 'ghost'` | |
32
+ | `size` | `'sm' \| 'md' \| 'lg'` | |
33
+ | `shape` | `'rect' \| 'square' \| 'pill' \| 'circle'` | |
34
+ | `onCopied` | `(value: string) => void` | |
35
+ | `onFailed` | `() => void` | |
36
+ | `disabled` | `boolean` | |
37
+ | `className` | `string` | |
38
+ | `aria-label` | `string` | |
39
+ | `children` | `ReactNode` | |
40
+
41
+ ## 함께 받는 것
42
+
43
+ `button`