@conscience-technology/ctds 0.4.4 → 0.4.6
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/ChatInput/ChatInput.module.css +54 -14
- package/assets/components/ChatInput/ChatInput.tsx +12 -1
- package/assets/components/ChatInput/SendButton.tsx +18 -1
- package/assets/components/ChatInput/index.ts +2 -0
- package/assets/components/Icon/Icon.tsx +1 -1
- package/assets/components/TabStrip/TabStrip.module.css +358 -0
- package/assets/components/TabStrip/TabStrip.tsx +396 -0
- package/assets/components/TabStrip/index.ts +7 -0
- package/assets/components/Toolbar/Toolbar.module.css +34 -7
- package/assets/components/Toolbar/Toolbar.tsx +9 -3
- package/assets/components/Toolbar/index.ts +1 -1
- package/assets/components/_data.ts +2 -1
- package/assets/components/index.ts +1 -0
- package/assets/prompts/ASSEMBLY.md +1 -0
- package/assets/prompts/components/chat-input.md +12 -2
- package/assets/prompts/components/tab-strip.md +157 -0
- package/assets/prompts/components/toolbar.md +5 -2
- package/assets/skills/ctds-page/SKILL.md +2 -0
- package/package.json +1 -1
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
box-sizing: border-box;
|
|
126
126
|
}
|
|
127
127
|
.multiField::placeholder {
|
|
128
|
-
color: var(--fg-
|
|
128
|
+
color: var(--fg-subtle);
|
|
129
129
|
}
|
|
130
130
|
/* attachments/quotation 아래에선 상단 padding 압축 (이미 attachments/quotation이 상단 책임). */
|
|
131
131
|
.attachments + .body .multiField,
|
|
@@ -200,7 +200,7 @@
|
|
|
200
200
|
box-sizing: border-box;
|
|
201
201
|
}
|
|
202
202
|
.singleField::placeholder {
|
|
203
|
-
color: var(--fg-
|
|
203
|
+
color: var(--fg-subtle);
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
/* Attach 버튼 */
|
|
@@ -287,14 +287,15 @@
|
|
|
287
287
|
padding: var(--space-1_5);
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
/* Send — Shape 축 반응. Button sm과 동일한 radius.
|
|
290
|
+
/* Send — Shape 축 반응. Button sm과 동일한 radius.
|
|
291
|
+
variant는 solid(기본)/ghost/soft 세 톤. 비활성→활성 전이는 색만 바뀜. */
|
|
291
292
|
.sendBtn {
|
|
292
293
|
display: inline-flex;
|
|
293
294
|
align-items: center;
|
|
294
295
|
justify-content: center;
|
|
295
296
|
padding: 0;
|
|
296
297
|
border: 0;
|
|
297
|
-
background:
|
|
298
|
+
background: transparent;
|
|
298
299
|
color: var(--fg-muted);
|
|
299
300
|
border-radius: var(--radius-sm);
|
|
300
301
|
cursor: pointer;
|
|
@@ -303,13 +304,6 @@
|
|
|
303
304
|
flex: 0 0 auto;
|
|
304
305
|
transition: var(--transition-colors);
|
|
305
306
|
}
|
|
306
|
-
.sendBtn[data-active] {
|
|
307
|
-
background: var(--primary);
|
|
308
|
-
color: var(--bg);
|
|
309
|
-
}
|
|
310
|
-
.sendBtn:hover:not(:disabled) {
|
|
311
|
-
opacity: 0.9;
|
|
312
|
-
}
|
|
313
307
|
.sendBtn:focus-visible {
|
|
314
308
|
outline: none;
|
|
315
309
|
box-shadow: var(--ring);
|
|
@@ -317,15 +311,61 @@
|
|
|
317
311
|
.sendBtn:disabled {
|
|
318
312
|
cursor: not-allowed;
|
|
319
313
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
.sendBtn
|
|
314
|
+
|
|
315
|
+
/* solid — 현재 기본. 비활성=bg-subtle, 활성=primary + 흰 아이콘. */
|
|
316
|
+
.sendBtn[data-variant="solid"] {
|
|
317
|
+
background: var(--bg-subtle);
|
|
318
|
+
}
|
|
319
|
+
.sendBtn[data-variant="solid"]:not([data-active]) {
|
|
323
320
|
color: var(--fg-disabled);
|
|
324
321
|
}
|
|
322
|
+
.sendBtn[data-variant="solid"][data-active] {
|
|
323
|
+
background: var(--primary);
|
|
324
|
+
color: var(--bg);
|
|
325
|
+
}
|
|
326
|
+
.sendBtn[data-variant="solid"]:hover:not(:disabled) {
|
|
327
|
+
opacity: 0.9;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* ghost — 투명. 아이콘만 보임. 비활성=fg-disabled, 활성=fg-strong.
|
|
331
|
+
hover에서만 bg-subtle이 살짝 들어와 클릭 가능성 신호. */
|
|
332
|
+
.sendBtn[data-variant="ghost"] {
|
|
333
|
+
background: transparent;
|
|
334
|
+
}
|
|
335
|
+
.sendBtn[data-variant="ghost"]:not([data-active]) {
|
|
336
|
+
color: var(--fg-disabled);
|
|
337
|
+
}
|
|
338
|
+
.sendBtn[data-variant="ghost"][data-active] {
|
|
339
|
+
color: var(--fg-strong);
|
|
340
|
+
}
|
|
341
|
+
.sendBtn[data-variant="ghost"]:hover:not(:disabled) {
|
|
342
|
+
background: var(--bg-subtle);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* soft — 늘 은은한 bg-subtle. 활성이어도 색 강조 없이 fg만 진해짐.
|
|
346
|
+
조용한 자리(사이드바 바닥 flush 등)에서 send 존재감을 낮추고 싶을 때. */
|
|
347
|
+
.sendBtn[data-variant="soft"] {
|
|
348
|
+
background: var(--bg-subtle);
|
|
349
|
+
}
|
|
350
|
+
.sendBtn[data-variant="soft"]:not([data-active]) {
|
|
351
|
+
color: var(--fg-disabled);
|
|
352
|
+
}
|
|
353
|
+
.sendBtn[data-variant="soft"][data-active] {
|
|
354
|
+
color: var(--fg-strong);
|
|
355
|
+
}
|
|
356
|
+
.sendBtn[data-variant="soft"]:hover:not(:disabled) {
|
|
357
|
+
opacity: 0.9;
|
|
358
|
+
}
|
|
359
|
+
|
|
325
360
|
.sendIcon {
|
|
326
361
|
width: 44%;
|
|
327
362
|
height: 44%;
|
|
328
363
|
}
|
|
364
|
+
/* return(↵) 아이콘은 화살표가 작게 그려져 있어 조금 키워야 눈에 들어옴. */
|
|
365
|
+
.sendBtn[data-icon="return"] .sendIcon {
|
|
366
|
+
width: 60%;
|
|
367
|
+
height: 60%;
|
|
368
|
+
}
|
|
329
369
|
|
|
330
370
|
/* 파일 input 숨김 */
|
|
331
371
|
.hiddenFile {
|
|
@@ -17,11 +17,13 @@ import { AttachmentThumb } from "@/components/ctds/AttachmentThumb";
|
|
|
17
17
|
import { DropdownMenu } from "@/components/ctds/DropdownMenu";
|
|
18
18
|
import { QuotationBar } from "@/components/ctds/QuotationBar";
|
|
19
19
|
import { ModelSelector } from "./ModelSelector";
|
|
20
|
-
import { SendButton } from "./SendButton";
|
|
20
|
+
import { SendButton, type SendButtonIcon, type SendButtonVariant } from "./SendButton";
|
|
21
21
|
import styles from "./ChatInput.module.css";
|
|
22
22
|
|
|
23
23
|
export type ChatInputShape = "multi" | "single";
|
|
24
24
|
export type ChatInputVariant = "framed" | "flush";
|
|
25
|
+
export type ChatInputSendVariant = SendButtonVariant;
|
|
26
|
+
export type ChatInputSendIcon = SendButtonIcon;
|
|
25
27
|
|
|
26
28
|
/** 첨부 공용 타입 — ChatInput·Message 양쪽에서 재사용. */
|
|
27
29
|
export interface ChatAttachmentImage {
|
|
@@ -72,6 +74,11 @@ export interface ChatInputProps {
|
|
|
72
74
|
selectedModel?: string;
|
|
73
75
|
onSelectModel?: (id: string) => void;
|
|
74
76
|
|
|
77
|
+
/** Send 버튼 톤. solid(기본)=primary bg + 흰 아이콘, ghost=투명·아이콘만, soft=bg-subtle 은은. */
|
|
78
|
+
sendVariant?: ChatInputSendVariant;
|
|
79
|
+
/** Send 아이콘 모양. 스트리밍 중엔 자동으로 square. */
|
|
80
|
+
sendIcon?: ChatInputSendIcon;
|
|
81
|
+
|
|
75
82
|
placeholder?: string;
|
|
76
83
|
disabled?: boolean;
|
|
77
84
|
/** multiLine일 때 textarea max rows. 넘으면 세로 스크롤. */
|
|
@@ -101,6 +108,8 @@ export const ChatInput = forwardRef<HTMLDivElement, ChatInputProps>(
|
|
|
101
108
|
models,
|
|
102
109
|
selectedModel,
|
|
103
110
|
onSelectModel,
|
|
111
|
+
sendVariant = "solid",
|
|
112
|
+
sendIcon = "arrow-up",
|
|
104
113
|
placeholder = "Ask me anything…",
|
|
105
114
|
disabled = false,
|
|
106
115
|
maxRows = 8,
|
|
@@ -233,6 +242,8 @@ export const ChatInput = forwardRef<HTMLDivElement, ChatInputProps>(
|
|
|
233
242
|
streaming={showStop}
|
|
234
243
|
disabled={disabled && !showStop}
|
|
235
244
|
onClick={handleSendClick}
|
|
245
|
+
variant={sendVariant}
|
|
246
|
+
icon={sendIcon}
|
|
236
247
|
/>
|
|
237
248
|
</div>
|
|
238
249
|
);
|
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
import type { MouseEvent } from "react";
|
|
4
4
|
import {
|
|
5
5
|
ArrowUp as PhArrowUp,
|
|
6
|
+
ArrowElbowDownLeft as PhReturn,
|
|
7
|
+
PaperPlaneTilt as PhPaperPlane,
|
|
6
8
|
Square as PhSquare,
|
|
7
9
|
} from "@phosphor-icons/react";
|
|
8
10
|
import styles from "./ChatInput.module.css";
|
|
9
11
|
|
|
12
|
+
export type SendButtonVariant = "solid" | "ghost" | "soft";
|
|
13
|
+
export type SendButtonIcon = "arrow-up" | "return" | "paper-plane";
|
|
14
|
+
|
|
10
15
|
interface SendButtonProps {
|
|
11
16
|
/** 입력에 텍스트/첨부가 있어 send 가능한지. */
|
|
12
17
|
active: boolean;
|
|
@@ -14,6 +19,10 @@ interface SendButtonProps {
|
|
|
14
19
|
streaming?: boolean;
|
|
15
20
|
disabled?: boolean;
|
|
16
21
|
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
|
|
22
|
+
/** 시각 톤. solid=primary bg(기본), ghost=투명 아이콘만, soft=bg-subtle 은은 */
|
|
23
|
+
variant?: SendButtonVariant;
|
|
24
|
+
/** 전송 아이콘 모양. 스트리밍 중엔 자동으로 square로 교체 */
|
|
25
|
+
icon?: SendButtonIcon;
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
export function SendButton({
|
|
@@ -21,11 +30,19 @@ export function SendButton({
|
|
|
21
30
|
streaming = false,
|
|
22
31
|
disabled,
|
|
23
32
|
onClick,
|
|
33
|
+
variant = "solid",
|
|
34
|
+
icon = "arrow-up",
|
|
24
35
|
}: SendButtonProps) {
|
|
36
|
+
const SendIconCmp =
|
|
37
|
+
icon === "return" ? PhReturn : icon === "paper-plane" ? PhPaperPlane : PhArrowUp;
|
|
38
|
+
const iconWeight = icon === "return" ? "regular" : "bold";
|
|
39
|
+
|
|
25
40
|
return (
|
|
26
41
|
<button
|
|
27
42
|
type="button"
|
|
28
43
|
className={styles.sendBtn}
|
|
44
|
+
data-variant={variant}
|
|
45
|
+
data-icon={icon}
|
|
29
46
|
data-active={active || streaming || undefined}
|
|
30
47
|
disabled={disabled}
|
|
31
48
|
onClick={onClick}
|
|
@@ -34,7 +51,7 @@ export function SendButton({
|
|
|
34
51
|
{streaming ? (
|
|
35
52
|
<PhSquare className={styles.sendIcon} weight="fill" />
|
|
36
53
|
) : (
|
|
37
|
-
<
|
|
54
|
+
<SendIconCmp className={styles.sendIcon} weight={iconWeight} />
|
|
38
55
|
)}
|
|
39
56
|
</button>
|
|
40
57
|
);
|
|
@@ -858,7 +858,7 @@ const solarSet: Partial<Record<IconName, SolarPair>> = {
|
|
|
858
858
|
sliders: { regular: SoSlidersL, fill: SoSlidersB },
|
|
859
859
|
trash: { regular: SoTrashL, fill: SoTrashB },
|
|
860
860
|
"arrow-right": { regular: SoArrowRightL, fill: SoArrowRightB },
|
|
861
|
-
x: { regular:
|
|
861
|
+
x: { regular: PhX, fill: PhX },
|
|
862
862
|
sun: { regular: SoSunL, fill: SoSunB },
|
|
863
863
|
moon: { regular: SoMoonL, fill: SoMoonB },
|
|
864
864
|
"caret-down": { regular: SoCaretDownL, fill: SoCaretDownB },
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/* TabStrip — 브라우저/문서 탭 (Chrome · Figma · VSCode 식).
|
|
2
|
+
각 탭 = 독립 문서. 섹션 전환 Tabs와 역할 분리.
|
|
3
|
+
|
|
4
|
+
활성 표시 = bg-muted 상시 배경 (pill 아님, hover 색이 그대로 lock).
|
|
5
|
+
비활성 탭 = 투명 배경, hover 시 bg-muted.
|
|
6
|
+
|
|
7
|
+
축 반응:
|
|
8
|
+
─ Density → --control-h-* (탭 높이), --space-scale (gap/padding)
|
|
9
|
+
─ Shape → 탭 radius (--radius-sm · soft에서도 과하지 않게)
|
|
10
|
+
─ Motion → hover·active 전환 duration/easing
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/* Root ─────────────────────────────────────────────────────── */
|
|
14
|
+
.root {
|
|
15
|
+
display: inline-flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
min-width: 0;
|
|
18
|
+
max-width: 100%;
|
|
19
|
+
gap: var(--space-0_5);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.track {
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
gap: var(--space-0_5);
|
|
26
|
+
min-width: 0;
|
|
27
|
+
overflow-x: auto;
|
|
28
|
+
scrollbar-width: none;
|
|
29
|
+
}
|
|
30
|
+
.track::-webkit-scrollbar {
|
|
31
|
+
display: none;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Sizes — Button·Tabs 계열과 동일한 --control-h-* 기반 */
|
|
35
|
+
.size_sm .tab { height: var(--control-h-sm); }
|
|
36
|
+
.size_md .tab { height: var(--control-h-md); }
|
|
37
|
+
|
|
38
|
+
/* Tab ──────────────────────────────────────────────────────── */
|
|
39
|
+
.tab {
|
|
40
|
+
position: relative;
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: var(--space-1_5);
|
|
44
|
+
padding-inline: var(--space-2_5);
|
|
45
|
+
border-radius: var(--radius-sm);
|
|
46
|
+
color: var(--fg-muted);
|
|
47
|
+
background: transparent;
|
|
48
|
+
font-family: var(--label-font);
|
|
49
|
+
font-weight: var(--label-weight);
|
|
50
|
+
letter-spacing: var(--label-tracking);
|
|
51
|
+
line-height: var(--lh-tight);
|
|
52
|
+
white-space: nowrap;
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
user-select: none;
|
|
55
|
+
flex-shrink: 0;
|
|
56
|
+
min-width: 0;
|
|
57
|
+
max-width: 20ch;
|
|
58
|
+
overflow: hidden;
|
|
59
|
+
transition:
|
|
60
|
+
background var(--duration-slow) var(--ease-out),
|
|
61
|
+
color var(--duration-slow) var(--ease-out),
|
|
62
|
+
box-shadow var(--duration-fast) var(--ease-out);
|
|
63
|
+
}
|
|
64
|
+
.size_sm .tab {
|
|
65
|
+
font-size: var(--label-sm-size);
|
|
66
|
+
padding-inline: var(--space-2);
|
|
67
|
+
gap: var(--space-1);
|
|
68
|
+
max-width: 20ch;
|
|
69
|
+
}
|
|
70
|
+
.size_md .tab { font-size: var(--label-md-size); }
|
|
71
|
+
|
|
72
|
+
.tab:focus-visible {
|
|
73
|
+
outline: none;
|
|
74
|
+
box-shadow: var(--ring-shadow);
|
|
75
|
+
position: relative;
|
|
76
|
+
z-index: var(--z-docked);
|
|
77
|
+
}
|
|
78
|
+
.tab[data-disabled="true"] {
|
|
79
|
+
cursor: not-allowed;
|
|
80
|
+
opacity: 0.5;
|
|
81
|
+
}
|
|
82
|
+
.tab:hover:not([data-disabled="true"]) {
|
|
83
|
+
background: var(--bg-muted);
|
|
84
|
+
color: var(--fg);
|
|
85
|
+
}
|
|
86
|
+
.tab[data-state="active"] {
|
|
87
|
+
background: var(--bg-muted);
|
|
88
|
+
color: var(--fg-strong);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Leading slot — Icon·사진·svg 전부 ReactNode로 허용. 톤만 조절. */
|
|
92
|
+
.leading {
|
|
93
|
+
display: inline-flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
justify-content: center;
|
|
96
|
+
flex-shrink: 0;
|
|
97
|
+
opacity: 0.75;
|
|
98
|
+
transition: opacity var(--duration-slow) var(--ease-out);
|
|
99
|
+
}
|
|
100
|
+
.leading > svg,
|
|
101
|
+
.leading > img {
|
|
102
|
+
display: block;
|
|
103
|
+
}
|
|
104
|
+
.tab:hover:not([data-disabled="true"]) .leading,
|
|
105
|
+
.tab[data-state="active"] .leading {
|
|
106
|
+
opacity: 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Label — overflow 숨김 컨테이너. 안쪽 .labelInner가 marquee 대상.
|
|
110
|
+
오른쪽 edge fade는 closeFade(hover 시)와 labelFade(기본)가 담당. */
|
|
111
|
+
.label {
|
|
112
|
+
display: block;
|
|
113
|
+
min-width: 0;
|
|
114
|
+
flex: 1 1 auto;
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* 기본 상태 (hover 아님)에서 라벨 오른쪽 edge를 bg로 자연스럽게 녹임.
|
|
119
|
+
closable·non-closable 모두 적용. 활성/hover가 되면 closeFade가 덮으니 거의 안 보임. */
|
|
120
|
+
.labelFade {
|
|
121
|
+
position: absolute;
|
|
122
|
+
top: 0;
|
|
123
|
+
bottom: 0;
|
|
124
|
+
right: 0;
|
|
125
|
+
width: var(--space-6);
|
|
126
|
+
pointer-events: none;
|
|
127
|
+
background: linear-gradient(to right, transparent 0%, var(--bg) 100%);
|
|
128
|
+
z-index: var(--z-base);
|
|
129
|
+
}
|
|
130
|
+
.tab[data-state="active"] .labelFade,
|
|
131
|
+
.tab:hover:not([data-disabled="true"]) .labelFade {
|
|
132
|
+
background: linear-gradient(to right, transparent 0%, var(--bg-muted) 100%);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.labelInner {
|
|
136
|
+
display: inline-flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
white-space: nowrap;
|
|
139
|
+
will-change: transform;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.labelPrimary {
|
|
143
|
+
display: inline-block;
|
|
144
|
+
white-space: nowrap;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Marquee — 전광판 스타일 loop.
|
|
148
|
+
라벨이 overflow되는 탭은 .labelInner가 "라벨 + gap + 라벨" 복제본을 가진다
|
|
149
|
+
(React에서 data-overflow="true" 세팅 시 복제본이 visible). 첫 번째 라벨이
|
|
150
|
+
왼쪽으로 완전히 빠지는 시점에 두 번째 라벨이 translateX(0) 자리에 자연스레
|
|
151
|
+
이어 받아 끊김 없이 반복된다.
|
|
152
|
+
|
|
153
|
+
--marquee-shift (px 수치)와 --marquee-duration (CSS time)은 React에서
|
|
154
|
+
ResizeObserver로 측정해 inline style로 설정. 짧은 라벨은 overflow 안 되므로
|
|
155
|
+
data-overflow가 안 붙고, keyframe도 적용되지 않는다. */
|
|
156
|
+
.tab[data-overflow="true"]:hover:not([data-disabled="true"]) .labelInner {
|
|
157
|
+
animation: marquee-loop var(--marquee-duration, var(--duration-slower)) var(--ease-linear) var(--marquee-delay, var(--duration-slower)) infinite;
|
|
158
|
+
}
|
|
159
|
+
.tab:not(:hover) .labelInner,
|
|
160
|
+
.tab:not([data-overflow="true"]) .labelInner {
|
|
161
|
+
animation: none;
|
|
162
|
+
transform: translateX(0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@keyframes marquee-loop {
|
|
166
|
+
0% {
|
|
167
|
+
transform: translateX(0);
|
|
168
|
+
}
|
|
169
|
+
100% {
|
|
170
|
+
transform: translateX(calc(var(--marquee-shift, 0) * -1px));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* 복제본 — overflow 시에만 보이고, 평상시엔 숨겨 레이아웃·측정에 영향 없음. */
|
|
175
|
+
.labelDupe {
|
|
176
|
+
display: inline-block;
|
|
177
|
+
padding-inline-start: var(--space-6);
|
|
178
|
+
visibility: hidden;
|
|
179
|
+
}
|
|
180
|
+
.tab[data-overflow="true"] .labelDupe {
|
|
181
|
+
visibility: visible;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@media (prefers-reduced-motion: reduce) {
|
|
185
|
+
.tab[data-overflow="true"]:hover:not([data-disabled="true"]) .labelInner {
|
|
186
|
+
animation: none;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Close button — absolute로 오른쪽에 띄워 라벨 위를 그라데이션으로 덮으며 등장.
|
|
191
|
+
활성·hover 아닐 땐 opacity 0, 그라데이션 fade도 opacity 0.
|
|
192
|
+
|
|
193
|
+
폭 구성: 왼쪽은 fade 구간, 오른쪽은 x 뒤를 가려주는 solid 구간.
|
|
194
|
+
solid 구간이 없으면 x 뒤로 흐르는 라벨이 비쳐 보임. */
|
|
195
|
+
.closeFade {
|
|
196
|
+
position: absolute;
|
|
197
|
+
top: 0;
|
|
198
|
+
bottom: 0;
|
|
199
|
+
right: 0;
|
|
200
|
+
width: calc(var(--control-h-xs) + var(--space-6));
|
|
201
|
+
pointer-events: none;
|
|
202
|
+
background: linear-gradient(
|
|
203
|
+
to right,
|
|
204
|
+
transparent 0%,
|
|
205
|
+
var(--bg-muted) 50%,
|
|
206
|
+
var(--bg-muted) 100%
|
|
207
|
+
);
|
|
208
|
+
opacity: 0;
|
|
209
|
+
transition: opacity var(--duration-slow) var(--ease-in-out);
|
|
210
|
+
z-index: var(--z-raise);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.closeButton {
|
|
214
|
+
position: absolute;
|
|
215
|
+
top: 50%;
|
|
216
|
+
right: var(--space-1);
|
|
217
|
+
transform: translateY(-50%);
|
|
218
|
+
display: inline-flex;
|
|
219
|
+
align-items: center;
|
|
220
|
+
justify-content: center;
|
|
221
|
+
flex-shrink: 0;
|
|
222
|
+
width: var(--control-h-xs);
|
|
223
|
+
height: var(--control-h-xs);
|
|
224
|
+
padding: 0;
|
|
225
|
+
border: 0;
|
|
226
|
+
border-radius: var(--radius-xs);
|
|
227
|
+
background: transparent;
|
|
228
|
+
color: var(--fg-muted);
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
opacity: 0;
|
|
231
|
+
transition:
|
|
232
|
+
background var(--duration-fast) var(--ease-out),
|
|
233
|
+
color var(--duration-fast) var(--ease-out),
|
|
234
|
+
opacity var(--duration-slow) var(--ease-in-out);
|
|
235
|
+
z-index: var(--z-docked);
|
|
236
|
+
}
|
|
237
|
+
.tab:hover:not([data-disabled="true"]) .closeButton,
|
|
238
|
+
.tab:focus-within .closeButton {
|
|
239
|
+
opacity: 1;
|
|
240
|
+
}
|
|
241
|
+
.tab:hover:not([data-disabled="true"]) .closeFade,
|
|
242
|
+
.tab:focus-within .closeFade {
|
|
243
|
+
opacity: 1;
|
|
244
|
+
}
|
|
245
|
+
.closeButton:hover {
|
|
246
|
+
background: var(--hover);
|
|
247
|
+
color: var(--fg-strong);
|
|
248
|
+
}
|
|
249
|
+
.closeButton:focus-visible {
|
|
250
|
+
outline: none;
|
|
251
|
+
opacity: 1;
|
|
252
|
+
box-shadow: var(--ring-shadow);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* Trailing slot — track 바깥 오른쪽. 소비처가 ReactNode로 자유 조립 (+·...·없음).
|
|
256
|
+
TabStrip 전체 hover·focus-within에서만 fade-in. 공간은 늘 차지해 흔들림 없음. */
|
|
257
|
+
.trailing {
|
|
258
|
+
display: inline-flex;
|
|
259
|
+
align-items: center;
|
|
260
|
+
flex-shrink: 0;
|
|
261
|
+
opacity: 0;
|
|
262
|
+
transition: opacity var(--duration-slow) var(--ease-in-out);
|
|
263
|
+
}
|
|
264
|
+
.root:hover .trailing,
|
|
265
|
+
.root:focus-within .trailing {
|
|
266
|
+
opacity: 1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* ───────────────────────────────────────────────────────────
|
|
270
|
+
Variants — ghost(default) · soft · segmented
|
|
271
|
+
기본 .tab 규칙은 ghost 기준. 여기선 variant별 override만.
|
|
272
|
+
*/
|
|
273
|
+
|
|
274
|
+
/* Soft ──────────────────────────────────────────────────────
|
|
275
|
+
모든 탭에 연한 bg 상시. 버튼처럼 "모양이 미리 잡힌" 느낌.
|
|
276
|
+
비활성=bg-subtle · hover=hover mix(subtle→muted 사이) · 활성=bg-muted. */
|
|
277
|
+
.variant_soft .track {
|
|
278
|
+
gap: var(--space-1);
|
|
279
|
+
}
|
|
280
|
+
.variant_soft .tab {
|
|
281
|
+
background: var(--bg-subtle);
|
|
282
|
+
}
|
|
283
|
+
.variant_soft .tab:hover:not([data-disabled="true"]) {
|
|
284
|
+
background: color-mix(in oklab, var(--bg-subtle) 50%, var(--bg-muted) 50%);
|
|
285
|
+
color: var(--fg);
|
|
286
|
+
}
|
|
287
|
+
.variant_soft .tab[data-state="active"] {
|
|
288
|
+
background: var(--bg-muted);
|
|
289
|
+
color: var(--fg-strong);
|
|
290
|
+
}
|
|
291
|
+
/* labelFade: 비활성 상태는 bg-subtle로 녹여야 자연스러움. */
|
|
292
|
+
.variant_soft .labelFade {
|
|
293
|
+
background: linear-gradient(to right, transparent 0%, var(--bg-subtle) 100%);
|
|
294
|
+
}
|
|
295
|
+
.variant_soft .tab:hover:not([data-disabled="true"]) .labelFade {
|
|
296
|
+
background: linear-gradient(to right, transparent 0%, color-mix(in oklab, var(--bg-subtle) 50%, var(--bg-muted) 50%) 100%);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* Segmented ──────────────────────────────────────────────────
|
|
300
|
+
Chrome·Figma 식 — 탭은 네모칸, 사이에 세로 hairline. 활성·hover 탭의
|
|
301
|
+
왼쪽 hairline, 그리고 활성·hover 탭 오른쪽 이웃 탭의 왼쪽 hairline을
|
|
302
|
+
숨겨 "구분선이 활성 탭에 닿지 않게" 처리.
|
|
303
|
+
gap은 아주 작게(--space-0_5) 둬 hairline이 숨을 때 탭이 완전히 붙어보이지 않게. */
|
|
304
|
+
.variant_segmented .track {
|
|
305
|
+
gap: var(--space-0_5);
|
|
306
|
+
}
|
|
307
|
+
.variant_segmented .tab {
|
|
308
|
+
border-radius: var(--radius-xs);
|
|
309
|
+
position: relative;
|
|
310
|
+
}
|
|
311
|
+
.variant_segmented .tab::before {
|
|
312
|
+
content: "";
|
|
313
|
+
position: absolute;
|
|
314
|
+
left: 0;
|
|
315
|
+
top: 50%;
|
|
316
|
+
transform: translateY(-50%);
|
|
317
|
+
width: var(--hairline);
|
|
318
|
+
height: calc(100% - var(--space-3));
|
|
319
|
+
background: var(--border);
|
|
320
|
+
transition: opacity var(--duration-slow) var(--ease-out);
|
|
321
|
+
}
|
|
322
|
+
/* 첫 탭은 왼쪽 hairline 없음. */
|
|
323
|
+
.variant_segmented .tab:first-child::before {
|
|
324
|
+
opacity: 0;
|
|
325
|
+
}
|
|
326
|
+
/* 활성·hover 탭 자신의 왼쪽 hairline 숨김. */
|
|
327
|
+
.variant_segmented .tab:hover:not([data-disabled="true"])::before,
|
|
328
|
+
.variant_segmented .tab[data-state="active"]::before {
|
|
329
|
+
opacity: 0;
|
|
330
|
+
}
|
|
331
|
+
/* 활성·hover 탭 바로 오른쪽 이웃의 왼쪽 hairline도 숨김 (경계가 활성 영역에 안 닿게). */
|
|
332
|
+
.variant_segmented .tab:hover:not([data-disabled="true"]) + .tab::before,
|
|
333
|
+
.variant_segmented .tab[data-state="active"] + .tab::before {
|
|
334
|
+
opacity: 0;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* ───────────────────────────────────────────────────────────
|
|
338
|
+
Trailing slot variant별 처리
|
|
339
|
+
소비처가 넣는 자식 요소(IconButton, Button, 등) 자체 시각은 건드리지 않고,
|
|
340
|
+
wrapper 쪽에서 간격·디바이더만 담당.
|
|
341
|
+
*/
|
|
342
|
+
.variant_segmented {
|
|
343
|
+
gap: var(--space-2);
|
|
344
|
+
}
|
|
345
|
+
.variant_segmented .trailing {
|
|
346
|
+
position: relative;
|
|
347
|
+
padding-inline-start: var(--space-2);
|
|
348
|
+
}
|
|
349
|
+
.variant_segmented .trailing::before {
|
|
350
|
+
content: "";
|
|
351
|
+
position: absolute;
|
|
352
|
+
left: 0;
|
|
353
|
+
top: 50%;
|
|
354
|
+
transform: translateY(-50%);
|
|
355
|
+
width: var(--hairline);
|
|
356
|
+
height: calc(100% - var(--space-3));
|
|
357
|
+
background: var(--border);
|
|
358
|
+
}
|