@conscience-technology/ctds 0.3.2 → 0.3.4
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/assets/components/Checkbox/Checkbox.module.css +1 -0
- package/assets/components/MenuItem/MenuItem.module.css +30 -5
- package/assets/components/MenuItem/MenuItem.tsx +86 -5
- package/assets/components/Radio/Radio.module.css +1 -0
- package/assets/components/Sidebar/Sidebar.tsx +4 -1
- package/assets/components/_data.ts +3 -7
- package/assets/components/index.ts +0 -1
- package/assets/prompts/components/menu-item.md +1 -0
- package/package.json +1 -1
- package/assets/components/Accordion/Accordion.module.css +0 -247
- package/assets/components/Accordion/Accordion.tsx +0 -207
- package/assets/components/Accordion/index.ts +0 -8
- package/assets/components/CommandPalette/CommandPalette.module.css +0 -203
- package/assets/components/CommandPalette/CommandPalette.tsx +0 -262
- package/assets/components/CommandPalette/index.ts +0 -9
- package/assets/components/CommandPalette/useCommandPaletteHotkey.ts +0 -49
- package/assets/components/EventCard/EventCard.module.css +0 -133
- package/assets/components/EventCard/EventCard.tsx +0 -149
- package/assets/components/EventCard/index.ts +0 -2
- package/assets/prompts/components/accordion.md +0 -126
- package/assets/prompts/components/command-palette.md +0 -148
- package/assets/prompts/components/event-card.md +0 -78
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Accordion ─ 여러 섹션을 접었다 폈다 하는 구조 컴포넌트.
|
|
5
|
-
*
|
|
6
|
-
* 스택: Radix `@radix-ui/react-accordion` 어댑터.
|
|
7
|
-
* - 열림/닫힘 상태·키보드 네비(↑↓ Home End)·ARIA(aria-expanded/controls)·
|
|
8
|
-
* 콘텐츠 높이 측정은 전부 Radix가 처리.
|
|
9
|
-
* - 시각(Trigger 레이아웃·caret·divider·card variant)만 CTDS가 담당.
|
|
10
|
-
*
|
|
11
|
-
* variant
|
|
12
|
-
* - plain (기본): 테두리 없음, 아이템 사이 hairline만. 설정 그룹·FAQ용.
|
|
13
|
-
* - card: 각 아이템이 테두리 + radius로 독립 블록. 콘텐츠 사이트의 섹션 리스트용.
|
|
14
|
-
*
|
|
15
|
-
* 소비 표면
|
|
16
|
-
* <Accordion type="single" collapsible defaultValue="general">
|
|
17
|
-
* <Accordion.Item value="general">
|
|
18
|
-
* <Accordion.Trigger leadingIcon="gear">일반</Accordion.Trigger>
|
|
19
|
-
* <Accordion.Content>...</Accordion.Content>
|
|
20
|
-
* </Accordion.Item>
|
|
21
|
-
* </Accordion>
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import * as RadixAccordion from "@radix-ui/react-accordion";
|
|
25
|
-
import {
|
|
26
|
-
createContext,
|
|
27
|
-
forwardRef,
|
|
28
|
-
useContext,
|
|
29
|
-
type ComponentPropsWithoutRef,
|
|
30
|
-
type ReactNode,
|
|
31
|
-
} from "react";
|
|
32
|
-
|
|
33
|
-
import { Icon, type IconName } from "../Icon";
|
|
34
|
-
import styles from "./Accordion.module.css";
|
|
35
|
-
|
|
36
|
-
export type AccordionSize = "sm" | "md" | "lg";
|
|
37
|
-
/**
|
|
38
|
-
* - bare: 선·좌우 여백 전부 없음. 본문 흐름에 가장 투명하게 녹음(툴바·설명 블록 내부).
|
|
39
|
-
* - plain: 테두리 없음, 아이템 사이 hairline, Trigger 좌우 padding 있음. 설정·FAQ 흐름.
|
|
40
|
-
* - card: 각 아이템이 테두리 + radius로 독립 블록. 콘텐츠 섹션 리스트.
|
|
41
|
-
*/
|
|
42
|
-
export type AccordionVariant = "bare" | "plain" | "card";
|
|
43
|
-
|
|
44
|
-
export type AccordionAction =
|
|
45
|
-
| ReactNode
|
|
46
|
-
| { node: ReactNode; key?: string };
|
|
47
|
-
|
|
48
|
-
/* Size·variant context — Trigger가 Root 설정 상속. */
|
|
49
|
-
type AccordionContextValue = {
|
|
50
|
-
size: AccordionSize;
|
|
51
|
-
variant: AccordionVariant;
|
|
52
|
-
};
|
|
53
|
-
const AccordionContext = createContext<AccordionContextValue>({
|
|
54
|
-
size: "sm",
|
|
55
|
-
variant: "plain",
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
/* Root ─────────────────────────────────────────────────────── */
|
|
59
|
-
|
|
60
|
-
type SingleRootProps = ComponentPropsWithoutRef<typeof RadixAccordion.Root> & {
|
|
61
|
-
type: "single";
|
|
62
|
-
};
|
|
63
|
-
type MultipleRootProps = ComponentPropsWithoutRef<typeof RadixAccordion.Root> & {
|
|
64
|
-
type: "multiple";
|
|
65
|
-
};
|
|
66
|
-
export type AccordionProps = (SingleRootProps | MultipleRootProps) & {
|
|
67
|
-
size?: AccordionSize;
|
|
68
|
-
variant?: AccordionVariant;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const Root = forwardRef<HTMLDivElement, AccordionProps>(function Accordion(
|
|
72
|
-
{ className, size = "sm", variant = "plain", children, ...rest },
|
|
73
|
-
ref,
|
|
74
|
-
) {
|
|
75
|
-
return (
|
|
76
|
-
<AccordionContext.Provider value={{ size, variant }}>
|
|
77
|
-
<RadixAccordion.Root
|
|
78
|
-
ref={ref}
|
|
79
|
-
data-size={size}
|
|
80
|
-
data-variant={variant}
|
|
81
|
-
className={[styles.root, className ?? ""].filter(Boolean).join(" ")}
|
|
82
|
-
{...(rest as ComponentPropsWithoutRef<typeof RadixAccordion.Root>)}
|
|
83
|
-
>
|
|
84
|
-
{children}
|
|
85
|
-
</RadixAccordion.Root>
|
|
86
|
-
</AccordionContext.Provider>
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
/* Item ─────────────────────────────────────────────────────── */
|
|
91
|
-
|
|
92
|
-
const Item = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<typeof RadixAccordion.Item>>(
|
|
93
|
-
function AccordionItem({ className, children, ...rest }, ref) {
|
|
94
|
-
return (
|
|
95
|
-
<RadixAccordion.Item
|
|
96
|
-
ref={ref}
|
|
97
|
-
className={[styles.item, className ?? ""].filter(Boolean).join(" ")}
|
|
98
|
-
{...rest}
|
|
99
|
-
>
|
|
100
|
-
{children}
|
|
101
|
-
</RadixAccordion.Item>
|
|
102
|
-
);
|
|
103
|
-
},
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
/* Trigger ──────────────────────────────────────────────────── */
|
|
107
|
-
|
|
108
|
-
export interface TriggerProps
|
|
109
|
-
extends Omit<ComponentPropsWithoutRef<typeof RadixAccordion.Trigger>, "children"> {
|
|
110
|
-
/** 좌측 아이콘 이름. leading 슬롯 간결 사용. */
|
|
111
|
-
leadingIcon?: IconName;
|
|
112
|
-
/** 커스텀 좌측 노드 배열. leadingIcon보다 우선. */
|
|
113
|
-
leadingActions?: AccordionAction[];
|
|
114
|
-
/** 우측 메타(배지·숫자 등). caret은 자동으로 가장 끝. */
|
|
115
|
-
trailingActions?: AccordionAction[];
|
|
116
|
-
children?: ReactNode;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const Trigger = forwardRef<HTMLButtonElement, TriggerProps>(function AccordionTrigger(
|
|
120
|
-
{ className, leadingIcon, leadingActions, trailingActions, children, ...rest },
|
|
121
|
-
ref,
|
|
122
|
-
) {
|
|
123
|
-
const leading =
|
|
124
|
-
leadingActions ?? (leadingIcon ? [<Icon key="icon" name={leadingIcon} />] : undefined);
|
|
125
|
-
return (
|
|
126
|
-
<RadixAccordion.Header className={styles.header}>
|
|
127
|
-
<RadixAccordion.Trigger
|
|
128
|
-
ref={ref}
|
|
129
|
-
className={[styles.trigger, className ?? ""].filter(Boolean).join(" ")}
|
|
130
|
-
{...rest}
|
|
131
|
-
>
|
|
132
|
-
{leading && leading.length > 0 && (
|
|
133
|
-
<span className={styles.leading} data-slot="leading" aria-hidden>
|
|
134
|
-
{leading.map((a, i) => {
|
|
135
|
-
const key =
|
|
136
|
-
typeof a === "object" && a !== null && "key" in a && a.key
|
|
137
|
-
? a.key
|
|
138
|
-
: String(i);
|
|
139
|
-
const node = typeof a === "object" && a !== null && "node" in a ? a.node : a;
|
|
140
|
-
return (
|
|
141
|
-
<span key={key} className={styles.leadingItem}>
|
|
142
|
-
{node as ReactNode}
|
|
143
|
-
</span>
|
|
144
|
-
);
|
|
145
|
-
})}
|
|
146
|
-
</span>
|
|
147
|
-
)}
|
|
148
|
-
<span className={styles.label}>{children}</span>
|
|
149
|
-
{trailingActions && trailingActions.length > 0 && (
|
|
150
|
-
<span className={styles.trailing} data-slot="trailing">
|
|
151
|
-
{trailingActions.map((a, i) => {
|
|
152
|
-
const key =
|
|
153
|
-
typeof a === "object" && a !== null && "key" in a && a.key
|
|
154
|
-
? a.key
|
|
155
|
-
: String(i);
|
|
156
|
-
const node = typeof a === "object" && a !== null && "node" in a ? a.node : a;
|
|
157
|
-
return (
|
|
158
|
-
<span key={key} className={styles.trailingItem}>
|
|
159
|
-
{node as ReactNode}
|
|
160
|
-
</span>
|
|
161
|
-
);
|
|
162
|
-
})}
|
|
163
|
-
</span>
|
|
164
|
-
)}
|
|
165
|
-
<span className={styles.caret} aria-hidden>
|
|
166
|
-
<Icon name="caret-down" />
|
|
167
|
-
</span>
|
|
168
|
-
</RadixAccordion.Trigger>
|
|
169
|
-
</RadixAccordion.Header>
|
|
170
|
-
);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
/* Content ──────────────────────────────────────────────────── */
|
|
174
|
-
|
|
175
|
-
const Content = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<typeof RadixAccordion.Content>>(
|
|
176
|
-
function AccordionContent({ className, children, ...rest }, ref) {
|
|
177
|
-
return (
|
|
178
|
-
<RadixAccordion.Content
|
|
179
|
-
ref={ref}
|
|
180
|
-
className={[styles.content, className ?? ""].filter(Boolean).join(" ")}
|
|
181
|
-
{...rest}
|
|
182
|
-
>
|
|
183
|
-
<div className={styles.contentInner}>{children}</div>
|
|
184
|
-
</RadixAccordion.Content>
|
|
185
|
-
);
|
|
186
|
-
},
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
/* Hook — 내부 Trigger가 size 참조할 때 (현재는 미사용, 향후 확장용) */
|
|
190
|
-
export function useAccordionContext() {
|
|
191
|
-
return useContext(AccordionContext);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/* Compound export ─────────────────────────────────────────── */
|
|
195
|
-
|
|
196
|
-
type AccordionComponent = typeof Root & {
|
|
197
|
-
Item: typeof Item;
|
|
198
|
-
Trigger: typeof Trigger;
|
|
199
|
-
Content: typeof Content;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const Accordion = Root as AccordionComponent;
|
|
203
|
-
Accordion.Item = Item;
|
|
204
|
-
Accordion.Trigger = Trigger;
|
|
205
|
-
Accordion.Content = Content;
|
|
206
|
-
|
|
207
|
-
export { Accordion };
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
/* CommandPalette ─────────────────────────────────────────────────
|
|
2
|
-
⌘K 전역 명령 창. Modal과 다른 쉘:
|
|
3
|
-
- 세로: top 20vh (중앙 아님). 리스트가 아래로 자라야.
|
|
4
|
-
- 너비: 좁음(640 수준). 리스트 모양 집중.
|
|
5
|
-
- dismiss: 바깥 클릭 허용(가벼운 상호작용).
|
|
6
|
-
아이템 시각은 MenuItem이 담당 — 여기선 쉘·Input·List 트랙·cmdk 매핑만. */
|
|
7
|
-
|
|
8
|
-
/* Overlay ───────────────────────────────────────────────── */
|
|
9
|
-
.overlay {
|
|
10
|
-
position: fixed;
|
|
11
|
-
inset: 0;
|
|
12
|
-
z-index: var(--z-overlay);
|
|
13
|
-
background: var(--scrim);
|
|
14
|
-
animation: cmdk-overlay-in var(--duration-base) var(--ease-out);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
.overlay[data-state="closed"] {
|
|
18
|
-
animation: cmdk-overlay-out var(--duration-fast) var(--ease-in);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/* Content ───────────────────────────────────────────────── */
|
|
22
|
-
.content {
|
|
23
|
-
position: fixed;
|
|
24
|
-
top: 20vh;
|
|
25
|
-
left: 50%;
|
|
26
|
-
z-index: var(--z-modal);
|
|
27
|
-
display: flex;
|
|
28
|
-
flex-direction: column;
|
|
29
|
-
width: calc(100vw - var(--space-8));
|
|
30
|
-
/* 640 = space-2(8) * 80. Modal(440)보다 넓다 — 리스트가 들어가야. */
|
|
31
|
-
max-width: calc(var(--space-2) * 80);
|
|
32
|
-
/* 아래로 자라는 영역: 상단 20vh + 하단 10vh 여백 확보 → 최대 70vh. */
|
|
33
|
-
max-height: 70vh;
|
|
34
|
-
background: var(--bg);
|
|
35
|
-
border: var(--hairline) solid var(--border);
|
|
36
|
-
border-radius: var(--radius-lg);
|
|
37
|
-
box-shadow: var(--shadow-xl);
|
|
38
|
-
font-family: var(--font-sans);
|
|
39
|
-
color: var(--fg);
|
|
40
|
-
outline: none;
|
|
41
|
-
overflow: hidden;
|
|
42
|
-
animation: cmdk-content-in var(--duration-slow) var(--ease-out);
|
|
43
|
-
transform: translateX(-50%);
|
|
44
|
-
will-change: transform, opacity;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.content[data-state="closed"] {
|
|
48
|
-
animation: cmdk-content-out var(--duration-base) var(--ease-in);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/* cmdk 루트 — content 내부 꽉 채움. 세로 flex. */
|
|
52
|
-
.command {
|
|
53
|
-
display: flex;
|
|
54
|
-
flex-direction: column;
|
|
55
|
-
flex: 1 1 auto;
|
|
56
|
-
min-height: 0;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/* Input row ─────────────────────────────────────────────── */
|
|
60
|
-
/* 상단 고정 영역: 좌측 아이콘 + 네이티브 input.
|
|
61
|
-
border-bottom만 — 전체 border는 content가 담당. */
|
|
62
|
-
.inputRow {
|
|
63
|
-
display: flex;
|
|
64
|
-
align-items: center;
|
|
65
|
-
gap: var(--space-2);
|
|
66
|
-
padding: 0 var(--space-4);
|
|
67
|
-
height: calc(var(--space-2) * 7);
|
|
68
|
-
border-bottom: var(--hairline) solid var(--border);
|
|
69
|
-
flex-shrink: 0;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.inputIcon {
|
|
73
|
-
display: inline-flex;
|
|
74
|
-
color: var(--fg-muted);
|
|
75
|
-
flex-shrink: 0;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.input {
|
|
79
|
-
flex: 1 1 auto;
|
|
80
|
-
min-width: 0;
|
|
81
|
-
height: 100%;
|
|
82
|
-
background: transparent;
|
|
83
|
-
border: 0;
|
|
84
|
-
outline: 0;
|
|
85
|
-
padding: 0;
|
|
86
|
-
margin: 0;
|
|
87
|
-
color: var(--fg);
|
|
88
|
-
font-family: inherit;
|
|
89
|
-
font-size: var(--text-base);
|
|
90
|
-
line-height: var(--lh-base);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
.input::placeholder {
|
|
94
|
-
color: var(--fg-subtle);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/* List ──────────────────────────────────────────────────── */
|
|
98
|
-
/* 2단 스크롤 패턴: 바깥(.list)이 overflow, padding은 아이템이 담당하는 그룹/트랙에.
|
|
99
|
-
cmdk가 내부에 .list 콘텐츠 트랙을 자동 생성하지 않으므로 padding을 여기 직접. */
|
|
100
|
-
.list {
|
|
101
|
-
flex: 1 1 auto;
|
|
102
|
-
min-height: 0;
|
|
103
|
-
overflow-y: auto;
|
|
104
|
-
padding: var(--space-1);
|
|
105
|
-
scroll-padding: var(--space-1);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/* Empty ─────────────────────────────────────────────────── */
|
|
109
|
-
.empty {
|
|
110
|
-
padding: var(--space-6) var(--space-4);
|
|
111
|
-
text-align: center;
|
|
112
|
-
color: var(--fg-muted);
|
|
113
|
-
font-size: var(--text-sm);
|
|
114
|
-
line-height: var(--lh-sm);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/* Group ─────────────────────────────────────────────────── */
|
|
118
|
-
/* heading은 MenuItem variant="section"으로 렌더되므로 Group 자체 스타일은 거의 없음.
|
|
119
|
-
그룹 간 상단 여백만 — 첫 그룹은 제외. */
|
|
120
|
-
.group {
|
|
121
|
-
/* cmdk가 heading을 [cmdk-group-heading] div로 감싸므로 그 내부 레이아웃만 조정. */
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
.group + .group {
|
|
125
|
-
margin-top: var(--space-1);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/* heading 자체는 MenuItem variant="section" 시각 — 추가 marginReset만. */
|
|
129
|
-
.groupHeading {
|
|
130
|
-
margin: 0;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/* Separator ─────────────────────────────────────────────── */
|
|
134
|
-
/* DropdownMenu·ContextMenu의 separator 규칙과 동일 — 토큰 재사용. */
|
|
135
|
-
.separator {
|
|
136
|
-
height: var(--hairline);
|
|
137
|
-
margin: var(--space-1) var(--space-2);
|
|
138
|
-
background: var(--border);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/* cmdk ↔ MenuItem 매핑 ────────────────────────────────────
|
|
142
|
-
cmdk는 활성 아이템(↑↓ 선택)에 data-selected="true"를 세팅.
|
|
143
|
-
MenuItem은 data-active를 fill 스타일로 처리하므로, 둘을 동일 셀렉터에 매핑.
|
|
144
|
-
asChild로 MenuItem에 cmdk 속성이 전달됨 → 하위 요소에 그대로 적용된다. */
|
|
145
|
-
.list [cmdk-item][data-selected="true"] {
|
|
146
|
-
background: var(--menu-active-bg, var(--hover));
|
|
147
|
-
color: var(--menu-active-fg, var(--fg-strong));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/* 활성 아이템 안의 leading 아이콘 색도 fg-strong으로 전파. MenuItem과 동일 규칙. */
|
|
151
|
-
.list [cmdk-item][data-selected="true"] [data-slot="leading"] {
|
|
152
|
-
color: var(--menu-active-fg, var(--fg-strong));
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/* 비활성(cmdk-disabled) 아이템은 MenuItem이 이미 disabled 시각 처리하지만,
|
|
156
|
-
cmdk 측 상호작용(선택/엔터)이 꺼지는 건 cmdk가 담당. */
|
|
157
|
-
|
|
158
|
-
/* Animations ────────────────────────────────────────────── */
|
|
159
|
-
@keyframes cmdk-overlay-in {
|
|
160
|
-
from { opacity: 0; }
|
|
161
|
-
to { opacity: 1; }
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
@keyframes cmdk-overlay-out {
|
|
165
|
-
from { opacity: 1; }
|
|
166
|
-
to { opacity: 0; }
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
@keyframes cmdk-content-in {
|
|
170
|
-
from {
|
|
171
|
-
opacity: 0;
|
|
172
|
-
transform: translate(-50%, calc(var(--space-1) * -1)) scale(var(--cmdk-scale-from, 0.98));
|
|
173
|
-
}
|
|
174
|
-
to {
|
|
175
|
-
opacity: 1;
|
|
176
|
-
transform: translateX(-50%) scale(1);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
@keyframes cmdk-content-out {
|
|
181
|
-
from {
|
|
182
|
-
opacity: 1;
|
|
183
|
-
transform: translateX(-50%) scale(1);
|
|
184
|
-
}
|
|
185
|
-
to {
|
|
186
|
-
opacity: 0;
|
|
187
|
-
transform: translate(-50%, calc(var(--space-1) * -1)) scale(var(--cmdk-scale-from, 0.98));
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
:root[data-motion="bouncy"] .content { --cmdk-scale-from: 0.96; }
|
|
192
|
-
:root[data-motion="standard"] .content { --cmdk-scale-from: 0.98; }
|
|
193
|
-
:root[data-motion="calm"] .content { --cmdk-scale-from: 0.99; }
|
|
194
|
-
:root[data-motion="none"] .content { --cmdk-scale-from: 1; }
|
|
195
|
-
|
|
196
|
-
@media (prefers-reduced-motion: reduce) {
|
|
197
|
-
.overlay,
|
|
198
|
-
.overlay[data-state="closed"],
|
|
199
|
-
.content,
|
|
200
|
-
.content[data-state="closed"] {
|
|
201
|
-
animation: none;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* CommandPalette ─ ⌘K 스타일 전역 명령 검색 창.
|
|
5
|
-
*
|
|
6
|
-
* 조립
|
|
7
|
-
* - Radix Dialog: 쉘(open/portal/overlay/focus trap/ESC)
|
|
8
|
-
* - cmdk: 검색 입력·결과 리스트·fuzzy 필터·↑↓ 네비게이션·role 매핑
|
|
9
|
-
* - MenuItem: 결과 아이템의 시각 (DropdownMenu·ContextMenu와 동일 어댑터)
|
|
10
|
-
*
|
|
11
|
-
* 소비 표면
|
|
12
|
-
* <CommandPalette open={open} onOpenChange={setOpen}>
|
|
13
|
-
* <CommandPalette.Input placeholder="검색..." />
|
|
14
|
-
* <CommandPalette.List>
|
|
15
|
-
* <CommandPalette.Empty>결과 없음</CommandPalette.Empty>
|
|
16
|
-
* <CommandPalette.Group heading="페이지">
|
|
17
|
-
* <CommandPalette.Item onSelect={...} leadingIcon="house">대시보드</CommandPalette.Item>
|
|
18
|
-
* </CommandPalette.Group>
|
|
19
|
-
* </CommandPalette.List>
|
|
20
|
-
* </CommandPalette>
|
|
21
|
-
*
|
|
22
|
-
* Modal과 다른 점
|
|
23
|
-
* - 위치: 상단 쪽(top 20vh). 리스트가 아래로 자라야 하므로.
|
|
24
|
-
* - dismiss: 바깥 클릭 허용. 가벼운 상호작용.
|
|
25
|
-
* - 포커스: 열리면 Input 자동 포커스 → ↑↓로 리스트 네비.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import * as RadixDialog from "@radix-ui/react-dialog";
|
|
29
|
-
import { Command } from "cmdk";
|
|
30
|
-
import {
|
|
31
|
-
createContext,
|
|
32
|
-
forwardRef,
|
|
33
|
-
useContext,
|
|
34
|
-
type ComponentPropsWithoutRef,
|
|
35
|
-
type ReactNode,
|
|
36
|
-
} from "react";
|
|
37
|
-
|
|
38
|
-
import { Icon, IconAxisProvider, useIconAxis, type IconName } from "../Icon";
|
|
39
|
-
import { MenuItem, type MenuItemAction, type MenuItemSize } from "../MenuItem";
|
|
40
|
-
import styles from "./CommandPalette.module.css";
|
|
41
|
-
|
|
42
|
-
/* Size context — Content가 Item 기본 size 결정. 메뉴들과 동일 패턴. */
|
|
43
|
-
const SizeContext = createContext<MenuItemSize>("md");
|
|
44
|
-
|
|
45
|
-
/* Root ─────────────────────────────────────────────────────────── */
|
|
46
|
-
|
|
47
|
-
export interface CommandPaletteProps {
|
|
48
|
-
open: boolean;
|
|
49
|
-
onOpenChange: (open: boolean) => void;
|
|
50
|
-
/** 리스트 아이템 밀도. 기본 md. */
|
|
51
|
-
size?: MenuItemSize;
|
|
52
|
-
/** 스크린리더 라벨. 시각적으로 숨겨진 다이얼로그 타이틀. */
|
|
53
|
-
label?: string;
|
|
54
|
-
children?: ReactNode;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const Root = forwardRef<HTMLDivElement, CommandPaletteProps>(function CommandPalette(
|
|
58
|
-
{ open, onOpenChange, size = "md", label = "명령 검색", children },
|
|
59
|
-
ref,
|
|
60
|
-
) {
|
|
61
|
-
const iconAxis = useIconAxis();
|
|
62
|
-
return (
|
|
63
|
-
<RadixDialog.Root open={open} onOpenChange={onOpenChange}>
|
|
64
|
-
<RadixDialog.Portal>
|
|
65
|
-
<RadixDialog.Overlay className={styles.overlay} />
|
|
66
|
-
<RadixDialog.Content
|
|
67
|
-
ref={ref}
|
|
68
|
-
className={styles.content}
|
|
69
|
-
aria-label={label}
|
|
70
|
-
>
|
|
71
|
-
<RadixDialog.Title className="visually-hidden">{label}</RadixDialog.Title>
|
|
72
|
-
<IconAxisProvider value={iconAxis}>
|
|
73
|
-
<SizeContext.Provider value={size}>
|
|
74
|
-
<Command className={styles.command} label={label}>
|
|
75
|
-
{children}
|
|
76
|
-
</Command>
|
|
77
|
-
</SizeContext.Provider>
|
|
78
|
-
</IconAxisProvider>
|
|
79
|
-
</RadixDialog.Content>
|
|
80
|
-
</RadixDialog.Portal>
|
|
81
|
-
</RadixDialog.Root>
|
|
82
|
-
);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
/* Input ────────────────────────────────────────────────────────── */
|
|
86
|
-
|
|
87
|
-
export interface InputProps
|
|
88
|
-
extends Omit<ComponentPropsWithoutRef<typeof Command.Input>, "ref"> {
|
|
89
|
-
/** 좌측 아이콘. 기본 search. null이면 숨김. */
|
|
90
|
-
leadingIcon?: IconName | null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const InputCmd = forwardRef<HTMLInputElement, InputProps>(function CommandPaletteInput(
|
|
94
|
-
{ className, leadingIcon = "search", placeholder = "검색...", ...rest },
|
|
95
|
-
ref,
|
|
96
|
-
) {
|
|
97
|
-
return (
|
|
98
|
-
<div className={styles.inputRow}>
|
|
99
|
-
{leadingIcon && (
|
|
100
|
-
<span className={styles.inputIcon} aria-hidden>
|
|
101
|
-
<Icon name={leadingIcon} />
|
|
102
|
-
</span>
|
|
103
|
-
)}
|
|
104
|
-
<Command.Input
|
|
105
|
-
ref={ref}
|
|
106
|
-
placeholder={placeholder}
|
|
107
|
-
className={[styles.input, className ?? ""].filter(Boolean).join(" ")}
|
|
108
|
-
{...rest}
|
|
109
|
-
/>
|
|
110
|
-
</div>
|
|
111
|
-
);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
/* List ─────────────────────────────────────────────────────────── */
|
|
115
|
-
|
|
116
|
-
const List = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<typeof Command.List>>(
|
|
117
|
-
function CommandPaletteList({ className, children, ...rest }, ref) {
|
|
118
|
-
return (
|
|
119
|
-
<Command.List
|
|
120
|
-
ref={ref}
|
|
121
|
-
className={[styles.list, className ?? ""].filter(Boolean).join(" ")}
|
|
122
|
-
{...rest}
|
|
123
|
-
>
|
|
124
|
-
{children}
|
|
125
|
-
</Command.List>
|
|
126
|
-
);
|
|
127
|
-
},
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
/* Empty ────────────────────────────────────────────────────────── */
|
|
131
|
-
|
|
132
|
-
const Empty = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<typeof Command.Empty>>(
|
|
133
|
-
function CommandPaletteEmpty({ className, children, ...rest }, ref) {
|
|
134
|
-
return (
|
|
135
|
-
<Command.Empty
|
|
136
|
-
ref={ref}
|
|
137
|
-
className={[styles.empty, className ?? ""].filter(Boolean).join(" ")}
|
|
138
|
-
{...rest}
|
|
139
|
-
>
|
|
140
|
-
{children ?? "결과가 없어요"}
|
|
141
|
-
</Command.Empty>
|
|
142
|
-
);
|
|
143
|
-
},
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
/* Group ────────────────────────────────────────────────────────── */
|
|
147
|
-
|
|
148
|
-
export interface GroupProps
|
|
149
|
-
extends Omit<ComponentPropsWithoutRef<typeof Command.Group>, "heading"> {
|
|
150
|
-
heading?: ReactNode;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const Group = forwardRef<HTMLDivElement, GroupProps>(function CommandPaletteGroup(
|
|
154
|
-
{ className, heading, children, ...rest },
|
|
155
|
-
ref,
|
|
156
|
-
) {
|
|
157
|
-
const size = useContext(SizeContext);
|
|
158
|
-
return (
|
|
159
|
-
<Command.Group
|
|
160
|
-
ref={ref}
|
|
161
|
-
className={[styles.group, className ?? ""].filter(Boolean).join(" ")}
|
|
162
|
-
heading={
|
|
163
|
-
heading ? (
|
|
164
|
-
<MenuItem variant="section" size={size} className={styles.groupHeading}>
|
|
165
|
-
{heading}
|
|
166
|
-
</MenuItem>
|
|
167
|
-
) : undefined
|
|
168
|
-
}
|
|
169
|
-
{...rest}
|
|
170
|
-
>
|
|
171
|
-
{children}
|
|
172
|
-
</Command.Group>
|
|
173
|
-
);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
/* Separator ────────────────────────────────────────────────────── */
|
|
177
|
-
|
|
178
|
-
const Separator = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<typeof Command.Separator>>(
|
|
179
|
-
function CommandPaletteSeparator({ className, ...rest }, ref) {
|
|
180
|
-
return (
|
|
181
|
-
<Command.Separator
|
|
182
|
-
ref={ref}
|
|
183
|
-
className={[styles.separator, className ?? ""].filter(Boolean).join(" ")}
|
|
184
|
-
{...rest}
|
|
185
|
-
/>
|
|
186
|
-
);
|
|
187
|
-
},
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
/* Item ─────────────────────────────────────────────────────────── */
|
|
191
|
-
|
|
192
|
-
export interface ItemProps
|
|
193
|
-
extends Omit<ComponentPropsWithoutRef<typeof Command.Item>, "children"> {
|
|
194
|
-
/** 좌측 아이콘 이름. leading 슬롯 간결 사용. */
|
|
195
|
-
leadingIcon?: IconName;
|
|
196
|
-
/** 커스텀 좌측 노드. leadingIcon보다 우선. */
|
|
197
|
-
leadingActions?: MenuItemAction[];
|
|
198
|
-
/** 우측 메타(배지·숫자 등). */
|
|
199
|
-
trailingActions?: MenuItemAction[];
|
|
200
|
-
/** 오른쪽 끝 단축키 (예: "⌘N"). */
|
|
201
|
-
shortcut?: string;
|
|
202
|
-
/** 라벨 아래 보조 설명 한 줄. */
|
|
203
|
-
description?: ReactNode;
|
|
204
|
-
size?: MenuItemSize;
|
|
205
|
-
children?: ReactNode;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const Item = forwardRef<HTMLDivElement, ItemProps>(function CommandPaletteItem(
|
|
209
|
-
{
|
|
210
|
-
leadingIcon,
|
|
211
|
-
leadingActions,
|
|
212
|
-
trailingActions,
|
|
213
|
-
shortcut,
|
|
214
|
-
description,
|
|
215
|
-
size,
|
|
216
|
-
disabled,
|
|
217
|
-
children,
|
|
218
|
-
...rest
|
|
219
|
-
},
|
|
220
|
-
ref,
|
|
221
|
-
) {
|
|
222
|
-
const ctxSize = useContext(SizeContext);
|
|
223
|
-
const effectiveSize = size ?? ctxSize;
|
|
224
|
-
const leading: MenuItemAction[] | undefined =
|
|
225
|
-
leadingActions ?? (leadingIcon ? [<Icon key="icon" name={leadingIcon} />] : undefined);
|
|
226
|
-
return (
|
|
227
|
-
<Command.Item disabled={disabled} {...rest} asChild>
|
|
228
|
-
<MenuItem
|
|
229
|
-
ref={ref}
|
|
230
|
-
size={effectiveSize}
|
|
231
|
-
leadingActions={leading}
|
|
232
|
-
trailingActions={trailingActions}
|
|
233
|
-
shortcut={shortcut}
|
|
234
|
-
description={description}
|
|
235
|
-
disabled={disabled}
|
|
236
|
-
>
|
|
237
|
-
{children}
|
|
238
|
-
</MenuItem>
|
|
239
|
-
</Command.Item>
|
|
240
|
-
);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
/* Compound export ──────────────────────────────────────────────── */
|
|
244
|
-
|
|
245
|
-
type CommandPaletteComponent = typeof Root & {
|
|
246
|
-
Input: typeof InputCmd;
|
|
247
|
-
List: typeof List;
|
|
248
|
-
Empty: typeof Empty;
|
|
249
|
-
Group: typeof Group;
|
|
250
|
-
Separator: typeof Separator;
|
|
251
|
-
Item: typeof Item;
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
const CommandPalette = Root as CommandPaletteComponent;
|
|
255
|
-
CommandPalette.Input = InputCmd;
|
|
256
|
-
CommandPalette.List = List;
|
|
257
|
-
CommandPalette.Empty = Empty;
|
|
258
|
-
CommandPalette.Group = Group;
|
|
259
|
-
CommandPalette.Separator = Separator;
|
|
260
|
-
CommandPalette.Item = Item;
|
|
261
|
-
|
|
262
|
-
export { CommandPalette };
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export { CommandPalette } from "./CommandPalette";
|
|
2
|
-
export type {
|
|
3
|
-
CommandPaletteProps,
|
|
4
|
-
InputProps as CommandPaletteInputProps,
|
|
5
|
-
GroupProps as CommandPaletteGroupProps,
|
|
6
|
-
ItemProps as CommandPaletteItemProps,
|
|
7
|
-
} from "./CommandPalette";
|
|
8
|
-
export { useCommandPaletteHotkey } from "./useCommandPaletteHotkey";
|
|
9
|
-
export type { HotkeyOptions as CommandPaletteHotkeyOptions } from "./useCommandPaletteHotkey";
|