@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.
- package/accordion/AGENTS.md +60 -0
- package/accordion/index.module.css +10 -0
- package/accordion/index.tsx +91 -0
- package/alert-dialog/AGENTS.md +78 -0
- package/alert-dialog/index.tsx +119 -0
- package/badge/AGENTS.md +35 -0
- package/badge/index.tsx +34 -0
- package/button/AGENTS.md +39 -0
- package/button/index.tsx +98 -0
- package/callout/AGENTS.md +42 -0
- package/callout/index.tsx +70 -0
- package/checkbox/AGENTS.md +54 -0
- package/checkbox/index.tsx +151 -0
- package/code-block/AGENTS.md +60 -0
- package/code-block/index.module.css +46 -0
- package/code-block/index.tsx +127 -0
- package/combobox/AGENTS.md +150 -0
- package/combobox/index.tsx +420 -0
- package/command-palette/AGENTS.md +44 -0
- package/command-palette/index.tsx +108 -0
- package/copy-button/AGENTS.md +43 -0
- package/copy-button/index.tsx +119 -0
- package/dialog/AGENTS.md +81 -0
- package/dialog/index.tsx +108 -0
- package/dist/index.js +189 -0
- package/drawer/AGENTS.md +66 -0
- package/drawer/index.module.css +17 -0
- package/drawer/index.tsx +198 -0
- package/dropdown-menu/AGENTS.md +133 -0
- package/dropdown-menu/index.tsx +223 -0
- package/field/AGENTS.md +96 -0
- package/field/index.tsx +149 -0
- package/input/AGENTS.md +35 -0
- package/input/index.tsx +36 -0
- package/kbd/AGENTS.md +17 -0
- package/kbd/index.tsx +13 -0
- package/label/AGENTS.md +21 -0
- package/label/index.tsx +13 -0
- package/number-field/AGENTS.md +53 -0
- package/number-field/index.tsx +227 -0
- package/package.json +73 -0
- package/popover/AGENTS.md +64 -0
- package/popover/index.tsx +66 -0
- package/press.css +37 -0
- package/radio-group/AGENTS.md +81 -0
- package/radio-group/index.tsx +139 -0
- package/registry.json +726 -0
- package/select/AGENTS.md +90 -0
- package/select/index.tsx +169 -0
- package/styles.css +347 -0
- package/switch/AGENTS.md +37 -0
- package/switch/index.tsx +42 -0
- package/table/AGENTS.md +82 -0
- package/table/index.tsx +120 -0
- package/tabs/AGENTS.md +60 -0
- package/tabs/index.module.css +14 -0
- package/tabs/index.tsx +105 -0
- package/test-utils.ts +10 -0
- package/textarea/AGENTS.md +35 -0
- package/textarea/index.tsx +37 -0
- package/toast/AGENTS.md +42 -0
- package/toast/index.tsx +71 -0
- package/toggle/AGENTS.md +37 -0
- package/toggle/index.tsx +36 -0
- package/tooltip/AGENTS.md +50 -0
- package/tooltip/index.tsx +57 -0
- package/tv.ts +23 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
import { Button } from '../button'
|
|
6
|
+
|
|
7
|
+
export type CopyState = 'idle' | 'copied' | 'failed'
|
|
8
|
+
|
|
9
|
+
function writeLegacy(value: string): boolean {
|
|
10
|
+
const ta = document.createElement('textarea')
|
|
11
|
+
ta.value = value
|
|
12
|
+
ta.setAttribute('readonly', '')
|
|
13
|
+
ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none'
|
|
14
|
+
document.body.appendChild(ta)
|
|
15
|
+
ta.select()
|
|
16
|
+
|
|
17
|
+
let ok: boolean
|
|
18
|
+
try { ok = document.execCommand('copy') }
|
|
19
|
+
catch { ok = false }
|
|
20
|
+
|
|
21
|
+
ta.remove()
|
|
22
|
+
return ok
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function write(value: string): Promise<boolean> {
|
|
26
|
+
try {
|
|
27
|
+
await navigator.clipboard.writeText(value)
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return writeLegacy(value)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function useCopy(resetAfter = 2000) {
|
|
36
|
+
const [state, setState] = useState<CopyState>('idle')
|
|
37
|
+
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
38
|
+
|
|
39
|
+
useEffect(() => () => clearTimeout(timer.current), [])
|
|
40
|
+
|
|
41
|
+
const copy = async (value: string) => {
|
|
42
|
+
clearTimeout(timer.current)
|
|
43
|
+
const next: CopyState = await write(value) ? 'copied' : 'failed'
|
|
44
|
+
setState(next)
|
|
45
|
+
timer.current = setTimeout(() => setState('idle'), resetAfter)
|
|
46
|
+
return next
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { state, copy }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CopyButtonProps {
|
|
53
|
+
value: string
|
|
54
|
+
color?: 'primary' | 'neutral' | 'danger'
|
|
55
|
+
emphasis?: 'solid' | 'weak' | 'outline' | 'ghost'
|
|
56
|
+
size?: 'sm' | 'md' | 'lg'
|
|
57
|
+
shape?: 'rect' | 'square' | 'pill' | 'circle'
|
|
58
|
+
onCopied?: (value: string) => void
|
|
59
|
+
onFailed?: () => void
|
|
60
|
+
disabled?: boolean
|
|
61
|
+
className?: string
|
|
62
|
+
'aria-label'?: string
|
|
63
|
+
children?: ReactNode
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function CopyButton({
|
|
67
|
+
value,
|
|
68
|
+
color = 'neutral',
|
|
69
|
+
emphasis = 'outline',
|
|
70
|
+
size = 'md',
|
|
71
|
+
shape = 'rect',
|
|
72
|
+
onCopied,
|
|
73
|
+
onFailed,
|
|
74
|
+
children,
|
|
75
|
+
...rest
|
|
76
|
+
}: CopyButtonProps) {
|
|
77
|
+
const { state, copy } = useCopy()
|
|
78
|
+
|
|
79
|
+
const run = async () => {
|
|
80
|
+
const result = await copy(value)
|
|
81
|
+
if (result === 'copied') onCopied?.(value)
|
|
82
|
+
else onFailed?.()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Button {...rest} color={color} emphasis={emphasis} size={size} shape={shape} onClick={run}>
|
|
87
|
+
<svg
|
|
88
|
+
viewBox="0 0 24 24"
|
|
89
|
+
fill="none"
|
|
90
|
+
stroke="currentColor"
|
|
91
|
+
strokeWidth="2"
|
|
92
|
+
strokeLinecap="round"
|
|
93
|
+
strokeLinejoin="round"
|
|
94
|
+
aria-hidden="true"
|
|
95
|
+
className="shrink-0"
|
|
96
|
+
>
|
|
97
|
+
{state === 'idle' && (
|
|
98
|
+
<g>
|
|
99
|
+
<rect x="8" y="8" width="14" height="14" rx="2" />
|
|
100
|
+
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
|
101
|
+
</g>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
{state === 'copied' && (
|
|
105
|
+
<path className="animate-draw motion-reduce:animate-none" d="M4 12l5 5L20 6" />
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{state === 'failed' && (
|
|
109
|
+
<g className="animate-enter motion-reduce:animate-none">
|
|
110
|
+
<path d="M18 6 6 18" />
|
|
111
|
+
<path d="m6 6 12 12" />
|
|
112
|
+
</g>
|
|
113
|
+
)}
|
|
114
|
+
</svg>
|
|
115
|
+
|
|
116
|
+
{children}
|
|
117
|
+
</Button>
|
|
118
|
+
)
|
|
119
|
+
}
|
package/dialog/AGENTS.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Dialog
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
받기 npx @pantoia/ui add dialog
|
|
5
|
+
층 ui
|
|
6
|
+
파일 dialog/index.tsx · dialog/AGENTS.md
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## 부위
|
|
10
|
+
|
|
11
|
+
| 이름 | props |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `Dialog` | `DialogProps` |
|
|
14
|
+
| `DialogTrigger` | — |
|
|
15
|
+
| `DialogClose` | — |
|
|
16
|
+
| `DialogContent` | `DialogContentProps` |
|
|
17
|
+
| `DialogTitle` | `ComponentProps<typeof DialogPrimitive.Title>` |
|
|
18
|
+
| `DialogDescription` | `ComponentProps<typeof DialogPrimitive.Description>` |
|
|
19
|
+
| `DialogHeader` | `DialogHeaderProps` |
|
|
20
|
+
| `DialogFooter` | — |
|
|
21
|
+
|
|
22
|
+
### `DialogTrigger`
|
|
23
|
+
|
|
24
|
+
| 이름 | 타입 | 필수 |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `children` | `ReactNode` | 예 |
|
|
27
|
+
|
|
28
|
+
### `DialogClose`
|
|
29
|
+
|
|
30
|
+
| 이름 | 타입 | 필수 |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `children` | `ReactNode` | 예 |
|
|
33
|
+
|
|
34
|
+
### `DialogFooter`
|
|
35
|
+
|
|
36
|
+
| 이름 | 타입 | 필수 |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `className` | `string` | |
|
|
39
|
+
| `children` | `ReactNode` | |
|
|
40
|
+
|
|
41
|
+
## props
|
|
42
|
+
|
|
43
|
+
### `DialogProps`
|
|
44
|
+
|
|
45
|
+
| 이름 | 타입 | 필수 |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `open` | `boolean` | |
|
|
48
|
+
| `defaultOpen` | `boolean` | |
|
|
49
|
+
| `onOpenChange` | `(open: boolean) => void` | |
|
|
50
|
+
| `children` | `ReactNode` | |
|
|
51
|
+
|
|
52
|
+
### `DialogContentProps`
|
|
53
|
+
|
|
54
|
+
`ComponentProps<typeof DialogPrimitive.Content>` 를 확장한다.
|
|
55
|
+
|
|
56
|
+
| 이름 | 타입 | 필수 |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `size` | `'md' \| 'full' \| 'top'` | |
|
|
59
|
+
|
|
60
|
+
### `DialogHeaderProps`
|
|
61
|
+
|
|
62
|
+
| 이름 | 타입 | 필수 |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `lead` | `ReactNode` | |
|
|
65
|
+
| `trail` | `ReactNode` | |
|
|
66
|
+
| `className` | `string` | |
|
|
67
|
+
| `children` | `ReactNode` | |
|
|
68
|
+
|
|
69
|
+
## 변형 — `dialog`
|
|
70
|
+
|
|
71
|
+
| 축 | 값 | 기본 |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `size` | `md` · `full` · `top` | `md` |
|
|
74
|
+
|
|
75
|
+
## 함께 받는 것
|
|
76
|
+
|
|
77
|
+
`tv`
|
|
78
|
+
|
|
79
|
+
## npm 의존
|
|
80
|
+
|
|
81
|
+
`radix-ui`
|
package/dialog/index.tsx
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Dialog as DialogPrimitive } from 'radix-ui'
|
|
4
|
+
import type { ComponentProps, ReactNode } from 'react'
|
|
5
|
+
|
|
6
|
+
import { tv } from '../tv'
|
|
7
|
+
|
|
8
|
+
export const dialog = tv({
|
|
9
|
+
base: 'bg-float text-fg z-overlay fixed flex flex-col gap-stack overflow-y-auto'
|
|
10
|
+
+ ' open:animate-fade-in closed:animate-fade-out focus-visible:outline-none',
|
|
11
|
+
|
|
12
|
+
variants: {
|
|
13
|
+
size: {
|
|
14
|
+
md: 'left-1/2 top-1/2 w-full max-w-120 -translate-x-1/2 -translate-y-1/2'
|
|
15
|
+
+ ' rounded-r20 p-6',
|
|
16
|
+
full: 'inset-0 p-6',
|
|
17
|
+
top: 'left-1/2 top-24 w-full max-w-144 -translate-x-1/2'
|
|
18
|
+
+ ' rounded-r20 gap-0 overflow-hidden',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
defaultVariants: { size: 'md' },
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export interface DialogProps {
|
|
26
|
+
open?: boolean
|
|
27
|
+
defaultOpen?: boolean
|
|
28
|
+
onOpenChange?: (open: boolean) => void
|
|
29
|
+
children?: ReactNode
|
|
30
|
+
}
|
|
31
|
+
|
|
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
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function DialogTrigger({ children }: { children: ReactNode }) {
|
|
41
|
+
return <DialogPrimitive.Trigger asChild>{children}</DialogPrimitive.Trigger>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function DialogClose({ children }: { children: ReactNode }) {
|
|
45
|
+
return <DialogPrimitive.Close asChild>{children}</DialogPrimitive.Close>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface DialogContentProps extends ComponentProps<typeof DialogPrimitive.Content> {
|
|
49
|
+
size?: 'md' | 'full' | 'top'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function DialogContent({ size, className, children, ...rest }: DialogContentProps) {
|
|
53
|
+
return (
|
|
54
|
+
<DialogPrimitive.Portal>
|
|
55
|
+
<DialogPrimitive.Overlay className="bg-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
|
|
56
|
+
|
|
57
|
+
<DialogPrimitive.Content {...rest} className={dialog({ size, className })}>
|
|
58
|
+
{children}
|
|
59
|
+
</DialogPrimitive.Content>
|
|
60
|
+
</DialogPrimitive.Portal>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function DialogTitle({ className, ...rest }: ComponentProps<typeof DialogPrimitive.Title>) {
|
|
65
|
+
return <DialogPrimitive.Title {...rest} className={tv({ base: 'text-section min-w-0' })({ className })} />
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function DialogDescription({ className, ...rest }: ComponentProps<typeof DialogPrimitive.Description>) {
|
|
69
|
+
return (
|
|
70
|
+
<DialogPrimitive.Description {...rest} className={tv({ base: 'text-prose text-fg-muted' })({ className })} />
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const dialogHeader = tv({
|
|
75
|
+
slots: {
|
|
76
|
+
root: 'flex items-start gap-stack',
|
|
77
|
+
lead: 'shrink-0',
|
|
78
|
+
body: 'flex min-w-0 grow flex-col gap-tight',
|
|
79
|
+
trail: 'shrink-0',
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
export interface DialogHeaderProps {
|
|
84
|
+
lead?: ReactNode
|
|
85
|
+
trail?: ReactNode
|
|
86
|
+
className?: string
|
|
87
|
+
children?: ReactNode
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function DialogHeader({ lead, trail, className, children }: DialogHeaderProps) {
|
|
91
|
+
const ui = dialogHeader()
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className={ui.root({ className })}>
|
|
95
|
+
{lead && <div className={ui.lead()}>{lead}</div>}
|
|
96
|
+
<div className={ui.body()}>{children}</div>
|
|
97
|
+
{trail && <div className={ui.trail()}>{trail}</div>}
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function DialogFooter({ className, children }: { className?: string, children?: ReactNode }) {
|
|
103
|
+
return (
|
|
104
|
+
<div className={tv({ base: 'flex justify-end gap-2' })({ className })}>
|
|
105
|
+
{children}
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { argv, cwd, exit, stdin, stdout } from 'node:process';
|
|
5
|
+
import { createInterface } from 'node:readline/promises';
|
|
6
|
+
const HERE = join(import.meta.dirname, '..');
|
|
7
|
+
const CONFIG = 'components.json';
|
|
8
|
+
const DEFAULT_PATH = 'components/ui';
|
|
9
|
+
const USAGE = `pantoia-ui <명령>
|
|
10
|
+
|
|
11
|
+
init [--path <디렉터리>] ${CONFIG} 을 만든다
|
|
12
|
+
add <이름...> [--force] 소스를 복사한다
|
|
13
|
+
list 받을 수 있는 것을 늘어놓는다
|
|
14
|
+
diff [이름] 복사본과 패키지의 지금 것을 견준다`;
|
|
15
|
+
function fail(message) {
|
|
16
|
+
console.error(message);
|
|
17
|
+
exit(1);
|
|
18
|
+
}
|
|
19
|
+
function version() {
|
|
20
|
+
const manifest = JSON.parse(readFileSync(join(HERE, 'package.json'), 'utf8'));
|
|
21
|
+
return manifest.version;
|
|
22
|
+
}
|
|
23
|
+
function registry() {
|
|
24
|
+
const doc = JSON.parse(readFileSync(join(HERE, 'registry.json'), 'utf8'));
|
|
25
|
+
return doc.items ?? [];
|
|
26
|
+
}
|
|
27
|
+
function find(name) {
|
|
28
|
+
const item = registry().find(candidate => candidate.name === name);
|
|
29
|
+
if (!item)
|
|
30
|
+
fail(`${name} 은 목록에 없다. pantoia-ui list 로 이름을 본다`);
|
|
31
|
+
return item;
|
|
32
|
+
}
|
|
33
|
+
function configPath() {
|
|
34
|
+
return join(cwd(), CONFIG);
|
|
35
|
+
}
|
|
36
|
+
function readConfig() {
|
|
37
|
+
if (!existsSync(configPath()))
|
|
38
|
+
return undefined;
|
|
39
|
+
return JSON.parse(readFileSync(configPath(), 'utf8'));
|
|
40
|
+
}
|
|
41
|
+
function writeConfig(config) {
|
|
42
|
+
const sorted = Object.fromEntries(Object.entries(config.components).sort());
|
|
43
|
+
writeFileSync(configPath(), `${JSON.stringify({ ...config, components: sorted }, null, 2)}\n`);
|
|
44
|
+
}
|
|
45
|
+
function option(args, flag) {
|
|
46
|
+
const at = args.indexOf(flag);
|
|
47
|
+
return at === -1 ? undefined : args[at + 1];
|
|
48
|
+
}
|
|
49
|
+
function resolve(names) {
|
|
50
|
+
const seen = new Map();
|
|
51
|
+
const queue = [...names];
|
|
52
|
+
while (queue.length > 0) {
|
|
53
|
+
const name = queue.shift();
|
|
54
|
+
if (seen.has(name))
|
|
55
|
+
continue;
|
|
56
|
+
const item = find(name);
|
|
57
|
+
seen.set(name, item);
|
|
58
|
+
queue.push(...(item.registryDependencies ?? []));
|
|
59
|
+
}
|
|
60
|
+
return [...seen.values()];
|
|
61
|
+
}
|
|
62
|
+
async function confirm(question) {
|
|
63
|
+
if (!stdin.isTTY)
|
|
64
|
+
return false;
|
|
65
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
66
|
+
const answer = await rl.question(`${question} (y/N) `);
|
|
67
|
+
rl.close();
|
|
68
|
+
return answer.trim().toLowerCase() === 'y';
|
|
69
|
+
}
|
|
70
|
+
async function copy(item, root, force) {
|
|
71
|
+
for (const file of item.files ?? []) {
|
|
72
|
+
const source = readFileSync(join(HERE, file.path), 'utf8');
|
|
73
|
+
const target = join(root, file.target ?? file.path);
|
|
74
|
+
if (existsSync(target) && readFileSync(target, 'utf8') !== source) {
|
|
75
|
+
if (!force && !await confirm(`${file.path} 이 이미 있고 내용이 다르다. 덮어쓰나?`)) {
|
|
76
|
+
console.log(` 건너뛴다 ${file.path} — 덮어쓰려면 --force`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
81
|
+
writeFileSync(target, source);
|
|
82
|
+
console.log(` 썼다 ${file.path}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
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";');
|
|
93
|
+
}
|
|
94
|
+
async function commandAdd(args) {
|
|
95
|
+
const names = args.filter(arg => !arg.startsWith('--'));
|
|
96
|
+
if (names.length === 0)
|
|
97
|
+
fail('받을 이름을 적는다 — pantoia-ui add button');
|
|
98
|
+
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
|
+
}
|
|
104
|
+
const items = resolve(names);
|
|
105
|
+
const root = join(cwd(), config.path);
|
|
106
|
+
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) {
|
|
110
|
+
console.log(item.name);
|
|
111
|
+
await copy(item, root, force);
|
|
112
|
+
config.components[item.name] = version();
|
|
113
|
+
}
|
|
114
|
+
writeConfig(config);
|
|
115
|
+
const dependencies = [...new Set(items.flatMap(item => item.dependencies ?? []))].sort();
|
|
116
|
+
if (dependencies.length > 0)
|
|
117
|
+
console.log(`\nnpm 의존을 적는다 — ${dependencies.join(' · ')}`);
|
|
118
|
+
}
|
|
119
|
+
function commandList() {
|
|
120
|
+
for (const item of registry()) {
|
|
121
|
+
const together = (item.registryDependencies ?? []).join(' · ');
|
|
122
|
+
console.log(`${item.name.padEnd(18)}${(item.meta?.layer ?? '').padEnd(9)}${together}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function lcs(a, b) {
|
|
126
|
+
const table = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
127
|
+
for (let i = a.length - 1; i >= 0; i--)
|
|
128
|
+
for (let j = b.length - 1; j >= 0; j--)
|
|
129
|
+
table[i][j] = a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
|
|
130
|
+
return table;
|
|
131
|
+
}
|
|
132
|
+
function difference(mine, theirs) {
|
|
133
|
+
const table = lcs(mine, theirs);
|
|
134
|
+
const out = [];
|
|
135
|
+
let i = 0;
|
|
136
|
+
let j = 0;
|
|
137
|
+
while (i < mine.length && j < theirs.length) {
|
|
138
|
+
if (mine[i] === theirs[j]) {
|
|
139
|
+
i++;
|
|
140
|
+
j++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (table[i + 1][j] >= table[i][j + 1])
|
|
144
|
+
out.push(`- ${mine[i++]}`);
|
|
145
|
+
else
|
|
146
|
+
out.push(`+ ${theirs[j++]}`);
|
|
147
|
+
}
|
|
148
|
+
while (i < mine.length)
|
|
149
|
+
out.push(`- ${mine[i++]}`);
|
|
150
|
+
while (j < theirs.length)
|
|
151
|
+
out.push(`+ ${theirs[j++]}`);
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
async function commandDiff(args) {
|
|
155
|
+
const config = readConfig();
|
|
156
|
+
if (!config)
|
|
157
|
+
fail(`${CONFIG} 이 없다 — pantoia-ui init 을 먼저 돌린다`);
|
|
158
|
+
const names = args.filter(arg => !arg.startsWith('--'));
|
|
159
|
+
const wanted = names.length > 0 ? names : Object.keys(config.components);
|
|
160
|
+
let changed = 0;
|
|
161
|
+
for (const item of wanted.map(find)) {
|
|
162
|
+
for (const file of item.files ?? []) {
|
|
163
|
+
const target = join(cwd(), config.path, file.target ?? file.path);
|
|
164
|
+
if (!existsSync(target))
|
|
165
|
+
continue;
|
|
166
|
+
const mine = readFileSync(target, 'utf8');
|
|
167
|
+
const theirs = readFileSync(join(HERE, file.path), 'utf8');
|
|
168
|
+
if (mine === theirs)
|
|
169
|
+
continue;
|
|
170
|
+
changed++;
|
|
171
|
+
console.log(`\n${file.path}`);
|
|
172
|
+
for (const line of difference(mine.split('\n'), theirs.split('\n')))
|
|
173
|
+
console.log(line);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (changed === 0)
|
|
177
|
+
console.log('갈린 것이 없다');
|
|
178
|
+
}
|
|
179
|
+
const [command, ...args] = argv.slice(2);
|
|
180
|
+
if (command === 'init')
|
|
181
|
+
await commandInit(args);
|
|
182
|
+
else if (command === 'add')
|
|
183
|
+
await commandAdd(args);
|
|
184
|
+
else if (command === 'list')
|
|
185
|
+
commandList();
|
|
186
|
+
else if (command === 'diff')
|
|
187
|
+
await commandDiff(args);
|
|
188
|
+
else
|
|
189
|
+
fail(USAGE);
|
package/drawer/AGENTS.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Drawer
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
받기 npx @pantoia/ui add drawer
|
|
5
|
+
층 ui
|
|
6
|
+
파일 drawer/index.tsx · drawer/index.module.css · drawer/AGENTS.md
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## 부위
|
|
10
|
+
|
|
11
|
+
| 이름 | props |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `Drawer` | `DrawerProps` |
|
|
14
|
+
| `DrawerTrigger` | — |
|
|
15
|
+
| `DrawerClose` | — |
|
|
16
|
+
| `DrawerContent` | `ComponentProps<typeof Dialog.Content>` |
|
|
17
|
+
| `DrawerHeader` | `DrawerHeaderProps` |
|
|
18
|
+
| `DrawerTitle` | `ComponentProps<typeof Dialog.Title>` |
|
|
19
|
+
| `DrawerDescription` | `ComponentProps<typeof Dialog.Description>` |
|
|
20
|
+
|
|
21
|
+
### `DrawerTrigger`
|
|
22
|
+
|
|
23
|
+
| 이름 | 타입 | 필수 |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `children` | `ReactNode` | 예 |
|
|
26
|
+
|
|
27
|
+
### `DrawerClose`
|
|
28
|
+
|
|
29
|
+
| 이름 | 타입 | 필수 |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `children` | `ReactNode` | 예 |
|
|
32
|
+
|
|
33
|
+
## props
|
|
34
|
+
|
|
35
|
+
### `DrawerProps`
|
|
36
|
+
|
|
37
|
+
| 이름 | 타입 | 필수 |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `side` | `Side` | |
|
|
40
|
+
| `open` | `boolean` | |
|
|
41
|
+
| `defaultOpen` | `boolean` | |
|
|
42
|
+
| `onOpenChange` | `(open: boolean) => void` | |
|
|
43
|
+
| `children` | `ReactNode` | 예 |
|
|
44
|
+
|
|
45
|
+
### `DrawerHeaderProps`
|
|
46
|
+
|
|
47
|
+
| 이름 | 타입 | 필수 |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `lead` | `ReactNode` | |
|
|
50
|
+
| `trail` | `ReactNode` | |
|
|
51
|
+
| `className` | `string` | |
|
|
52
|
+
| `children` | `ReactNode` | |
|
|
53
|
+
|
|
54
|
+
## 변형 — `drawerContent`
|
|
55
|
+
|
|
56
|
+
| 축 | 값 | 기본 |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `side` | `left` · `right` | — |
|
|
59
|
+
|
|
60
|
+
## 함께 받는 것
|
|
61
|
+
|
|
62
|
+
`tv`
|
|
63
|
+
|
|
64
|
+
## npm 의존
|
|
65
|
+
|
|
66
|
+
`radix-ui`
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
@keyframes slide-in { from { translate: var(--drawer-slide) 0; opacity: 0; } }
|
|
2
|
+
@keyframes slide-out { to { translate: var(--drawer-slide) 0; opacity: 0; } }
|
|
3
|
+
|
|
4
|
+
.panel {
|
|
5
|
+
--drawer-slide: -100%;
|
|
6
|
+
transform: translateX(var(--drawer-swipe-movement-x, 0px));
|
|
7
|
+
transition: transform var(--dur-enter) var(--ease-enter);
|
|
8
|
+
}
|
|
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); }
|
|
12
|
+
.panel[data-swiping] { transition-duration: 0ms; }
|
|
13
|
+
.panel-right { --drawer-slide: 100%; }
|
|
14
|
+
|
|
15
|
+
@media (prefers-reduced-motion: reduce) {
|
|
16
|
+
.panel { --drawer-slide: 0%; }
|
|
17
|
+
}
|