@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
|
@@ -133,6 +133,12 @@
|
|
|
133
133
|
border-inline-end: var(--hairline) solid var(--border);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/* opt-out: 소비처가 data-sidebar-border="none"을 걸면 사이드바 오른쪽 hairline을 숨김.
|
|
137
|
+
docs처럼 사이드바가 bg·padding만으로 자체 영역을 표현할 때 사용. */
|
|
138
|
+
.root[data-sidebar-border="none"] .sidebarSlot {
|
|
139
|
+
border-inline-end: none;
|
|
140
|
+
}
|
|
141
|
+
|
|
136
142
|
/* floating ────────────────────────────────────────────
|
|
137
143
|
sidebar/topbar/content가 각각 카드로 떠 있음. */
|
|
138
144
|
.root[data-variant="floating"] {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* AttachmentItem — [leading] filename (meta) [actions]
|
|
2
2
|
size sm/md: 1줄 pill
|
|
3
3
|
size lg: 2줄 큰 카드 (파일명 위 / meta 아래)
|
|
4
4
|
폭은 전부 --input-w-lg (400px) 고정 — 소비처가 폭 걱정 없이 넣기만. */
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
color: var(--fg);
|
|
15
15
|
transition: var(--transition-colors);
|
|
16
16
|
box-sizing: border-box;
|
|
17
|
+
/* 아주 연한 drop shadow — 종이가 살짝 떠 있는 정도 (hairline보다 아주 약간만 강조). */
|
|
18
|
+
box-shadow: var(--shadow-xs);
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/* ───────── size ─────────
|
|
@@ -69,7 +71,7 @@
|
|
|
69
71
|
.meta {
|
|
70
72
|
flex: 1 1 auto;
|
|
71
73
|
min-width: 0;
|
|
72
|
-
color: var(--fg-
|
|
74
|
+
color: var(--fg-subtle);
|
|
73
75
|
font-weight: var(--font-weight-regular);
|
|
74
76
|
line-height: 1.2;
|
|
75
77
|
overflow: hidden;
|
|
@@ -89,7 +91,7 @@
|
|
|
89
91
|
align-items: center;
|
|
90
92
|
justify-content: center;
|
|
91
93
|
flex: 0 0 auto;
|
|
92
|
-
color: var(--
|
|
94
|
+
color: var(--primary);
|
|
93
95
|
width: var(--space-6);
|
|
94
96
|
height: var(--space-6);
|
|
95
97
|
}
|
|
@@ -101,11 +103,6 @@
|
|
|
101
103
|
width: var(--space-8);
|
|
102
104
|
height: var(--space-8);
|
|
103
105
|
}
|
|
104
|
-
.loadingSpinner {
|
|
105
|
-
width: 100%;
|
|
106
|
-
height: 100%;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
106
|
/* ───────── actions ───────── */
|
|
110
107
|
.actions {
|
|
111
108
|
display: inline-flex;
|
package/assets/components/{AttachmentChip/AttachmentChip.tsx → AttachmentItem/AttachmentItem.tsx}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import type { HTMLAttributes, MouseEvent, ReactNode } from "react";
|
|
3
|
+
import type { CSSProperties, HTMLAttributes, MouseEvent, ReactNode } from "react";
|
|
4
4
|
import {
|
|
5
5
|
DotsThree as PhDotsThree,
|
|
6
6
|
DownloadSimple as PhDownload,
|
|
@@ -9,19 +9,19 @@ import {
|
|
|
9
9
|
import { FileIcon, type FileIconSize } from "@/components/ctds/FileIcon";
|
|
10
10
|
import { IconButton, type IconButtonSize } from "@/components/ctds/IconButton";
|
|
11
11
|
import { Spinner } from "@/components/ctds/Spinner";
|
|
12
|
-
import styles from "./
|
|
12
|
+
import styles from "./AttachmentItem.module.css";
|
|
13
13
|
|
|
14
|
-
export type
|
|
15
|
-
export type
|
|
14
|
+
export type AttachmentItemSize = "sm" | "md" | "lg";
|
|
15
|
+
export type AttachmentItemState = "idle" | "loading";
|
|
16
16
|
|
|
17
|
-
export interface
|
|
17
|
+
export interface AttachmentItemProps
|
|
18
18
|
extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "onError"> {
|
|
19
19
|
/** 파일명. 확장자가 FileIcon 색/라벨을 결정. */
|
|
20
20
|
filename: string;
|
|
21
21
|
/** sm/md=1줄, lg=2줄(파일명 위 / meta 아래). 폭은 전부 400px 고정. */
|
|
22
|
-
size?:
|
|
22
|
+
size?: AttachmentItemSize;
|
|
23
23
|
/** loading이면 좌측 뱃지가 Spinner(lottie-dots)로 교체. */
|
|
24
|
-
state?:
|
|
24
|
+
state?: AttachmentItemState;
|
|
25
25
|
/** sm/md: 파일명 옆 inline 보조 영역. lg: 파일명 아래 2번째 줄.
|
|
26
26
|
* 문자열이면 muted 라벨로, ReactNode면 그대로 렌더 (ex. Button ghost xs). */
|
|
27
27
|
meta?: ReactNode;
|
|
@@ -33,26 +33,34 @@ export interface AttachmentChipProps
|
|
|
33
33
|
onMore?: (e: MouseEvent<HTMLButtonElement>) => void;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
const sizeClass: Record<
|
|
36
|
+
const sizeClass: Record<AttachmentItemSize, string> = {
|
|
37
37
|
sm: styles.sizeSm,
|
|
38
38
|
md: styles.sizeMd,
|
|
39
39
|
lg: styles.sizeLg,
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
/** 좌측 FileIcon 크기 — sm=sm(16), md=md(24), lg=lg(32). */
|
|
43
|
-
const FILE_ICON_SIZE: Record<
|
|
43
|
+
const FILE_ICON_SIZE: Record<AttachmentItemSize, FileIconSize> = {
|
|
44
44
|
sm: "sm",
|
|
45
45
|
md: "md",
|
|
46
46
|
lg: "lg",
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
/** trailing IconButton 크기 — sm/md=sm(20), lg=md(24). IconButton 공식 사이즈만 사용. */
|
|
50
|
-
const ACTION_BTN_SIZE: Record<
|
|
50
|
+
const ACTION_BTN_SIZE: Record<AttachmentItemSize, IconButtonSize> = {
|
|
51
51
|
sm: "sm",
|
|
52
52
|
md: "sm",
|
|
53
53
|
lg: "md",
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
/** loading 자리의 Spinner 크기 — loadingSlot(=FileIcon 치수) 16/24/32와 일치시켜야
|
|
57
|
+
* lottie 내부 scale/offset 계산이 안 깨짐. slot 치수를 그대로 --loader-size로 override. */
|
|
58
|
+
const SPINNER_LOADER_SIZE: Record<AttachmentItemSize, string> = {
|
|
59
|
+
sm: "var(--space-4)",
|
|
60
|
+
md: "var(--space-6)",
|
|
61
|
+
lg: "var(--space-8)",
|
|
62
|
+
};
|
|
63
|
+
|
|
56
64
|
/** middle ellipsis — 폭 400px 안에서 충분히 들어가는 글자 수.
|
|
57
65
|
* 확장자는 항상 끝에 남긴다. CSS text-overflow:ellipsis가 뒤에서
|
|
58
66
|
* 확장자를 다시 자르지 않도록 폭 cap은 CSS와 함께 넉넉히 잡는다. */
|
|
@@ -60,7 +68,6 @@ function middleEllipsis(filename: string, max: number): string {
|
|
|
60
68
|
if (filename.length <= max) return filename;
|
|
61
69
|
const dot = filename.lastIndexOf(".");
|
|
62
70
|
const extLen = filename.length - dot;
|
|
63
|
-
// 확장자가 없거나 비정상적으로 긴 경우만 폴백.
|
|
64
71
|
if (dot <= 0 || extLen > 8 || extLen >= max - 4) {
|
|
65
72
|
return filename.slice(0, Math.max(1, max - 3)) + "...";
|
|
66
73
|
}
|
|
@@ -69,7 +76,7 @@ function middleEllipsis(filename: string, max: number): string {
|
|
|
69
76
|
return head + "..." + ext;
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
export function
|
|
79
|
+
export function AttachmentItem({
|
|
73
80
|
filename,
|
|
74
81
|
size = "md",
|
|
75
82
|
state = "idle",
|
|
@@ -79,7 +86,7 @@ export function AttachmentChip({
|
|
|
79
86
|
onMore,
|
|
80
87
|
className,
|
|
81
88
|
...rest
|
|
82
|
-
}:
|
|
89
|
+
}: AttachmentItemProps) {
|
|
83
90
|
const cls = [
|
|
84
91
|
styles.root,
|
|
85
92
|
sizeClass[size],
|
|
@@ -89,7 +96,6 @@ export function AttachmentChip({
|
|
|
89
96
|
.filter(Boolean)
|
|
90
97
|
.join(" ");
|
|
91
98
|
|
|
92
|
-
// 400px 폭에서 안전하게 들어가는 글자 수 (대략치).
|
|
93
99
|
const maxChars = size === "lg" ? 44 : size === "md" ? 40 : 36;
|
|
94
100
|
const label = middleEllipsis(filename, maxChars);
|
|
95
101
|
|
|
@@ -99,7 +105,7 @@ export function AttachmentChip({
|
|
|
99
105
|
<Spinner
|
|
100
106
|
variant="lottie-dots"
|
|
101
107
|
label="업로드 중"
|
|
102
|
-
|
|
108
|
+
style={{ "--loader-size": SPINNER_LOADER_SIZE[size] } as CSSProperties}
|
|
103
109
|
/>
|
|
104
110
|
</span>
|
|
105
111
|
) : (
|
|
@@ -38,14 +38,17 @@
|
|
|
38
38
|
/* Disabled — variant 무관 통일 스타일.
|
|
39
39
|
회색 버튼(--bg-muted) + 실버 글자(--fg-disabled) + 투명 보더.
|
|
40
40
|
loading은 opacity로 유지 (스피너 컨텍스트 보존). */
|
|
41
|
-
.root:disabled {
|
|
41
|
+
.root:disabled:not([data-loading="true"]) {
|
|
42
42
|
cursor: not-allowed;
|
|
43
43
|
background: var(--bg-muted);
|
|
44
44
|
color: var(--fg-disabled);
|
|
45
45
|
border-color: transparent;
|
|
46
46
|
box-shadow: none;
|
|
47
47
|
}
|
|
48
|
-
.root:disabled
|
|
48
|
+
.root:disabled[data-loading="true"] {
|
|
49
|
+
cursor: progress;
|
|
50
|
+
}
|
|
51
|
+
.root:disabled:not([data-loading="true"]) .icon {
|
|
49
52
|
opacity: 1;
|
|
50
53
|
}
|
|
51
54
|
/* Loading — 라벨/아이콘은 보이지만 톤 다운, Spinner가 중앙에 overlay.
|
|
@@ -227,3 +230,9 @@
|
|
|
227
230
|
.spinner {
|
|
228
231
|
display: inline-flex;
|
|
229
232
|
}
|
|
233
|
+
/* 투명·배경 위 variant는 로딩 스피너를 muted gray로 — primary 색이 흰 배경 위에서
|
|
234
|
+
혼자 둥둥 떠 보이는 걸 막는다. */
|
|
235
|
+
.outline .spinner,
|
|
236
|
+
.ghost .spinner {
|
|
237
|
+
color: var(--fg-muted);
|
|
238
|
+
}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type ReactNode,
|
|
13
13
|
} from "react";
|
|
14
14
|
import { Paperclip as PhPaperclip } from "@phosphor-icons/react";
|
|
15
|
-
import {
|
|
15
|
+
import { AttachmentItem } from "@/components/ctds/AttachmentItem";
|
|
16
16
|
import { AttachmentThumb } from "@/components/ctds/AttachmentThumb";
|
|
17
17
|
import { DropdownMenu } from "@/components/ctds/DropdownMenu";
|
|
18
18
|
import { QuotationBar } from "@/components/ctds/QuotationBar";
|
|
@@ -262,7 +262,7 @@ export const ChatInput = forwardRef<HTMLDivElement, ChatInputProps>(
|
|
|
262
262
|
}
|
|
263
263
|
/>
|
|
264
264
|
) : (
|
|
265
|
-
<
|
|
265
|
+
<AttachmentItem
|
|
266
266
|
filename={a.filename}
|
|
267
267
|
onRemove={
|
|
268
268
|
onRemoveAttachment
|
|
@@ -104,7 +104,7 @@ const MonthView = memo(function MonthView({
|
|
|
104
104
|
))}
|
|
105
105
|
</div>
|
|
106
106
|
|
|
107
|
-
<div className={styles.grid} role="grid">
|
|
107
|
+
<div className={styles.grid} role="grid" aria-label={monthLabelText(month)}>
|
|
108
108
|
{cells.map((day) => {
|
|
109
109
|
const outside = !isSameMonth(day, month);
|
|
110
110
|
const disabled =
|
|
@@ -144,6 +144,8 @@ const MonthView = memo(function MonthView({
|
|
|
144
144
|
onMouseEnter={() => onHoverDay(day)}
|
|
145
145
|
onMouseLeave={() => onHoverDay(null)}
|
|
146
146
|
aria-selected={isSelected}
|
|
147
|
+
aria-disabled={disabled || undefined}
|
|
148
|
+
aria-label={dayAriaLabel(day, { isToday, isSelected })}
|
|
147
149
|
role="gridcell"
|
|
148
150
|
>
|
|
149
151
|
<span className={styles.dayLabel}>{day.getDate()}</span>
|
|
@@ -155,6 +157,22 @@ const MonthView = memo(function MonthView({
|
|
|
155
157
|
);
|
|
156
158
|
});
|
|
157
159
|
|
|
160
|
+
/** "2026년 10월" 류 — grid aria-label용. 월 이름이 바뀌면 utils 쪽 MONTH_LABELS_KO 교체. */
|
|
161
|
+
function monthLabelText(d: Date): string {
|
|
162
|
+
return `${d.getFullYear()}년 ${MONTH_LABELS_KO[d.getMonth()]}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** "2026년 10월 9일 (금), 오늘, 선택됨" 류 스크린리더용 라벨. */
|
|
166
|
+
function dayAriaLabel(d: Date, flags: { isToday: boolean; isSelected: boolean }): string {
|
|
167
|
+
const parts = [
|
|
168
|
+
`${d.getFullYear()}년 ${MONTH_LABELS_KO[d.getMonth()]} ${d.getDate()}일`,
|
|
169
|
+
`(${WEEKDAY_LABELS_KO[d.getDay()]})`,
|
|
170
|
+
];
|
|
171
|
+
if (flags.isToday) parts.push("오늘");
|
|
172
|
+
if (flags.isSelected) parts.push("선택됨");
|
|
173
|
+
return parts.join(", ");
|
|
174
|
+
}
|
|
175
|
+
|
|
158
176
|
export interface CalendarProps {
|
|
159
177
|
mode: "single" | "range";
|
|
160
178
|
months: 1 | 2;
|
|
@@ -44,7 +44,16 @@ export interface DrawerProps {
|
|
|
44
44
|
backdrop?: DrawerBackdrop;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* 간편 모드 — 가장 흔한 "title + body + footer" 레이아웃을 flat prop으로 넘긴다.
|
|
49
|
+
* 커스터마이즈가 필요하면 compound 모드:
|
|
50
|
+
* <Drawer.Root open onOpenChange={set} side="right" size="md">
|
|
51
|
+
* <Drawer.Header title="…" />
|
|
52
|
+
* <Drawer.Body>…</Drawer.Body>
|
|
53
|
+
* <Drawer.Footer>…</Drawer.Footer>
|
|
54
|
+
* </Drawer.Root>
|
|
55
|
+
*/
|
|
56
|
+
const DrawerComponent = forwardRef<HTMLDivElement, DrawerProps>(function Drawer(
|
|
48
57
|
{
|
|
49
58
|
open,
|
|
50
59
|
onOpenChange,
|
|
@@ -62,15 +71,48 @@ export const Drawer = forwardRef<HTMLDivElement, DrawerProps>(function Drawer(
|
|
|
62
71
|
},
|
|
63
72
|
ref,
|
|
64
73
|
) {
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
return (
|
|
75
|
+
<DrawerRoot
|
|
76
|
+
open={open}
|
|
77
|
+
onOpenChange={onOpenChange}
|
|
78
|
+
disableEscape={disableEscape}
|
|
79
|
+
side={side}
|
|
80
|
+
size={size}
|
|
81
|
+
backdrop={backdrop}
|
|
82
|
+
ref={ref}
|
|
83
|
+
>
|
|
84
|
+
<DrawerHeader
|
|
85
|
+
title={title}
|
|
86
|
+
description={description}
|
|
87
|
+
titleSrOnly={titleSrOnly}
|
|
88
|
+
dismissible={dismissible}
|
|
89
|
+
dismissLabel={dismissLabel}
|
|
90
|
+
/>
|
|
91
|
+
{children !== undefined && children !== null && <DrawerBody>{children}</DrawerBody>}
|
|
92
|
+
{footer && <DrawerFooter>{footer}</DrawerFooter>}
|
|
93
|
+
</DrawerRoot>
|
|
94
|
+
);
|
|
95
|
+
});
|
|
67
96
|
|
|
97
|
+
export interface DrawerRootProps {
|
|
98
|
+
open: boolean;
|
|
99
|
+
onOpenChange: (open: boolean) => void;
|
|
100
|
+
children: ReactNode;
|
|
101
|
+
disableEscape?: boolean;
|
|
102
|
+
side?: DrawerSide;
|
|
103
|
+
size?: DrawerSize;
|
|
104
|
+
backdrop?: DrawerBackdrop;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const DrawerRoot = forwardRef<HTMLDivElement, DrawerRootProps>(function DrawerRoot(
|
|
108
|
+
{ open, onOpenChange, children, disableEscape, side = "right", size = "md", backdrop = "scrim" },
|
|
109
|
+
ref,
|
|
110
|
+
) {
|
|
111
|
+
const isModal = backdrop !== "none";
|
|
68
112
|
return (
|
|
69
113
|
<RadixDialog.Root open={open} onOpenChange={onOpenChange} modal={isModal}>
|
|
70
114
|
<RadixDialog.Portal>
|
|
71
|
-
{backdrop !== "none" &&
|
|
72
|
-
<RadixDialog.Overlay className={styles.overlay} />
|
|
73
|
-
)}
|
|
115
|
+
{backdrop !== "none" && <RadixDialog.Overlay className={styles.overlay} />}
|
|
74
116
|
<RadixDialog.Content
|
|
75
117
|
ref={ref}
|
|
76
118
|
className={styles.content}
|
|
@@ -80,47 +122,86 @@ export const Drawer = forwardRef<HTMLDivElement, DrawerProps>(function Drawer(
|
|
|
80
122
|
if (disableEscape) e.preventDefault();
|
|
81
123
|
}}
|
|
82
124
|
onPointerDownOutside={(e) => {
|
|
83
|
-
// scrim만 바깥 클릭 닫힘. locked/none은 Radix 기본 닫힘 차단.
|
|
84
125
|
if (backdrop !== "scrim") e.preventDefault();
|
|
85
126
|
}}
|
|
86
127
|
onInteractOutside={(e) => {
|
|
87
128
|
if (backdrop === "none") e.preventDefault();
|
|
88
129
|
}}
|
|
89
130
|
>
|
|
90
|
-
|
|
91
|
-
<div className={styles.headerText}>
|
|
92
|
-
<RadixDialog.Title
|
|
93
|
-
className={[styles.title, titleSrOnly && "visually-hidden"]
|
|
94
|
-
.filter(Boolean)
|
|
95
|
-
.join(" ")}
|
|
96
|
-
>
|
|
97
|
-
{title}
|
|
98
|
-
</RadixDialog.Title>
|
|
99
|
-
{description && (
|
|
100
|
-
<RadixDialog.Description className={styles.description}>
|
|
101
|
-
{description}
|
|
102
|
-
</RadixDialog.Description>
|
|
103
|
-
)}
|
|
104
|
-
</div>
|
|
105
|
-
{dismissible && (
|
|
106
|
-
<RadixDialog.Close asChild>
|
|
107
|
-
<IconButton
|
|
108
|
-
className={styles.close}
|
|
109
|
-
variant="ghost"
|
|
110
|
-
icon={<Icon name="x" />}
|
|
111
|
-
aria-label={dismissLabel}
|
|
112
|
-
/>
|
|
113
|
-
</RadixDialog.Close>
|
|
114
|
-
)}
|
|
115
|
-
</div>
|
|
116
|
-
{children !== undefined && children !== null && (
|
|
117
|
-
<div className={styles.body}>
|
|
118
|
-
<div className={styles.bodyInner}>{children}</div>
|
|
119
|
-
</div>
|
|
120
|
-
)}
|
|
121
|
-
{footer && <div className={styles.footer}>{footer}</div>}
|
|
131
|
+
{children}
|
|
122
132
|
</RadixDialog.Content>
|
|
123
133
|
</RadixDialog.Portal>
|
|
124
134
|
</RadixDialog.Root>
|
|
125
135
|
);
|
|
126
136
|
});
|
|
137
|
+
|
|
138
|
+
export interface DrawerHeaderProps {
|
|
139
|
+
title: ReactNode;
|
|
140
|
+
description?: ReactNode;
|
|
141
|
+
titleSrOnly?: boolean;
|
|
142
|
+
dismissible?: boolean;
|
|
143
|
+
dismissLabel?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function DrawerHeader({
|
|
147
|
+
title,
|
|
148
|
+
description,
|
|
149
|
+
titleSrOnly,
|
|
150
|
+
dismissible = true,
|
|
151
|
+
dismissLabel,
|
|
152
|
+
}: DrawerHeaderProps) {
|
|
153
|
+
return (
|
|
154
|
+
<div className={styles.header}>
|
|
155
|
+
<div className={styles.headerText}>
|
|
156
|
+
<RadixDialog.Title
|
|
157
|
+
className={[styles.title, titleSrOnly && "visually-hidden"]
|
|
158
|
+
.filter(Boolean)
|
|
159
|
+
.join(" ")}
|
|
160
|
+
>
|
|
161
|
+
{title}
|
|
162
|
+
</RadixDialog.Title>
|
|
163
|
+
{description && (
|
|
164
|
+
<RadixDialog.Description className={styles.description}>
|
|
165
|
+
{description}
|
|
166
|
+
</RadixDialog.Description>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
{dismissible && (
|
|
170
|
+
<RadixDialog.Close asChild>
|
|
171
|
+
<IconButton
|
|
172
|
+
className={styles.close}
|
|
173
|
+
variant="ghost"
|
|
174
|
+
icon={<Icon name="x" />}
|
|
175
|
+
aria-label={dismissLabel}
|
|
176
|
+
/>
|
|
177
|
+
</RadixDialog.Close>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function DrawerBody({ children }: { children: ReactNode }) {
|
|
184
|
+
return (
|
|
185
|
+
<div className={styles.body}>
|
|
186
|
+
<div className={styles.bodyInner}>{children}</div>
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function DrawerFooter({ children }: { children: ReactNode }) {
|
|
192
|
+
return <div className={styles.footer}>{children}</div>;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
type DrawerCompound = typeof DrawerComponent & {
|
|
196
|
+
Root: typeof DrawerRoot;
|
|
197
|
+
Header: typeof DrawerHeader;
|
|
198
|
+
Body: typeof DrawerBody;
|
|
199
|
+
Footer: typeof DrawerFooter;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export const Drawer = Object.assign(DrawerComponent, {
|
|
203
|
+
Root: DrawerRoot,
|
|
204
|
+
Header: DrawerHeader,
|
|
205
|
+
Body: DrawerBody,
|
|
206
|
+
Footer: DrawerFooter,
|
|
207
|
+
}) as DrawerCompound;
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
Drawer,
|
|
3
|
+
DrawerRoot,
|
|
4
|
+
DrawerHeader,
|
|
5
|
+
DrawerBody,
|
|
6
|
+
DrawerFooter,
|
|
7
|
+
} from "./Drawer";
|
|
2
8
|
export type {
|
|
3
9
|
DrawerProps,
|
|
10
|
+
DrawerRootProps,
|
|
11
|
+
DrawerHeaderProps,
|
|
4
12
|
DrawerSide,
|
|
5
13
|
DrawerSize,
|
|
6
14
|
DrawerBackdrop,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
/*
|
|
2
|
-
loading / placeholder (src 없음) / image 세 상태. radius-md 고정, 생성 중 sparkle 숨쉼. */
|
|
1
|
+
/* GeneratedImage — AI가 생성한 이미지 큰 표시. loading / placeholder / image 세 상태. */
|
|
3
2
|
|
|
4
3
|
.root {
|
|
5
4
|
position: relative;
|
|
@@ -45,6 +44,8 @@
|
|
|
45
44
|
|
|
46
45
|
.spinner {
|
|
47
46
|
color: var(--bg);
|
|
47
|
+
/* image-gen 로띠는 공간이 넉넉해야 보기 좋음 — loader-size override. */
|
|
48
|
+
--loader-size: var(--space-16);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
/* Download 버튼 — hover 시만 노출, 우상단 */
|
|
@@ -4,30 +4,30 @@ import type { HTMLAttributes, MouseEvent } from "react";
|
|
|
4
4
|
import { DownloadSimple as PhDownload } from "@phosphor-icons/react";
|
|
5
5
|
import { Spinner } from "@/components/ctds/Spinner";
|
|
6
6
|
import { Tooltip } from "@/components/ctds/Tooltip";
|
|
7
|
-
import styles from "./
|
|
7
|
+
import styles from "./GeneratedImage.module.css";
|
|
8
8
|
|
|
9
|
-
export type
|
|
9
|
+
export type GeneratedImageAspect = "square" | "portrait" | "landscape";
|
|
10
10
|
|
|
11
|
-
export interface
|
|
11
|
+
export interface GeneratedImageProps
|
|
12
12
|
extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
13
13
|
/** 생성된 이미지 URL. 없거나 loading이면 placeholder. */
|
|
14
14
|
src?: string;
|
|
15
15
|
alt?: string;
|
|
16
16
|
/** 가로세로 비율. portrait=3/4(기본, 레퍼런스), square=1/1, landscape=4/3. */
|
|
17
|
-
aspect?:
|
|
17
|
+
aspect?: GeneratedImageAspect;
|
|
18
18
|
/** 생성 중 — placeholder + sparkle breathe 애니. */
|
|
19
19
|
loading?: boolean;
|
|
20
20
|
/** 다운로드 버튼 클릭. 미지정 시 버튼 숨김. */
|
|
21
21
|
onDownload?: (e: MouseEvent<HTMLButtonElement>) => void;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const aspectClass: Record<
|
|
24
|
+
const aspectClass: Record<GeneratedImageAspect, string> = {
|
|
25
25
|
square: styles.aspectSquare,
|
|
26
26
|
portrait: styles.aspectPortrait,
|
|
27
27
|
landscape: styles.aspectLandscape,
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
export function
|
|
30
|
+
export function GeneratedImage({
|
|
31
31
|
src,
|
|
32
32
|
alt = "",
|
|
33
33
|
aspect = "portrait",
|
|
@@ -35,7 +35,7 @@ export function MessageAttachment({
|
|
|
35
35
|
onDownload,
|
|
36
36
|
className,
|
|
37
37
|
...rest
|
|
38
|
-
}:
|
|
38
|
+
}: GeneratedImageProps) {
|
|
39
39
|
const cls = [
|
|
40
40
|
styles.root,
|
|
41
41
|
aspectClass[aspect],
|
|
@@ -48,7 +48,7 @@ export function MessageAttachment({
|
|
|
48
48
|
return (
|
|
49
49
|
<div className={cls} {...rest}>
|
|
50
50
|
{loading || !src ? (
|
|
51
|
-
<Spinner variant="lottie-
|
|
51
|
+
<Spinner variant="lottie-image-gen" size="lg" className={styles.spinner} />
|
|
52
52
|
) : (
|
|
53
53
|
<img src={src} alt={alt} className={styles.img} />
|
|
54
54
|
)}
|
|
@@ -128,6 +128,7 @@ import {
|
|
|
128
128
|
Scissors as PhScissors,
|
|
129
129
|
ClipboardText as PhClipboardText,
|
|
130
130
|
Camera as PhCamera,
|
|
131
|
+
Globe as PhGlobe,
|
|
131
132
|
} from "@phosphor-icons/react";
|
|
132
133
|
|
|
133
134
|
// Solar (Linear = regular, Bold = fill) ───────────────────────────
|
|
@@ -308,7 +309,8 @@ export type IconName =
|
|
|
308
309
|
| "copy-simple"
|
|
309
310
|
| "scissors"
|
|
310
311
|
| "clipboard-text"
|
|
311
|
-
| "camera"
|
|
312
|
+
| "camera"
|
|
313
|
+
| "globe";
|
|
312
314
|
|
|
313
315
|
type AnyComp = React.ComponentType<any>;
|
|
314
316
|
// Solar는 weight별 두 컴포넌트가 필요. 다른 family는 단일 컴포넌트.
|
|
@@ -422,6 +424,7 @@ const phosphorSet: Partial<Record<IconName, AnyComp>> = {
|
|
|
422
424
|
scissors: PhScissors,
|
|
423
425
|
"clipboard-text": PhClipboardText,
|
|
424
426
|
camera: PhCamera,
|
|
427
|
+
globe: PhGlobe,
|
|
425
428
|
};
|
|
426
429
|
|
|
427
430
|
/* solar 세트 — 라이브러리에 자연스러운 1:1 대응이 있는 것만.
|