@pantoia/ui 0.4.0 → 0.6.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.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # Pantoia UI
2
+
3
+ React 디자인 시스템 패키지. 문서는 `design.pantoia.com`
4
+
5
+ ## 사용
6
+
7
+ ```
8
+ npx @pantoia/ui init components.json을 만든다
9
+ npx @pantoia/ui add button components/ui/button/에 소스를 복사한다
10
+ npx @pantoia/ui list 컴포넌트 목록
11
+ npx @pantoia/ui diff 복사한 소스와 배포본의 차이
12
+ ```
13
+
14
+ `init`이 전역 CSS에 아래 줄을 추가한다.
15
+
16
+ ```css
17
+ @import "@pantoia/ui/tailwind.css";
18
+ ```
19
+
20
+ ## 개발
21
+
22
+ 호스트에는 Docker만 있으면 된다.
23
+
24
+ ```
25
+ git clone git@github.com:pantoia/pantoia-ui.git
26
+ cd pantoia-ui
27
+ docker compose up -d dev
28
+ ```
29
+
30
+ 편집기는 `.devcontainer/devcontainer.json`으로 컨테이너 안에서 연다.
31
+
32
+ ```
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
37
+ ```
38
+
39
+ ## 구조
40
+
41
+ ```
42
+ <이름>/ 컴포넌트
43
+ cli/ CLI
44
+ docs/decisions/ 결정 기록
45
+ .claude/rules/ 일하는 규칙
46
+ ```
@@ -0,0 +1,211 @@
1
+ ---
2
+ name: pantoia-ui
3
+ description: PANTOIA Design System(@pantoia/ui)으로 화면을 만들거나 고칠 때 쓴다. 컴포넌트 고르기, 토큰과 클래스, 커스텀 값 추가, 컴포넌트 내려받기를 다룬다
4
+ ---
5
+
6
+ # @pantoia/ui
7
+
8
+ React 디자인 시스템이다. Base UI 위에 스타일을 얹고 Tailwind CSS 4로 토큰을 노출한다.
9
+ 사람이 보는 문서는 https://design.pantoia.com 에 있다.
10
+
11
+ ## 설치
12
+
13
+ ```sh
14
+ npx @pantoia/ui init
15
+ ```
16
+
17
+ init은 컴포넌트를 저장할 위치와 전역 CSS 파일을 묻고 `components.json`을 만든다.
18
+ 글꼴 패키지 `pretendard`, `@fontsource/jetbrains-mono`는 설치할지 묻는다.
19
+ 전역 CSS에는 `@import "tailwindcss";` 바로 아래에 이 줄이 들어간다.
20
+
21
+ ```css
22
+ @import "@pantoia/ui/tailwind.css";
23
+ ```
24
+
25
+ 다크 모드는 `.dark` 클래스로 켠다. `html`이나 `body`에 `.dark`를 두면 그 안의 토큰이 모두 바뀐다.
26
+ next-themes를 쓸 때는 `attribute="class"`로 둔다.
27
+
28
+ ## 쓰는 법
29
+
30
+ 컴포넌트는 폴더 이름으로 가져온다.
31
+
32
+ ```tsx
33
+ import { Button } from '@pantoia/ui/button'
34
+ import { Field, FieldLabel, FieldHelp } from '@pantoia/ui/field'
35
+ ```
36
+
37
+ 쓰는 방식은 2가지다.
38
+
39
+ ```
40
+ 패키지로 쓴다 import해서 쓴다. 버전을 올리면 고친 내용이 따라온다
41
+ 복사해서 쓴다 npx @pantoia/ui add로 소스를 가져와 프로젝트 안에서 고친다
42
+ ```
43
+
44
+ 기본은 패키지로 쓰는 것이다. 동작이나 마크업을 프로젝트에 맞게 바꿔야 할 때만 복사한다.
45
+
46
+ 제어 속성은 Base UI 규약을 따른다. `value`와 `defaultValue`, `onValueChange`를 짝으로 쓴다.
47
+ 컴포넌트마다의 속성과 이벤트는 https://design.pantoia.com 의 각 페이지에 있다.
48
+
49
+ ## 디자인 규칙
50
+
51
+ ### 색
52
+
53
+ 색은 토큰으로 쓴다. 이름은 `<속성>-<역할>-<변형>-<상태>`다.
54
+
55
+ ```
56
+ 속성 fg 글자, bg 배경, line 선, shadow 그림자
57
+ 역할 neutral, brand, info, success, warning, danger
58
+ 변형 solid 기준색, weak 옅은 배경, subtle 더 옅게, strong 더 진하게,
59
+ muted 약한 글자, placeholder 값 없는 Input의 글자, contrast 그 배경 위의 글자
60
+ 상태 hover, disabled, checked, selected
61
+ ```
62
+
63
+ ```tsx
64
+ <div className="bg-bg-brand-solid text-fg-brand-solid-contrast">
65
+ ```
66
+
67
+ 역할은 뜻으로 고른다. info는 진행 중인 일, success는 끝난 일, warning은 살펴볼 일, danger는 막힌 일이다.
68
+
69
+ ### Surface
70
+
71
+ 경계를 만드는 유틸리티가 4개다.
72
+
73
+ ```
74
+ frame 배경과 선. 페이지 안의 영역
75
+ card 배경과 그림자. 목록의 항목
76
+ float 배경과 선과 그림자. 페이지 위에 뜨는 것
77
+ sheet 배경만. 화면을 덮는 것
78
+ well bg-bg-well. 파인 면
79
+ ```
80
+
81
+ ### 간격, 둥글기, 글자, 그림자
82
+
83
+ ```
84
+ 간격 gap-tight 4, gap-stack 16, gap-block 32, gap-section 48, gap-gutter 16
85
+ 둥글기 rounded-r2부터 rounded-r24까지 10단계
86
+ 글자 text-display, text-title, text-section, text-subsection, text-prose,
87
+ text-value, text-label, text-caption, text-micro
88
+ 크기만 text-t10부터 text-t64까지
89
+ 그림자 shadow-s1, shadow-s2, shadow-s3
90
+ 전환 transition-state, animate-enter, animate-exit
91
+ 층 z-sticky 10, z-overlay 50, z-toast 60
92
+ ```
93
+
94
+ ### 쓰지 않는 것
95
+
96
+ ```
97
+ 임의 값 bg-[#fff], p-[13px]. 토큰을 쓰거나 토큰을 더한다
98
+ dark: 변형자 토큰이 이미 양쪽 모드를 담는다
99
+ Tailwind 기본 색 bg-blue-500, text-gray-700. 역할 토큰을 쓴다
100
+ px 직접 지정 간격과 둥글기는 토큰으로 쓴다
101
+ ```
102
+
103
+ ## 커스텀 값 추가
104
+
105
+ 전역 CSS에 적는다. 값은 라이트와 다크 양쪽에 모두 적는다. `:root`에만 적으면 다크가 따라오지 않는다.
106
+
107
+ ```css
108
+ /* 모드와 무관한 값 */
109
+ @theme {
110
+ --radius-r18: 18px;
111
+ --spacing-wide: 64px;
112
+ }
113
+
114
+ /* 모드마다 다른 값 */
115
+ :root, .light { --pds-bg-brand-solid: #2563eb; }
116
+ .dark { --pds-bg-brand-solid: #3b82f6; }
117
+
118
+ /* 새 유틸리티 */
119
+ @utility raised { @apply bg-bg-panel shadow-s2; }
120
+ ```
121
+
122
+ 색 역할을 더하려면 팔레트 10단계를 양쪽 모드에 적고, 토큰 6개를 만든다.
123
+
124
+ ```
125
+ --pds-bg-<역할>-solid --pds-bg-<역할>-weak
126
+ --pds-fg-<역할> --pds-fg-<역할>-contrast
127
+ --pds-line-<역할> --pds-line-<역할>-weak
128
+ ```
129
+
130
+ 토큰을 덮어쓸 때 미치는 범위는 3가지다.
131
+
132
+ ```
133
+ 팔레트 단계 그 단계를 참조하는 토큰이 모두 바뀐다
134
+ 토큰 그 토큰을 쓰는 곳이 모두 바뀐다
135
+ className 그 한 곳만 바뀐다
136
+ ```
137
+
138
+ ## 컴포넌트
139
+
140
+ `variant`는 tailwind-variants로 만든 스타일 축이다. `-`는 축이 없다는 뜻이다.
141
+
142
+ | 컴포넌트 | import | 하위 컴포넌트 | variant |
143
+ |---|---|---|---|
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 | - |
170
+
171
+ 컴포넌트를 고를 때 헷갈리는 짝은 이렇다.
172
+
173
+ ```
174
+ Dialog와 AlertDialog 내용을 보여 주면 Dialog, 되돌릴 수 없는 일을 한 번 더 물으면 AlertDialog
175
+ Dialog와 Drawer 화면 가운데면 Dialog, 가장자리에서 열리면 Drawer
176
+ Select와 Combobox 목록에서 고르면 Select, 입력해서 찾으면 Combobox
177
+ Checkbox와 Switch 여러 개를 고르면 Checkbox, 설정을 바로 켜고 끄면 Switch
178
+ Toggle과 Switch 도구 모음에서 상태를 누르면 Toggle, 설정이면 Switch
179
+ Badge와 Button 누를 수 없으면 Badge, 누르면 Button
180
+ Tabs와 Accordion 한 번에 하나만 보이면 Tabs, 여러 개를 펴 두면 Accordion
181
+ Tooltip과 Popover 짧은 설명이면 Tooltip, 안에서 조작하면 Popover
182
+ ```
183
+
184
+ Field는 라벨, 도움말, 오류 메시지를 입력 컨트롤에 묶는다. Input, Textarea, NumberField, Select, Combobox와 함께 쓴다.
185
+
186
+ 앱에 한 번만 두는 것이 2개다.
187
+
188
+ ```tsx
189
+ import { Toast, toast } from '@pantoia/ui/toast'
190
+ import { TooltipProvider } from '@pantoia/ui/tooltip'
191
+
192
+ <TooltipProvider>
193
+ <App />
194
+ <Toast />
195
+ </TooltipProvider>
196
+ ```
197
+
198
+ 알림은 `toast('저장했어요.')`로 띄운다. `toast.success`, `toast.error`, `toast.warning`, `toast.info`, `toast.dismiss`도 있다.
199
+
200
+ ## 컴포넌트 내려받기
201
+
202
+ ```sh
203
+ npx @pantoia/ui list 추가할 수 있는 컴포넌트를 확인한다
204
+ npx @pantoia/ui add button 소스를 components.json의 위치로 복사한다
205
+ npx @pantoia/ui diff 복사한 파일을 설치된 패키지의 파일과 비교한다
206
+ ```
207
+
208
+ `add`는 함께 필요한 파일까지 복사하고 `components.json`에 버전을 적는다.
209
+ 복사한 파일을 고치면 패키지를 올려도 그 파일은 그대로다. `diff`로 달라진 곳을 확인하고 직접 반영한다.
210
+
211
+ 복사한 파일도 위의 디자인 규칙을 따른다. 임의 값과 `dark:` 변형자를 쓰지 않는다.
package/dist/index.js CHANGED
@@ -6,6 +6,8 @@ import { argv, cwd, exit, platform, stdin, stdout } from 'node:process';
6
6
  import { createInterface } from 'node:readline/promises';
7
7
  const HERE = join(import.meta.dirname, '..');
8
8
  const CONFIG = 'components.json';
9
+ const AGENTS = 'agents/pantoia-ui.md';
10
+ const AGENTS_TARGET = '.claude/agents/pantoia-ui.md';
9
11
  const STYLES = '@import "@pantoia/ui/tailwind.css";';
10
12
  const TAILWIND = /^@import\s+["']tailwindcss["'][^;]*;\s*$/;
11
13
  const IMPORTED = /@import\s+["']@pantoia\/ui\//;
@@ -122,6 +124,20 @@ async function copy(item, base, force) {
122
124
  }
123
125
  return skipped;
124
126
  }
127
+ async function copyAgents() {
128
+ const source = readFileSync(join(HERE, AGENTS), 'utf8');
129
+ const target = join(cwd(), AGENTS_TARGET);
130
+ if (existsSync(target) && readFileSync(target, 'utf8') === source)
131
+ return version();
132
+ if (!await confirm(`Claude Code 에이전트 파일 ${AGENTS_TARGET}을 만들까요?`, true))
133
+ return undefined;
134
+ if (existsSync(target) && !await confirm(`${AGENTS_TARGET} 파일이 이미 있어요. 덮어쓸까요?`))
135
+ return undefined;
136
+ mkdirSync(dirname(target), { recursive: true });
137
+ writeFileSync(target, source);
138
+ console.log(`${AGENTS_TARGET}을 만들었어요.`);
139
+ return version();
140
+ }
125
141
  function detect() {
126
142
  return LAYOUTS.find(layout => existsSync(join(cwd(), layout.css))) ?? LAYOUTS[0];
127
143
  }
@@ -212,7 +228,8 @@ async function commandInit(args) {
212
228
  const { head, tail } = additions(text, fonts);
213
229
  const needed = head.length > 0 || tail.length > 0;
214
230
  const added = needed && await importStyles(css, head, tail);
215
- writeConfig({ path, css, components: {} });
231
+ const agents = await copyAgents();
232
+ writeConfig({ path, css, agents, components: {} });
216
233
  if (stdin.isTTY)
217
234
  console.log('');
218
235
  console.log(`${CONFIG}을 만들었어요. 컴포넌트는 ${path}에 저장돼요.`);
@@ -313,23 +330,28 @@ async function commandDiff(args) {
313
330
  const names = args.filter(arg => !arg.startsWith('--'));
314
331
  const wanted = names.length > 0 ? names : Object.keys(config.components);
315
332
  let changed = 0;
333
+ const show = (shown, mine, theirs) => {
334
+ if (mine === theirs)
335
+ return;
336
+ if (changed === 0)
337
+ console.log('- 줄은 내 파일, + 줄은 설치된 패키지의 파일이에요.');
338
+ changed++;
339
+ console.log(`\n${shown}`);
340
+ for (const line of difference(mine.split('\n'), theirs.split('\n')))
341
+ console.log(line);
342
+ };
316
343
  for (const item of wanted.map(find)) {
317
344
  for (const file of item.files ?? []) {
318
- const target = join(cwd(), config.path, file.target ?? file.path);
345
+ const shown = join(config.path, file.target ?? file.path);
346
+ const target = join(cwd(), shown);
319
347
  if (!existsSync(target))
320
348
  continue;
321
- const mine = readFileSync(target, 'utf8');
322
- const theirs = readFileSync(join(HERE, file.path), 'utf8');
323
- if (mine === theirs)
324
- continue;
325
- if (changed === 0)
326
- console.log('- 줄은 내 파일, + 줄은 설치된 패키지의 파일이에요.');
327
- changed++;
328
- console.log(`\n${join(config.path, file.target ?? file.path)}`);
329
- for (const line of difference(mine.split('\n'), theirs.split('\n')))
330
- console.log(line);
349
+ show(shown, readFileSync(target, 'utf8'), readFileSync(join(HERE, file.path), 'utf8'));
331
350
  }
332
351
  }
352
+ const agents = join(cwd(), AGENTS_TARGET);
353
+ if (names.length === 0 && config.agents && existsSync(agents))
354
+ show(AGENTS_TARGET, readFileSync(agents, 'utf8'), readFileSync(join(HERE, AGENTS), 'utf8'));
333
355
  if (changed === 0)
334
356
  console.log('달라진 파일이 없어요.');
335
357
  }
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@pantoia/ui",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Pantoia Design System",
5
5
  "homepage": "https://design.pantoia.com",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/pantoia/pantoia-design.git",
9
- "directory": "packages/registry"
8
+ "url": "git+https://github.com/pantoia/pantoia-ui.git"
10
9
  },
11
10
  "license": "MIT",
12
11
  "type": "module",
@@ -14,6 +13,7 @@
14
13
  "pantoia-ui": "./dist/index.js"
15
14
  },
16
15
  "files": [
16
+ "agents/pantoia-ui.md",
17
17
  "*/index.tsx",
18
18
  "*/index.module.css",
19
19
  "dist",
@@ -44,7 +44,13 @@
44
44
  },
45
45
  "scripts": {
46
46
  "prepack": "tsc cli/index.ts --ignoreConfig --outDir dist --target es2022 --module esnext --moduleResolution bundler --skipLibCheck --types node",
47
- "typecheck": "tsc --noEmit"
47
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.node.json",
48
+ "check": "tsx scripts/check.ts",
49
+ "lint": "oxlint --deny-warnings",
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"
48
54
  },
49
55
  "dependencies": {
50
56
  "@base-ui/react": "^1.8.0",
@@ -56,12 +62,19 @@
56
62
  "tailwindcss": "^4.0.0"
57
63
  },
58
64
  "devDependencies": {
59
- "@pantoia/typescript-config": "*",
65
+ "@testing-library/dom": "^10.4.1",
66
+ "@testing-library/react": "^16.3.3",
67
+ "@testing-library/user-event": "^14.6.6",
60
68
  "@types/node": "26.4.0",
61
69
  "@types/react": "19.2.18",
62
70
  "@types/react-dom": "19.2.5",
71
+ "happy-dom": "^20.12.0",
72
+ "oxlint": "1.81.0",
63
73
  "react": "19.2.8",
64
74
  "react-dom": "19.2.8",
65
- "typescript": "7.0.2"
75
+ "tailwindcss": "4.3.3",
76
+ "tsx": "4.23.12",
77
+ "typescript": "7.0.2",
78
+ "vitest": "^4.1.11"
66
79
  }
67
80
  }