@pantoia/ui 0.1.0 → 0.3.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 (68) hide show
  1. package/accordion/AGENTS.md +5 -5
  2. package/accordion/index.tsx +21 -11
  3. package/alert-dialog/AGENTS.md +7 -7
  4. package/alert-dialog/index.tsx +20 -10
  5. package/badge/AGENTS.md +5 -5
  6. package/badge/index.tsx +8 -8
  7. package/button/AGENTS.md +7 -7
  8. package/button/index.tsx +34 -34
  9. package/callout/AGENTS.md +4 -9
  10. package/callout/index.tsx +42 -22
  11. package/checkbox/AGENTS.md +5 -5
  12. package/checkbox/index.tsx +19 -16
  13. package/combobox/AGENTS.md +13 -13
  14. package/combobox/index.tsx +90 -42
  15. package/command-palette/AGENTS.md +3 -6
  16. package/command-palette/index.tsx +7 -3
  17. package/dialog/AGENTS.md +6 -6
  18. package/dialog/index.tsx +11 -5
  19. package/dist/index.js +200 -43
  20. package/drawer/AGENTS.md +5 -5
  21. package/drawer/index.tsx +8 -4
  22. package/dropdown-menu/AGENTS.md +12 -12
  23. package/dropdown-menu/index.tsx +27 -12
  24. package/field/AGENTS.md +8 -12
  25. package/field/index.tsx +39 -13
  26. package/input/AGENTS.md +4 -4
  27. package/input/index.tsx +5 -5
  28. package/kbd/AGENTS.md +1 -1
  29. package/kbd/index.tsx +6 -2
  30. package/label/AGENTS.md +1 -1
  31. package/label/index.tsx +3 -1
  32. package/number-field/AGENTS.md +6 -10
  33. package/number-field/index.tsx +26 -16
  34. package/package.json +13 -15
  35. package/palette.css +114 -0
  36. package/popover/AGENTS.md +4 -4
  37. package/popover/index.tsx +14 -4
  38. package/radio-group/AGENTS.md +8 -8
  39. package/radio-group/index.tsx +20 -16
  40. package/registry.json +9 -71
  41. package/select/AGENTS.md +10 -10
  42. package/select/index.tsx +46 -21
  43. package/switch/AGENTS.md +4 -4
  44. package/switch/index.tsx +13 -13
  45. package/table/AGENTS.md +9 -9
  46. package/table/index.tsx +16 -6
  47. package/tabs/AGENTS.md +5 -4
  48. package/tabs/index.module.css +2 -0
  49. package/tabs/index.tsx +22 -11
  50. package/tailwind.css +3 -0
  51. package/textarea/AGENTS.md +4 -4
  52. package/textarea/index.tsx +5 -5
  53. package/toast/AGENTS.md +5 -13
  54. package/toast/index.tsx +27 -33
  55. package/toggle/AGENTS.md +4 -4
  56. package/toggle/index.tsx +8 -7
  57. package/tokens.css +272 -0
  58. package/tooltip/AGENTS.md +2 -2
  59. package/tooltip/index.tsx +14 -4
  60. package/tv.ts +0 -1
  61. package/utilities.css +119 -0
  62. package/code-block/AGENTS.md +0 -60
  63. package/code-block/index.module.css +0 -46
  64. package/code-block/index.tsx +0 -127
  65. package/copy-button/AGENTS.md +0 -43
  66. package/copy-button/index.tsx +0 -119
  67. package/press.css +0 -37
  68. package/styles.css +0 -347
package/dist/index.js CHANGED
@@ -1,17 +1,45 @@
1
1
  #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
2
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
4
  import { dirname, join } from 'node:path';
4
- import { argv, cwd, exit, stdin, stdout } from 'node:process';
5
+ import { argv, cwd, exit, platform, stdin, stdout } from 'node:process';
5
6
  import { createInterface } from 'node:readline/promises';
6
7
  const HERE = join(import.meta.dirname, '..');
7
8
  const CONFIG = 'components.json';
8
- const DEFAULT_PATH = 'components/ui';
9
- const USAGE = `pantoia-ui <명령>
9
+ const STYLES = '@import "@pantoia/ui/tailwind.css";';
10
+ const TAILWIND = /^@import\s+["']tailwindcss["'][^;]*;\s*$/;
11
+ const IMPORTED = /@import\s+["']@pantoia\/ui\//;
12
+ const FONTS = ['pretendard@^1.3.9', '@fontsource/jetbrains-mono@^5.3.0'];
13
+ const FONT_IMPORT = '@import "pretendard/dist/web/variable/pretendardvariable-dynamic-subset.css";';
14
+ const MONO = '"JetBrains Mono"';
15
+ const MONO_FACE = [
16
+ '@font-face {',
17
+ ` font-family: ${MONO};`,
18
+ ' src: url("@fontsource/jetbrains-mono/files/jetbrains-mono-latin-400-normal.woff2") format("woff2");',
19
+ ' font-weight: 400;',
20
+ ' font-display: swap;',
21
+ ' unicode-range: U+0000-00FF, U+2000-206F;',
22
+ '}',
23
+ ];
24
+ const MANAGERS = [
25
+ { lock: 'pnpm-lock.yaml', command: 'pnpm', verb: 'add' },
26
+ { lock: 'yarn.lock', command: 'yarn', verb: 'add' },
27
+ { lock: 'bun.lock', command: 'bun', verb: 'add' },
28
+ { lock: 'bun.lockb', command: 'bun', verb: 'add' },
29
+ { lock: 'package-lock.json', command: 'npm', verb: 'install' },
30
+ ];
31
+ const LAYOUTS = [
32
+ { css: 'app/globals.css', path: 'components/ui' },
33
+ { css: 'src/app/globals.css', path: 'src/components/ui' },
34
+ { css: 'app/app.css', path: 'app/components/ui' },
35
+ { css: 'src/index.css', path: 'src/components/ui' },
36
+ ];
37
+ const USAGE = `npx @pantoia/ui <명령어>
10
38
 
11
- init [--path <디렉터리>] ${CONFIG} 을 만든다
12
- add <이름...> [--force] 소스를 복사한다
13
- list 받을 수 있는 것을 늘어놓는다
14
- diff [이름] 복사본과 패키지의 지금 것을 견준다`;
39
+ init [--path <경로>] [--css <경로>] 컴포넌트를 저장할 위치와 전역 CSS 파일을 정해요
40
+ add <이름...> [--force] 컴포넌트를 추가해요
41
+ list 추가할 수 있는 컴포넌트를 확인해요
42
+ diff [이름] 추가한 파일과 최신 파일을 비교해요`;
15
43
  function fail(message) {
16
44
  console.error(message);
17
45
  exit(1);
@@ -27,7 +55,7 @@ function registry() {
27
55
  function find(name) {
28
56
  const item = registry().find(candidate => candidate.name === name);
29
57
  if (!item)
30
- fail(`${name} 은 목록에 없다. pantoia-ui list 로 이름을 본다`);
58
+ fail(`목록에 없는 컴포넌트예요: ${name}\nnpx @pantoia/ui list로 이름을 확인하세요.`);
31
59
  return item;
32
60
  }
33
61
  function configPath() {
@@ -59,67 +87,194 @@ function resolve(names) {
59
87
  }
60
88
  return [...seen.values()];
61
89
  }
62
- async function confirm(question) {
90
+ async function ask(question, fallback) {
63
91
  if (!stdin.isTTY)
64
- return false;
92
+ return fallback;
93
+ const rl = createInterface({ input: stdin, output: stdout });
94
+ const answer = await rl.question(`${question} (${fallback}) `);
95
+ rl.close();
96
+ return answer.trim() || fallback;
97
+ }
98
+ async function confirm(question, fallback = false) {
99
+ if (!stdin.isTTY)
100
+ return fallback;
65
101
  const rl = createInterface({ input: stdin, output: stdout });
66
- const answer = await rl.question(`${question} (y/N) `);
102
+ const answer = (await rl.question(`${question} ${fallback ? '(Y/n)' : '(y/N)'} `)).trim().toLowerCase();
67
103
  rl.close();
68
- return answer.trim().toLowerCase() === 'y';
104
+ return answer === '' ? fallback : answer === 'y';
69
105
  }
70
- async function copy(item, root, force) {
106
+ async function copy(item, base, force) {
107
+ let skipped = false;
71
108
  for (const file of item.files ?? []) {
72
109
  const source = readFileSync(join(HERE, file.path), 'utf8');
73
- const target = join(root, file.target ?? file.path);
110
+ const shown = join(base, file.target ?? file.path);
111
+ const target = join(cwd(), shown);
74
112
  if (existsSync(target) && readFileSync(target, 'utf8') !== source) {
75
- if (!force && !await confirm(`${file.path} 이 이미 있고 내용이 다르다. 덮어쓰나?`)) {
76
- console.log(` 건너뛴다 ${file.path} — 덮어쓰려면 --force`);
113
+ if (!force && !await confirm(`${shown} 파일이 이미 있고 내용이 달라요. 덮어쓸까요?`)) {
114
+ console.log(` ${shown} (건너뜀)`);
115
+ skipped = true;
77
116
  continue;
78
117
  }
79
118
  }
80
119
  mkdirSync(dirname(target), { recursive: true });
81
120
  writeFileSync(target, source);
82
- console.log(` 썼다 ${file.path}`);
121
+ console.log(` ${shown}`);
122
+ }
123
+ return skipped;
124
+ }
125
+ function detect() {
126
+ return LAYOUTS.find(layout => existsSync(join(cwd(), layout.css))) ?? LAYOUTS[0];
127
+ }
128
+ const packageName = (spec) => spec.slice(0, spec.lastIndexOf('@'));
129
+ function installed() {
130
+ const file = join(cwd(), 'package.json');
131
+ if (!existsSync(file))
132
+ return new Set();
133
+ const pkg = JSON.parse(readFileSync(file, 'utf8'));
134
+ return new Set(Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }));
135
+ }
136
+ async function installPackages() {
137
+ const have = installed();
138
+ const specs = have.has('@pantoia/ui') ? [] : [`@pantoia/ui@${version()}`];
139
+ let fonts = FONTS.every(spec => have.has(packageName(spec)));
140
+ if (!fonts) {
141
+ console.log('');
142
+ fonts = await confirm(`글꼴 패키지를 설치할까요? ${FONTS.map(packageName).join(', ')}`, true);
143
+ if (fonts)
144
+ specs.push(...FONTS);
145
+ }
146
+ const manager = MANAGERS.find(m => existsSync(join(cwd(), m.lock))) ?? MANAGERS.at(-1);
147
+ if (!fonts && !FONTS.every(spec => have.has(packageName(spec)))) {
148
+ console.log('글꼴을 설치하지 않았어요. 쓰려면 이 명령으로 설치하세요.');
149
+ console.log('');
150
+ console.log(` ${[manager.command, manager.verb, ...FONTS].join(' ')}`);
151
+ }
152
+ if (specs.length === 0)
153
+ return fonts;
154
+ const done = spawnSync(manager.command, [manager.verb, ...specs], { stdio: 'inherit', shell: platform === 'win32' });
155
+ if (done.status === 0)
156
+ return fonts;
157
+ console.log('패키지를 설치하지 못했어요. 이 명령으로 직접 설치하세요.');
158
+ console.log('');
159
+ console.log(` ${[manager.command, manager.verb, ...specs].join(' ')}`);
160
+ return false;
161
+ }
162
+ function additions(text, fonts) {
163
+ const head = IMPORTED.test(text) ? [] : [STYLES];
164
+ if (fonts && !text.includes(FONT_IMPORT))
165
+ head.push(FONT_IMPORT);
166
+ const tail = fonts && !text.includes(MONO) ? MONO_FACE : [];
167
+ return { head, tail };
168
+ }
169
+ async function importStyles(css, head, tail) {
170
+ const file = join(cwd(), css);
171
+ const text = readFileSync(file, 'utf8');
172
+ const ending = text.endsWith('\n') ? '\n' : '';
173
+ const lines = text.slice(0, text.length - ending.length).split('\n');
174
+ const ours = lines.findIndex(line => IMPORTED.test(line));
175
+ const at = ours === -1 ? lines.findIndex(line => TAILWIND.test(line)) : ours;
176
+ console.log('');
177
+ console.log(`${css}를 이렇게 수정해요.`);
178
+ console.log('');
179
+ for (const line of lines.slice(Math.max(0, at - 2), at + 1))
180
+ console.log(` ${line}`);
181
+ for (const line of head)
182
+ console.log(`+ ${line}`);
183
+ for (const line of lines.slice(at + 1, at + 3))
184
+ console.log(` ${line}`);
185
+ if (tail.length > 0) {
186
+ if (lines.length > at + 3)
187
+ console.log(' ...');
188
+ for (const line of tail)
189
+ console.log(`+ ${line}`);
83
190
  }
191
+ console.log('');
192
+ if (!await confirm('수정할까요?', true))
193
+ return false;
194
+ const body = [...lines.slice(0, at + 1), ...head, ...lines.slice(at + 1)];
195
+ if (tail.length > 0)
196
+ body.push('', ...tail);
197
+ writeFileSync(file, body.join('\n') + ending);
198
+ return true;
84
199
  }
85
200
  async function commandInit(args) {
86
- const existing = readConfig();
87
- if (existing)
88
- fail(`${CONFIG} 이 이미 있다`);
89
- const path = option(args, '--path') ?? DEFAULT_PATH;
90
- writeConfig({ path, components: {} });
91
- console.log(`${CONFIG} 을 만들었다 — 소스는 ${path} 아래로 간다`);
92
- console.log('스타일을 한 줄 넣는다 — @import "@pantoia/ui/styles.css";');
201
+ if (readConfig())
202
+ fail(`${CONFIG}이 이미 있어요.`);
203
+ const detected = detect();
204
+ const path = option(args, '--path') ?? await ask('컴포넌트를 저장할 위치를 입력하세요.', detected.path);
205
+ const css = option(args, '--css') ?? await ask('전역 CSS 파일 위치를 입력하세요.', detected.css);
206
+ if (!existsSync(join(cwd(), css)))
207
+ fail(`${css} 파일이 없어요. 전역 CSS 파일 위치를 확인하고 다시 실행하세요.`);
208
+ const text = readFileSync(join(cwd(), css), 'utf8');
209
+ if (!IMPORTED.test(text) && !text.split('\n').some(line => TAILWIND.test(line)))
210
+ fail(`${css}에 @import "tailwindcss";가 없어요. Tailwind CSS를 먼저 설치하세요.`);
211
+ const fonts = await installPackages();
212
+ const { head, tail } = additions(text, fonts);
213
+ const needed = head.length > 0 || tail.length > 0;
214
+ const added = needed && await importStyles(css, head, tail);
215
+ writeConfig({ path, css, components: {} });
216
+ if (stdin.isTTY)
217
+ console.log('');
218
+ console.log(`${CONFIG}을 만들었어요. 컴포넌트는 ${path}에 저장돼요.`);
219
+ if (!needed)
220
+ console.log(`${css}에 스타일이 이미 있어요.`);
221
+ else if (added)
222
+ console.log(`${css}에 스타일을 추가했어요.`);
223
+ else {
224
+ console.log(`${css}를 수정하지 않았어요. @import "tailwindcss"; 바로 아래에 이 줄을 직접 추가하세요.`);
225
+ console.log('');
226
+ for (const line of head)
227
+ console.log(` ${line}`);
228
+ if (tail.length > 0) {
229
+ console.log('');
230
+ console.log('파일 끝에는 이 블록을 추가하세요.');
231
+ console.log('');
232
+ for (const line of tail)
233
+ console.log(` ${line}`);
234
+ }
235
+ }
93
236
  }
94
237
  async function commandAdd(args) {
95
238
  const names = args.filter(arg => !arg.startsWith('--'));
96
239
  if (names.length === 0)
97
- fail('받을 이름을 적는다 — pantoia-ui add button');
240
+ fail('추가할 컴포넌트 이름을 입력하세요. 예) npx @pantoia/ui add button');
98
241
  const force = args.includes('--force');
99
- let config = readConfig();
100
- if (!config) {
101
- config = { path: option(args, '--path') ?? DEFAULT_PATH, components: {} };
102
- console.log(`${CONFIG} 이 없어 만들었다 — 소스는 ${config.path} 아래로 간다`);
103
- }
242
+ const config = readConfig();
243
+ if (!config)
244
+ fail(`${CONFIG}이 없어요. 먼저 npx @pantoia/ui init을 실행하세요.`);
104
245
  const items = resolve(names);
105
- const root = join(cwd(), config.path);
246
+ const asked = items.filter(item => names.includes(item.name));
106
247
  const extra = items.filter(item => !names.includes(item.name));
107
- if (extra.length > 0)
108
- console.log(`함께 받는다 — ${extra.map(item => item.name).join(' · ')}`);
109
- for (const item of items) {
248
+ let skipped = false;
249
+ let first = true;
250
+ for (const item of asked) {
251
+ if (!first)
252
+ console.log('');
253
+ first = false;
110
254
  console.log(item.name);
111
- await copy(item, root, force);
255
+ skipped = await copy(item, config.path, force) || skipped;
112
256
  config.components[item.name] = version();
113
257
  }
258
+ if (extra.length > 0) {
259
+ console.log('');
260
+ console.log('함께 필요한 파일도 추가했어요.');
261
+ for (const item of extra) {
262
+ skipped = await copy(item, config.path, force) || skipped;
263
+ config.components[item.name] = version();
264
+ }
265
+ }
114
266
  writeConfig(config);
115
- const dependencies = [...new Set(items.flatMap(item => item.dependencies ?? []))].sort();
116
- if (dependencies.length > 0)
117
- console.log(`\nnpm 의존을 적는다 — ${dependencies.join(' · ')}`);
267
+ if (skipped) {
268
+ console.log('');
269
+ console.log('덮어쓰려면 --force 옵션을 붙이세요.');
270
+ }
118
271
  }
119
272
  function commandList() {
273
+ console.log(`${'이름'.padEnd(16)}${'종류'.padEnd(7)}같이 추가되는 컴포넌트`);
120
274
  for (const item of registry()) {
121
- const together = (item.registryDependencies ?? []).join(' · ');
122
- console.log(`${item.name.padEnd(18)}${(item.meta?.layer ?? '').padEnd(9)}${together}`);
275
+ const layer = item.meta?.layer ?? (item.type === 'registry:lib' ? 'lib' : '');
276
+ const together = (item.registryDependencies ?? []).join(', ');
277
+ console.log(`${item.name.padEnd(18)}${layer.padEnd(9)}${together}`.trimEnd());
123
278
  }
124
279
  }
125
280
  function lcs(a, b) {
@@ -154,7 +309,7 @@ function difference(mine, theirs) {
154
309
  async function commandDiff(args) {
155
310
  const config = readConfig();
156
311
  if (!config)
157
- fail(`${CONFIG} 이 없다 — pantoia-ui init 을 먼저 돌린다`);
312
+ fail(`${CONFIG}이 없어요. 먼저 npx @pantoia/ui add로 컴포넌트를 추가하세요.`);
158
313
  const names = args.filter(arg => !arg.startsWith('--'));
159
314
  const wanted = names.length > 0 ? names : Object.keys(config.components);
160
315
  let changed = 0;
@@ -167,14 +322,16 @@ async function commandDiff(args) {
167
322
  const theirs = readFileSync(join(HERE, file.path), 'utf8');
168
323
  if (mine === theirs)
169
324
  continue;
325
+ if (changed === 0)
326
+ console.log('- 는 내 파일, + 는 최신 파일이에요.');
170
327
  changed++;
171
- console.log(`\n${file.path}`);
328
+ console.log(`\n${join(config.path, file.target ?? file.path)}`);
172
329
  for (const line of difference(mine.split('\n'), theirs.split('\n')))
173
330
  console.log(line);
174
331
  }
175
332
  }
176
333
  if (changed === 0)
177
- console.log('갈린 것이 없다');
334
+ console.log('달라진 파일이 없어요.');
178
335
  }
179
336
  const [command, ...args] = argv.slice(2);
180
337
  if (command === 'init')
package/drawer/AGENTS.md CHANGED
@@ -3,7 +3,7 @@
3
3
  ```
4
4
  받기 npx @pantoia/ui add drawer
5
5
  층 ui
6
- 파일 drawer/index.tsx · drawer/index.module.css · drawer/AGENTS.md
6
+ 파일 drawer/index.tsx, drawer/index.module.css, drawer/AGENTS.md
7
7
  ```
8
8
 
9
9
  ## 부위
@@ -11,8 +11,8 @@
11
11
  | 이름 | props |
12
12
  |---|---|
13
13
  | `Drawer` | `DrawerProps` |
14
- | `DrawerTrigger` | — |
15
- | `DrawerClose` | — |
14
+ | `DrawerTrigger` | - |
15
+ | `DrawerClose` | - |
16
16
  | `DrawerContent` | `ComponentProps<typeof Dialog.Content>` |
17
17
  | `DrawerHeader` | `DrawerHeaderProps` |
18
18
  | `DrawerTitle` | `ComponentProps<typeof Dialog.Title>` |
@@ -51,11 +51,11 @@
51
51
  | `className` | `string` | |
52
52
  | `children` | `ReactNode` | |
53
53
 
54
- ## 변형 — `drawerContent`
54
+ ## 변형 - `drawerContent`
55
55
 
56
56
  | 축 | 값 | 기본 |
57
57
  |---|---|---|
58
- | `side` | `left` · `right` | — |
58
+ | `side` | `left`, `right` | - |
59
59
 
60
60
  ## 함께 받는 것
61
61
 
package/drawer/index.tsx CHANGED
@@ -117,7 +117,7 @@ export function DrawerClose({ children }: { children: ReactNode }) {
117
117
  }
118
118
 
119
119
  export const drawerContent = tv({
120
- base: 'bg-float text-fg z-overlay fixed inset-y-0'
120
+ base: 'sheet text-fg z-overlay fixed inset-y-0'
121
121
  + ' flex w-full flex-col gap-stack overflow-y-auto p-4 sm:w-120'
122
122
  + ' focus-visible:outline-none',
123
123
  variants: {
@@ -137,7 +137,7 @@ export function DrawerContent({
137
137
 
138
138
  return (
139
139
  <Dialog.Portal>
140
- <Dialog.Overlay className="bg-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
140
+ <Dialog.Overlay className="bg-bg-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
141
141
 
142
142
  <Dialog.Content
143
143
  {...rest}
@@ -187,12 +187,16 @@ export function DrawerHeader({ lead, trail, className, children }: DrawerHeaderP
187
187
  )
188
188
  }
189
189
 
190
+ const drawerTitle = tv({ base: 'text-section min-w-0' })
191
+
190
192
  export function DrawerTitle({ className, ...rest }: ComponentProps<typeof Dialog.Title>) {
191
- return <Dialog.Title {...rest} className={tv({ base: 'text-section min-w-0' })({ className })} />
193
+ return <Dialog.Title {...rest} className={drawerTitle({ className })} />
192
194
  }
193
195
 
196
+ const drawerDescription = tv({ base: 'text-prose text-fg-muted' })
197
+
194
198
  export function DrawerDescription({ className, ...rest }: ComponentProps<typeof Dialog.Description>) {
195
199
  return (
196
- <Dialog.Description {...rest} className={tv({ base: 'text-prose text-fg-muted' })({ className })} />
200
+ <Dialog.Description {...rest} className={drawerDescription({ className })} />
197
201
  )
198
202
  }
@@ -3,7 +3,7 @@
3
3
  ```
4
4
  받기 npx @pantoia/ui add dropdown-menu
5
5
  층 ui
6
- 파일 dropdown-menu/index.tsx · dropdown-menu/AGENTS.md
6
+ 파일 dropdown-menu/index.tsx, dropdown-menu/AGENTS.md
7
7
  ```
8
8
 
9
9
  ## 부위
@@ -11,16 +11,16 @@
11
11
  | 이름 | props |
12
12
  |---|---|
13
13
  | `DropdownMenu` | `DropdownMenuProps` |
14
- | `DropdownMenuTrigger` | — |
14
+ | `DropdownMenuTrigger` | - |
15
15
  | `DropdownMenuContent` | `DropdownMenuContentProps` |
16
- | `DropdownMenuGroup` | — |
16
+ | `DropdownMenuGroup` | - |
17
17
  | `DropdownMenuItem` | `DropdownMenuItemProps` |
18
- | `DropdownMenuCheckboxItem` | — |
19
- | `DropdownMenuRadioGroup` | — |
20
- | `DropdownMenuRadioItem` | — |
21
- | `DropdownMenuSeparator` | — |
22
- | `DropdownMenuSub` | — |
23
- | `DropdownMenuSubTrigger` | — |
18
+ | `DropdownMenuCheckboxItem` | - |
19
+ | `DropdownMenuRadioGroup` | - |
20
+ | `DropdownMenuRadioItem` | - |
21
+ | `DropdownMenuSeparator` | - |
22
+ | `DropdownMenuSub` | - |
23
+ | `DropdownMenuSubTrigger` | - |
24
24
  | `DropdownMenuSubContent` | `ComponentProps<typeof MenuPrimitive.SubContent>` |
25
25
 
26
26
  ### `DropdownMenuTrigger`
@@ -118,11 +118,11 @@
118
118
  | `lead` | `ReactNode` | |
119
119
  | `trail` | `ReactNode` | |
120
120
 
121
- ## 변형 — `menuItem`
121
+ ## 변형 - `menuItem`
122
122
 
123
123
  | 축 | 값 | 기본 |
124
124
  |---|---|---|
125
- | `color` | `neutral` · `danger` | `neutral` |
125
+ | `color` | `neutral`, `danger` | `neutral` |
126
126
 
127
127
  ## 함께 받는 것
128
128
 
@@ -130,4 +130,4 @@
130
130
 
131
131
  ## npm 의존
132
132
 
133
- `lucide-react` · `radix-ui`
133
+ `radix-ui`
@@ -1,6 +1,5 @@
1
1
  'use client'
2
2
 
3
- import { Check, ChevronRight } from 'lucide-react'
4
3
  import { DropdownMenu as MenuPrimitive } from 'radix-ui'
5
4
  import type { ComponentProps, ReactNode } from 'react'
6
5
 
@@ -14,15 +13,15 @@ export const menuItem = tv({
14
13
 
15
14
  variants: {
16
15
  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',
16
+ neutral: 'text-fg data-highlighted:bg-bg-neutral-hover data-disabled:text-fg-disabled',
17
+ danger: 'text-fg-danger data-highlighted:bg-bg-danger-weak data-disabled:text-fg-danger/40',
19
18
  },
20
19
  },
21
20
 
22
21
  defaultVariants: { color: 'neutral' },
23
22
  })
24
23
 
25
- const PANEL = 'menu bg-float text-fg rounded-float shadow-s3 z-overlay'
24
+ const PANEL = 'menu float text-fg rounded-r12 z-overlay'
26
25
  + ' flex min-w-44 flex-col gap-px p-1'
27
26
  + ' open:animate-enter closed:animate-exit'
28
27
  + ' focus-visible:outline-none'
@@ -51,6 +50,8 @@ export interface DropdownMenuContentProps extends Omit<ComponentProps<typeof Men
51
50
  arrow?: boolean
52
51
  }
53
52
 
53
+ const menuPanel = tv({ base: PANEL })
54
+
54
55
  export function DropdownMenuContent({
55
56
  align = 'start',
56
57
  arrow = true,
@@ -65,23 +66,31 @@ export function DropdownMenuContent({
65
66
  align={align}
66
67
  sideOffset={8}
67
68
  arrowPadding={16}
68
- className={tv({ base: PANEL })({ className })}
69
+ className={menuPanel({ className })}
69
70
  >
70
71
  {children}
71
72
 
72
- {arrow && <MenuPrimitive.Arrow className="fill-float" width={12} height={6} />}
73
+ {arrow && (
74
+ <MenuPrimitive.Arrow asChild width={12} height={6}>
75
+ <svg viewBox="0 0 12 6" preserveAspectRatio="none" className="fill-bg-panel -translate-y-px">
76
+ <path d="M0 0L4.58579 4.58579C5.36683 5.36683 6.63316 5.36684 7.41421 4.58579L12 0" className="stroke-line stroke-1" />
77
+ </svg>
78
+ </MenuPrimitive.Arrow>
79
+ )}
73
80
  </MenuPrimitive.Content>
74
81
  </MenuPrimitive.Portal>
75
82
  )
76
83
  }
77
84
 
85
+ const menuGroup = tv({ base: 'flex flex-col gap-px' })
86
+
78
87
  export function DropdownMenuGroup({ label, className, children }: {
79
88
  label?: string
80
89
  className?: string
81
90
  children?: ReactNode
82
91
  }) {
83
92
  return (
84
- <MenuPrimitive.Group className={tv({ base: 'flex flex-col gap-px' })({ className })}>
93
+ <MenuPrimitive.Group className={menuGroup({ className })}>
85
94
  {label && (
86
95
  <MenuPrimitive.Label className="text-caption text-fg-subtle px-2 pb-1 pt-2">
87
96
  {label}
@@ -130,7 +139,9 @@ export function DropdownMenuCheckboxItem({ checked, onCheckedChange, disabled, c
130
139
  >
131
140
  <span className="menu-lead grid size-4 shrink-0 place-items-center">
132
141
  <MenuPrimitive.ItemIndicator>
133
- <Check className="size-4" />
142
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4">
143
+ <path d="M20 6 9 17l-5-5" />
144
+ </svg>
134
145
  </MenuPrimitive.ItemIndicator>
135
146
  </span>
136
147
 
@@ -161,7 +172,7 @@ export function DropdownMenuRadioItem({ value, disabled, className, children }:
161
172
  <MenuPrimitive.RadioItem value={value} disabled={disabled} className={menuItem({ className })}>
162
173
  <span className="menu-lead grid size-4 shrink-0 place-items-center">
163
174
  <MenuPrimitive.ItemIndicator>
164
- <span className="bg-control-on in-disabled:bg-control-on-disabled size-2 rounded-full" />
175
+ <span className="bg-bg-neutral-solid in-disabled:bg-bg-neutral-solid-checked-disabled size-2 rounded-full" />
165
176
  </MenuPrimitive.ItemIndicator>
166
177
  </span>
167
178
 
@@ -170,8 +181,10 @@ export function DropdownMenuRadioItem({ value, disabled, className, children }:
170
181
  )
171
182
  }
172
183
 
184
+ const menuSeparator = tv({ base: 'bg-line -mx-1 my-1 h-px' })
185
+
173
186
  export function DropdownMenuSeparator({ className }: { className?: string }) {
174
- return <MenuPrimitive.Separator className={tv({ base: 'bg-line -mx-1 my-1 h-px' })({ className })} />
187
+ return <MenuPrimitive.Separator className={menuSeparator({ className })} />
175
188
  }
176
189
 
177
190
  export function DropdownMenuSub({ open, defaultOpen, onOpenChange, children }: {
@@ -203,7 +216,9 @@ export function DropdownMenuSubTrigger({ disabled, lead, className, children }:
203
216
 
204
217
  <span className="min-w-0 grow">{children}</span>
205
218
 
206
- <ChevronRight className="text-fg-subtle size-4 shrink-0" />
219
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="text-fg-subtle size-4 shrink-0">
220
+ <path d="m9 18 6-6-6-6" />
221
+ </svg>
207
222
  </MenuPrimitive.SubTrigger>
208
223
  )
209
224
  }
@@ -214,7 +229,7 @@ export function DropdownMenuSubContent({ className, children, ...rest }: Compone
214
229
  <MenuPrimitive.SubContent
215
230
  {...rest}
216
231
  sideOffset={4}
217
- className={tv({ base: PANEL })({ className })}
232
+ className={menuPanel({ className })}
218
233
  >
219
234
  {children}
220
235
  </MenuPrimitive.SubContent>
package/field/AGENTS.md CHANGED
@@ -3,7 +3,7 @@
3
3
  ```
4
4
  받기 npx @pantoia/ui add field
5
5
  층 ui
6
- 파일 field/index.tsx · field/AGENTS.md
6
+ 파일 field/index.tsx, field/AGENTS.md
7
7
  ```
8
8
 
9
9
  ## 부위
@@ -11,11 +11,11 @@
11
11
  | 이름 | props |
12
12
  |---|---|
13
13
  | `Field` | `FieldProps` |
14
- | `FieldLabel` | — |
15
- | `FieldHelp` | — |
14
+ | `FieldLabel` | - |
15
+ | `FieldHelp` | - |
16
16
  | `FieldError` | `FieldErrorProps` |
17
- | `FieldPending` | — |
18
- | `FieldSuccess` | — |
17
+ | `FieldPending` | - |
18
+ | `FieldSuccess` | - |
19
19
 
20
20
  ### `FieldLabel`
21
21
 
@@ -81,16 +81,12 @@
81
81
  | `className` | `string` | |
82
82
  | `children` | `ReactNode` | |
83
83
 
84
- ## 변형 — `field`
84
+ ## 변형 - `field`
85
85
 
86
86
  | 축 | 값 | 기본 |
87
87
  |---|---|---|
88
- | `layout` | `stack` · `inline` · `split` | `stack` |
88
+ | `layout` | `stack`, `inline`, `split` | `stack` |
89
89
 
90
90
  ## 함께 받는 것
91
91
 
92
- `label` · `tv`
93
-
94
- ## npm 의존
95
-
96
- `lucide-react`
92
+ `label`, `tv`