@conscience-technology/ctds 0.1.9 → 0.1.11
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/AIMessage/AIMessage.module.css +129 -0
- package/assets/components/AIMessage/AIMessage.tsx +104 -0
- package/assets/components/AIMessage/index.ts +2 -0
- package/assets/components/AttachmentChip/AttachmentChip.module.css +3 -1
- package/assets/components/AttachmentThumb/AttachmentThumb.module.css +13 -7
- package/assets/components/AttachmentThumb/AttachmentThumb.tsx +4 -1
- package/assets/components/Bubble/Bubble.module.css +65 -0
- package/assets/components/Bubble/Bubble.tsx +63 -0
- package/assets/components/Bubble/index.ts +2 -0
- package/assets/components/Button/Button.module.css +15 -2
- package/assets/components/Button/Button.tsx +13 -0
- package/assets/components/Calendar/Calendar.module.css +163 -0
- package/assets/components/Calendar/Calendar.tsx +323 -0
- package/assets/components/Calendar/index.ts +2 -0
- package/assets/components/ChatInput/ChatInput.module.css +82 -45
- package/assets/components/ChatInput/ChatInput.tsx +46 -34
- package/assets/components/ChatInput/ModelSelector.tsx +23 -27
- package/assets/components/ChatInput/index.ts +0 -1
- package/assets/components/CodeBlock/CodeBlock.module.css +79 -0
- package/assets/components/CodeBlock/CodeBlock.tsx +71 -0
- package/assets/components/CodeBlock/index.ts +2 -0
- package/assets/components/ContextMenu/ContextMenu.tsx +10 -1
- package/assets/components/DropdownMenu/DropdownMenu.module.css +26 -0
- package/assets/components/DropdownMenu/DropdownMenu.tsx +367 -0
- package/assets/components/DropdownMenu/index.ts +9 -0
- package/assets/components/EventChip/EventChip.module.css +57 -0
- package/assets/components/EventChip/EventChip.tsx +41 -0
- package/assets/components/EventChip/index.ts +2 -0
- package/assets/components/Icon/Icon.tsx +3 -0
- package/assets/components/IdentityButton/IdentityButton.module.css +119 -0
- package/assets/components/IdentityButton/IdentityButton.tsx +113 -0
- package/assets/components/IdentityButton/index.ts +6 -0
- package/assets/components/MenuItem/MenuItem.module.css +67 -2
- package/assets/components/MenuItem/MenuItem.tsx +22 -2
- package/assets/components/MessageActions/MessageActions.module.css +43 -0
- package/assets/components/MessageActions/MessageActions.tsx +94 -0
- package/assets/components/MessageActions/index.ts +6 -0
- package/assets/components/MessageQuote/MessageQuote.module.css +29 -0
- package/assets/components/MessageQuote/MessageQuote.tsx +33 -0
- package/assets/components/MessageQuote/index.ts +2 -0
- package/assets/components/MessageSources/MessageSources.module.css +58 -0
- package/assets/components/MessageSources/MessageSources.tsx +64 -0
- package/assets/components/MessageSources/index.ts +2 -0
- package/assets/components/MessageTable/MessageTable.module.css +59 -0
- package/assets/components/MessageTable/MessageTable.tsx +21 -0
- package/assets/components/MessageTable/index.ts +2 -0
- package/assets/components/QuotationBar/QuotationBar.module.css +2 -2
- package/assets/components/Sidebar/Sidebar.module.css +3 -57
- package/assets/components/Sidebar/Sidebar.tsx +18 -21
- package/assets/components/Spinner/LottiePlayer.tsx +82 -0
- package/assets/components/Spinner/Spinner.module.css +170 -0
- package/assets/components/Spinner/Spinner.tsx +117 -0
- package/assets/components/Spinner/index.ts +10 -0
- package/assets/components/Spinner/lottieColor.ts +106 -0
- package/assets/components/Spinner/lottieManifest.ts +100 -0
- package/assets/components/Tabs/Tabs.module.css +3 -3
- package/assets/components/UserMessage/UserMessage.module.css +41 -0
- package/assets/components/UserMessage/UserMessage.tsx +66 -0
- package/assets/components/UserMessage/index.ts +2 -0
- package/assets/components/_data.ts +18 -4
- package/assets/components/tokens.css +20 -0
- package/assets/prompts/components/ai-message.md +45 -0
- package/assets/prompts/components/bubble.md +56 -0
- package/assets/prompts/components/calendar.md +116 -0
- package/assets/prompts/components/code-block.md +35 -0
- package/assets/prompts/components/dropdown-menu.md +145 -0
- package/assets/prompts/components/event-chip.md +74 -0
- package/assets/prompts/components/identity-button.md +168 -0
- package/assets/prompts/components/message-actions.md +49 -0
- package/assets/prompts/components/message-quote.md +26 -0
- package/assets/prompts/components/message-sources.md +33 -0
- package/assets/prompts/components/message-table.md +27 -0
- package/assets/prompts/components/sidebar.md +48 -3
- package/assets/prompts/components/spinner.md +109 -0
- package/assets/prompts/components/user-message.md +45 -0
- package/assets/prompts/primitives/scroll.md +22 -11
- package/package.json +1 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/* Spinner — 공통 바깥 박스
|
|
2
|
+
variant 지정 → data-variant로 분기
|
|
3
|
+
variant 미지정 → [data-motion="…"] 조상 셀렉터로 자동 매핑 (Motion 축 추종)
|
|
4
|
+
|
|
5
|
+
색은 currentColor로 묶여 부모 color를 그대로 승계 (Palette/Hue 축 호환). */
|
|
6
|
+
|
|
7
|
+
.root {
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
position: relative;
|
|
12
|
+
color: currentColor;
|
|
13
|
+
width: var(--loader-size, var(--loader-size-sm));
|
|
14
|
+
height: var(--loader-size, var(--loader-size-sm));
|
|
15
|
+
flex-shrink: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.xs { --loader-size: var(--loader-size-xs); }
|
|
19
|
+
.sm { --loader-size: var(--loader-size-sm); }
|
|
20
|
+
.md { --loader-size: var(--loader-size-md); }
|
|
21
|
+
.lg { --loader-size: var(--loader-size-lg); }
|
|
22
|
+
|
|
23
|
+
/* 자식 전부 기본 숨김 — 활성 variant만 켬. */
|
|
24
|
+
.ring,
|
|
25
|
+
.dot,
|
|
26
|
+
.pulse {
|
|
27
|
+
display: none;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* ─────────────────────────────────────────────────────────
|
|
31
|
+
공통 variant 규칙 (키: ring / dots-bounce / dots-fade / pulse)
|
|
32
|
+
아래는 "활성 variant → 어떤 자식을 어떻게 보이게 할지"만 정의.
|
|
33
|
+
무엇이 활성 variant인가는 더 아래 "매칭" 섹션이 결정.
|
|
34
|
+
───────────────────────────────────────────────────────── */
|
|
35
|
+
|
|
36
|
+
/* ring */
|
|
37
|
+
.v-ring .ring {
|
|
38
|
+
display: block;
|
|
39
|
+
width: 100%;
|
|
40
|
+
height: 100%;
|
|
41
|
+
animation: m-spin calc(var(--duration-slower) * 2) linear infinite; /* lint-hardcoded: ignore */
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* dots 공통 */
|
|
45
|
+
.v-dots {
|
|
46
|
+
width: auto;
|
|
47
|
+
min-width: var(--loader-size);
|
|
48
|
+
gap: calc(var(--loader-size) * 0.22);
|
|
49
|
+
}
|
|
50
|
+
.v-dots .dot {
|
|
51
|
+
display: block;
|
|
52
|
+
width: calc(var(--loader-size) * 0.32);
|
|
53
|
+
height: calc(var(--loader-size) * 0.32);
|
|
54
|
+
border-radius: 50%;
|
|
55
|
+
background: currentColor;
|
|
56
|
+
flex-shrink: 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* dots-bounce */
|
|
60
|
+
.v-dots-bounce .dot {
|
|
61
|
+
animation: m-dots-bounce calc(var(--duration-slower) * 1.5) var(--ease-in-out) infinite;
|
|
62
|
+
}
|
|
63
|
+
.v-dots-bounce .dot:nth-child(3) {
|
|
64
|
+
animation-delay: calc(var(--duration-base) * -1);
|
|
65
|
+
}
|
|
66
|
+
.v-dots-bounce .dot:nth-child(4) {
|
|
67
|
+
animation-delay: calc(var(--duration-slow) * -1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* dots-fade */
|
|
71
|
+
.v-dots-fade .dot {
|
|
72
|
+
animation: m-dots-fade calc(var(--duration-slower) * 1.5) var(--ease-in-out) infinite;
|
|
73
|
+
}
|
|
74
|
+
.v-dots-fade .dot:nth-child(3) {
|
|
75
|
+
animation-delay: calc(var(--duration-base) * -1);
|
|
76
|
+
}
|
|
77
|
+
.v-dots-fade .dot:nth-child(4) {
|
|
78
|
+
animation-delay: calc(var(--duration-slow) * -1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* pulse */
|
|
82
|
+
.v-pulse .pulse {
|
|
83
|
+
display: block;
|
|
84
|
+
width: 100%;
|
|
85
|
+
height: 100%;
|
|
86
|
+
border-radius: 50%;
|
|
87
|
+
background: currentColor;
|
|
88
|
+
opacity: 0.6;
|
|
89
|
+
animation: m-pulse calc(var(--duration-slower) * 2) var(--ease-in-out) infinite;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ─────────────────────────────────────────────────────────
|
|
93
|
+
Lottie variants — 전부 정사각 bounds. LottiePlayer가 preserveAspectRatio=slice로
|
|
94
|
+
비정사각 캔버스를 가장자리 잘라 cover. visualScale/offset은 매니페스트에서.
|
|
95
|
+
───────────────────────────────────────────────────────── */
|
|
96
|
+
.v-lottie {
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
position: relative;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* 안쪽 로티 박스를 scale 배수만큼 "실제로" 크게 그려서 wrapper를 꽉 채우게 함.
|
|
102
|
+
transform:scale은 lottie-react wrapper에서 letterbox가 생겨 체감이 반감되므로 사용하지 않음.
|
|
103
|
+
offset은 중앙 기준 translate(%) — 상위 bounds 비율 기반 (ex. 5% = 아래로 bounds의 5% 밀기). */
|
|
104
|
+
.lottieFill {
|
|
105
|
+
position: absolute;
|
|
106
|
+
width: calc(100% * var(--lottie-scale, 1));
|
|
107
|
+
height: calc(100% * var(--lottie-scale, 1));
|
|
108
|
+
left: 50%;
|
|
109
|
+
top: 50%;
|
|
110
|
+
transform:
|
|
111
|
+
translate(-50%, -50%)
|
|
112
|
+
translate(var(--lottie-offset-x, 0), var(--lottie-offset-y, 0));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* ─────────────────────────────────────────────────────────
|
|
116
|
+
v-auto — variant 미지정 시 Motion 축을 따라감.
|
|
117
|
+
조상 data-motion 값에 따라 분기. 기본(미지정)은 ring.
|
|
118
|
+
───────────────────────────────────────────────────────── */
|
|
119
|
+
|
|
120
|
+
.v-auto .ring {
|
|
121
|
+
display: block;
|
|
122
|
+
width: 100%;
|
|
123
|
+
height: 100%;
|
|
124
|
+
animation: m-spin calc(var(--duration-slower) * 2) linear infinite; /* lint-hardcoded: ignore */
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
[data-motion="bouncy"] .v-auto .ring,
|
|
128
|
+
[data-motion="calm"] .v-auto .ring {
|
|
129
|
+
display: none;
|
|
130
|
+
animation: none;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
[data-motion="bouncy"] .v-auto,
|
|
134
|
+
[data-motion="calm"] .v-auto {
|
|
135
|
+
width: auto;
|
|
136
|
+
min-width: var(--loader-size);
|
|
137
|
+
gap: calc(var(--loader-size) * 0.22);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
[data-motion="bouncy"] .v-auto .dot,
|
|
141
|
+
[data-motion="calm"] .v-auto .dot {
|
|
142
|
+
display: block;
|
|
143
|
+
width: calc(var(--loader-size) * 0.32);
|
|
144
|
+
height: calc(var(--loader-size) * 0.32);
|
|
145
|
+
border-radius: 50%;
|
|
146
|
+
background: currentColor;
|
|
147
|
+
flex-shrink: 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* bouncy → dots-bounce */
|
|
151
|
+
[data-motion="bouncy"] .v-auto .dot {
|
|
152
|
+
animation: m-dots-bounce calc(var(--duration-slower) * 1.5) var(--ease-in-out) infinite;
|
|
153
|
+
}
|
|
154
|
+
[data-motion="bouncy"] .v-auto .dot:nth-child(3) {
|
|
155
|
+
animation-delay: calc(var(--duration-base) * -1);
|
|
156
|
+
}
|
|
157
|
+
[data-motion="bouncy"] .v-auto .dot:nth-child(4) {
|
|
158
|
+
animation-delay: calc(var(--duration-slow) * -1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* calm → dots-fade */
|
|
162
|
+
[data-motion="calm"] .v-auto .dot {
|
|
163
|
+
animation: m-dots-fade calc(var(--duration-slower) * 1.5) var(--ease-in-out) infinite;
|
|
164
|
+
}
|
|
165
|
+
[data-motion="calm"] .v-auto .dot:nth-child(3) {
|
|
166
|
+
animation-delay: calc(var(--duration-base) * -1);
|
|
167
|
+
}
|
|
168
|
+
[data-motion="calm"] .v-auto .dot:nth-child(4) {
|
|
169
|
+
animation-delay: calc(var(--duration-slow) * -1);
|
|
170
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Spinner — Motion 축이 로더 성격을 결정.
|
|
5
|
+
*
|
|
6
|
+
* variant 생략 시 Motion 축의 `--loader-style`을 추종한다.
|
|
7
|
+
* - bouncy → dots-bounce
|
|
8
|
+
* - standard → ring
|
|
9
|
+
* - calm → dots-fade
|
|
10
|
+
* - none → ring (접근성 예외: 로딩 신호는 유지)
|
|
11
|
+
*
|
|
12
|
+
* `lottie-<name>` variant는 번들된 Lottie 로티를 렌더 (colorBind=true면 currentColor 추종).
|
|
13
|
+
*
|
|
14
|
+
* 색은 currentColor로 묶여 부모 color를 그대로 승계 (Palette/Hue 축 호환).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
18
|
+
import styles from "./Spinner.module.css";
|
|
19
|
+
import type { LottieName } from "./lottieManifest";
|
|
20
|
+
import { LottiePlayer } from "./LottiePlayer";
|
|
21
|
+
|
|
22
|
+
export type SpinnerSize = "xs" | "sm" | "md" | "lg";
|
|
23
|
+
export type SpinnerBaseVariant = "ring" | "dots-bounce" | "dots-fade" | "pulse";
|
|
24
|
+
export type SpinnerLottieVariant = `lottie-${LottieName}`;
|
|
25
|
+
export type SpinnerVariant = SpinnerBaseVariant | SpinnerLottieVariant;
|
|
26
|
+
|
|
27
|
+
export type SpinnerProps = HTMLAttributes<HTMLSpanElement> & {
|
|
28
|
+
size?: SpinnerSize;
|
|
29
|
+
/** 생략 시 현재 Motion 축의 `--loader-style`을 따름 */
|
|
30
|
+
variant?: SpinnerVariant;
|
|
31
|
+
/** 스크린리더 라벨 */
|
|
32
|
+
label?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const sizeClass: Record<SpinnerSize, string> = {
|
|
36
|
+
xs: styles.xs,
|
|
37
|
+
sm: styles.sm,
|
|
38
|
+
md: styles.md,
|
|
39
|
+
lg: styles.lg,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const baseVariantClass: Record<SpinnerBaseVariant, string> = {
|
|
43
|
+
ring: styles["v-ring"],
|
|
44
|
+
"dots-bounce": `${styles["v-dots"]} ${styles["v-dots-bounce"]}`,
|
|
45
|
+
"dots-fade": `${styles["v-dots"]} ${styles["v-dots-fade"]}`,
|
|
46
|
+
pulse: styles["v-pulse"],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function isLottieVariant(v: SpinnerVariant): v is SpinnerLottieVariant {
|
|
50
|
+
return v.startsWith("lottie-");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const Spinner = forwardRef<HTMLSpanElement, SpinnerProps>(
|
|
54
|
+
({ className, size = "sm", variant, label = "Loading", ...rest }, ref) => {
|
|
55
|
+
let variantPart = "";
|
|
56
|
+
let lottieName: LottieName | null = null;
|
|
57
|
+
|
|
58
|
+
if (variant) {
|
|
59
|
+
if (isLottieVariant(variant)) {
|
|
60
|
+
lottieName = variant.slice("lottie-".length) as LottieName;
|
|
61
|
+
variantPart = styles["v-lottie"];
|
|
62
|
+
} else {
|
|
63
|
+
variantPart = baseVariantClass[variant];
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
variantPart = styles["v-auto"];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const rootClass = [styles.root, sizeClass[size], variantPart, className ?? ""]
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.join(" ");
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<span
|
|
75
|
+
ref={ref}
|
|
76
|
+
className={rootClass}
|
|
77
|
+
role="status"
|
|
78
|
+
aria-label={label}
|
|
79
|
+
{...rest}
|
|
80
|
+
>
|
|
81
|
+
{lottieName ? <LottiePlayer name={lottieName} /> : <SpinnerBody />}
|
|
82
|
+
</span>
|
|
83
|
+
);
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
Spinner.displayName = "Spinner";
|
|
88
|
+
|
|
89
|
+
/* 비-로티 variant용 공통 바디. 활성 variant 클래스가 해당하는 자식만 display. */
|
|
90
|
+
function SpinnerBody() {
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
<svg className={styles.ring} viewBox="0 0 24 24" fill="none" aria-hidden>
|
|
94
|
+
<circle
|
|
95
|
+
cx="12"
|
|
96
|
+
cy="12"
|
|
97
|
+
r="9"
|
|
98
|
+
stroke="currentColor"
|
|
99
|
+
strokeOpacity="0.2"
|
|
100
|
+
strokeWidth="3"
|
|
101
|
+
/>
|
|
102
|
+
<path
|
|
103
|
+
d="M12 3a9 9 0 0 1 9 9"
|
|
104
|
+
stroke="currentColor"
|
|
105
|
+
strokeWidth="3"
|
|
106
|
+
strokeLinecap="round"
|
|
107
|
+
/>
|
|
108
|
+
</svg>
|
|
109
|
+
<span className={styles.dot} aria-hidden />
|
|
110
|
+
<span className={styles.dot} aria-hidden />
|
|
111
|
+
<span className={styles.dot} aria-hidden />
|
|
112
|
+
<span className={styles.pulse} aria-hidden />
|
|
113
|
+
</>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { Spinner };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Spinner } from "./Spinner";
|
|
2
|
+
export type {
|
|
3
|
+
SpinnerProps,
|
|
4
|
+
SpinnerSize,
|
|
5
|
+
SpinnerVariant,
|
|
6
|
+
SpinnerBaseVariant,
|
|
7
|
+
SpinnerLottieVariant,
|
|
8
|
+
} from "./Spinner";
|
|
9
|
+
export { LOTTIE_MANIFEST, LOTTIE_NAMES } from "./lottieManifest";
|
|
10
|
+
export type { LottieName, LottieManifestEntry } from "./lottieManifest";
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lottie JSON 안의 모든 색 배열을 target 색으로 치환.
|
|
3
|
+
*
|
|
4
|
+
* Lottie 색 포맷: `[r, g, b]` 또는 `[r, g, b, a]` (0~1 정규화).
|
|
5
|
+
* 매니페스트에서 `colorBind: true`인 단색 로티에만 사용 — 다색 로티는 색 구조가
|
|
6
|
+
* 흐트러져서 적용 불가.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type Rgb = [number, number, number];
|
|
10
|
+
|
|
11
|
+
/** CSS 색 문자열 → [r,g,b] 0~1 정규화. 서버에서는 null 반환.
|
|
12
|
+
* oklch()/color() 같은 비-sRGB 색공간을 Canvas 2D fillStyle로 넣고 다시 읽어
|
|
13
|
+
* 브라우저가 rgb()/rgba() 문자열로 토해내게 한 뒤 파싱 (가장 안정적인 변환). */
|
|
14
|
+
export function parseCssColor(css: string): Rgb | null {
|
|
15
|
+
if (typeof document === "undefined") return null;
|
|
16
|
+
const canvas = document.createElement("canvas");
|
|
17
|
+
canvas.width = 1;
|
|
18
|
+
canvas.height = 1;
|
|
19
|
+
const ctx = canvas.getContext("2d");
|
|
20
|
+
if (!ctx) return null;
|
|
21
|
+
ctx.fillStyle = "#000";
|
|
22
|
+
ctx.fillStyle = css;
|
|
23
|
+
const computed = ctx.fillStyle as string;
|
|
24
|
+
// Canvas가 지원 못 하면 "#000"이 그대로 남음 → 원본이 지원되지 않는 색이거나 oklch 미지원 브라우저.
|
|
25
|
+
// getComputedStyle로 폴백 (대부분 rgb() 반환).
|
|
26
|
+
let hex = /^#([0-9a-f]{6})$/i.exec(computed);
|
|
27
|
+
if (!hex) {
|
|
28
|
+
const el = document.createElement("div");
|
|
29
|
+
el.style.color = css;
|
|
30
|
+
document.body.appendChild(el);
|
|
31
|
+
ctx.fillStyle = "#000";
|
|
32
|
+
ctx.fillStyle = getComputedStyle(el).color;
|
|
33
|
+
document.body.removeChild(el);
|
|
34
|
+
hex = /^#([0-9a-f]{6})$/i.exec(ctx.fillStyle as string);
|
|
35
|
+
}
|
|
36
|
+
if (hex) {
|
|
37
|
+
const n = parseInt(hex[1], 16);
|
|
38
|
+
return [((n >> 16) & 0xff) / 255, ((n >> 8) & 0xff) / 255, (n & 0xff) / 255];
|
|
39
|
+
}
|
|
40
|
+
// 최후 폴백 — rgba() / rgb() 문자열 파싱
|
|
41
|
+
const m = (computed as string).match(/-?\d+(\.\d+)?/g);
|
|
42
|
+
if (!m || m.length < 3) return null;
|
|
43
|
+
return [+m[0] / 255, +m[1] / 255, +m[2] / 255];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 색 자리를 target으로 치환. colorBind=true 전용 — 단색 로티 가정.
|
|
47
|
+
* 원본 색의 알파만 보존하고 rgb는 target 그대로.
|
|
48
|
+
* (이전 버전은 원본 luma 비율로 scale → primary처럼 중간 밝기 target일 때
|
|
49
|
+
* 원본이 검정이면 결과도 검정으로 묶여버렸음.) */
|
|
50
|
+
export function recolorLottie(data: unknown, target: Rgb): unknown {
|
|
51
|
+
const [tr, tg, tb] = target;
|
|
52
|
+
|
|
53
|
+
function walk(node: unknown, parentKey?: string): unknown {
|
|
54
|
+
if (Array.isArray(node)) {
|
|
55
|
+
if (
|
|
56
|
+
(node.length === 3 || node.length === 4) &&
|
|
57
|
+
node.every((v) => typeof v === "number" && v >= 0 && v <= 1)
|
|
58
|
+
) {
|
|
59
|
+
const alpha = node.length === 4 ? node[3] : undefined;
|
|
60
|
+
return alpha !== undefined ? [tr, tg, tb, alpha] : [tr, tg, tb];
|
|
61
|
+
}
|
|
62
|
+
return node.map((v) => walk(v));
|
|
63
|
+
}
|
|
64
|
+
if (node && typeof node === "object") {
|
|
65
|
+
const obj = node as Record<string, unknown>;
|
|
66
|
+
const out: Record<string, unknown> = {};
|
|
67
|
+
// 그라데이션 fill(ty:"gf"|"gs")은 g.k.k가 플랫 배열 [stop, r, g, b, stop, r, g, b, ...].
|
|
68
|
+
// 전체를 target 단색으로 눕힘 (stop만 유지).
|
|
69
|
+
if (
|
|
70
|
+
parentKey === "g" &&
|
|
71
|
+
obj.k &&
|
|
72
|
+
typeof obj.k === "object" &&
|
|
73
|
+
!Array.isArray(obj.k) &&
|
|
74
|
+
Array.isArray((obj.k as Record<string, unknown>).k)
|
|
75
|
+
) {
|
|
76
|
+
const inner = obj.k as Record<string, unknown>;
|
|
77
|
+
const flat = inner.k as unknown[];
|
|
78
|
+
if (
|
|
79
|
+
flat.length >= 4 &&
|
|
80
|
+
flat.length % 4 === 0 &&
|
|
81
|
+
flat.every((v) => typeof v === "number" && v >= 0 && v <= 1)
|
|
82
|
+
) {
|
|
83
|
+
const flatted: number[] = [];
|
|
84
|
+
for (let i = 0; i < flat.length; i += 4) {
|
|
85
|
+
flatted.push(flat[i] as number, tr, tg, tb);
|
|
86
|
+
}
|
|
87
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
88
|
+
if (k === "k") {
|
|
89
|
+
out[k] = { ...inner, k: flatted };
|
|
90
|
+
} else {
|
|
91
|
+
out[k] = walk(v, k);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
98
|
+
out[k] = walk(v, k);
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
return node;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return walk(data);
|
|
106
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lottie 로더 매니페스트.
|
|
3
|
+
*
|
|
4
|
+
* `name`: Spinner variant로 쓰는 식별자 (`lottie-<name>` 또는 매니페스트의 name 그대로)
|
|
5
|
+
* `path`: /public 밑 상대 경로
|
|
6
|
+
* `colorBind`:
|
|
7
|
+
* true — 단색 로티. 런타임에 모든 색을 currentColor로 치환해 Palette/Hue 축을 추종.
|
|
8
|
+
* false — 다색 로티. 원본 색 유지 (브랜드 톤 또는 작가 의도).
|
|
9
|
+
*
|
|
10
|
+
* 모든 Spinner 로티는 정사각형 bounds로 강제 렌더(SVG preserveAspectRatio=xMidYMid slice).
|
|
11
|
+
* 원본 캔버스 비율이 1:1이 아니어도 bounds는 항상 1:1. 비정사각 로티는 가장자리가
|
|
12
|
+
* 잘릴 수 있으므로 중요한 디테일이 중앙에 모여있는 로티만 등록.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type LottieName =
|
|
16
|
+
| "rabbit"
|
|
17
|
+
| "infiniti"
|
|
18
|
+
| "dots"
|
|
19
|
+
| "spinner"
|
|
20
|
+
| "clock"
|
|
21
|
+
| "cosmos"
|
|
22
|
+
| "image-gen"
|
|
23
|
+
| "loading-square";
|
|
24
|
+
|
|
25
|
+
export type LottieManifestEntry = {
|
|
26
|
+
name: LottieName;
|
|
27
|
+
path: string;
|
|
28
|
+
colorBind: boolean;
|
|
29
|
+
/** 원본 캔버스가 bounds를 다 채우지 못할 때(여백이 커 보일 때) scale-up 보정.
|
|
30
|
+
* 1 = 원본 그대로. 비정사각 캔버스가 cover로 잘린 뒤 추가 확대도 가능.
|
|
31
|
+
* overflow:hidden이 켜져있어 bounds 밖은 잘림. */
|
|
32
|
+
visualScale?: number;
|
|
33
|
+
/** 캔버스 안에서 그림이 중앙에서 벗어나 있을 때 translate 보정. 단위는 bounds 비율(0~1 권장). */
|
|
34
|
+
offsetX?: number;
|
|
35
|
+
offsetY?: number;
|
|
36
|
+
/** 한줄 설명 — showcase · 자동완성용 */
|
|
37
|
+
description: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const LOTTIE_MANIFEST: Record<LottieName, LottieManifestEntry> = {
|
|
41
|
+
rabbit: {
|
|
42
|
+
name: "rabbit",
|
|
43
|
+
path: "/lottie/rabbit.json",
|
|
44
|
+
colorBind: true,
|
|
45
|
+
visualScale: 1.4,
|
|
46
|
+
description: "뛰어가는 토끼. 긴 작업·AI 생성 대기",
|
|
47
|
+
},
|
|
48
|
+
infiniti: {
|
|
49
|
+
name: "infiniti",
|
|
50
|
+
path: "/lottie/infiniti.json",
|
|
51
|
+
colorBind: true,
|
|
52
|
+
visualScale: 3.2,
|
|
53
|
+
description: "무한 루프 선. 네트워크·동기화",
|
|
54
|
+
},
|
|
55
|
+
dots: {
|
|
56
|
+
name: "dots",
|
|
57
|
+
path: "/lottie/dots.json",
|
|
58
|
+
colorBind: true,
|
|
59
|
+
visualScale: 0.95,
|
|
60
|
+
offsetY: 0.025,
|
|
61
|
+
description: "그라데이션 dot 흐름. 포인트 로더",
|
|
62
|
+
},
|
|
63
|
+
spinner: {
|
|
64
|
+
name: "spinner",
|
|
65
|
+
path: "/lottie/spinner.json",
|
|
66
|
+
colorBind: true,
|
|
67
|
+
visualScale: 2,
|
|
68
|
+
description: "표준 라인 스피너",
|
|
69
|
+
},
|
|
70
|
+
clock: {
|
|
71
|
+
name: "clock",
|
|
72
|
+
path: "/lottie/clock.json",
|
|
73
|
+
colorBind: true,
|
|
74
|
+
visualScale: 3.8,
|
|
75
|
+
description: "회전 시계 바늘. 처리 대기·타임아웃",
|
|
76
|
+
},
|
|
77
|
+
cosmos: {
|
|
78
|
+
name: "cosmos",
|
|
79
|
+
path: "/lottie/cosmos.json",
|
|
80
|
+
colorBind: true,
|
|
81
|
+
visualScale: 1.2,
|
|
82
|
+
description: "그라데이션 궤도. 몽환적·온보딩",
|
|
83
|
+
},
|
|
84
|
+
"image-gen": {
|
|
85
|
+
name: "image-gen",
|
|
86
|
+
path: "/lottie/image-gen.json",
|
|
87
|
+
colorBind: true,
|
|
88
|
+
visualScale: 1.1,
|
|
89
|
+
description: "이미지 생성 대기. AI 이미지",
|
|
90
|
+
},
|
|
91
|
+
"loading-square": {
|
|
92
|
+
name: "loading-square",
|
|
93
|
+
path: "/lottie/loading-square.json",
|
|
94
|
+
colorBind: true,
|
|
95
|
+
visualScale: 3.5,
|
|
96
|
+
description: "회전하는 사각형. 미니멀 처리 대기",
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const LOTTIE_NAMES = Object.keys(LOTTIE_MANIFEST) as LottieName[];
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
position: relative;
|
|
108
108
|
}
|
|
109
109
|
.size_sm .triggerUnderline { padding-inline: var(--space-1_5); }
|
|
110
|
-
.size_lg .triggerUnderline { padding-inline: var(--space-
|
|
110
|
+
.size_lg .triggerUnderline { padding-inline: var(--space-1_5); }
|
|
111
111
|
|
|
112
112
|
.listUnderline { position: relative; }
|
|
113
113
|
|
|
@@ -126,8 +126,8 @@
|
|
|
126
126
|
transform: translateX(calc(var(--tab-indicator-x) + var(--space-1_5)));
|
|
127
127
|
}
|
|
128
128
|
.size_lg .underlineBar {
|
|
129
|
-
width: calc(var(--tab-indicator-w) - var(--space-
|
|
130
|
-
transform: translateX(calc(var(--tab-indicator-x) + var(--space-
|
|
129
|
+
width: calc(var(--tab-indicator-w) - var(--space-1_5) * 2);
|
|
130
|
+
transform: translateX(calc(var(--tab-indicator-x) + var(--space-1_5)));
|
|
131
131
|
}
|
|
132
132
|
/* ready=true 이후에만 transition — 초기 배치는 순간이동. */
|
|
133
133
|
.listUnderline[data-ready="true"] .underlineBar {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* UserMessage — Bubble + MessageActions. 오른쪽 정렬. */
|
|
2
|
+
|
|
3
|
+
.root {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
align-items: flex-end;
|
|
7
|
+
gap: var(--space-1);
|
|
8
|
+
width: 100%;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* 첨부·인용 등 bubble 위에 올리는 영역 */
|
|
12
|
+
.before {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
align-items: flex-end;
|
|
16
|
+
gap: var(--space-1);
|
|
17
|
+
max-width: 100%;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.actions {
|
|
21
|
+
display: flex;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* actions-visibility=show: 상시 노출 */
|
|
25
|
+
.root[data-actions-visibility="show"] .actions {
|
|
26
|
+
opacity: 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* actions-visibility=hover: hover/focus-within에서만. 부드러운 fade + slight translate. */
|
|
30
|
+
.root[data-actions-visibility="hover"] .actions {
|
|
31
|
+
opacity: 0;
|
|
32
|
+
transform: translateY(calc(var(--space-0_5) * -1));
|
|
33
|
+
transition:
|
|
34
|
+
opacity var(--duration-slow) var(--ease-out),
|
|
35
|
+
transform var(--duration-slow) var(--ease-out);
|
|
36
|
+
}
|
|
37
|
+
.root[data-actions-visibility="hover"]:hover .actions,
|
|
38
|
+
.root[data-actions-visibility="hover"]:focus-within .actions {
|
|
39
|
+
opacity: 1;
|
|
40
|
+
transform: translateY(0);
|
|
41
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
4
|
+
import { Bubble, type BubbleFill, type BubbleShape, type BubbleSize } from "@/components/ctds/Bubble";
|
|
5
|
+
import {
|
|
6
|
+
MessageActions,
|
|
7
|
+
type MessageAction,
|
|
8
|
+
} from "@/components/ctds/MessageActions";
|
|
9
|
+
import styles from "./UserMessage.module.css";
|
|
10
|
+
|
|
11
|
+
export type UserMessageActionsVisibility = "show" | "hover";
|
|
12
|
+
|
|
13
|
+
export interface UserMessageProps extends HTMLAttributes<HTMLDivElement> {
|
|
14
|
+
/** 메시지 본문. string 또는 ReactNode. */
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
/** 버블 모양. 기본 circular-drop. */
|
|
17
|
+
shape?: BubbleShape;
|
|
18
|
+
/** 버블 fill. 기본 translucent. */
|
|
19
|
+
fill?: BubbleFill;
|
|
20
|
+
/** 버블 size. 기본 md. */
|
|
21
|
+
size?: BubbleSize;
|
|
22
|
+
/** 액션 노출 — "show" 상시 / "hover" 호버 시만. 기본 hover. */
|
|
23
|
+
actionsVisibility?: UserMessageActionsVisibility;
|
|
24
|
+
/** 액션 커스터마이즈. 기본 [copy, edit]. 비우려면 []. */
|
|
25
|
+
actions?: MessageAction[];
|
|
26
|
+
/** 메시지 위에 올라올 첨부(이미지/파일) 등 — 상위에서 자유 조합. */
|
|
27
|
+
before?: ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEFAULT_ACTIONS: MessageAction[] = [
|
|
31
|
+
{ kind: "copy" },
|
|
32
|
+
{ kind: "edit" },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export function UserMessage({
|
|
36
|
+
children,
|
|
37
|
+
shape = "circular-drop",
|
|
38
|
+
fill = "translucent",
|
|
39
|
+
size = "md",
|
|
40
|
+
actionsVisibility = "hover",
|
|
41
|
+
actions = DEFAULT_ACTIONS,
|
|
42
|
+
before,
|
|
43
|
+
className,
|
|
44
|
+
...rest
|
|
45
|
+
}: UserMessageProps) {
|
|
46
|
+
const cls = [styles.root, className].filter(Boolean).join(" ");
|
|
47
|
+
const hasActions = actions.length > 0;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
className={cls}
|
|
52
|
+
data-actions-visibility={actionsVisibility}
|
|
53
|
+
{...rest}
|
|
54
|
+
>
|
|
55
|
+
{before ? <div className={styles.before}>{before}</div> : null}
|
|
56
|
+
<Bubble shape={shape} fill={fill} size={size} tail="end">
|
|
57
|
+
{children}
|
|
58
|
+
</Bubble>
|
|
59
|
+
{hasActions ? (
|
|
60
|
+
<div className={styles.actions}>
|
|
61
|
+
<MessageActions align="end" actions={actions} />
|
|
62
|
+
</div>
|
|
63
|
+
) : null}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|