@conscience-technology/ctds 0.1.14 → 0.2.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/assets/components/AppFrame/AppFrame.module.css +6 -0
- package/assets/components/{AttachmentChip/AttachmentChip.module.css → AttachmentItem/AttachmentItem.module.css} +5 -8
- package/assets/components/{AttachmentChip/AttachmentChip.tsx → AttachmentItem/AttachmentItem.tsx} +21 -15
- package/assets/components/AttachmentItem/index.ts +6 -0
- package/assets/components/Button/Button.module.css +11 -2
- package/assets/components/ChatInput/ChatInput.tsx +2 -2
- package/assets/components/DatePicker/Calendar.tsx +19 -1
- package/assets/components/Drawer/Drawer.tsx +120 -39
- package/assets/components/Drawer/index.ts +9 -1
- package/assets/components/{MessageAttachment/MessageAttachment.module.css → GeneratedImage/GeneratedImage.module.css} +3 -2
- package/assets/components/{MessageAttachment/MessageAttachment.tsx → GeneratedImage/GeneratedImage.tsx} +8 -8
- package/assets/components/GeneratedImage/index.ts +5 -0
- package/assets/components/Icon/Icon.tsx +4 -1
- package/assets/components/Modal/Modal.tsx +114 -33
- package/assets/components/Modal/index.ts +12 -2
- package/assets/components/Spinner/lottieManifest.ts +8 -8
- package/assets/components/Switch/Switch.tsx +6 -0
- package/assets/components/TableHeader/TableHeader.module.css +29 -20
- package/assets/components/TableRow/TableRow.module.css +7 -5
- package/assets/components/TableRowEdit/TableRowEdit.module.css +10 -3
- package/assets/components/TableRowEdit/TableRowEdit.tsx +7 -2
- package/assets/components/Toast/Toast.tsx +19 -0
- package/assets/components/_data.ts +39 -16
- package/assets/components/app-shell.css +17 -0
- package/assets/components/index.ts +77 -0
- package/assets/components/reset.css +63 -0
- package/assets/components/themeBootScript.ts +40 -0
- package/assets/components/tokens.css +68 -57
- package/assets/prompts/GUIDE.md +1 -1
- package/assets/prompts/PRINCIPLES.md +9 -0
- package/assets/prompts/axes/cascade.md +49 -0
- package/assets/prompts/axes/mode.md +3 -1
- package/assets/prompts/components/_variants.md +84 -0
- package/assets/prompts/components/{attachment-chip.md → attachment-item.md} +12 -10
- package/assets/prompts/components/attachment-thumb.md +2 -2
- package/assets/prompts/components/avatar-upload.md +53 -0
- package/assets/prompts/components/bubble.md +2 -2
- package/assets/prompts/components/chat-input.md +7 -8
- package/assets/prompts/components/code-block.md +2 -2
- package/assets/prompts/components/disclosure.md +47 -0
- package/assets/prompts/components/file-icon.md +3 -3
- package/assets/prompts/components/generated-image.md +38 -0
- package/assets/prompts/components/message-quote.md +2 -2
- package/assets/prompts/components/message-sources.md +5 -4
- package/assets/prompts/components/message-table.md +9 -6
- package/assets/prompts/components/onboarding.md +43 -0
- package/assets/prompts/components/quotation-bar.md +1 -1
- package/assets/prompts/components/sidebar-split.md +70 -0
- package/assets/prompts/components/sidebar.md +2 -0
- package/assets/prompts/components/time-picker.md +69 -0
- package/assets/prompts/components/toolbar.md +69 -0
- package/assets/prompts/components/user-message.md +2 -2
- package/package.json +80 -3
- package/src/commands/init.js +53 -23
- package/src/utils/deps.js +23 -17
- package/src/utils/patch.js +155 -0
- package/assets/components/AttachmentChip/index.ts +0 -6
- package/assets/components/Calendar/Calendar.module.css +0 -163
- package/assets/components/Calendar/Calendar.tsx +0 -323
- package/assets/components/Calendar/index.ts +0 -2
- package/assets/components/MessageAttachment/index.ts +0 -5
- package/assets/prompts/components/calendar.md +0 -116
- package/assets/prompts/components/form-field.md +0 -74
- package/assets/prompts/components/message-attachment.md +0 -40
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
forwardRef,
|
|
5
|
-
memo,
|
|
6
|
-
useLayoutEffect,
|
|
7
|
-
useMemo,
|
|
8
|
-
useRef,
|
|
9
|
-
useState,
|
|
10
|
-
type HTMLAttributes,
|
|
11
|
-
type ReactNode,
|
|
12
|
-
} from "react";
|
|
13
|
-
|
|
14
|
-
import { Badge } from "../Badge";
|
|
15
|
-
import { Button } from "../Button";
|
|
16
|
-
import { EventChip, type EventChipTone } from "../EventChip";
|
|
17
|
-
import { Icon } from "../Icon";
|
|
18
|
-
import { IconButton } from "../IconButton";
|
|
19
|
-
import {
|
|
20
|
-
addMonths,
|
|
21
|
-
buildMonthGrid,
|
|
22
|
-
isSameDay,
|
|
23
|
-
isSameMonth,
|
|
24
|
-
formatISODate,
|
|
25
|
-
MONTH_LABELS_KO,
|
|
26
|
-
WEEKDAY_LABELS_KO,
|
|
27
|
-
startOfMonth,
|
|
28
|
-
} from "../DatePicker/utils";
|
|
29
|
-
import styles from "./Calendar.module.css";
|
|
30
|
-
|
|
31
|
-
export type CalendarEventTone = EventChipTone;
|
|
32
|
-
|
|
33
|
-
export type CalendarEvent = {
|
|
34
|
-
id: string;
|
|
35
|
-
/** ISO yyyy-MM-dd */
|
|
36
|
-
date: string;
|
|
37
|
-
title: string;
|
|
38
|
-
tone?: CalendarEventTone;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export type CalendarProps = Omit<HTMLAttributes<HTMLDivElement>, "onChange"> & {
|
|
42
|
-
/** 현재 보여줄 달. controlled. */
|
|
43
|
-
month: Date;
|
|
44
|
-
/** 오늘 날짜. 테스트/스토리북에서 고정하려면 넘김. 기본 new Date(). */
|
|
45
|
-
today?: Date;
|
|
46
|
-
/** 선택된 날짜. 셀 하이라이트. */
|
|
47
|
-
selectedDate?: Date | null;
|
|
48
|
-
events?: CalendarEvent[];
|
|
49
|
-
/** 요일 시작. 0=일, 1=월. 기본 1 (업무 캘린더 관행). */
|
|
50
|
-
weekStartsOn?: 0 | 1;
|
|
51
|
-
showWeekend?: boolean;
|
|
52
|
-
/** 상단바 왼쪽 커스텀 슬롯 (예: Google 연결 상태). */
|
|
53
|
-
leadingSlot?: ReactNode;
|
|
54
|
-
/** 상단바 오른쪽 "오늘" 버튼 표시 여부. 기본 true. */
|
|
55
|
-
showTodayButton?: boolean;
|
|
56
|
-
onMonthChange?: (next: Date) => void;
|
|
57
|
-
onSelectedDateChange?: (next: Date) => void;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const Calendar = forwardRef<HTMLDivElement, CalendarProps>(function Calendar(
|
|
61
|
-
{
|
|
62
|
-
className,
|
|
63
|
-
month,
|
|
64
|
-
today: todayProp,
|
|
65
|
-
selectedDate = null,
|
|
66
|
-
events = [],
|
|
67
|
-
weekStartsOn = 1,
|
|
68
|
-
showWeekend = true,
|
|
69
|
-
leadingSlot,
|
|
70
|
-
showTodayButton = true,
|
|
71
|
-
onMonthChange,
|
|
72
|
-
onSelectedDateChange,
|
|
73
|
-
...rest
|
|
74
|
-
},
|
|
75
|
-
ref,
|
|
76
|
-
) {
|
|
77
|
-
const today = useMemo(() => todayProp ?? new Date(), [todayProp]);
|
|
78
|
-
const monthStart = useMemo(() => startOfMonth(month), [month]);
|
|
79
|
-
const isTodayMonth = isSameMonth(monthStart, today);
|
|
80
|
-
|
|
81
|
-
const allDays = useMemo(
|
|
82
|
-
() => buildMonthGrid(monthStart, weekStartsOn),
|
|
83
|
-
[monthStart, weekStartsOn],
|
|
84
|
-
);
|
|
85
|
-
const weeks = allDays.length / 7;
|
|
86
|
-
|
|
87
|
-
const weekdays = useMemo(() => {
|
|
88
|
-
const list = [...WEEKDAY_LABELS_KO];
|
|
89
|
-
for (let i = 0; i < weekStartsOn; i += 1) list.push(list.shift() as string);
|
|
90
|
-
return list;
|
|
91
|
-
}, [weekStartsOn]);
|
|
92
|
-
|
|
93
|
-
const visibleWeekdays = showWeekend ? weekdays : weekdays.filter((_, i) => !isWeekendIndex(i, weekStartsOn));
|
|
94
|
-
const visibleDays = showWeekend
|
|
95
|
-
? allDays
|
|
96
|
-
: allDays.filter((d) => d.getDay() !== 0 && d.getDay() !== 6);
|
|
97
|
-
|
|
98
|
-
const eventsByDate = useMemo(() => groupEventsByDate(events), [events]);
|
|
99
|
-
|
|
100
|
-
const shiftMonth = (delta: number) => onMonthChange?.(addMonths(monthStart, delta));
|
|
101
|
-
const shiftYear = (delta: number) => {
|
|
102
|
-
const next = new Date(monthStart);
|
|
103
|
-
next.setFullYear(next.getFullYear() + delta);
|
|
104
|
-
onMonthChange?.(next);
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const columns = showWeekend ? 7 : 5;
|
|
108
|
-
|
|
109
|
-
return (
|
|
110
|
-
<div
|
|
111
|
-
ref={ref}
|
|
112
|
-
className={[styles.root, className].filter(Boolean).join(" ")}
|
|
113
|
-
{...rest}
|
|
114
|
-
>
|
|
115
|
-
<div className={styles.toolbar}>
|
|
116
|
-
<div className={styles.toolbarSide}>{leadingSlot}</div>
|
|
117
|
-
|
|
118
|
-
<div className={styles.monthNav}>
|
|
119
|
-
<IconButton
|
|
120
|
-
variant="ghost"
|
|
121
|
-
size="sm"
|
|
122
|
-
aria-label="이전 해"
|
|
123
|
-
icon={<Icon name="caret-double-left" />}
|
|
124
|
-
onClick={() => shiftYear(-1)}
|
|
125
|
-
/>
|
|
126
|
-
<IconButton
|
|
127
|
-
variant="ghost"
|
|
128
|
-
size="sm"
|
|
129
|
-
aria-label="이전 달"
|
|
130
|
-
icon={<Icon name="caret-left" />}
|
|
131
|
-
onClick={() => shiftMonth(-1)}
|
|
132
|
-
/>
|
|
133
|
-
<span className={styles.monthTitle}>
|
|
134
|
-
{monthStart.getFullYear()}년 {MONTH_LABELS_KO[monthStart.getMonth()]}
|
|
135
|
-
</span>
|
|
136
|
-
<IconButton
|
|
137
|
-
variant="ghost"
|
|
138
|
-
size="sm"
|
|
139
|
-
aria-label="다음 달"
|
|
140
|
-
icon={<Icon name="caret-right" />}
|
|
141
|
-
onClick={() => shiftMonth(1)}
|
|
142
|
-
/>
|
|
143
|
-
<IconButton
|
|
144
|
-
variant="ghost"
|
|
145
|
-
size="sm"
|
|
146
|
-
aria-label="다음 해"
|
|
147
|
-
icon={<Icon name="caret-double-right" />}
|
|
148
|
-
onClick={() => shiftYear(1)}
|
|
149
|
-
/>
|
|
150
|
-
</div>
|
|
151
|
-
|
|
152
|
-
<div className={styles.toolbarSide} data-align="end">
|
|
153
|
-
{showTodayButton && (
|
|
154
|
-
<div data-visible={(!isTodayMonth) || undefined} className={styles.todaySlot}>
|
|
155
|
-
<Button
|
|
156
|
-
variant="outline"
|
|
157
|
-
size="sm"
|
|
158
|
-
onClick={() => {
|
|
159
|
-
onMonthChange?.(startOfMonth(today));
|
|
160
|
-
onSelectedDateChange?.(today);
|
|
161
|
-
}}
|
|
162
|
-
>
|
|
163
|
-
오늘
|
|
164
|
-
</Button>
|
|
165
|
-
</div>
|
|
166
|
-
)}
|
|
167
|
-
</div>
|
|
168
|
-
</div>
|
|
169
|
-
|
|
170
|
-
<div
|
|
171
|
-
className={styles.weekdays}
|
|
172
|
-
style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}
|
|
173
|
-
>
|
|
174
|
-
{visibleWeekdays.map((w) => (
|
|
175
|
-
<div
|
|
176
|
-
key={w}
|
|
177
|
-
className={styles.weekday}
|
|
178
|
-
data-tone={weekdayTone(w)}
|
|
179
|
-
>
|
|
180
|
-
{w}
|
|
181
|
-
</div>
|
|
182
|
-
))}
|
|
183
|
-
</div>
|
|
184
|
-
|
|
185
|
-
<div
|
|
186
|
-
className={styles.grid}
|
|
187
|
-
style={{
|
|
188
|
-
gridTemplateColumns: `repeat(${columns}, 1fr)`,
|
|
189
|
-
gridTemplateRows: `repeat(${weeks}, minmax(0, 1fr))`,
|
|
190
|
-
}}
|
|
191
|
-
>
|
|
192
|
-
{visibleDays.map((day) => {
|
|
193
|
-
const key = formatISODate(day);
|
|
194
|
-
return (
|
|
195
|
-
<DayCell
|
|
196
|
-
key={key}
|
|
197
|
-
day={day}
|
|
198
|
-
month={monthStart}
|
|
199
|
-
today={today}
|
|
200
|
-
events={eventsByDate.get(key) ?? []}
|
|
201
|
-
isSelected={!!selectedDate && isSameDay(day, selectedDate)}
|
|
202
|
-
onSelect={() => onSelectedDateChange?.(day)}
|
|
203
|
-
/>
|
|
204
|
-
);
|
|
205
|
-
})}
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
export { Calendar };
|
|
212
|
-
|
|
213
|
-
/* ───────────────────────── DayCell ───────────────────────── */
|
|
214
|
-
|
|
215
|
-
type DayCellProps = {
|
|
216
|
-
day: Date;
|
|
217
|
-
month: Date;
|
|
218
|
-
today: Date;
|
|
219
|
-
events: CalendarEvent[];
|
|
220
|
-
isSelected: boolean;
|
|
221
|
-
onSelect: () => void;
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
const DayCell = memo(function DayCell({
|
|
225
|
-
day,
|
|
226
|
-
month,
|
|
227
|
-
today,
|
|
228
|
-
events,
|
|
229
|
-
isSelected,
|
|
230
|
-
onSelect,
|
|
231
|
-
}: DayCellProps) {
|
|
232
|
-
const outside = !isSameMonth(day, month);
|
|
233
|
-
const isToday = isSameDay(day, today);
|
|
234
|
-
const isPast = day.getTime() < new Date(today).setHours(0, 0, 0, 0);
|
|
235
|
-
const dow = day.getDay();
|
|
236
|
-
const tone = outside || isPast ? "muted" : dow === 0 ? "sun" : dow === 6 ? "sat" : "default";
|
|
237
|
-
|
|
238
|
-
const eventsRef = useRef<HTMLDivElement>(null);
|
|
239
|
-
const [hiddenCount, setHiddenCount] = useState(0);
|
|
240
|
-
|
|
241
|
-
useLayoutEffect(() => {
|
|
242
|
-
const container = eventsRef.current;
|
|
243
|
-
if (!container) return;
|
|
244
|
-
const recalc = () => {
|
|
245
|
-
const containerBottom = container.getBoundingClientRect().bottom;
|
|
246
|
-
const children = Array.from(container.children) as HTMLElement[];
|
|
247
|
-
let visible = 0;
|
|
248
|
-
for (const child of children) {
|
|
249
|
-
if (child.getBoundingClientRect().bottom <= containerBottom + 1) {
|
|
250
|
-
visible += 1;
|
|
251
|
-
} else {
|
|
252
|
-
break;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
setHiddenCount(Math.max(0, events.length - visible));
|
|
256
|
-
};
|
|
257
|
-
recalc();
|
|
258
|
-
const ro = new ResizeObserver(recalc);
|
|
259
|
-
ro.observe(container);
|
|
260
|
-
return () => ro.disconnect();
|
|
261
|
-
}, [events.length]);
|
|
262
|
-
|
|
263
|
-
return (
|
|
264
|
-
<div
|
|
265
|
-
className={styles.cell}
|
|
266
|
-
data-outside={outside || undefined}
|
|
267
|
-
data-today={isToday || undefined}
|
|
268
|
-
data-selected={isSelected || undefined}
|
|
269
|
-
role="button"
|
|
270
|
-
tabIndex={0}
|
|
271
|
-
onClick={onSelect}
|
|
272
|
-
onKeyDown={(e) => {
|
|
273
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
274
|
-
e.preventDefault();
|
|
275
|
-
onSelect();
|
|
276
|
-
}
|
|
277
|
-
}}
|
|
278
|
-
>
|
|
279
|
-
<div className={styles.cellHead}>
|
|
280
|
-
<span className={styles.dayNum} data-tone={tone} data-selected={isSelected || undefined}>
|
|
281
|
-
{day.getDate()}
|
|
282
|
-
</span>
|
|
283
|
-
<span className={styles.cellBadges}>
|
|
284
|
-
{hiddenCount > 0 && <Badge size="sm" variant="soft">+{hiddenCount}</Badge>}
|
|
285
|
-
{isToday && <Badge size="sm" variant="soft">오늘</Badge>}
|
|
286
|
-
</span>
|
|
287
|
-
</div>
|
|
288
|
-
<div ref={eventsRef} className={styles.cellEvents}>
|
|
289
|
-
{events.map((ev) => (
|
|
290
|
-
<EventChip
|
|
291
|
-
key={ev.id}
|
|
292
|
-
title={ev.title}
|
|
293
|
-
tone={ev.tone}
|
|
294
|
-
faded={outside}
|
|
295
|
-
/>
|
|
296
|
-
))}
|
|
297
|
-
</div>
|
|
298
|
-
</div>
|
|
299
|
-
);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
/* ───────────────────────── helpers ───────────────────────── */
|
|
303
|
-
|
|
304
|
-
function groupEventsByDate(events: CalendarEvent[]): Map<string, CalendarEvent[]> {
|
|
305
|
-
const map = new Map<string, CalendarEvent[]>();
|
|
306
|
-
for (const ev of events) {
|
|
307
|
-
const list = map.get(ev.date) ?? [];
|
|
308
|
-
list.push(ev);
|
|
309
|
-
map.set(ev.date, list);
|
|
310
|
-
}
|
|
311
|
-
return map;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function isWeekendIndex(i: number, weekStartsOn: 0 | 1): boolean {
|
|
315
|
-
if (weekStartsOn === 0) return i === 0 || i === 6;
|
|
316
|
-
return i === 5 || i === 6;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function weekdayTone(label: string): "sat" | "sun" | "default" {
|
|
320
|
-
if (label === "토") return "sat";
|
|
321
|
-
if (label === "일") return "sun";
|
|
322
|
-
return "default";
|
|
323
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
# Calendar
|
|
2
|
-
|
|
3
|
-
### 원칙
|
|
4
|
-
|
|
5
|
-
페이지에 **인라인**으로 들어가는 월간 캘린더. 날짜 하나를 "고르기만 하는" [[components/date-picker]]와는 역할이 완전히 다르다 — Calendar는 **콘텐츠로서의 달력**이다. 셀마다 이벤트 pill이 쌓이고, 선택된 날짜가 페이지 다른 영역(사이드 리스트·상세 패널)의 입력이 된다. 그래서 Popover 안에 넣지 않는다. 날짜 입력이 필요하면 DatePicker, 월간 뷰가 필요하면 Calendar.
|
|
6
|
-
|
|
7
|
-
### Do
|
|
8
|
-
- `month`는 controlled — 외부 상태로 쥔다. `onMonthChange` 짝. `selectedDate`도 controlled.
|
|
9
|
-
- 이벤트 pill의 `tone`은 **의미**에 매핑. 색은 7개 토큰(default/accent/success/warning/danger/info/neutral)만 — Presto류 14색 팔레트 재현 금지.
|
|
10
|
-
- 셀을 넘치는 이벤트는 자동으로 `+N` 뱃지로 접힌다. 내부 스크롤을 만들지 않는다 — 셀 안 스크롤은 밀도 신호를 깬다.
|
|
11
|
-
- `weekStartsOn`은 기본 `1` (월요일) — 업무 캘린더 관행. 생활 캘린더는 `0`으로 명시.
|
|
12
|
-
- 토요일은 `--info`, 일요일은 `--destructive` 톤. 이 두 색만 요일 신호로 쓰고 임의 색 추가 금지.
|
|
13
|
-
- 선택된 셀은 inset ring(--primary)로만 신호한다. 배경을 primary로 채우지 않는다 — 그 자리는 이벤트 pill 공간이다.
|
|
14
|
-
- 보더는 그리드가 top/left를, 각 셀이 right/bottom을 그린다 — 열 수가 바뀌어도(5↔7) 왼쪽 세로선이 안정적으로 유지된다. 셀 nth-child 보더 제거 트릭 쓰지 말 것.
|
|
15
|
-
|
|
16
|
-
### Don't
|
|
17
|
-
- Popover로 감싸지 않는다 — 이건 DatePicker 자리. Calendar는 페이지 레이아웃의 블록 영역을 차지한다.
|
|
18
|
-
- 셀 안에 "클릭 가능한 이벤트 pill"을 만들지 않는다 — 셀 전체가 하나의 클릭 타겟이고, 상세는 사이드 패널·Drawer에서 연다. pill 안에 또 버튼이 들어가면 클릭 영역이 미궁에 빠진다.
|
|
19
|
-
- 요일 헤더에 "월요일/화요일" 두 글자 표기 금지 — "월·화" 한 글자. 두 글자는 가로 공간을 먹고 셀 수가 줄어든 느낌을 준다.
|
|
20
|
-
- 하루 셀에 이벤트 pill 5개 이상 노출 금지. 넘치면 `+N`으로 접고 선택 시 사이드 리스트에 전부 뿌린다.
|
|
21
|
-
- `showWeekend=false`일 때 토·일 셀 자리를 유지하지 않는다 — 숨길 거면 열 자체를 5열로 접는다.
|
|
22
|
-
- 이벤트 pill에 `pill` radius(완전 둥근) 쓰지 않는다 — Shape 축이 radius를 쥐고 있는데 pill로 고정하면 Soft/Standard/Crisp 신호가 셀 안에서 깨진다.
|
|
23
|
-
- Calendar 자체 상단바에 "주말 숨김" 류 **뷰 옵션 토글**을 넣지 않는다 — Calendar는 콘텐츠 블록이고, 뷰 옵션은 호스트 화면의 툴바 영역이 담당한다. 외부 state로 `showWeekend`를 넘겨 조립할 것. (월 이동·오늘 복귀는 Calendar 내부 — 뷰 옵션이 아니라 "지금 보는 좌표" 조작이므로 예외.)
|
|
24
|
-
|
|
25
|
-
### Fallback
|
|
26
|
-
`today` 생략하면 `new Date()`. `selectedDate` 없이도 성립 — 그냥 뷰어로 쓸 수 있다. `events` 빈 배열이어도 그리드만 그려진다. `tone` 애매하면 `default`.
|
|
27
|
-
|
|
28
|
-
### 관련
|
|
29
|
-
- [[components/event-chip]] — 셀 안에 쌓이는 일정 블록. Calendar가 자동으로 그려주지만 톤·props 체계는 EventChip 문서를 1차 참조.
|
|
30
|
-
- [[components/date-picker]] — "날짜 입력"이 목적이면 그쪽. Popover + Input 트리거. Calendar와 utils(buildMonthGrid 등) 공유.
|
|
31
|
-
- [[components/badge]] — "+N"·"오늘" 메타 뱃지는 Badge soft. EventChip 톤 체계도 Badge와 동일 토큰.
|
|
32
|
-
- [[components/icon-button]] — 월 이동 네비게이션(이전해·이전달·다음달·다음해 4개). ghost + sm.
|
|
33
|
-
- [[components/button]] — "오늘" 복귀 버튼은 outline·sm.
|
|
34
|
-
- [[axes/shape]] — 셀 radius는 없음(격자형이라 radius 신호를 셀이 받지 않음). EventChip·Badge만 Shape 축에 반응.
|
|
35
|
-
- [[axes/density]] — compact일수록 셀 padding이 줄고 EventChip이 더 많이 보임.
|
|
36
|
-
- [[tokens/color]] — `--primary`(선택), `--hover`(셀 hover), `--border`(hairline), `--info`/`--destructive`(요일 신호).
|
|
37
|
-
|
|
38
|
-
### Props 시그니처
|
|
39
|
-
|
|
40
|
-
```tsx
|
|
41
|
-
<Calendar
|
|
42
|
-
month={Date} // required — 현재 보여줄 달 (controlled)
|
|
43
|
-
today={Date} // default: new Date()
|
|
44
|
-
selectedDate={Date | null} // 셀 하이라이트
|
|
45
|
-
events={CalendarEvent[]} // id/date/title/code?/tone?/resolved?
|
|
46
|
-
weekStartsOn={0 | 1} // default: 1 (월)
|
|
47
|
-
showWeekend={boolean} // default: true
|
|
48
|
-
leadingSlot={ReactNode} // 상단바 왼쪽 — Google 연결 뱃지 등
|
|
49
|
-
showTodayButton={boolean} // default: true
|
|
50
|
-
onMonthChange={(next: Date) => void}
|
|
51
|
-
onSelectedDateChange={(next: Date) => void}
|
|
52
|
-
/>
|
|
53
|
-
|
|
54
|
-
type CalendarEvent = {
|
|
55
|
-
id: string;
|
|
56
|
-
date: string; // yyyy-MM-dd
|
|
57
|
-
title: string;
|
|
58
|
-
tone?: "default" | "accent" | "success" | "warning" | "danger" | "info" | "neutral";
|
|
59
|
-
};
|
|
60
|
-
// 셀 안 렌더링은 EventChip이 담당. 상세 라벨(코드·시각·주최자)은 Calendar 밖
|
|
61
|
-
// 사이드 패널·상세 뷰에서 다룬다. [[components/event-chip]] 참조.
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### Tone
|
|
65
|
-
|
|
66
|
-
이벤트 톤 7개(default/neutral/accent/success/warning/danger/info) — 상세 매트릭스는 [[components/event-chip]]에서 1차 정의한다. Calendar는 `event.tone`을 그대로 EventChip에 전달.
|
|
67
|
-
|
|
68
|
-
**금지 조합**
|
|
69
|
-
- `tone="danger"`를 "눈에 띄게" 쓰려고 전부 danger로 깔기 — 색이 의미 신호를 잃는다. 셀당 1개가 danger라면 다른 셀은 default/accent여야 한다.
|
|
70
|
-
- Calendar를 가로 너비 300px 미만에 끼워넣기 — 셀이 사각형을 못 지키고 pill 1개도 안 들어간다. 이 자리는 DatePicker.
|
|
71
|
-
- `showWeekend=false` + 월말 날짜가 토/일에 몰린 달 — 선택 가능한 셀이 사라져 "클릭이 안 되는 날짜"가 생긴다. 주말 숨김은 평일만 다루는 업무 캘린더 전용.
|
|
72
|
-
|
|
73
|
-
### 조합 예시
|
|
74
|
-
|
|
75
|
-
```tsx
|
|
76
|
-
// 기본 — 페이지 메인에 깔리는 월간 뷰
|
|
77
|
-
const [month, setMonth] = useState(new Date());
|
|
78
|
-
const [selected, setSelected] = useState<Date | null>(new Date());
|
|
79
|
-
|
|
80
|
-
<Calendar
|
|
81
|
-
month={month}
|
|
82
|
-
selectedDate={selected}
|
|
83
|
-
events={events}
|
|
84
|
-
onMonthChange={setMonth}
|
|
85
|
-
onSelectedDateChange={setSelected}
|
|
86
|
-
/>
|
|
87
|
-
|
|
88
|
-
// 2단 레이아웃 — 선택 날짜 상세 패널과 짝. 리스트는 ListItem으로 조립.
|
|
89
|
-
<div className="flex min-h-0 flex-1">
|
|
90
|
-
<Calendar
|
|
91
|
-
month={month}
|
|
92
|
-
selectedDate={selected}
|
|
93
|
-
events={events}
|
|
94
|
-
onMonthChange={setMonth}
|
|
95
|
-
onSelectedDateChange={setSelected}
|
|
96
|
-
/>
|
|
97
|
-
<aside className="w-80 shrink-0 border-l border-[var(--border)]">
|
|
98
|
-
<SelectedDayPanel date={selected} items={eventsOf(selected)} />
|
|
99
|
-
</aside>
|
|
100
|
-
</div>
|
|
101
|
-
|
|
102
|
-
// 외부 상태 신호가 있는 상단바 — leadingSlot으로 Badge 꽂음
|
|
103
|
-
<Calendar
|
|
104
|
-
leadingSlot={<Badge variant="soft" status="info">Google 연결됨</Badge>}
|
|
105
|
-
month={month}
|
|
106
|
-
onMonthChange={setMonth}
|
|
107
|
-
/>
|
|
108
|
-
|
|
109
|
-
// 업무 전용 — 주말 숨김, 월요일 시작
|
|
110
|
-
<Calendar
|
|
111
|
-
month={month}
|
|
112
|
-
showWeekend={false}
|
|
113
|
-
weekStartsOn={1}
|
|
114
|
-
onMonthChange={setMonth}
|
|
115
|
-
/>
|
|
116
|
-
```
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# FormField
|
|
2
|
-
|
|
3
|
-
### 원칙
|
|
4
|
-
|
|
5
|
-
라벨 · 설명 · 에러 · required 별표를 담당하는 폼 필드 래퍼. 자식 입력 컨트롤([[components/input]] · [[components/date-picker]] · Select 등)의 접근성 속성(`id` · `aria-describedby` · `aria-invalid` · `required`)을 자동으로 연결한다. 입력 컨트롤 자체는 이 속성들을 직접 관리하지 않는다.
|
|
6
|
-
|
|
7
|
-
### Do
|
|
8
|
-
- 라벨이 필요한 모든 폼 컨트롤은 `FormField`로 감싼다.
|
|
9
|
-
- `FormField.Label`은 항상 첫 번째 자식. 그 다음 `Description`, 컨트롤, `Error` 순.
|
|
10
|
-
- `invalid`는 FormField에 준다 — 자식 컨트롤이 자동으로 `aria-invalid`를 받는다. 컨트롤에도 `invalid`를 넘겨 시각 피드백을 켜준다.
|
|
11
|
-
- `required`는 FormField에 준다 — 라벨에 별표(*)가 자동으로 붙고 컨트롤에 `required` 속성이 전파된다.
|
|
12
|
-
- 에러 문구는 반드시 `FormField.Error` 컴포넌트로 넣는다. 임의의 `<p>`나 텍스트로 두지 않는다 — `role="alert"`과 aria 연결이 깨진다.
|
|
13
|
-
|
|
14
|
-
### Don't
|
|
15
|
-
- 한 FormField 안에 두 개 이상의 컨트롤을 두지 않는다. 라벨/에러 대응이 1:1이 아니게 됨.
|
|
16
|
-
- `Description`을 에러 메시지로 쓰지 않는다. Description은 상시 힌트, Error는 검증 실패 시 표시.
|
|
17
|
-
- `Label`의 `htmlFor`를 수동 지정하지 않는다 — FormField가 자동 생성한 id와 어긋난다.
|
|
18
|
-
|
|
19
|
-
### Fallback
|
|
20
|
-
`required` 여부가 애매하면 `false`. 필수 입력이 확정되면 켠다. `invalid`는 상위 폼 상태에서 파생된 값을 넘긴다 (컴포넌트가 스스로 판단하지 않음).
|
|
21
|
-
|
|
22
|
-
### 관련
|
|
23
|
-
- [[components/input]] — 가장 자주 조합되는 자식.
|
|
24
|
-
- [[components/date-picker]] — 트리거 Input이 FormField의 자식이 될 수 있음.
|
|
25
|
-
- [[tokens/color]] — `--destructive`(에러 · required 별표), `--fg-muted`(설명).
|
|
26
|
-
- [[tokens/typography]] — 라벨은 `--text-sm` · `medium`, 설명/에러는 `--text-xs`.
|
|
27
|
-
|
|
28
|
-
### Props 시그니처
|
|
29
|
-
|
|
30
|
-
```tsx
|
|
31
|
-
<FormField
|
|
32
|
-
invalid={boolean}
|
|
33
|
-
disabled={boolean}
|
|
34
|
-
required={boolean}
|
|
35
|
-
id={string} // 없으면 자동 생성
|
|
36
|
-
>
|
|
37
|
-
<FormField.Label>...</FormField.Label>
|
|
38
|
-
<FormField.Description>...</FormField.Description>
|
|
39
|
-
<Input ... /> // 자식 컨트롤
|
|
40
|
-
<FormField.Error>...</FormField.Error>
|
|
41
|
-
</FormField>
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
**하위:**
|
|
45
|
-
- `FormField.Label` — `htmlFor` 자동. `required=true`면 별표 자동.
|
|
46
|
-
- `FormField.Description` — 상시 힌트. `id` 자동 → 컨트롤의 `aria-describedby`에 연결.
|
|
47
|
-
- `FormField.Error` — `invalid=true`일 때만 렌더링. `role="alert"` 포함.
|
|
48
|
-
|
|
49
|
-
### Variant 매트릭스
|
|
50
|
-
|
|
51
|
-
FormField 자체는 variant가 없다. 자식 컨트롤의 variant를 따른다.
|
|
52
|
-
|
|
53
|
-
| 상태 | 시각 반응 |
|
|
54
|
-
|---|---|
|
|
55
|
-
| `required` | 라벨에 destructive 색 별표(*) |
|
|
56
|
-
| `invalid` | 자식 컨트롤에 `aria-invalid` 전파 + Error 노출 |
|
|
57
|
-
| `disabled` | 자식 컨트롤에 `disabled` 전파 |
|
|
58
|
-
|
|
59
|
-
### 조합 예시
|
|
60
|
-
|
|
61
|
-
```tsx
|
|
62
|
-
<FormField required invalid={!!emailError}>
|
|
63
|
-
<FormField.Label>이메일</FormField.Label>
|
|
64
|
-
<FormField.Description>업무용 이메일을 입력해주세요.</FormField.Description>
|
|
65
|
-
<Input
|
|
66
|
-
type="email"
|
|
67
|
-
value={email}
|
|
68
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
69
|
-
invalid={!!emailError}
|
|
70
|
-
leftIcon={<MailIcon />}
|
|
71
|
-
/>
|
|
72
|
-
<FormField.Error>{emailError}</FormField.Error>
|
|
73
|
-
</FormField>
|
|
74
|
-
```
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# Message Attachment
|
|
2
|
-
|
|
3
|
-
### 원칙
|
|
4
|
-
AI가 생성한 **큰 이미지**를 메시지 안에 보여주는 블록. 작은 썸네일(첨부물 바에 들어가는 것)은 [[components/attachment-thumb]]. 생성 중엔 sparkle 숨쉼, 완료되면 cover 이미지. hover 시 다운로드 버튼.
|
|
5
|
-
|
|
6
|
-
### Do
|
|
7
|
-
- 세로형 포스터가 기본 (`aspect="portrait"` = 3:4) — 레퍼런스 톤.
|
|
8
|
-
- `loading` 중엔 그라데이션 + sparkle 애니. src 세팅되면 자동 전환.
|
|
9
|
-
- 다운로드가 필요하면 `onDownload` 전달 — hover 시 우상단 버튼 노출.
|
|
10
|
-
- 이미지 폭은 `--input-w-lg` 기준으로 약간 크게. 메시지 너비 안에서 자동 fit.
|
|
11
|
-
|
|
12
|
-
### Don't
|
|
13
|
-
- MessageAttachment를 작은 썸네일 용도로 쓰지 않는다 — [[components/attachment-thumb]].
|
|
14
|
-
- loading과 src 동시에 넘기지 않는다 — loading이 우선.
|
|
15
|
-
- 다운로드 버튼을 상시 노출하지 않는다 — hover/focus-within에서만.
|
|
16
|
-
|
|
17
|
-
### Fallback
|
|
18
|
-
aspect 애매하면 `portrait`. src 없으면 placeholder (sparkle 정적).
|
|
19
|
-
|
|
20
|
-
### 관련
|
|
21
|
-
- [[components/attachment-thumb]] — 작은 썸네일
|
|
22
|
-
- [[components/ai-message]] — 상위 소비처
|
|
23
|
-
|
|
24
|
-
### Props 시그니처
|
|
25
|
-
```tsx
|
|
26
|
-
<MessageAttachment
|
|
27
|
-
src={string} // 생성된 이미지 URL
|
|
28
|
-
alt={string}
|
|
29
|
-
aspect="portrait | square | landscape" // default: portrait
|
|
30
|
-
loading={boolean} // 생성 중 — sparkle 애니
|
|
31
|
-
onDownload={(e) => void} // 지정 시 hover download 버튼
|
|
32
|
-
/>
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### 레이아웃
|
|
36
|
-
- 외곽: `radius-md`, `max-width: input-w-lg × 1.1`, overflow hidden
|
|
37
|
-
- aspect-ratio로 비율 고정 (`3/4`·`1/1`·`4/3`)
|
|
38
|
-
- loading/placeholder bg: `/illustrations/image-gen-placeholder.png` (public에 저장된 그라데이션 placeholder)
|
|
39
|
-
- 로딩 중 중앙 아이콘: [[components/spinner]] `variant="lottie-dots"` `size="md"`, 색은 `--bg` (bg 이미지 위에서 흰 톤)
|
|
40
|
-
- Download 버튼: `space-8` 정사각, 우상단 inset `space-2`, hover 시 fade-in + scale
|