@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,149 @@
1
+ 'use client'
2
+
3
+ import { CircleAlert, CircleCheck, LoaderCircle } from 'lucide-react'
4
+ import { createContext, use, useId, type ReactNode } from 'react'
5
+
6
+ import { Label } from '../label'
7
+ import { tv } from '../tv'
8
+
9
+ export const field = tv({
10
+ base: 'group flex flex-wrap gap-x-2 gap-y-tight',
11
+
12
+ variants: {
13
+ layout: {
14
+ stack: 'flex-col',
15
+ inline: 'flex-row items-center',
16
+ split: 'flex-row items-center justify-between',
17
+ },
18
+ },
19
+
20
+ defaultVariants: { layout: 'stack' },
21
+ })
22
+
23
+ export interface FieldContext {
24
+ id: string
25
+ labelId: string
26
+ messageId: string
27
+ }
28
+
29
+ export interface FieldControl {
30
+ id: string
31
+ 'aria-labelledby': string
32
+ 'aria-invalid': 'true' | undefined
33
+ 'aria-describedby': string
34
+ }
35
+
36
+ const Context = createContext<FieldContext | null>(null)
37
+
38
+ export interface FieldProps {
39
+ invalid?: boolean
40
+ layout?: 'stack' | 'inline' | 'split'
41
+ className?: string
42
+ children?: ReactNode | ((args: { control: FieldControl }) => ReactNode)
43
+ }
44
+
45
+ export function Field({ invalid = false, layout, className, children }: FieldProps) {
46
+ const id = useId()
47
+ const ctx: FieldContext = {
48
+ id,
49
+ labelId: `${id}-label`,
50
+ messageId: `${id}-message`,
51
+ }
52
+
53
+ const control: FieldControl = {
54
+ 'id': id,
55
+ 'aria-labelledby': ctx.labelId,
56
+ 'aria-invalid': invalid ? 'true' : undefined,
57
+ 'aria-describedby': ctx.messageId,
58
+ }
59
+
60
+ return (
61
+ <Context value={ctx}>
62
+ <div className={field({ layout, className })}>
63
+ {typeof children === 'function' ? children({ control }) : children}
64
+ </div>
65
+ </Context>
66
+ )
67
+ }
68
+
69
+ export function FieldLabel({ className, children }: { className?: string, children?: ReactNode }) {
70
+ const ctx = use(Context)
71
+
72
+ return (
73
+ <Label
74
+ id={ctx?.labelId}
75
+ htmlFor={ctx?.id}
76
+ className={tv({ base: 'group-has-disabled:text-fg-disabled' })({ className })}
77
+ >
78
+ {children}
79
+ </Label>
80
+ )
81
+ }
82
+
83
+ export function FieldHelp({ className, children }: { className?: string, children?: ReactNode }) {
84
+ const ctx = use(Context)
85
+
86
+ return (
87
+ <p
88
+ id={ctx?.messageId}
89
+ className={tv({ base: 'basis-full text-caption text-fg-subtle'
90
+ + ' group-has-disabled:text-fg-disabled' })({ className })}
91
+ >
92
+ {children}
93
+ </p>
94
+ )
95
+ }
96
+
97
+ export interface FieldErrorProps {
98
+ items?: string | string[]
99
+ className?: string
100
+ children?: ReactNode
101
+ }
102
+
103
+ export function FieldError({ items, className, children }: FieldErrorProps) {
104
+ const ctx = use(Context)
105
+ const list = items == null ? [] : Array.isArray(items) ? items : [items]
106
+
107
+ return (
108
+ <p
109
+ id={ctx?.messageId}
110
+ className={tv({ base: 'basis-full text-caption text-danger'
111
+ + ' flex items-start gap-tight' })({ className })}
112
+ >
113
+ <CircleAlert className="mt-0.5 size-4 shrink-0" />
114
+ <span className="flex min-w-0 flex-col gap-0.5">
115
+ {children ?? list.map(t => <span key={t}>{t}</span>)}
116
+ </span>
117
+ </p>
118
+ )
119
+ }
120
+
121
+ export function FieldPending({ className, children }: { className?: string, children?: ReactNode }) {
122
+ const ctx = use(Context)
123
+
124
+ return (
125
+ <p
126
+ id={ctx?.messageId}
127
+ className={tv({ base: 'basis-full text-caption text-fg-subtle'
128
+ + ' flex items-start gap-tight' })({ className })}
129
+ >
130
+ <LoaderCircle className="mt-0.5 size-4 shrink-0 animate-spin motion-reduce:animate-none" />
131
+ <span className="min-w-0">{children}</span>
132
+ </p>
133
+ )
134
+ }
135
+
136
+ export function FieldSuccess({ className, children }: { className?: string, children?: ReactNode }) {
137
+ const ctx = use(Context)
138
+
139
+ return (
140
+ <p
141
+ id={ctx?.messageId}
142
+ className={tv({ base: 'basis-full text-caption text-success'
143
+ + ' flex items-start gap-tight' })({ className })}
144
+ >
145
+ <CircleCheck className="mt-0.5 size-4 shrink-0" />
146
+ <span className="min-w-0">{children}</span>
147
+ </p>
148
+ )
149
+ }
@@ -0,0 +1,35 @@
1
+ # Input
2
+
3
+ ```
4
+ 받기 npx @pantoia/ui add input
5
+ 층 ui
6
+ 파일 input/index.tsx · input/AGENTS.md
7
+ ```
8
+
9
+ ## 부위
10
+
11
+ | 이름 | props |
12
+ |---|---|
13
+ | `Input` | `InputProps` |
14
+
15
+ ## props
16
+
17
+ ### `InputProps`
18
+
19
+ `Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>` 를 확장한다.
20
+
21
+ | 이름 | 타입 | 필수 |
22
+ |---|---|---|
23
+ | `surface` | `'bare' \| 'well'` | |
24
+ | `size` | `'sm' \| 'md' \| 'lg'` | |
25
+
26
+ ## 변형 — `input`
27
+
28
+ | 축 | 값 | 기본 |
29
+ |---|---|---|
30
+ | `surface` | `bare` · `well` | `bare` |
31
+ | `size` | `sm` · `md` · `lg` | `md` |
32
+
33
+ ## 함께 받는 것
34
+
35
+ `tv`
@@ -0,0 +1,36 @@
1
+ import type { InputHTMLAttributes } from 'react'
2
+
3
+ import { tv } from '../tv'
4
+
5
+ export const input = tv({
6
+ base: 'w-full rounded-r8 text-value text-fg'
7
+ + ' placeholder:text-fg-placeholder'
8
+ + ' invalid:ring-2 invalid:ring-danger'
9
+ + ' read-only:text-fg-muted'
10
+ + ' transition-state'
11
+ + ' focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
12
+ + ' disabled:bg-disabled disabled:text-fg-disabled disabled:cursor-not-allowed',
13
+
14
+ variants: {
15
+ surface: {
16
+ bare: 'bg-transparent ring-1 ring-inset ring-line-strong read-only:bg-well',
17
+ well: 'bg-well',
18
+ },
19
+ size: {
20
+ sm: 'h-8 px-3',
21
+ md: 'h-9 px-3',
22
+ lg: 'h-10 px-3 text-t16',
23
+ },
24
+ },
25
+
26
+ defaultVariants: { surface: 'bare', size: 'md' },
27
+ })
28
+
29
+ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
30
+ surface?: 'bare' | 'well'
31
+ size?: 'sm' | 'md' | 'lg'
32
+ }
33
+
34
+ export function Input({ surface, size, className, ...rest }: InputProps) {
35
+ return <input {...rest} className={input({ surface, size, className })} />
36
+ }
package/kbd/AGENTS.md ADDED
@@ -0,0 +1,17 @@
1
+ # Kbd
2
+
3
+ ```
4
+ 받기 npx @pantoia/ui add kbd
5
+ 층 ui
6
+ 파일 kbd/index.tsx · kbd/AGENTS.md
7
+ ```
8
+
9
+ ## 부위
10
+
11
+ | 이름 | props |
12
+ |---|---|
13
+ | `Kbd` | `HTMLAttributes<HTMLElement>` |
14
+
15
+ ## 함께 받는 것
16
+
17
+ `tv`
package/kbd/index.tsx ADDED
@@ -0,0 +1,13 @@
1
+ import type { HTMLAttributes } from 'react'
2
+
3
+ import { tv } from '../tv'
4
+
5
+ export function Kbd({ className, ...rest }: HTMLAttributes<HTMLElement>) {
6
+ return (
7
+ <kbd
8
+ {...rest}
9
+ className={tv({ base: 'bg-well text-fg-muted text-micro rounded-mark inline-flex items-center'
10
+ + ' border border-line px-1.5 py-0.5 font-mono' })({ className })}
11
+ />
12
+ )
13
+ }
@@ -0,0 +1,21 @@
1
+ # Label
2
+
3
+ ```
4
+ 받기 npx @pantoia/ui add label
5
+ 층 ui
6
+ 파일 label/index.tsx · label/AGENTS.md
7
+ ```
8
+
9
+ ## 부위
10
+
11
+ | 이름 | props |
12
+ |---|---|
13
+ | `Label` | `ComponentProps<typeof LabelPrimitive.Root>` |
14
+
15
+ ## 함께 받는 것
16
+
17
+ `tv`
18
+
19
+ ## npm 의존
20
+
21
+ `radix-ui`
@@ -0,0 +1,13 @@
1
+ import { Label as LabelPrimitive } from 'radix-ui'
2
+ import type { ComponentProps } from 'react'
3
+
4
+ import { tv } from '../tv'
5
+
6
+ export function Label({ className, ...rest }: ComponentProps<typeof LabelPrimitive.Root>) {
7
+ return (
8
+ <LabelPrimitive.Root
9
+ {...rest}
10
+ className={tv({ base: 'text-label text-fg' })({ className })}
11
+ />
12
+ )
13
+ }
@@ -0,0 +1,53 @@
1
+ # NumberField
2
+
3
+ ```
4
+ 받기 npx @pantoia/ui add number-field
5
+ 층 ui
6
+ 파일 number-field/index.tsx · number-field/AGENTS.md
7
+ ```
8
+
9
+ ## 부위
10
+
11
+ | 이름 | props |
12
+ |---|---|
13
+ | `NumberField` | `NumberFieldProps` |
14
+
15
+ ## props
16
+
17
+ ### `NumberFieldProps`
18
+
19
+ `Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'defaultValue' | 'onChange' | 'size' | 'min' | 'max' | 'step'>` 를 확장한다.
20
+
21
+ | 이름 | 타입 | 필수 |
22
+ |---|---|---|
23
+ | `value` | `number \| undefined` | |
24
+ | `defaultValue` | `number` | |
25
+ | `onValueChange` | `(value: number \| undefined) => void` | |
26
+ | `decrementLabel` | `string` | 예 |
27
+ | `incrementLabel` | `string` | 예 |
28
+ | `min` | `number` | |
29
+ | `max` | `number` | |
30
+ | `step` | `number` | |
31
+ | `surface` | `'bare' \| 'well'` | |
32
+ | `size` | `'sm' \| 'md' \| 'lg'` | |
33
+ | `disabled` | `boolean` | |
34
+ | `readOnly` | `boolean` | |
35
+ | `className` | `string` | |
36
+
37
+ ## 변형 — `numberField`
38
+
39
+ 슬롯 `root` · `step` · `input`
40
+
41
+ | 축 | 값 | 기본 |
42
+ |---|---|---|
43
+ | `surface` | `bare` · `well` | `bare` |
44
+ | `size` | `sm` · `md` · `lg` | `md` |
45
+ | `readonly` | `true` | — |
46
+
47
+ ## 함께 받는 것
48
+
49
+ `tv`
50
+
51
+ ## npm 의존
52
+
53
+ `lucide-react`
@@ -0,0 +1,227 @@
1
+ 'use client'
2
+
3
+ import { Minus, Plus } from 'lucide-react'
4
+ import { useEffect, useRef, useState, type InputHTMLAttributes, type PointerEvent as ReactPointerEvent, type KeyboardEvent as ReactKeyboardEvent } from 'react'
5
+
6
+ import { tv } from '../tv'
7
+
8
+ export const numberField = tv({
9
+ slots: {
10
+ root: 'flex items-center rounded-r8'
11
+ + ' transition-state'
12
+ + ' invalid-within:ring-2 invalid-within:ring-danger'
13
+ + ' has-disabled:bg-disabled has-disabled:cursor-not-allowed'
14
+ + ' has-focus-visible:outline-2 has-focus-visible:outline-offset-2'
15
+ + ' has-focus-visible:outline-ring',
16
+ step: 'text-fg-muted grid shrink-0 place-items-center rounded-r8'
17
+ + ' transition-state press press-shrink enabled:hover:bg-hover'
18
+ + ' disabled:text-fg-disabled disabled:cursor-not-allowed',
19
+ input: 'text-value text-fg w-full min-w-0 bg-transparent text-center outline-none'
20
+ + ' disabled:text-fg-disabled disabled:cursor-not-allowed',
21
+ },
22
+
23
+ variants: {
24
+ surface: {
25
+ bare: { root: 'bg-transparent ring-1 ring-inset ring-line-strong' },
26
+ well: { root: 'bg-well' },
27
+ },
28
+ size: {
29
+ sm: { root: 'h-8', step: 'size-8' },
30
+ md: { root: 'h-9', step: 'size-9' },
31
+ lg: { root: 'h-10', step: 'size-10', input: 'text-t16' },
32
+ },
33
+ readonly: {
34
+ true: { root: 'bg-well', input: 'text-fg-muted' },
35
+ },
36
+ },
37
+
38
+ defaultVariants: { surface: 'bare', size: 'md' },
39
+ })
40
+
41
+ const HOLD_DELAY = 400
42
+ const HOLD_EVERY = 60
43
+
44
+ const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n))
45
+
46
+ const GROUPED = new Intl.NumberFormat('en-US')
47
+
48
+ const TYPING = /[^0-9.-]/g
49
+
50
+ function snap(n: number, min: number, step: number) {
51
+ const from = Number.isFinite(min) ? min : 0
52
+ return from + Math.round((n - from) / step) * step
53
+ }
54
+
55
+ function useHold(step: () => void, blocked: boolean) {
56
+ const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
57
+ const beat = useRef<ReturnType<typeof setInterval> | undefined>(undefined)
58
+ const [pressed, setPressed] = useState(false)
59
+
60
+ const stop = () => {
61
+ clearTimeout(timer.current)
62
+ clearInterval(beat.current)
63
+ setPressed(false)
64
+ }
65
+
66
+ useEffect(() => stop, [])
67
+
68
+ const onPointerDown = (e: ReactPointerEvent<HTMLButtonElement>) => {
69
+ if (e.button !== 0 || blocked) return
70
+
71
+ step()
72
+ setPressed(true)
73
+ e.currentTarget.setPointerCapture(e.pointerId)
74
+
75
+ timer.current = setTimeout(() => {
76
+ beat.current = setInterval(step, HOLD_EVERY)
77
+ }, HOLD_DELAY)
78
+ }
79
+
80
+ return { pressed, onPointerDown, onPointerUp: stop, onPointerCancel: stop, onLostPointerCapture: stop }
81
+ }
82
+
83
+ export interface NumberFieldProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'defaultValue' | 'onChange' | 'size' | 'min' | 'max' | 'step'> {
84
+ value?: number | undefined
85
+ defaultValue?: number
86
+ onValueChange?: (value: number | undefined) => void
87
+ decrementLabel: string
88
+ incrementLabel: string
89
+ min?: number
90
+ max?: number
91
+ step?: number
92
+ surface?: 'bare' | 'well'
93
+ size?: 'sm' | 'md' | 'lg'
94
+ disabled?: boolean
95
+ readOnly?: boolean
96
+ className?: string
97
+ }
98
+
99
+ export function NumberField(props: NumberFieldProps) {
100
+ const {
101
+ value,
102
+ defaultValue,
103
+ onValueChange,
104
+ decrementLabel,
105
+ incrementLabel,
106
+ min = Number.NEGATIVE_INFINITY,
107
+ max = Number.POSITIVE_INFINITY,
108
+ step = 1,
109
+ surface,
110
+ size,
111
+ disabled = false,
112
+ readOnly = false,
113
+ className,
114
+ ...rest
115
+ } = props
116
+
117
+ const controlled = 'value' in props
118
+ const [own, setOwn] = useState<number | undefined>(defaultValue)
119
+ const [typing, setTyping] = useState<string | null>(null)
120
+ const current = controlled ? value : own
121
+
122
+ const ui = numberField({ surface, size, readonly: readOnly })
123
+
124
+ const set = (next: number | undefined) => {
125
+ const after = next === undefined ? undefined : clamp(next, min, max)
126
+ if (!controlled) setOwn(after)
127
+ if (after !== current) onValueChange?.(after)
128
+ }
129
+
130
+ const blocked = disabled || readOnly
131
+ const from = current ?? (Number.isFinite(min) ? min : 0)
132
+ const atMin = current !== undefined && current <= min
133
+ const atMax = current !== undefined && current >= max
134
+
135
+ const down = useHold(() => { setTyping(null); set(from - step) }, blocked)
136
+ const up = useHold(() => { setTyping(null); set(from + step) }, blocked)
137
+
138
+ const onKeyDown = (e: ReactKeyboardEvent<HTMLInputElement>) => {
139
+ if (blocked) return
140
+
141
+ const moves: Record<string, number> = {
142
+ ArrowUp: from + step,
143
+ ArrowDown: from - step,
144
+ PageUp: from + step * 10,
145
+ PageDown: from - step * 10,
146
+ Home: min,
147
+ End: max,
148
+ }
149
+
150
+ const next = moves[e.key]
151
+ if (next === undefined || !Number.isFinite(next)) return
152
+
153
+ e.preventDefault()
154
+ setTyping(null)
155
+ set(next)
156
+ }
157
+
158
+ const onWheel = (e: React.WheelEvent<HTMLInputElement>) => {
159
+ e.currentTarget.blur()
160
+ }
161
+
162
+ const onInput = (e: React.ChangeEvent<HTMLInputElement>) => {
163
+ const raw = e.target.value.replace(TYPING, '')
164
+ setTyping(min >= 0 ? raw.replace(/-/g, '') : raw)
165
+ }
166
+
167
+ const onBlur = () => {
168
+ if (typing === null) return
169
+
170
+ const parsed = Number(typing)
171
+ const empty = typing.trim() === ''
172
+ setTyping(null)
173
+
174
+ if (empty) set(undefined)
175
+ else if (Number.isFinite(parsed)) set(snap(parsed, min, step))
176
+ }
177
+
178
+ return (
179
+ <div className={ui.root({ className })}>
180
+ <button
181
+ type="button"
182
+ aria-label={decrementLabel}
183
+ disabled={blocked || atMin}
184
+ data-pressed={down.pressed ? '' : undefined}
185
+ className={ui.step()}
186
+ onPointerDown={down.onPointerDown}
187
+ onPointerUp={down.onPointerUp}
188
+ onPointerCancel={down.onPointerCancel}
189
+ onLostPointerCapture={down.onLostPointerCapture}
190
+ >
191
+ <Minus className="size-4" />
192
+ </button>
193
+
194
+ <input
195
+ {...rest}
196
+ type="text"
197
+ inputMode="decimal"
198
+ role="spinbutton"
199
+ aria-valuenow={current}
200
+ aria-valuemin={Number.isFinite(min) ? min : undefined}
201
+ aria-valuemax={Number.isFinite(max) ? max : undefined}
202
+ value={typing ?? (current === undefined ? '' : GROUPED.format(current))}
203
+ disabled={disabled}
204
+ readOnly={readOnly}
205
+ onChange={onInput}
206
+ onBlur={onBlur}
207
+ onKeyDown={onKeyDown}
208
+ onWheel={onWheel}
209
+ className={ui.input()}
210
+ />
211
+
212
+ <button
213
+ type="button"
214
+ aria-label={incrementLabel}
215
+ disabled={blocked || atMax}
216
+ data-pressed={up.pressed ? '' : undefined}
217
+ className={ui.step()}
218
+ onPointerDown={up.onPointerDown}
219
+ onPointerUp={up.onPointerUp}
220
+ onPointerCancel={up.onPointerCancel}
221
+ onLostPointerCapture={up.onLostPointerCapture}
222
+ >
223
+ <Plus className="size-4" />
224
+ </button>
225
+ </div>
226
+ )
227
+ }
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@pantoia/ui",
3
+ "version": "0.1.0",
4
+ "description": "Pantoia 디자인 시스템 — 소스를 복사해 가는 컴포넌트 스물여덟",
5
+ "homepage": "https://design.pantoia.com",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/pantoia/pantoia-design.git",
9
+ "directory": "packages/registry"
10
+ },
11
+ "license": "MIT",
12
+ "type": "module",
13
+ "bin": {
14
+ "pantoia-ui": "./dist/index.js"
15
+ },
16
+ "files": [
17
+ "*/index.tsx",
18
+ "*/index.module.css",
19
+ "*/AGENTS.md",
20
+ "dist",
21
+ "tv.ts",
22
+ "test-utils.ts",
23
+ "styles.css",
24
+ "press.css",
25
+ "registry.json"
26
+ ],
27
+ "exports": {
28
+ "./tv": "./tv.ts",
29
+ "./styles.css": "./styles.css",
30
+ "./press.css": "./press.css",
31
+ "./registry.json": "./registry.json",
32
+ "./test-utils": "./test-utils.ts",
33
+ "./*": "./*/index.tsx"
34
+ },
35
+ "sideEffects": [
36
+ "*.css"
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "engines": {
42
+ "node": ">=24"
43
+ },
44
+ "scripts": {
45
+ "prepack": "tsc cli/index.ts --ignoreConfig --outDir dist --target es2022 --module esnext --moduleResolution bundler --skipLibCheck --types node",
46
+ "lint": "eslint . --max-warnings 0",
47
+ "typecheck": "tsc --noEmit"
48
+ },
49
+ "dependencies": {
50
+ "@fontsource/jetbrains-mono": "5.3.0",
51
+ "lucide-react": "^1.39.0",
52
+ "pretendard": "1.3.9",
53
+ "radix-ui": "^1.6.7",
54
+ "sonner": "^2.0.8",
55
+ "tailwind-variants": "^3.3.1"
56
+ },
57
+ "peerDependencies": {
58
+ "react": "^19",
59
+ "react-dom": "^19",
60
+ "tailwindcss": "^4.3.3"
61
+ },
62
+ "devDependencies": {
63
+ "@pantoia/eslint-config": "*",
64
+ "@pantoia/typescript-config": "*",
65
+ "@types/node": "26.4.0",
66
+ "@types/react": "19.2.18",
67
+ "@types/react-dom": "19.2.5",
68
+ "eslint": "10.9.1",
69
+ "react": "19.2.8",
70
+ "react-dom": "19.2.8",
71
+ "typescript": "6.0.3"
72
+ }
73
+ }