@pantoia/ui 0.6.0 → 0.7.1

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.
package/README.md CHANGED
@@ -19,21 +19,17 @@ npx @pantoia/ui diff 복사한 소스와 배포본의 차이
19
19
 
20
20
  ## 개발
21
21
 
22
- 호스트에는 Docker만 있으면 된다.
22
+ Node 20.11 이상이 필요하다.
23
23
 
24
24
  ```
25
25
  git clone git@github.com:pantoia/pantoia-ui.git
26
26
  cd pantoia-ui
27
- docker compose up -d dev
27
+ npm install
28
28
  ```
29
29
 
30
- 편집기는 `.devcontainer/devcontainer.json`으로 컨테이너 안에서 연다.
31
-
32
30
  ```
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
31
+ npm run verify typecheck, check, lint, test
32
+ npm pack tarball
37
33
  ```
38
34
 
39
35
  ## 구조
@@ -10,6 +10,13 @@ React 디자인 시스템이다. Base UI 위에 스타일을 얹고 Tailwind CSS
10
10
 
11
11
  ## 설치
12
12
 
13
+ ```
14
+ Node 20.11 이상
15
+ react 19
16
+ react-dom 19
17
+ tailwindcss 4.0 이상. Tabs의 스크롤바 모양은 4.3부터
18
+ ```
19
+
13
20
  ```sh
14
21
  npx @pantoia/ui init
15
22
  ```
@@ -25,25 +32,28 @@ init은 컴포넌트를 저장할 위치와 전역 CSS 파일을 묻고 `compone
25
32
  다크 모드는 `.dark` 클래스로 켠다. `html`이나 `body`에 `.dark`를 두면 그 안의 토큰이 모두 바뀐다.
26
33
  next-themes를 쓸 때는 `attribute="class"`로 둔다.
27
34
 
35
+ React Router 프레임워크 모드는 `react-router.config.ts`에 `future: { unstable_optimizeDeps: true }`를 더한다.
36
+ 없으면 개발 서버에서 처음 여는 route마다 페이지가 새로고침된다.
37
+
28
38
  ## 쓰는 법
29
39
 
30
- 컴포넌트는 폴더 이름으로 가져온다.
40
+ 컴포넌트는 `add`로 프로젝트에 복사해서 쓴다. `components.json`의 `path` 아래에 `<이름>/index.tsx`로 들어가고, 함께 필요한 파일도 복사된다.
31
41
 
32
- ```tsx
33
- import { Button } from '@pantoia/ui/button'
34
- import { Field, FieldLabel, FieldHelp } from '@pantoia/ui/field'
42
+ ```sh
43
+ npx @pantoia/ui add button field
35
44
  ```
36
45
 
37
- 쓰는 방식은 2가지다.
38
-
39
- ```
40
- 패키지로 쓴다 import해서 쓴다. 버전을 올리면 고친 내용이 따라온다
41
- 복사해서 쓴다 npx @pantoia/ui add로 소스를 가져와 프로젝트 안에서 고친다
46
+ ```tsx
47
+ import { Button } from '@/components/ui/button'
48
+ import { Field, FieldLabel, FieldHelp } from '@/components/ui/field'
42
49
  ```
43
50
 
44
- 기본은 패키지로 쓰는 것이다. 동작이나 마크업을 프로젝트에 맞게 바꿔야 할 때만 복사한다.
51
+ import 경로는 저장 위치와 프로젝트의 경로 별칭을 따른다. 위는 저장 위치가 `components/ui`이고 별칭이 `@/`인 예다.
52
+ `@pantoia/ui/<이름>`에서 컴포넌트를 import하지 않는다. Tailwind가 `node_modules`를 검사하지 않아서 스타일이 빠진다.
45
53
 
46
54
  제어 속성은 Base UI 규약을 따른다. `value`와 `defaultValue`, `onValueChange`를 짝으로 쓴다.
55
+ 고르지 않은 상태는 `null`이다. Select, RadioGroup, Combobox, NumberField가 같다.
56
+ Select와 RadioGroup의 state는 `useState<string | null>`로 둔다. 문자열만 받는 setter를 `onValueChange`에 넘기면 타입 오류가 난다.
47
57
  컴포넌트마다의 속성과 이벤트는 https://design.pantoia.com 의 각 페이지에 있다.
48
58
 
49
59
  ## 디자인 규칙
@@ -119,6 +129,8 @@ px 직접 지정 간격과 둥글기는 토큰으로 쓴다
119
129
  @utility raised { @apply bg-bg-panel shadow-s2; }
120
130
  ```
121
131
 
132
+ 토큰 이름을 더했으면 저장 위치의 `tv.ts`에 있는 `twMergeConfig`의 그룹 목록에도 더한다. 빠지면 className으로 덮어쓸 때 겹치는 클래스가 함께 남는다.
133
+
122
134
  색 역할을 더하려면 팔레트 10단계를 양쪽 모드에 적고, 토큰 6개를 만든다.
123
135
 
124
136
  ```
@@ -139,34 +151,34 @@ className 그 한 곳만 바뀐다
139
151
 
140
152
  `variant`는 tailwind-variants로 만든 스타일 축이다. `-`는 축이 없다는 뜻이다.
141
153
 
142
- | 컴포넌트 | import | 하위 컴포넌트 | variant |
154
+ | 컴포넌트 | add | 하위 컴포넌트 | variant |
143
155
  |---|---|---|---|
144
- | Accordion | @pantoia/ui/accordion | Accordion, AccordionItem, AccordionTrigger, AccordionContent | - |
145
- | AlertDialog | @pantoia/ui/alert-dialog | AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogTitle, AlertDialogDescription, AlertDialogHeader, AlertDialogFooter, AlertDialogAction, AlertDialogCancel | - |
146
- | Badge | @pantoia/ui/badge | Badge | color, shape |
147
- | Button | @pantoia/ui/button | Button | color, emphasis, size, shape |
148
- | Callout | @pantoia/ui/callout | Callout | color |
149
- | Checkbox | @pantoia/ui/checkbox | CheckboxGroup, Checkbox | color |
150
- | Combobox | @pantoia/ui/combobox | Combobox, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxCancel, ComboboxContent, ComboboxViewport, ComboboxCollection, ComboboxItem, ComboboxItemText, ComboboxItemDescription, ComboboxGroup, ComboboxEmpty, ComboboxSeparator | surface, size |
151
- | CommandPalette | @pantoia/ui/command-palette | CommandPalette | - |
152
- | Dialog | @pantoia/ui/dialog | Dialog, DialogTrigger, DialogClose, DialogContent, DialogTitle, DialogDescription, DialogHeader, DialogFooter | size |
153
- | Drawer | @pantoia/ui/drawer | Drawer, DrawerTrigger, DrawerClose, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription | side |
154
- | DropdownMenu | @pantoia/ui/dropdown-menu | DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent | color |
155
- | Field | @pantoia/ui/field | Field, FieldLabel, FieldHelp, FieldError, FieldPending, FieldSuccess | layout |
156
- | Input | @pantoia/ui/input | Input | surface, size |
157
- | Kbd | @pantoia/ui/kbd | Kbd | - |
158
- | Label | @pantoia/ui/label | Label | - |
159
- | NumberField | @pantoia/ui/number-field | NumberField | surface, size, readonly |
160
- | Popover | @pantoia/ui/popover | Popover, PopoverTrigger, PopoverAnchor, PopoverClose, PopoverContent | - |
161
- | RadioGroup | @pantoia/ui/radio-group | RadioGroup, RadioGroupItem, RadioGroupItemLabel, RadioGroupItemDescription | color |
162
- | Select | @pantoia/ui/select | Select, SelectGroup, SelectItem, SelectItemText, SelectItemDescription, SelectSeparator | surface, size |
163
- | Switch | @pantoia/ui/switch | Switch | color |
164
- | Table | @pantoia/ui/table | Table, TableHeader, TableHeadCell, TableBody, TableRow, TableCell | fixed, mono |
165
- | Tabs | @pantoia/ui/tabs | Tabs, TabsList, TabsTrigger, TabsContent | - |
166
- | Textarea | @pantoia/ui/textarea | Textarea | surface, size |
167
- | Toast | @pantoia/ui/toast | Toast | - |
168
- | Toggle | @pantoia/ui/toggle | Toggle | surface |
169
- | Tooltip | @pantoia/ui/tooltip | TooltipProvider, Tooltip, TooltipTrigger, TooltipContent | - |
156
+ | Accordion | accordion | Accordion, AccordionItem, AccordionTrigger, AccordionContent | - |
157
+ | AlertDialog | alert-dialog | AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogTitle, AlertDialogDescription, AlertDialogHeader, AlertDialogFooter, AlertDialogAction, AlertDialogCancel | - |
158
+ | Badge | badge | Badge | color, shape |
159
+ | Button | button | Button | color, emphasis, size, shape |
160
+ | Callout | callout | Callout | color |
161
+ | Checkbox | checkbox | CheckboxGroup, Checkbox | color |
162
+ | Combobox | combobox | Combobox, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxCancel, ComboboxContent, ComboboxViewport, ComboboxCollection, ComboboxItem, ComboboxItemText, ComboboxItemDescription, ComboboxGroup, ComboboxEmpty, ComboboxSeparator | surface, size |
163
+ | CommandPalette | command-palette | CommandPalette | - |
164
+ | Dialog | dialog | Dialog, DialogTrigger, DialogClose, DialogContent, DialogTitle, DialogDescription, DialogHeader, DialogFooter | size |
165
+ | Drawer | drawer | Drawer, DrawerTrigger, DrawerClose, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription | side |
166
+ | DropdownMenu | dropdown-menu | DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent | color |
167
+ | Field | field | Field, FieldLabel, FieldHelp, FieldError, FieldPending, FieldSuccess | layout |
168
+ | Input | input | Input | surface, size |
169
+ | Kbd | kbd | Kbd | - |
170
+ | Label | label | Label | - |
171
+ | NumberField | number-field | NumberField | surface, size, readonly |
172
+ | Popover | popover | Popover, PopoverTrigger, PopoverAnchor, PopoverClose, PopoverContent | - |
173
+ | RadioGroup | radio-group | RadioGroup, RadioGroupItem, RadioGroupItemLabel, RadioGroupItemDescription | color |
174
+ | Select | select | Select, SelectGroup, SelectItem, SelectItemText, SelectItemDescription, SelectSeparator | surface, size |
175
+ | Switch | switch | Switch | color |
176
+ | Table | table | Table, TableHeader, TableHeadCell, TableBody, TableRow, TableCell | fixed, mono |
177
+ | Tabs | tabs | Tabs, TabsList, TabsTrigger, TabsContent | - |
178
+ | Textarea | textarea | Textarea | surface, size |
179
+ | Toast | toast | Toast | - |
180
+ | Toggle | toggle | Toggle | surface |
181
+ | Tooltip | tooltip | TooltipProvider, Tooltip, TooltipTrigger, TooltipContent | - |
170
182
 
171
183
  컴포넌트를 고를 때 헷갈리는 짝은 이렇다.
172
184
 
@@ -183,14 +195,19 @@ Tooltip과 Popover 짧은 설명이면 Tooltip, 안에서 조작하면 Po
183
195
 
184
196
  Field는 라벨, 도움말, 오류 메시지를 입력 컨트롤에 묶는다. Input, Textarea, NumberField, Select, Combobox와 함께 쓴다.
185
197
 
186
- 앱에 한 번만 두는 것이 2개다.
198
+ Select는 고른 항목의 `SelectItemText`나 글자를 트리거에 보인다. `SelectItem`을 다른 컴포넌트로 감싸면 `items`로 라벨을 넘긴다.
199
+ Combobox 항목이 `{ value, label }`이 아닌 객체면 `itemToStringLabel`로 `ComboboxInput`에 보일 글자를 정한다.
200
+
201
+ 앱에 한 번만 두는 것이 2개다. 플로팅 요소가 페이지의 z-index에 가려지지 않게 앱은 `isolate`로 감싼다.
187
202
 
188
203
  ```tsx
189
- import { Toast, toast } from '@pantoia/ui/toast'
190
- import { TooltipProvider } from '@pantoia/ui/tooltip'
204
+ import { Toast, toast } from '@/components/ui/toast'
205
+ import { TooltipProvider } from '@/components/ui/tooltip'
191
206
 
192
207
  <TooltipProvider>
193
- <App />
208
+ <div className="isolate">
209
+ <App />
210
+ </div>
194
211
  <Toast />
195
212
  </TooltipProvider>
196
213
  ```
package/field/index.tsx CHANGED
@@ -6,13 +6,13 @@ import { Label } from '../label'
6
6
  import { tv } from '../tv'
7
7
 
8
8
  export const field = tv({
9
- base: 'group flex flex-wrap gap-x-2 gap-y-tight',
9
+ base: 'group flex gap-x-2 gap-y-tight',
10
10
 
11
11
  variants: {
12
12
  layout: {
13
13
  stack: 'flex-col',
14
- inline: 'flex-row items-center',
15
- split: 'flex-row items-center justify-between',
14
+ inline: 'flex-row flex-wrap content-start items-center',
15
+ split: 'flex-row flex-wrap content-start items-center justify-between',
16
16
  },
17
17
  },
18
18
 
@@ -39,7 +39,7 @@ export function FieldLabel({ render = <Label />, className, ...rest }: FieldLabe
39
39
  }
40
40
 
41
41
  const fieldHelp = tv({
42
- base: 'basis-full text-caption text-fg-subtle'
42
+ base: 'w-full text-caption text-fg-subtle'
43
43
  + ' group-has-disabled:text-fg-disabled',
44
44
  })
45
45
 
@@ -57,7 +57,7 @@ export type FieldErrorProps = Omit<FieldPrimitive.Error.Props, 'className'> & {
57
57
  }
58
58
 
59
59
  const fieldError = tv({
60
- base: 'basis-full text-caption text-fg-danger'
60
+ base: 'w-full text-caption text-fg-danger'
61
61
  + ' flex items-start gap-tight',
62
62
  })
63
63
 
@@ -79,7 +79,7 @@ export function FieldError({ items, match = true, render = <p />, className, chi
79
79
  }
80
80
 
81
81
  const fieldPending = tv({
82
- base: 'basis-full text-caption text-fg-subtle'
82
+ base: 'w-full text-caption text-fg-subtle'
83
83
  + ' flex items-start gap-tight',
84
84
  })
85
85
 
@@ -97,7 +97,7 @@ export function FieldPending({ className, children, ...rest }: FieldPendingProps
97
97
  }
98
98
 
99
99
  const fieldSuccess = tv({
100
- base: 'basis-full text-caption text-fg-success'
100
+ base: 'w-full text-caption text-fg-success'
101
101
  + ' flex items-start gap-tight',
102
102
  })
103
103
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pantoia/ui",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Pantoia Design System",
5
5
  "homepage": "https://design.pantoia.com",
6
6
  "repository": {
@@ -48,9 +48,7 @@
48
48
  "check": "tsx scripts/check.ts",
49
49
  "lint": "oxlint --deny-warnings",
50
50
  "test": "vitest run",
51
- "verify": "npm run typecheck && npm run check && npm run lint && npm run test",
52
- "dev:up": "docker compose up -d dev",
53
- "dev:down": "docker compose down"
51
+ "verify": "npm run typecheck && npm run check && npm run lint && npm run test"
54
52
  },
55
53
  "dependencies": {
56
54
  "@base-ui/react": "^1.8.0",
@@ -46,7 +46,7 @@ export const radioGroupItem = tv({
46
46
  defaultVariants: { color: 'neutral' },
47
47
  })
48
48
 
49
- export type RadioGroupProps = Omit<RadioGroupPrimitive.Props<string>, 'className'> & {
49
+ export type RadioGroupProps = Omit<RadioGroupPrimitive.Props<string | null>, 'className'> & {
50
50
  color?: RadioColor
51
51
  className?: string
52
52
  }
@@ -56,7 +56,7 @@ const radioGroup = tv({ base: 'flex flex-col gap-3' })
56
56
  export function RadioGroup({ color = 'neutral', className, ...rest }: RadioGroupProps) {
57
57
  return (
58
58
  <ColorContext value={color}>
59
- <RadioGroupPrimitive<string> {...rest} className={radioGroup({ className })} />
59
+ <RadioGroupPrimitive<string | null> {...rest} className={radioGroup({ className })} />
60
60
  </ColorContext>
61
61
  )
62
62
  }
package/select/index.tsx CHANGED
@@ -51,12 +51,16 @@ function findItemText(children: ReactNode): { label: ReactNode } | undefined {
51
51
  return found
52
52
  }
53
53
 
54
+ function plainText(children: ReactNode) {
55
+ return typeof children === 'string' || typeof children === 'number' ? children : undefined
56
+ }
57
+
54
58
  function collectItems(children: ReactNode, items: Record<string, ReactNode>) {
55
59
  Children.forEach(children, child => {
56
60
  if (!isValidElement<ItemElementProps>(child)) return
57
61
  if (child.type === SelectItem && child.props.value !== undefined) {
58
- const text = findItemText(child.props.children)
59
- if (text) items[child.props.value] = text.label
62
+ const label = findItemText(child.props.children)?.label ?? plainText(child.props.children)
63
+ if (label !== undefined) items[child.props.value] = label
60
64
  return
61
65
  }
62
66
  collectItems(child.props.children, items)
@@ -67,9 +71,9 @@ function collectItems(children: ReactNode, items: Record<string, ReactNode>) {
67
71
  export type SelectProps = Omit<ComponentProps<typeof SelectPrimitive.Trigger>, 'className' | 'value' | 'defaultValue'>
68
72
  & Pick<SelectPrimitive.Root.Props<string>, 'open' | 'defaultOpen' | 'onOpenChange' | 'items' | 'name' | 'required' | 'readOnly' | 'modal' | 'form'>
69
73
  & {
70
- value?: string
71
- defaultValue?: string
72
- onValueChange?: (value: string, eventDetails: SelectPrimitive.Root.ChangeEventDetails) => void
74
+ value?: string | null
75
+ defaultValue?: string | null
76
+ onValueChange?: (value: string | null, eventDetails: SelectPrimitive.Root.ChangeEventDetails) => void
73
77
  placeholder?: string
74
78
  surface?: 'bare' | 'well'
75
79
  size?: 'sm' | 'md' | 'lg'
@@ -118,7 +122,7 @@ export function Select({
118
122
  <SelectPrimitive.Root<string>
119
123
  value={value}
120
124
  defaultValue={defaultValue}
121
- onValueChange={(next, eventDetails) => onValueChange?.(next ?? '', eventDetails)}
125
+ onValueChange={onValueChange}
122
126
  open={open}
123
127
  defaultOpen={defaultOpen}
124
128
  onOpenChange={(next, eventDetails) => {