@conscience-technology/ctds 0.3.1 → 0.3.3
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/Breadcrumb/Breadcrumb.module.css +64 -0
- package/assets/components/Breadcrumb/Breadcrumb.tsx +120 -47
- package/assets/components/Icon/Icon.tsx +823 -114
- package/assets/components/Onboarding/Onboarding.module.css +186 -0
- package/assets/components/Sidebar/Sidebar.module.css +36 -74
- package/assets/components/Sidebar/Sidebar.tsx +15 -2
- package/assets/components/_data.ts +3 -7
- package/assets/components/_utils.css +105 -0
- package/assets/components/index.ts +0 -1
- package/assets/prompts/components/breadcrumb.md +44 -4
- package/assets/prompts/components/sidebar.md +14 -5
- package/assets/skills/ctds/SKILL.md +1 -3
- 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,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";
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* useCommandPaletteHotkey ─ Command Palette 토글 전역 단축키 훅.
|
|
5
|
-
* 기본값 ⌘K / Ctrl+K. 입력 요소에서도 동작한다(검색 창은 어디서든 열려야 함).
|
|
6
|
-
*
|
|
7
|
-
* 사용
|
|
8
|
-
* const [open, setOpen] = useState(false);
|
|
9
|
-
* useCommandPaletteHotkey(() => setOpen((v) => !v));
|
|
10
|
-
*
|
|
11
|
-
* 커스텀 키
|
|
12
|
-
* useCommandPaletteHotkey(toggle, { key: "p", mod: true, shift: true }); // ⌘⇧P
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { useEffect } from "react";
|
|
16
|
-
|
|
17
|
-
export type HotkeyOptions = {
|
|
18
|
-
/** 단일 문자 키 (소문자). 기본 "k". */
|
|
19
|
-
key?: string;
|
|
20
|
-
/** Meta(⌘)/Ctrl 필수 여부. 기본 true. */
|
|
21
|
-
mod?: boolean;
|
|
22
|
-
/** Shift 필수 여부. 기본 false. */
|
|
23
|
-
shift?: boolean;
|
|
24
|
-
/** Alt 필수 여부. 기본 false. */
|
|
25
|
-
alt?: boolean;
|
|
26
|
-
/** 비활성 플래그. 조건부 활성 때 유용. */
|
|
27
|
-
disabled?: boolean;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export function useCommandPaletteHotkey(
|
|
31
|
-
callback: () => void,
|
|
32
|
-
options: HotkeyOptions = {},
|
|
33
|
-
): void {
|
|
34
|
-
const { key = "k", mod = true, shift = false, alt = false, disabled = false } = options;
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
if (disabled) return;
|
|
37
|
-
function onKeyDown(e: KeyboardEvent) {
|
|
38
|
-
if (e.key.toLowerCase() !== key.toLowerCase()) return;
|
|
39
|
-
if (mod && !(e.metaKey || e.ctrlKey)) return;
|
|
40
|
-
if (!mod && (e.metaKey || e.ctrlKey)) return;
|
|
41
|
-
if (shift !== e.shiftKey) return;
|
|
42
|
-
if (alt !== e.altKey) return;
|
|
43
|
-
e.preventDefault();
|
|
44
|
-
callback();
|
|
45
|
-
}
|
|
46
|
-
window.addEventListener("keydown", onKeyDown);
|
|
47
|
-
return () => window.removeEventListener("keydown", onKeyDown);
|
|
48
|
-
}, [callback, key, mod, shift, alt, disabled]);
|
|
49
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* EventCard — 리스트에 쌓이는 일정 아이템.
|
|
3
|
-
* 좌: 날짜 블록(큰 숫자 + 요일 소문자) · 세로 hairline · 우: 제목 + status + meta.
|
|
4
|
-
*
|
|
5
|
-
* chrome: --radius-lg + 1px --border + --bg. 그림자 금지.
|
|
6
|
-
* interactive: href 있을 때만 hover/focus ring.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
.root {
|
|
10
|
-
display: flex;
|
|
11
|
-
align-items: stretch;
|
|
12
|
-
gap: var(--space-3);
|
|
13
|
-
padding: var(--space-3) var(--space-4);
|
|
14
|
-
background: var(--bg);
|
|
15
|
-
border: 1px solid var(--border);
|
|
16
|
-
border-radius: var(--radius-lg);
|
|
17
|
-
color: inherit;
|
|
18
|
-
text-decoration: none;
|
|
19
|
-
min-width: 0;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.interactive {
|
|
23
|
-
cursor: pointer;
|
|
24
|
-
transition: var(--transition-colors), var(--transition-ring);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.interactive:hover {
|
|
28
|
-
background: var(--hover);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
.interactive:focus-visible {
|
|
32
|
-
outline: none;
|
|
33
|
-
box-shadow: var(--ring-shadow);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/* 날짜 블록 — 좌측 고정 폭. 숫자가 2자리 기준으로 흔들리지 않도록 min-width 트랙. */
|
|
37
|
-
.dateBlock {
|
|
38
|
-
display: flex;
|
|
39
|
-
flex-direction: column;
|
|
40
|
-
align-items: center;
|
|
41
|
-
justify-content: center;
|
|
42
|
-
min-width: var(--space-8); /* 2자리 숫자 트랙 (한 단계 조임) */
|
|
43
|
-
padding-inline: var(--space-0_5);
|
|
44
|
-
flex-shrink: 0;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.day {
|
|
48
|
-
font-size: var(--text-xl);
|
|
49
|
-
line-height: var(--lh-tight);
|
|
50
|
-
font-weight: var(--font-weight-semibold); /* xl=20px → semibold 허용 */
|
|
51
|
-
font-variant-numeric: tabular-nums;
|
|
52
|
-
color: var(--fg-strong);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.weekday {
|
|
56
|
-
font-size: var(--text-3xs);
|
|
57
|
-
line-height: var(--lh-3xs);
|
|
58
|
-
font-weight: var(--font-weight-regular);
|
|
59
|
-
color: var(--fg-muted);
|
|
60
|
-
/* 소문자 유지. uppercase 금지 */
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/* 세로 hairline — border-left shorthand로 (리터럴 width 금지 회피) */
|
|
64
|
-
.divider {
|
|
65
|
-
border-inline-start: 1px solid var(--border);
|
|
66
|
-
align-self: stretch;
|
|
67
|
-
flex-shrink: 0;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/* 본문 — 제목 + 메타 라인 */
|
|
71
|
-
.body {
|
|
72
|
-
flex: 1;
|
|
73
|
-
min-width: 0;
|
|
74
|
-
display: flex;
|
|
75
|
-
flex-direction: column;
|
|
76
|
-
gap: var(--space-1);
|
|
77
|
-
justify-content: center;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.title {
|
|
81
|
-
margin: 0;
|
|
82
|
-
font-size: var(--text-xs);
|
|
83
|
-
line-height: var(--lh-xs);
|
|
84
|
-
font-weight: var(--font-weight-medium); /* 20px 미만 → medium */
|
|
85
|
-
color: var(--fg-strong);
|
|
86
|
-
min-width: 0;
|
|
87
|
-
overflow: hidden;
|
|
88
|
-
text-overflow: ellipsis;
|
|
89
|
-
white-space: nowrap;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/* 메타 라인 — status Badge + 아이콘+텍스트 N개. 사이는 짧은 세로 hairline. */
|
|
93
|
-
.metaRow {
|
|
94
|
-
display: flex;
|
|
95
|
-
align-items: center;
|
|
96
|
-
gap: var(--space-1_5);
|
|
97
|
-
flex-wrap: wrap;
|
|
98
|
-
min-width: 0;
|
|
99
|
-
color: var(--fg-muted);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
.metaItem {
|
|
103
|
-
display: inline-flex;
|
|
104
|
-
align-items: center;
|
|
105
|
-
gap: var(--space-1);
|
|
106
|
-
font-size: var(--text-2xs);
|
|
107
|
-
line-height: var(--lh-2xs);
|
|
108
|
-
color: var(--fg-muted);
|
|
109
|
-
min-width: 0;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/* 메타 라인 안에서 "앞선 형제가 있을 때만" 왼쪽 hairline. 첫 아이템은 항상 제거,
|
|
113
|
-
앞 slot이 비었다가 뒤에 하나만 나오는 경우에도 자동으로 안 그려진다. */
|
|
114
|
-
.metaRow > * + .metaItem {
|
|
115
|
-
padding-inline-start: var(--space-1_5);
|
|
116
|
-
border-inline-start: 1px solid var(--border);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
.metaIcon {
|
|
120
|
-
display: inline-flex;
|
|
121
|
-
align-items: center;
|
|
122
|
-
justify-content: center;
|
|
123
|
-
color: var(--fg-muted);
|
|
124
|
-
line-height: 1;
|
|
125
|
-
flex-shrink: 0;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
.metaLabel {
|
|
129
|
-
min-width: 0;
|
|
130
|
-
overflow: hidden;
|
|
131
|
-
text-overflow: ellipsis;
|
|
132
|
-
white-space: nowrap;
|
|
133
|
-
}
|