@pantoia/ui 0.2.0 → 0.4.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 (66) hide show
  1. package/accordion/index.module.css +4 -4
  2. package/accordion/index.tsx +30 -58
  3. package/alert-dialog/index.tsx +38 -44
  4. package/badge/index.tsx +2 -2
  5. package/button/index.tsx +31 -30
  6. package/callout/index.tsx +29 -8
  7. package/checkbox/index.tsx +24 -88
  8. package/combobox/index.tsx +133 -323
  9. package/command-palette/index.tsx +55 -34
  10. package/dialog/index.tsx +30 -26
  11. package/dist/index.js +155 -27
  12. package/drawer/index.module.css +2 -2
  13. package/drawer/index.tsx +54 -132
  14. package/dropdown-menu/index.tsx +95 -100
  15. package/field/index.tsx +43 -94
  16. package/input/index.tsx +13 -9
  17. package/kbd/index.tsx +2 -2
  18. package/label/index.tsx +7 -9
  19. package/number-field/index.tsx +129 -176
  20. package/package.json +12 -15
  21. package/palette.css +114 -0
  22. package/popover/index.tsx +62 -33
  23. package/radio-group/index.tsx +36 -39
  24. package/registry.json +65 -169
  25. package/select/index.tsx +154 -76
  26. package/switch/index.tsx +13 -6
  27. package/table/index.tsx +5 -6
  28. package/tabs/index.module.css +5 -3
  29. package/tabs/index.tsx +26 -77
  30. package/tailwind.css +3 -0
  31. package/textarea/index.tsx +25 -13
  32. package/toast/index.module.css +187 -0
  33. package/toast/index.tsx +141 -46
  34. package/toggle/index.tsx +8 -7
  35. package/{styles.css → tokens.css} +82 -303
  36. package/tooltip/index.tsx +62 -31
  37. package/tv.ts +0 -1
  38. package/utilities.css +124 -0
  39. package/accordion/AGENTS.md +0 -60
  40. package/alert-dialog/AGENTS.md +0 -78
  41. package/badge/AGENTS.md +0 -35
  42. package/button/AGENTS.md +0 -39
  43. package/callout/AGENTS.md +0 -41
  44. package/checkbox/AGENTS.md +0 -54
  45. package/combobox/AGENTS.md +0 -150
  46. package/command-palette/AGENTS.md +0 -45
  47. package/dialog/AGENTS.md +0 -81
  48. package/drawer/AGENTS.md +0 -66
  49. package/dropdown-menu/AGENTS.md +0 -133
  50. package/field/AGENTS.md +0 -96
  51. package/input/AGENTS.md +0 -35
  52. package/kbd/AGENTS.md +0 -17
  53. package/label/AGENTS.md +0 -21
  54. package/number-field/AGENTS.md +0 -53
  55. package/popover/AGENTS.md +0 -64
  56. package/press.css +0 -37
  57. package/radio-group/AGENTS.md +0 -81
  58. package/select/AGENTS.md +0 -90
  59. package/switch/AGENTS.md +0 -37
  60. package/table/AGENTS.md +0 -82
  61. package/tabs/AGENTS.md +0 -60
  62. package/test-utils.ts +0 -10
  63. package/textarea/AGENTS.md +0 -35
  64. package/toast/AGENTS.md +0 -34
  65. package/toggle/AGENTS.md +0 -37
  66. package/tooltip/AGENTS.md +0 -50
@@ -1,17 +1,17 @@
1
1
  'use client'
2
2
 
3
- import { Search } from 'lucide-react'
4
3
  import { useState } from 'react'
5
4
 
6
5
  import {
7
6
  Combobox,
7
+ ComboboxCollection,
8
8
  ComboboxEmpty,
9
+ ComboboxGroup,
9
10
  ComboboxInput,
10
11
  ComboboxItem,
11
12
  ComboboxItemDescription,
12
13
  ComboboxItemText,
13
14
  ComboboxViewport,
14
- contains,
15
15
  } from '../combobox'
16
16
  import { Dialog, DialogContent, DialogDescription, DialogTitle } from '../dialog'
17
17
  import { Kbd } from '../kbd'
@@ -19,52 +19,68 @@ import { Kbd } from '../kbd'
19
19
  export interface CommandPaletteItem {
20
20
  value: string
21
21
  label: string
22
- group?: string
22
+ description?: string
23
+ textValue?: string
24
+ }
25
+
26
+ export interface CommandPaletteGroup {
27
+ label: string
28
+ items: CommandPaletteItem[]
23
29
  }
24
30
 
25
31
  export interface CommandPaletteProps {
26
32
  open?: boolean
27
33
  defaultOpen?: boolean
28
34
  onOpenChange?: (open: boolean) => void
29
- items: CommandPaletteItem[]
35
+ items: CommandPaletteItem[] | CommandPaletteGroup[]
30
36
  title: string
37
+ description?: string
31
38
  placeholder: string
32
39
  emptyText: string
33
40
  onSelect?: (value: string) => void
34
41
  className?: string
35
42
  }
36
43
 
44
+ const searchText = (item: CommandPaletteItem) => item.textValue ?? item.label
45
+
46
+ const isGrouped = (items: CommandPaletteProps['items']): items is CommandPaletteGroup[] =>
47
+ items.length > 0 && 'items' in items[0]!
48
+
49
+ function renderItem(item: CommandPaletteItem) {
50
+ return (
51
+ <ComboboxItem key={item.value} value={item}>
52
+ <ComboboxItemText>{item.label}</ComboboxItemText>
53
+ {item.description && (
54
+ <ComboboxItemDescription>{item.description}</ComboboxItemDescription>
55
+ )}
56
+ </ComboboxItem>
57
+ )
58
+ }
59
+
37
60
  export function CommandPalette({
38
61
  open,
39
62
  defaultOpen = false,
40
63
  onOpenChange,
41
64
  items,
42
65
  title,
66
+ description,
43
67
  placeholder,
44
68
  emptyText,
45
69
  onSelect,
46
70
  className,
47
71
  }: CommandPaletteProps) {
48
72
  const [own, setOwn] = useState(defaultOpen)
49
- const [search, setSearch] = useState('')
50
- const [seen, setSeen] = useState(defaultOpen)
51
73
 
52
74
  const isOpen = open ?? own
53
75
 
54
- if (seen !== isOpen) {
55
- setSeen(isOpen)
56
- if (!isOpen) setSearch('')
57
- }
58
-
59
76
  const setOpen = (next: boolean) => {
60
77
  if (open === undefined) setOwn(next)
61
78
  onOpenChange?.(next)
62
79
  }
63
80
 
64
- const filtered = search ? items.filter(i => contains(i.label, search)) : items
65
-
66
- const pick = (value: string) => {
67
- onSelect?.(value)
81
+ const pick = (item: CommandPaletteItem | null) => {
82
+ if (!item) return
83
+ onSelect?.(item.value)
68
84
  setOpen(false)
69
85
  }
70
86
 
@@ -72,16 +88,22 @@ export function CommandPalette({
72
88
  <Dialog open={isOpen} onOpenChange={setOpen}>
73
89
  <DialogContent size="top" className={className}>
74
90
  <DialogTitle className="sr-only">{title}</DialogTitle>
75
- <DialogDescription className="sr-only">
76
- {placeholder}
77
- </DialogDescription>
91
+ {description && <DialogDescription className="sr-only">{description}</DialogDescription>}
78
92
 
79
- <Combobox open onValueChange={pick}>
93
+ <Combobox<CommandPaletteItem>
94
+ inline
95
+ open={isOpen}
96
+ items={items}
97
+ itemToStringLabel={searchText}
98
+ value={null}
99
+ onValueChange={pick}
100
+ >
80
101
  <div className="border-line flex h-14 items-center gap-stack border-b px-4">
81
- <Search className="text-foreground-subtle size-5 shrink-0" />
102
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="text-fg-subtle size-5 shrink-0">
103
+ <path d="m21 21-4.34-4.34" />
104
+ <circle cx="11" cy="11" r="8" />
105
+ </svg>
82
106
  <ComboboxInput
83
- value={search}
84
- onValueChange={setSearch}
85
107
  placeholder={placeholder}
86
108
  className="text-t16"
87
109
  autoFocus
@@ -89,19 +111,18 @@ export function CommandPalette({
89
111
  <Kbd>Esc</Kbd>
90
112
  </div>
91
113
 
114
+ <ComboboxEmpty className="m-2 py-6">
115
+ {emptyText}
116
+ </ComboboxEmpty>
117
+
92
118
  <ComboboxViewport className="max-h-80 gap-1 p-2">
93
- <ComboboxEmpty className="py-6">
94
- {emptyText}
95
- </ComboboxEmpty>
96
-
97
- {filtered.map(item => (
98
- <ComboboxItem key={item.value} value={item.value}>
99
- <ComboboxItemText>{item.label}</ComboboxItemText>
100
- {item.group && (
101
- <ComboboxItemDescription>{item.group}</ComboboxItemDescription>
102
- )}
103
- </ComboboxItem>
104
- ))}
119
+ {isGrouped(items)
120
+ ? (group: CommandPaletteGroup) => (
121
+ <ComboboxGroup key={group.label} label={group.label} items={group.items}>
122
+ <ComboboxCollection>{renderItem}</ComboboxCollection>
123
+ </ComboboxGroup>
124
+ )
125
+ : renderItem}
105
126
  </ComboboxViewport>
106
127
  </Combobox>
107
128
  </DialogContent>
package/dialog/index.tsx CHANGED
@@ -1,12 +1,12 @@
1
1
  'use client'
2
2
 
3
- import { Dialog as DialogPrimitive } from 'radix-ui'
3
+ import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
4
4
  import type { ComponentProps, ReactNode } from 'react'
5
5
 
6
6
  import { tv } from '../tv'
7
7
 
8
8
  export const dialog = tv({
9
- base: 'sheet text-foreground z-overlay fixed flex flex-col gap-stack overflow-y-auto'
9
+ base: 'sheet text-fg z-overlay fixed flex flex-col gap-stack overflow-y-auto'
10
10
  + ' open:animate-fade-in closed:animate-fade-out focus-visible:outline-none',
11
11
 
12
12
  variants: {
@@ -22,54 +22,58 @@ export const dialog = tv({
22
22
  defaultVariants: { size: 'md' },
23
23
  })
24
24
 
25
- export interface DialogProps {
26
- open?: boolean
27
- defaultOpen?: boolean
28
- onOpenChange?: (open: boolean) => void
29
- children?: ReactNode
30
- }
25
+ export type DialogProps = ComponentProps<typeof DialogPrimitive.Root>
31
26
 
32
- export function Dialog({ open, defaultOpen, onOpenChange, children }: DialogProps) {
33
- return (
34
- <DialogPrimitive.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
35
- {children}
36
- </DialogPrimitive.Root>
37
- )
27
+ export function Dialog(props: DialogProps) {
28
+ return <DialogPrimitive.Root {...props} />
38
29
  }
39
30
 
40
- export function DialogTrigger({ children }: { children: ReactNode }) {
41
- return <DialogPrimitive.Trigger asChild>{children}</DialogPrimitive.Trigger>
31
+ export type DialogTriggerProps = ComponentProps<typeof DialogPrimitive.Trigger>
32
+
33
+ export function DialogTrigger(props: DialogTriggerProps) {
34
+ return <DialogPrimitive.Trigger {...props} />
42
35
  }
43
36
 
44
- export function DialogClose({ children }: { children: ReactNode }) {
45
- return <DialogPrimitive.Close asChild>{children}</DialogPrimitive.Close>
37
+ export type DialogCloseProps = ComponentProps<typeof DialogPrimitive.Close>
38
+
39
+ export function DialogClose(props: DialogCloseProps) {
40
+ return <DialogPrimitive.Close {...props} />
46
41
  }
47
42
 
48
- export interface DialogContentProps extends ComponentProps<typeof DialogPrimitive.Content> {
43
+ export type DialogContentProps = Omit<ComponentProps<typeof DialogPrimitive.Popup>, 'className'> & {
49
44
  size?: 'md' | 'full' | 'top'
45
+ className?: string
50
46
  }
51
47
 
52
48
  export function DialogContent({ size, className, children, ...rest }: DialogContentProps) {
53
49
  return (
54
50
  <DialogPrimitive.Portal>
55
- <DialogPrimitive.Overlay className="bg-background-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
51
+ <DialogPrimitive.Backdrop className="bg-bg-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
56
52
 
57
- <DialogPrimitive.Content {...rest} className={dialog({ size, className })}>
53
+ <DialogPrimitive.Popup {...rest} className={dialog({ size, className })}>
58
54
  {children}
59
- </DialogPrimitive.Content>
55
+ </DialogPrimitive.Popup>
60
56
  </DialogPrimitive.Portal>
61
57
  )
62
58
  }
63
59
 
64
60
  const dialogTitle = tv({ base: 'text-section min-w-0' })
65
61
 
66
- export function DialogTitle({ className, ...rest }: ComponentProps<typeof DialogPrimitive.Title>) {
62
+ export type DialogTitleProps = Omit<ComponentProps<typeof DialogPrimitive.Title>, 'className'> & {
63
+ className?: string
64
+ }
65
+
66
+ export function DialogTitle({ className, ...rest }: DialogTitleProps) {
67
67
  return <DialogPrimitive.Title {...rest} className={dialogTitle({ className })} />
68
68
  }
69
69
 
70
- const dialogDescription = tv({ base: 'text-prose text-foreground-muted' })
70
+ const dialogDescription = tv({ base: 'text-prose text-fg-muted' })
71
+
72
+ export type DialogDescriptionProps = Omit<ComponentProps<typeof DialogPrimitive.Description>, 'className'> & {
73
+ className?: string
74
+ }
71
75
 
72
- export function DialogDescription({ className, ...rest }: ComponentProps<typeof DialogPrimitive.Description>) {
76
+ export function DialogDescription({ className, ...rest }: DialogDescriptionProps) {
73
77
  return (
74
78
  <DialogPrimitive.Description {...rest} className={dialogDescription({ className })} />
75
79
  )
@@ -103,7 +107,7 @@ export function DialogHeader({ lead, trail, className, children }: DialogHeaderP
103
107
  )
104
108
  }
105
109
 
106
- const dialogFooter = tv({ base: 'flex justify-end gap-2' })
110
+ const dialogFooter = tv({ base: 'mt-auto flex justify-end gap-2' })
107
111
 
108
112
  export function DialogFooter({ className, children }: { className?: string, children?: ReactNode }) {
109
113
  return (
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 <경로>] 컴포넌트를 저장할 위치를 정해요
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}\npantoia-ui list 로 이름을 확인하세요.`);
58
+ fail(`목록에 없는 컴포넌트예요: ${name}\nnpx @pantoia/ui list로 이름을 확인하세요.`);
31
59
  return item;
32
60
  }
33
61
  function configPath() {
@@ -67,13 +95,13 @@ async function ask(question, fallback) {
67
95
  rl.close();
68
96
  return answer.trim() || fallback;
69
97
  }
70
- async function confirm(question) {
98
+ async function confirm(question, fallback = false) {
71
99
  if (!stdin.isTTY)
72
- return false;
100
+ return fallback;
73
101
  const rl = createInterface({ input: stdin, output: stdout });
74
- const answer = await rl.question(`${question} (y/N) `);
102
+ const answer = (await rl.question(`${question} ${fallback ? '(Y/n)' : '(y/N)'} `)).trim().toLowerCase();
75
103
  rl.close();
76
- return answer.trim().toLowerCase() === 'y';
104
+ return answer === '' ? fallback : answer === 'y';
77
105
  }
78
106
  async function copy(item, base, force) {
79
107
  let skipped = false;
@@ -94,26 +122,126 @@ async function copy(item, base, force) {
94
122
  }
95
123
  return skipped;
96
124
  }
97
- async function commandInit(args) {
98
- const existing = readConfig();
99
- if (existing)
100
- fail(`${CONFIG} 이 이미 있어요.`);
101
- const path = option(args, '--path')
102
- ?? await ask('컴포넌트를 저장할 위치를 입력하세요.', DEFAULT_PATH);
103
- writeConfig({ path, components: {} });
104
- console.log(`${CONFIG} 을 만들었어요. 컴포넌트는 ${path} 에 저장돼요.`);
105
- console.log('전역 CSS 에 아래 한 줄을 추가하세요.');
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;
106
176
  console.log('');
107
- console.log(' @import "@pantoia/ui/styles.css";');
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}`);
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;
199
+ }
200
+ async function commandInit(args) {
201
+ if (readConfig())
202
+ fail(`${CONFIG}이 이미 있어요. 컴포넌트는 npx @pantoia/ui add로 추가하세요.`);
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
+ }
108
236
  }
109
237
  async function commandAdd(args) {
110
238
  const names = args.filter(arg => !arg.startsWith('--'));
111
239
  if (names.length === 0)
112
- fail('추가할 컴포넌트 이름을 입력하세요. 예) pantoia-ui add button');
240
+ fail('추가할 컴포넌트 이름을 입력하세요. 예) npx @pantoia/ui add button');
113
241
  const force = args.includes('--force');
114
242
  const config = readConfig();
115
243
  if (!config)
116
- fail(`${CONFIG} 이 없어요. 먼저 pantoia-ui init 을 실행하세요.`);
244
+ fail(`${CONFIG}이 없어요. 먼저 npx @pantoia/ui init을 실행하세요.`);
117
245
  const items = resolve(names);
118
246
  const asked = items.filter(item => names.includes(item.name));
119
247
  const extra = items.filter(item => !names.includes(item.name));
@@ -142,7 +270,7 @@ async function commandAdd(args) {
142
270
  }
143
271
  }
144
272
  function commandList() {
145
- console.log(`${'이름'.padEnd(16)}${'종류'.padEnd(7)}같이 추가되는 컴포넌트`);
273
+ console.log(`${'이름'.padEnd(16)}${'종류'.padEnd(7)}함께 추가되는 컴포넌트`);
146
274
  for (const item of registry()) {
147
275
  const layer = item.meta?.layer ?? (item.type === 'registry:lib' ? 'lib' : '');
148
276
  const together = (item.registryDependencies ?? []).join(', ');
@@ -181,7 +309,7 @@ function difference(mine, theirs) {
181
309
  async function commandDiff(args) {
182
310
  const config = readConfig();
183
311
  if (!config)
184
- fail(`${CONFIG} 이 없어요. 먼저 pantoia-ui add 로 컴포넌트를 추가하세요.`);
312
+ fail(`${CONFIG}이 없어요. 먼저 npx @pantoia/ui add로 컴포넌트를 추가하세요.`);
185
313
  const names = args.filter(arg => !arg.startsWith('--'));
186
314
  const wanted = names.length > 0 ? names : Object.keys(config.components);
187
315
  let changed = 0;
@@ -195,7 +323,7 @@ async function commandDiff(args) {
195
323
  if (mine === theirs)
196
324
  continue;
197
325
  if (changed === 0)
198
- console.log('- 는 내 파일, + 는 최신 파일이에요.');
326
+ console.log('- 줄은 내 파일, + 줄은 설치된 패키지의 파일이에요.');
199
327
  changed++;
200
328
  console.log(`\n${join(config.path, file.target ?? file.path)}`);
201
329
  for (const line of difference(mine.split('\n'), theirs.split('\n')))
@@ -7,8 +7,8 @@
7
7
  transition: transform var(--dur-enter) var(--ease-enter);
8
8
  }
9
9
 
10
- .panel[data-state="open"] { animation: slide-in var(--dur-enter) var(--ease-enter); }
11
- .panel[data-state="closed"]{ animation: slide-out var(--dur-exit) var(--ease-exit); }
10
+ .panel[data-open] { animation: slide-in var(--dur-enter) var(--ease-enter); }
11
+ .panel[data-closed] { animation: slide-out var(--dur-exit) var(--ease-exit); }
12
12
  .panel[data-swiping] { transition-duration: 0ms; }
13
13
  .panel-right { --drawer-slide: 100%; }
14
14