@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.
- package/accordion/index.module.css +4 -4
- package/accordion/index.tsx +30 -58
- package/alert-dialog/index.tsx +38 -44
- package/badge/index.tsx +2 -2
- package/button/index.tsx +31 -30
- package/callout/index.tsx +29 -8
- package/checkbox/index.tsx +24 -88
- package/combobox/index.tsx +133 -323
- package/command-palette/index.tsx +55 -34
- package/dialog/index.tsx +30 -26
- package/dist/index.js +155 -27
- package/drawer/index.module.css +2 -2
- package/drawer/index.tsx +54 -132
- package/dropdown-menu/index.tsx +95 -100
- package/field/index.tsx +43 -94
- package/input/index.tsx +13 -9
- package/kbd/index.tsx +2 -2
- package/label/index.tsx +7 -9
- package/number-field/index.tsx +129 -176
- package/package.json +12 -15
- package/palette.css +114 -0
- package/popover/index.tsx +62 -33
- package/radio-group/index.tsx +36 -39
- package/registry.json +65 -169
- package/select/index.tsx +154 -76
- package/switch/index.tsx +13 -6
- package/table/index.tsx +5 -6
- package/tabs/index.module.css +5 -3
- package/tabs/index.tsx +26 -77
- package/tailwind.css +3 -0
- package/textarea/index.tsx +25 -13
- package/toast/index.module.css +187 -0
- package/toast/index.tsx +141 -46
- package/toggle/index.tsx +8 -7
- package/{styles.css → tokens.css} +82 -303
- package/tooltip/index.tsx +62 -31
- package/tv.ts +0 -1
- package/utilities.css +124 -0
- package/accordion/AGENTS.md +0 -60
- package/alert-dialog/AGENTS.md +0 -78
- package/badge/AGENTS.md +0 -35
- package/button/AGENTS.md +0 -39
- package/callout/AGENTS.md +0 -41
- package/checkbox/AGENTS.md +0 -54
- package/combobox/AGENTS.md +0 -150
- package/command-palette/AGENTS.md +0 -45
- package/dialog/AGENTS.md +0 -81
- package/drawer/AGENTS.md +0 -66
- package/dropdown-menu/AGENTS.md +0 -133
- package/field/AGENTS.md +0 -96
- package/input/AGENTS.md +0 -35
- package/kbd/AGENTS.md +0 -17
- package/label/AGENTS.md +0 -21
- package/number-field/AGENTS.md +0 -53
- package/popover/AGENTS.md +0 -64
- package/press.css +0 -37
- package/radio-group/AGENTS.md +0 -81
- package/select/AGENTS.md +0 -90
- package/switch/AGENTS.md +0 -37
- package/table/AGENTS.md +0 -82
- package/tabs/AGENTS.md +0 -60
- package/test-utils.ts +0 -10
- package/textarea/AGENTS.md +0 -35
- package/toast/AGENTS.md +0 -34
- package/toggle/AGENTS.md +0 -37
- 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
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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
|
-
<
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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 '
|
|
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-
|
|
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
|
|
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(
|
|
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
|
|
41
|
-
|
|
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
|
|
45
|
-
|
|
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
|
|
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.
|
|
51
|
+
<DialogPrimitive.Backdrop className="bg-bg-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
|
|
56
52
|
|
|
57
|
-
<DialogPrimitive.
|
|
53
|
+
<DialogPrimitive.Popup {...rest} className={dialog({ size, className })}>
|
|
58
54
|
{children}
|
|
59
|
-
</DialogPrimitive.
|
|
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
|
|
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-
|
|
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 }:
|
|
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
|
|
9
|
-
const
|
|
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}\
|
|
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
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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(
|
|
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
|
|
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}
|
|
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}
|
|
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')))
|
package/drawer/index.module.css
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
transition: transform var(--dur-enter) var(--ease-enter);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
.panel[data-
|
|
11
|
-
.panel[data-
|
|
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
|
|