@conscience-technology/ctds 0.3.4 → 0.4.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.
Files changed (37) hide show
  1. package/assets/components/Button/Button.module.css +2 -2
  2. package/assets/components/EventListItem/EventListItem.module.css +220 -0
  3. package/assets/components/EventListItem/EventListItem.tsx +205 -0
  4. package/assets/components/EventListItem/index.ts +8 -0
  5. package/assets/components/Icon/Icon.tsx +80 -41
  6. package/assets/components/ListItem/ListItem.module.css +91 -211
  7. package/assets/components/ListItem/ListItem.tsx +130 -154
  8. package/assets/components/ListItem/index.ts +2 -2
  9. package/assets/components/MetricCard/MetricCard.module.css +5 -5
  10. package/assets/components/Sidebar/Sidebar.module.css +81 -4
  11. package/assets/components/Sidebar/Sidebar.tsx +11 -2
  12. package/assets/components/SplitButton/SplitButton.module.css +3 -3
  13. package/assets/components/Stepper/Stepper.module.css +196 -52
  14. package/assets/components/Stepper/Stepper.tsx +68 -13
  15. package/assets/components/Switch/Switch.module.css +1 -9
  16. package/assets/components/Topbar/Topbar.module.css +33 -5
  17. package/assets/components/Topbar/Topbar.tsx +62 -7
  18. package/assets/components/_data.ts +11 -4
  19. package/assets/components/index.ts +2 -0
  20. package/assets/components/tokens.css +23 -10
  21. package/assets/prompts/ASSEMBLY.md +219 -0
  22. package/assets/prompts/PRINCIPLES.md +70 -0
  23. package/assets/prompts/axes/icon.md +7 -1
  24. package/assets/prompts/axes/palette.md +11 -0
  25. package/assets/prompts/components/_variants.md +5 -1
  26. package/assets/prompts/components/callout.md +3 -3
  27. package/assets/prompts/components/event-list-item.md +103 -0
  28. package/assets/prompts/components/generated-image.md +80 -0
  29. package/assets/prompts/components/list-item.md +182 -204
  30. package/assets/prompts/components/metric-card.md +2 -2
  31. package/assets/prompts/components/toast.md +1 -0
  32. package/assets/prompts/components/topbar.md +77 -59
  33. package/assets/prompts/themes/focus.md +72 -0
  34. package/assets/skills/ctds/SKILL.md +1 -0
  35. package/assets/skills/ctds-page/SKILL.md +12 -2
  36. package/assets/skills/ctds-wireframe/SKILL.md +35 -1
  37. package/package.json +2 -4
@@ -1,42 +1,78 @@
1
1
  /* Stepper — 다단계 진행 표시 컴포넌트
2
- 구성: root(container) > item(button) > marker(dot|number) + body(title + hint)
3
- 축 반응:
4
- ─ Density → --space-scale, --font-scale (여백·라벨 크기)
5
- ─ Shape → focus ring radius (marker는 항상 full)
6
- ─ Motion → 상태 전환 duration/easing (transition-colors)
2
+ 구성: root(container) > item(button) > marker + body(title + hint)
3
+
4
+ 축 (data-* attr):
5
+ ─ data-orientation : vertical | horizontal
6
+ ─ data-marker : dot | number
7
+ ─ data-variant : md | sm
8
+ ─ data-tone : primary | neutral
9
+ ─ data-density : comfortable | spacious
10
+ ─ data-hint-placement : below | inline
11
+ ─ data-line-style : dashed | dotted | solid
7
12
 
8
13
  로컬 색 변수 (root에서 오버라이드 가능):
9
- ─ --stepper-fg : 활성/진행중 스텝 텍스트
14
+ ─ --stepper-fg : 활성 스텝 텍스트
10
15
  ─ --stepper-fg-muted : 완료 스텝 텍스트
11
16
  ─ --stepper-fg-subtle : 미도달 스텝 텍스트 · hint
12
- ─ --stepper-line : 스텝 사이 dashed 트리 라인
13
- ─ --stepper-dot-idle : 미도달 dot border
14
- ─ --stepper-marker-fg : done marker 안쪽 텍스트/아이콘 색 (배경 위)
17
+ ─ --stepper-line : 스텝 사이 연결선 색
18
+ ─ --stepper-dot-idle : 미도달 marker border
19
+ ─ --stepper-marker : active/done 공통 강조색 (기본 Primary)
20
+ ─ --stepper-marker-fg : done marker 안쪽 글자/아이콘 색
21
+ ─ --stepper-marker-soft: upcoming·neutral·active 등 soft fill 색
15
22
  */
16
23
 
17
24
  .root {
18
25
  --stepper-fg: var(--fg);
19
26
  --stepper-fg-muted: var(--fg-muted);
20
27
  --stepper-fg-subtle: var(--fg-subtle);
21
- --stepper-line: var(--border);
22
- --stepper-dot-idle: var(--border);
28
+ --stepper-line: var(--border-strong);
29
+ --stepper-dot-idle: var(--border-strong);
30
+ --stepper-marker: var(--primary);
23
31
  --stepper-marker-fg: var(--bg);
32
+ --stepper-marker-soft: color-mix(in oklab, var(--fg) 8%, transparent);
24
33
 
25
34
  --stepper-marker-size: var(--space-5);
26
35
  --stepper-marker-gap: var(--space-4);
27
36
  --stepper-item-gap: var(--space-6);
37
+ --stepper-title-lh: var(--lh-sm); /* marker 수직 보정 기준 */
38
+ --stepper-line-gap: var(--space-1); /* marker ↔ 선 간격 */
28
39
 
29
40
  display: flex;
30
41
  min-width: 0;
31
42
  min-height: 0;
32
43
  }
33
44
 
34
- /* Orientation ─────────────────────────────────────────── */
45
+ /* ─ Orientation ────────────────────────────────────── */
35
46
 
36
47
  .vertical { flex-direction: column; }
37
- .horizontal { flex-direction: row; align-items: flex-start; }
48
+ .horizontal {
49
+ flex-direction: row;
50
+ align-items: flex-start;
51
+ --stepper-line-gap: var(--space-2);
52
+ }
53
+
54
+ /* ─ Variant: md (기본, 큰 marker 24px) vs sm (작은 dot 10px) ─ */
55
+
56
+ .root[data-variant="md"] {
57
+ --stepper-marker-size: var(--space-6); /* 24px — 01 Stripe */
58
+ --stepper-marker-gap: var(--space-4);
59
+ --stepper-item-gap: var(--space-6);
60
+ --stepper-title-lh: var(--lh-base);
61
+ }
62
+ .root[data-variant="sm"] {
63
+ --stepper-marker-size: var(--space-2_5); /* 10px — 02 헤이딜러 */
64
+ --stepper-marker-gap: var(--space-3);
65
+ --stepper-item-gap: var(--space-10); /* 작은 marker는 호흡 넓게 */
66
+ --stepper-title-lh: var(--lh-sm);
67
+ }
68
+
69
+ /* ─ Tone: neutral이면 done·marker 색이 모노톤 ──────── */
70
+
71
+ .root[data-tone="neutral"] {
72
+ --stepper-marker: var(--fg);
73
+ }
38
74
 
39
- /* Item ─────────────────────────────────────────────────── */
75
+ /* ─ Item ───────────────────────────────────────────── */
40
76
 
41
77
  .item {
42
78
  appearance: none;
@@ -61,7 +97,7 @@
61
97
  box-shadow: var(--ring-shadow);
62
98
  }
63
99
 
64
- /* Vertical item: 2-column grid, 아래로 dashed line */
100
+ /* Vertical item: 2-column grid */
65
101
  .vertical .item {
66
102
  display: grid;
67
103
  grid-template-columns: var(--stepper-marker-size) 1fr;
@@ -70,23 +106,8 @@
70
106
  padding-bottom: var(--stepper-item-gap);
71
107
  }
72
108
  .vertical .item:last-child { padding-bottom: 0; }
73
- .vertical .item:not(:last-child)::before {
74
- content: "";
75
- position: absolute;
76
- left: calc(var(--stepper-marker-size) / 2 - var(--hairline) / 2);
77
- top: var(--stepper-marker-size);
78
- bottom: 0;
79
- width: var(--hairline);
80
- background-image: linear-gradient(
81
- to bottom,
82
- var(--stepper-line) 50%,
83
- transparent 50%
84
- );
85
- background-size: var(--hairline) var(--space-1_5);
86
- background-repeat: repeat-y;
87
- }
88
109
 
89
- /* Horizontal item: marker 위, body 아래로 쌓임, 옆으로 dashed line */
110
+ /* Horizontal item: marker 위, body 아래 */
90
111
  .horizontal .item {
91
112
  display: grid;
92
113
  grid-template-rows: var(--stepper-marker-size) 1fr;
@@ -100,24 +121,61 @@
100
121
  flex: 0 0 auto;
101
122
  padding-right: 0;
102
123
  }
124
+
125
+ /* ─ Connecting line (vertical: 기본 dashed) ─ */
126
+
127
+ .vertical .item:not(:last-child)::before {
128
+ content: "";
129
+ position: absolute;
130
+ left: calc(var(--stepper-marker-size) / 2 - var(--hairline) / 2);
131
+ /* marker가 margin-top만큼 아래로 밀렸고, 선은 marker 바닥에서
132
+ --stepper-line-gap 만큼 떨어진 지점부터 시작, 아래쪽도 같은 간격 띄움 */
133
+ top: calc(
134
+ (var(--stepper-title-lh) - var(--stepper-marker-size)) / 2 +
135
+ var(--stepper-marker-size) + var(--stepper-line-gap)
136
+ );
137
+ bottom: var(--stepper-line-gap);
138
+ width: var(--hairline);
139
+ background-image: linear-gradient(
140
+ to bottom,
141
+ var(--stepper-line) 65%,
142
+ transparent 35%
143
+ );
144
+ background-size: var(--hairline) var(--space-1_5);
145
+ background-repeat: repeat-y;
146
+ }
147
+
103
148
  .horizontal .item:not(:last-child)::before {
104
149
  content: "";
105
150
  position: absolute;
106
151
  top: calc(var(--stepper-marker-size) / 2 - var(--hairline) / 2);
107
- left: var(--stepper-marker-size);
108
- right: 0;
152
+ left: calc(var(--stepper-marker-size) + var(--stepper-line-gap));
153
+ right: var(--stepper-line-gap);
109
154
  height: var(--hairline);
110
155
  background-image: linear-gradient(
111
156
  to right,
112
- var(--stepper-line) 50%,
113
- transparent 50%
157
+ var(--stepper-line) 65%,
158
+ transparent 35%
114
159
  );
115
160
  background-size: var(--space-1_5) var(--hairline);
116
161
  background-repeat: repeat-x;
117
- margin-inline: var(--space-2);
118
162
  }
119
163
 
120
- /* Marker ───────────────────────────────────────────────── */
164
+ /* line-style: solid (01 — 매끈한 1px) — selector 주의:
165
+ .vertical/.horizontal과 data-line-style attr가 같은 root element에 붙어 있으므로
166
+ descendant가 아닌 compound selector로 작성. */
167
+ .vertical[data-line-style="solid"] .item:not(:last-child)::before {
168
+ background: var(--stepper-line);
169
+ background-image: none;
170
+ background-size: auto;
171
+ }
172
+ .horizontal[data-line-style="solid"] .item:not(:last-child)::before {
173
+ background: var(--stepper-line);
174
+ background-image: none;
175
+ background-size: auto;
176
+ }
177
+
178
+ /* ─ Marker ────────────────────────────────────────── */
121
179
 
122
180
  .marker {
123
181
  width: var(--stepper-marker-size);
@@ -131,34 +189,98 @@
131
189
  flex-shrink: 0;
132
190
  transition: var(--transition-colors);
133
191
  font-size: var(--text-xs);
134
- font-weight: var(--font-weight-semibold);
192
+ font-weight: var(--font-weight-medium);
135
193
  line-height: 1;
136
- color: inherit;
194
+ color: var(--stepper-fg-subtle);
195
+ font-variant-numeric: tabular-nums;
196
+ position: relative;
197
+ /* vertical일 때 marker가 title 첫 줄 중앙선에 맞도록 아래로 밀기.
198
+ --stepper-title-lh는 variant별로 공급되는 title line-height. */
199
+ margin-top: calc(
200
+ (var(--stepper-title-lh) - var(--stepper-marker-size)) / 2
201
+ );
202
+ }
203
+ /* horizontal에서는 marker가 body 위에 위치하므로 보정 off */
204
+ .horizontal .marker {
205
+ margin-top: 0;
206
+ }
207
+
208
+ /* ── variant=md (기본) marker scaling ──
209
+ 24px marker 안의 숫자/check도 균형 잡힌 사이즈. */
210
+ .root[data-variant="md"] .marker {
211
+ font-size: var(--text-xs);
212
+ }
213
+
214
+ /* ── variant=sm marker는 solid dot (border 없음, soft fill) ── */
215
+ .root[data-variant="sm"] .marker {
216
+ border: none;
217
+ background: var(--stepper-dot-idle);
218
+ }
219
+
220
+ /* ── Upcoming (md variant): soft bg + muted 숫자 (01 Stripe) ── */
221
+ .root[data-variant="md"] .item[data-state="upcoming"] .marker {
222
+ background: var(--stepper-marker-soft);
223
+ border-color: transparent;
224
+ }
225
+
226
+ /* ── Done marker ── */
227
+ /* md + primary tone (기본): Primary fill */
228
+ .item[data-state="done"] .marker {
229
+ border-color: transparent;
230
+ background: var(--stepper-marker);
231
+ color: var(--stepper-marker-fg);
232
+ }
233
+ /* md + neutral tone (01 Stripe): soft gray fill + muted check */
234
+ .root[data-tone="neutral"] .item[data-state="done"] .marker {
235
+ background: var(--stepper-marker-soft);
236
+ color: var(--stepper-fg-muted);
237
+ }
238
+ /* sm variant done: 색만 marker 색, dot 크기 그대로 */
239
+ .root[data-variant="sm"] .item[data-state="done"] .marker {
240
+ background: var(--stepper-dot-idle);
241
+ color: transparent;
242
+ }
243
+ .root[data-variant="sm"][data-tone="primary"]
244
+ .item[data-state="done"]
245
+ .marker {
246
+ background: var(--stepper-marker);
137
247
  }
138
248
 
139
- /* Active marker: 굵은 링 */
249
+ /* ── Active marker ──
250
+ marker 자체가 큰 soft Primary 동그라미 + 중앙에 작은 Primary dot. */
140
251
  .item[data-state="active"] .marker {
141
- border-color: var(--stepper-fg);
142
- border-width: calc(var(--hairline) * 2);
143
- color: var(--stepper-fg);
252
+ background: color-mix(in oklab, var(--stepper-marker) 18%, transparent);
253
+ border-color: transparent;
254
+ color: var(--stepper-marker);
255
+ }
256
+ /* sm variant active: marker 자체는 solid Primary dot + 바깥에 soft Primary halo.
257
+ 작은 dot(10px)이라 안쪽 split 대신 외곽 halo로 "지금 여기" 신호를 줌. */
258
+ .root[data-variant="sm"] .item[data-state="active"] .marker {
259
+ background: var(--stepper-marker);
260
+ color: var(--stepper-marker-fg);
261
+ box-shadow: 0 0 0 var(--space-1)
262
+ color-mix(in oklab, var(--stepper-marker) 22%, transparent);
144
263
  }
145
264
 
146
- /* dot marker의 active: 안쪽 작은 점 */
265
+ /* ─ markerInnerDot: active marker 중앙 작은 Primary dot ─ */
147
266
  .markerInnerDot {
148
267
  width: var(--space-1_5);
149
268
  height: var(--space-1_5);
150
269
  border-radius: var(--radius-full);
151
- background: var(--stepper-fg);
270
+ background: var(--stepper-marker);
271
+ flex-shrink: 0;
152
272
  }
153
-
154
- /* Done marker: 배경 채움, 안에 check (dot) 또는 숫자 유지 (number) */
155
- .item[data-state="done"] .marker {
156
- border-color: transparent;
157
- background: var(--stepper-fg);
158
- color: var(--stepper-marker-fg);
273
+ /* md (24px marker)에서는 inner dot도 조금 더 크게 */
274
+ .root[data-variant="md"] .markerInnerDot {
275
+ width: var(--space-2);
276
+ height: var(--space-2);
277
+ }
278
+ /* sm variant는 marker 자체가 10px dot이라 inner dot 숨김 */
279
+ .root[data-variant="sm"] .markerInnerDot {
280
+ display: none;
159
281
  }
160
282
 
161
- /* Body ─────────────────────────────────────────────────── */
283
+ /* ─ Body ─────────────────────────────────────────── */
162
284
 
163
285
  .body {
164
286
  display: flex;
@@ -168,15 +290,37 @@
168
290
  padding-right: var(--space-2);
169
291
  }
170
292
 
293
+ /* hintPlacement=inline (02): title | hint 가로 배치 */
294
+ .root[data-hint-placement="inline"] .body {
295
+ flex-direction: row;
296
+ align-items: baseline;
297
+ gap: var(--space-2);
298
+ flex-wrap: wrap;
299
+ }
300
+
171
301
  .title {
172
302
  font-size: var(--text-sm);
303
+ font-weight: var(--font-weight-medium);
304
+ line-height: var(--lh-sm);
305
+ }
306
+ /* md variant는 title 조금 더 큼 (01 Stripe 톤) */
307
+ .root[data-variant="md"] .title {
308
+ font-size: var(--text-base);
309
+ line-height: var(--lh-base);
310
+ }
311
+ .item[data-state="active"] .title {
173
312
  font-weight: var(--font-weight-semibold);
174
313
  }
314
+
175
315
  .hint {
176
316
  font-size: var(--text-xs);
177
317
  color: var(--stepper-fg-subtle);
178
318
  line-height: var(--lh-xs);
179
319
  }
320
+ .root[data-variant="md"] .hint {
321
+ font-size: var(--text-sm);
322
+ line-height: var(--lh-sm);
323
+ }
180
324
  .item[data-state="active"] .hint {
181
325
  color: var(--stepper-fg-muted);
182
326
  }
@@ -18,15 +18,23 @@ import styles from "./Stepper.module.css";
18
18
 
19
19
  type Orientation = "vertical" | "horizontal";
20
20
  type Marker = "dot" | "number";
21
+ type Variant = "md" | "sm";
22
+ type Tone = "primary" | "neutral";
23
+ type HintPlacement = "below" | "inline";
24
+ type LineStyle = "dashed" | "solid";
21
25
  type ItemState = "upcoming" | "active" | "done";
22
26
 
23
27
  type StepperContextValue = {
24
28
  orientation: Orientation;
25
29
  marker: Marker;
30
+ variant: Variant;
31
+ tone: Tone;
32
+ hintPlacement: HintPlacement;
26
33
  activeIndex: number;
27
34
  onStepClick?: (index: number) => void;
28
35
  };
29
36
 
37
+
30
38
  const StepperContext = createContext<StepperContextValue | null>(null);
31
39
 
32
40
  function useStepperContext(): StepperContextValue {
@@ -46,6 +54,22 @@ export type StepperProps = Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> & {
46
54
  orientation?: Orientation;
47
55
  /** marker 종류. dot(기본) | number. */
48
56
  marker?: Marker;
57
+ /**
58
+ * marker 사이즈.
59
+ * - `md` : 24px 동그라미 marker (기본, 01 Stripe 류)
60
+ * - `sm` : 10px 작은 solid dot (02 헤이딜러 류, title 중심)
61
+ */
62
+ variant?: Variant;
63
+ /**
64
+ * 색 톤.
65
+ * - `primary` : done/active 모두 Primary (기본)
66
+ * - `neutral` : done은 모노톤, active만 Primary 혹은 fg-strong
67
+ */
68
+ tone?: Tone;
69
+ /** hint 배치. below(기본) | inline(title 옆). */
70
+ hintPlacement?: HintPlacement;
71
+ /** 스텝 사이 연결선 스타일. dashed(기본) | solid. */
72
+ lineStyle?: LineStyle;
49
73
  /** 스텝 클릭 시. 자식 Item에 onClick이 있으면 그쪽이 우선. */
50
74
  onStepClick?: (index: number) => void;
51
75
  children: ReactNode;
@@ -57,6 +81,10 @@ const StepperRoot = forwardRef<HTMLDivElement, StepperProps>(
57
81
  activeIndex,
58
82
  orientation = "vertical",
59
83
  marker = "dot",
84
+ variant = "md",
85
+ tone = "primary",
86
+ hintPlacement = "below",
87
+ lineStyle = "dashed",
60
88
  onStepClick,
61
89
  className,
62
90
  children,
@@ -65,11 +93,26 @@ const StepperRoot = forwardRef<HTMLDivElement, StepperProps>(
65
93
  ref,
66
94
  ) => {
67
95
  const ctxValue = useMemo<StepperContextValue>(
68
- () => ({ orientation, marker, activeIndex, onStepClick }),
69
- [orientation, marker, activeIndex, onStepClick],
96
+ () => ({
97
+ orientation,
98
+ marker,
99
+ variant,
100
+ tone,
101
+ hintPlacement,
102
+ activeIndex,
103
+ onStepClick,
104
+ }),
105
+ [
106
+ orientation,
107
+ marker,
108
+ variant,
109
+ tone,
110
+ hintPlacement,
111
+ activeIndex,
112
+ onStepClick,
113
+ ],
70
114
  );
71
115
 
72
- // 자식 Item에 index를 자동 주입
73
116
  const items = Children.toArray(children).filter(isValidElement);
74
117
  const injected = items.map((child, i) =>
75
118
  cloneElement(child as ReactElement<StepperItemProps>, {
@@ -91,6 +134,10 @@ const StepperRoot = forwardRef<HTMLDivElement, StepperProps>(
91
134
  .join(" ")}
92
135
  data-orientation={orientation}
93
136
  data-marker={marker}
137
+ data-variant={variant}
138
+ data-tone={tone}
139
+ data-hint-placement={hintPlacement}
140
+ data-line-style={lineStyle}
94
141
  {...rest}
95
142
  >
96
143
  {injected}
@@ -107,11 +154,9 @@ export type StepperItemProps = Omit<
107
154
  ButtonHTMLAttributes<HTMLButtonElement>,
108
155
  "title"
109
156
  > & {
110
- /** Stepper가 자동 주입. 수동 override 가능. */
111
157
  index?: number;
112
158
  title: ReactNode;
113
159
  hint?: ReactNode;
114
- /** 명시적 상태 지정. 미지정이면 activeIndex 기준으로 자동 결정. */
115
160
  state?: ItemState;
116
161
  };
117
162
 
@@ -130,7 +175,7 @@ const StepperItem = forwardRef<HTMLButtonElement, StepperItemProps>(
130
175
  },
131
176
  ref,
132
177
  ) => {
133
- const { orientation, marker, activeIndex, onStepClick } =
178
+ const { orientation, marker, variant, activeIndex, onStepClick } =
134
179
  useStepperContext();
135
180
 
136
181
  const idx = index ?? 0;
@@ -138,6 +183,22 @@ const StepperItem = forwardRef<HTMLButtonElement, StepperItemProps>(
138
183
  stateProp ??
139
184
  (idx === activeIndex ? "active" : idx < activeIndex ? "done" : "upcoming");
140
185
 
186
+ // marker 내부 렌더링 규칙:
187
+ // ─ sm variant : 내부 글자/dot 없음 (marker 자체가 작은 solid dot)
188
+ // ─ done : check 아이콘
189
+ // ─ number marker: 숫자 (active여도 숫자 유지)
190
+ // ─ dot + active : 작은 Primary dot
191
+ const markerContent =
192
+ variant === "sm"
193
+ ? null
194
+ : state === "done"
195
+ ? <Icon name="check" size={10} />
196
+ : marker === "number"
197
+ ? idx + 1
198
+ : state === "active"
199
+ ? <span className={styles.markerInnerDot} />
200
+ : null;
201
+
141
202
  return (
142
203
  <button
143
204
  ref={ref}
@@ -155,13 +216,7 @@ const StepperItem = forwardRef<HTMLButtonElement, StepperItemProps>(
155
216
  {...rest}
156
217
  >
157
218
  <span className={styles.marker} aria-hidden>
158
- {state === "done" ? (
159
- <Icon name="check" size={12} />
160
- ) : marker === "number" ? (
161
- idx + 1
162
- ) : state === "active" ? (
163
- <span className={styles.markerInnerDot} />
164
- ) : null}
219
+ {markerContent}
165
220
  </span>
166
221
  <span className={styles.body}>
167
222
  <span className={styles.title}>{title}</span>
@@ -56,11 +56,9 @@
56
56
  width: var(--_track-w);
57
57
  height: var(--_track-h);
58
58
  background: var(--bg-muted);
59
- border: var(--hairline) solid var(--border);
60
59
  border-radius: var(--radius-full);
61
60
  transition:
62
61
  background var(--duration-slow) var(--ease-out),
63
- border-color var(--duration-slow) var(--ease-out),
64
62
  box-shadow var(--duration-fast) var(--ease-out);
65
63
  }
66
64
 
@@ -82,19 +80,14 @@
82
80
  /* Checked — 트랙은 primary로 채우고, 썸을 우측 끝 - pad로 이동. */
83
81
  .input:checked + .track {
84
82
  background: var(--primary);
85
- border-color: var(--primary);
86
83
  }
87
84
  .input:checked + .track .thumb {
88
- left: calc(var(--_track-w) - var(--_thumb) - var(--_pad) - var(--hairline) * 2);
85
+ left: calc(var(--_track-w) - var(--_thumb) - var(--_pad));
89
86
  }
90
87
 
91
88
  /* Hover */
92
- .root:hover .input:not(:disabled) + .track {
93
- border-color: var(--border-strong);
94
- }
95
89
  .root:hover .input:not(:disabled):checked + .track {
96
90
  background: var(--primary-hover);
97
- border-color: var(--primary-hover);
98
91
  }
99
92
 
100
93
  /* Focus — 트랙에 링 표시 */
@@ -108,7 +101,6 @@
108
101
  }
109
102
  .input:disabled + .track {
110
103
  background: var(--bg-muted);
111
- border-color: transparent;
112
104
  }
113
105
  .input:disabled + .track .thumb {
114
106
  background: var(--fg-disabled);
@@ -1,10 +1,12 @@
1
1
  /* Root ─────────────────────────────────────────────────
2
- 투명. 배경·보더·그림자 없음. children 자유 배치.
2
+ 투명. 배경·보더·그림자 없음. 부모 폭을 반드시 꽉 채운다 — 이게 없으면
3
+ AppFrame.topbarSlot(flex) 안에서 콘텐츠 폭만큼만 차지해 좌측에 몰려버림.
3
4
  size는 높이만 결정 — padding/gap은 동일. */
4
5
  .root {
5
6
  display: flex;
6
7
  align-items: center;
7
- gap: var(--space-2);
8
+ width: 100%;
9
+ min-width: 0;
8
10
  height: var(--topbar-h);
9
11
  padding-inline: var(--space-5);
10
12
  background: transparent;
@@ -20,7 +22,33 @@
20
22
  height: var(--topbar-h-md);
21
23
  }
22
24
 
23
- /* Spacer ───────────────────────────────────────────── */
24
- .spacer {
25
- flex: 1 1 auto;
25
+ /* Zone ─────────────────────────────────────────────────
26
+ 공통. 자식은 가로 flex + 작은 컴포넌트 간 gap(--space-1, 메모리 규칙). */
27
+ .zone {
28
+ display: flex;
29
+ align-items: center;
30
+ gap: var(--space-1);
31
+ min-width: 0;
32
+ }
33
+
34
+ /* leading — 왼쪽, 자연 폭. */
35
+ .leading {
36
+ flex: 0 0 auto;
37
+ }
38
+
39
+ /* center — 가운데 1fr, 내용은 중앙 정렬. 있으면 L과 T 사이 공간을 1fr로 먹음. */
40
+ .center {
41
+ flex: 1 1 0;
42
+ justify-content: center;
43
+ }
44
+
45
+ /* trailing — 오른쪽 자연 폭.
46
+ margin-inline-start: auto 가 핵심 — center가 없는 조합(L + T만)에서도
47
+ 남는 공간을 전부 왼쪽 margin이 흡수해 자동으로 오른쪽 끝에 붙는다.
48
+ center가 있으면 center의 flex:1이 먼저 공간을 요구하기 때문에 margin은
49
+ 0으로 수렴. 어떤 조합이든 안정적 배치. */
50
+ .trailing {
51
+ flex: 0 0 auto;
52
+ margin-inline-start: auto;
53
+ justify-content: flex-end;
26
54
  }
@@ -8,6 +8,10 @@ import styles from "./Topbar.module.css";
8
8
  /* Root ─────────────────────────────────────────────────
9
9
  자유 슬롯 컨테이너. 자체 표현(bg/border/shadow) 없음.
10
10
  컨테이너 표현은 상위 AppFrame variant가 담당.
11
+
12
+ 구조는 leading / center / trailing 3-zone 고정. 루트가 부모 폭을 꽉 채우고
13
+ 세 zone이 flex로 자리를 나눠서 "왼쪽에 몰림" 현상을 구조적으로 차단.
14
+
11
15
  size는 세로 높이만 결정 (sm=얇게, md=기본). */
12
16
  export type TopbarSize = "sm" | "md";
13
17
 
@@ -31,17 +35,68 @@ const TopbarRoot = forwardRef<HTMLElement, TopbarProps>(function TopbarRoot(
31
35
  );
32
36
  });
33
37
 
34
- /* Spacer ───────────────────────────────────────────────
35
- 좌/우 정렬용 flex:1 빈 블록. */
36
- function TopbarSpacer() {
37
- return <div className={styles.spacer} aria-hidden />;
38
- }
38
+ /* Zones ────────────────────────────────────────────────
39
+ leading / center / trailing. zone 안 자식은 가로 flex + space-1 gap.
40
+ center는 flex:1 + 가운데 정렬 — 비워두면 leading과 trailing 사이가 자연히 벌어짐. */
41
+ type ZoneProps = HTMLAttributes<HTMLDivElement>;
42
+
43
+ const TopbarLeading = forwardRef<HTMLDivElement, ZoneProps>(function TopbarLeading(
44
+ { className, children, ...rest },
45
+ ref,
46
+ ) {
47
+ return (
48
+ <div
49
+ ref={ref}
50
+ className={cn(styles.zone, styles.leading, className)}
51
+ data-topbar-zone="leading"
52
+ {...rest}
53
+ >
54
+ {children}
55
+ </div>
56
+ );
57
+ });
58
+
59
+ const TopbarCenter = forwardRef<HTMLDivElement, ZoneProps>(function TopbarCenter(
60
+ { className, children, ...rest },
61
+ ref,
62
+ ) {
63
+ return (
64
+ <div
65
+ ref={ref}
66
+ className={cn(styles.zone, styles.center, className)}
67
+ data-topbar-zone="center"
68
+ {...rest}
69
+ >
70
+ {children}
71
+ </div>
72
+ );
73
+ });
74
+
75
+ const TopbarTrailing = forwardRef<HTMLDivElement, ZoneProps>(function TopbarTrailing(
76
+ { className, children, ...rest },
77
+ ref,
78
+ ) {
79
+ return (
80
+ <div
81
+ ref={ref}
82
+ className={cn(styles.zone, styles.trailing, className)}
83
+ data-topbar-zone="trailing"
84
+ {...rest}
85
+ >
86
+ {children}
87
+ </div>
88
+ );
89
+ });
39
90
 
40
91
  type TopbarComponent = typeof TopbarRoot & {
41
- Spacer: typeof TopbarSpacer;
92
+ Leading: typeof TopbarLeading;
93
+ Center: typeof TopbarCenter;
94
+ Trailing: typeof TopbarTrailing;
42
95
  };
43
96
 
44
97
  const Topbar = TopbarRoot as TopbarComponent;
45
- Topbar.Spacer = TopbarSpacer;
98
+ Topbar.Leading = TopbarLeading;
99
+ Topbar.Center = TopbarCenter;
100
+ Topbar.Trailing = TopbarTrailing;
46
101
 
47
102
  export { Topbar };