@conscience-technology/ctds 0.3.4 → 0.4.1
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/Button/Button.module.css +2 -2
- package/assets/components/EventListItem/EventListItem.module.css +220 -0
- package/assets/components/EventListItem/EventListItem.tsx +205 -0
- package/assets/components/EventListItem/index.ts +8 -0
- package/assets/components/Icon/Icon.tsx +80 -41
- package/assets/components/Kanban/Kanban.module.css +339 -0
- package/assets/components/Kanban/Kanban.tsx +371 -0
- package/assets/components/Kanban/index.ts +10 -0
- package/assets/components/ListItem/ListItem.module.css +91 -211
- package/assets/components/ListItem/ListItem.tsx +130 -154
- package/assets/components/ListItem/index.ts +2 -2
- package/assets/components/MetricCard/MetricCard.module.css +5 -5
- package/assets/components/PageHeader/PageHeader.module.css +109 -0
- package/assets/components/PageHeader/PageHeader.tsx +112 -0
- package/assets/components/PageHeader/index.ts +2 -0
- package/assets/components/Sidebar/Sidebar.module.css +81 -4
- package/assets/components/Sidebar/Sidebar.tsx +11 -2
- package/assets/components/SplitButton/SplitButton.module.css +3 -3
- package/assets/components/Stepper/Stepper.module.css +196 -52
- package/assets/components/Stepper/Stepper.tsx +68 -13
- package/assets/components/Switch/Switch.module.css +1 -9
- package/assets/components/Topbar/Topbar.module.css +33 -5
- package/assets/components/Topbar/Topbar.tsx +62 -7
- package/assets/components/_data.ts +11 -4
- package/assets/components/index.ts +2 -0
- package/assets/components/tokens.css +23 -10
- package/assets/prompts/ASSEMBLY.md +219 -0
- package/assets/prompts/PRINCIPLES.md +70 -0
- package/assets/prompts/axes/icon.md +7 -1
- package/assets/prompts/axes/palette.md +11 -0
- package/assets/prompts/components/_variants.md +5 -1
- package/assets/prompts/components/callout.md +3 -3
- package/assets/prompts/components/event-list-item.md +103 -0
- package/assets/prompts/components/generated-image.md +80 -0
- package/assets/prompts/components/hint.md +57 -0
- package/assets/prompts/components/kanban.md +167 -0
- package/assets/prompts/components/list-item.md +182 -204
- package/assets/prompts/components/metric-card.md +2 -2
- package/assets/prompts/components/page-header.md +155 -0
- package/assets/prompts/components/toast.md +1 -0
- package/assets/prompts/components/topbar.md +77 -59
- package/assets/prompts/recipes/README.md +102 -0
- package/assets/prompts/recipes/card-row.md +58 -0
- package/assets/prompts/recipes/form-footer.md +76 -0
- package/assets/prompts/recipes/meta-line.md +61 -0
- package/assets/prompts/recipes/page-banner.md +121 -0
- package/assets/prompts/recipes/page-header.md +93 -0
- package/assets/prompts/recipes/section-header.md +90 -0
- package/assets/prompts/themes/focus.md +72 -0
- package/assets/skills/ctds/SKILL.md +1 -0
- package/assets/skills/ctds-page/SKILL.md +12 -2
- package/assets/skills/ctds-wireframe/SKILL.md +35 -1
- package/package.json +2 -4
|
@@ -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)
|
|
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
|
-
투명. 배경·보더·그림자 없음.
|
|
2
|
+
투명. 배경·보더·그림자 없음. 부모 폭을 반드시 꽉 채운다 — 이게 없으면
|
|
3
|
+
AppFrame.topbarSlot(flex) 안에서 콘텐츠 폭만큼만 차지해 좌측에 몰려버림.
|
|
3
4
|
size는 높이만 결정 — padding/gap은 동일. */
|
|
4
5
|
.root {
|
|
5
6
|
display: flex;
|
|
6
7
|
align-items: center;
|
|
7
|
-
|
|
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
|
-
/*
|
|
24
|
-
.
|
|
25
|
-
|
|
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
|
-
/*
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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.
|
|
98
|
+
Topbar.Leading = TopbarLeading;
|
|
99
|
+
Topbar.Center = TopbarCenter;
|
|
100
|
+
Topbar.Trailing = TopbarTrailing;
|
|
46
101
|
|
|
47
102
|
export { Topbar };
|
|
@@ -527,6 +527,7 @@ export const componentSlugs: Record<string, string> = {
|
|
|
527
527
|
"Menu Item": "general/menu-item",
|
|
528
528
|
Badge: "general/badge",
|
|
529
529
|
Avatar: "general/avatar",
|
|
530
|
+
"Avatar Upload": "general/avatar-upload",
|
|
530
531
|
Tooltip: "general/tooltip",
|
|
531
532
|
Shortcut: "general/shortcut",
|
|
532
533
|
"Filter Button": "general/filter-button",
|
|
@@ -552,8 +553,11 @@ export const componentSlugs: Record<string, string> = {
|
|
|
552
553
|
"Identity Button": "nav/identity-button",
|
|
553
554
|
Pagination: "nav/pagination",
|
|
554
555
|
"App Frame": "layout/app-frame",
|
|
556
|
+
"Page Header": "nav/page-header",
|
|
555
557
|
Stepper: "nav/stepper",
|
|
556
558
|
"Context Menu": "overlay/context-menu",
|
|
559
|
+
"Dropdown Menu": "overlay/dropdown-menu",
|
|
560
|
+
Disclosure: "overlay/disclosure",
|
|
557
561
|
Callout: "feedback/callout",
|
|
558
562
|
Modal: "feedback/modal",
|
|
559
563
|
Drawer: "feedback/drawer",
|
|
@@ -562,9 +566,11 @@ export const componentSlugs: Record<string, string> = {
|
|
|
562
566
|
"Table Row": "data/table-row",
|
|
563
567
|
"Table Row Select": "data/table-row-select",
|
|
564
568
|
"Table Row Edit": "data/table-row-edit",
|
|
569
|
+
"Event List Item": "data/event-list-item",
|
|
565
570
|
"Event Chip": "data/event-chip",
|
|
566
571
|
"List Item": "data/list-item",
|
|
567
572
|
"Metric Card": "data/metric-card",
|
|
573
|
+
"Kanban": "data/kanban",
|
|
568
574
|
"Gallery Item": "data/gallery-item",
|
|
569
575
|
"Empty State": "data/empty-state",
|
|
570
576
|
"Progress Bar": "feedback/progress-bar",
|
|
@@ -596,7 +602,7 @@ export const components: { id: string; title: string; items: string[] }[] = [
|
|
|
596
602
|
id: "general",
|
|
597
603
|
title: "General",
|
|
598
604
|
items: [
|
|
599
|
-
"Button", "Split Button", "Icon Button", "Menu Item", "Toolbar", "Badge", "Avatar",
|
|
605
|
+
"Button", "Split Button", "Icon Button", "Menu Item", "Toolbar", "Badge", "Avatar", "Avatar Upload",
|
|
600
606
|
"Tooltip", "Shortcut", "Filter Button", "Chip", "Separator", "Skeleton", "Aspect Ratio",
|
|
601
607
|
"Code Block",
|
|
602
608
|
],
|
|
@@ -616,7 +622,8 @@ export const components: { id: string; title: string; items: string[] }[] = [
|
|
|
616
622
|
title: "Data Display",
|
|
617
623
|
items: [
|
|
618
624
|
"Table Header", "Table Row", "Table Row Select", "Table Row Edit",
|
|
619
|
-
"List Item", "Metric Card", "Gallery Item", "File Icon", "Attachment Item", "Event Chip",
|
|
625
|
+
"List Item", "Metric Card", "Gallery Item", "File Icon", "Attachment Item", "Event Chip", "Event List Item",
|
|
626
|
+
"Kanban",
|
|
620
627
|
"Empty State", "Description List", "Timeline",
|
|
621
628
|
],
|
|
622
629
|
},
|
|
@@ -624,7 +631,7 @@ export const components: { id: string; title: string; items: string[] }[] = [
|
|
|
624
631
|
id: "nav",
|
|
625
632
|
title: "Navigation",
|
|
626
633
|
items: [
|
|
627
|
-
"App Frame", "Tabs", "Breadcrumb", "Sidebar", "Sidebar Split", "Topbar", "Identity Button", "Pagination",
|
|
634
|
+
"App Frame", "Page Header", "Tabs", "Breadcrumb", "Sidebar", "Sidebar Split", "Topbar", "Identity Button", "Pagination",
|
|
628
635
|
"Stepper",
|
|
629
636
|
],
|
|
630
637
|
},
|
|
@@ -640,7 +647,7 @@ export const components: { id: string; title: string; items: string[] }[] = [
|
|
|
640
647
|
id: "overlay",
|
|
641
648
|
title: "Overlay / Menu",
|
|
642
649
|
items: [
|
|
643
|
-
"Context Menu", "Collapsible", "Toggle Group",
|
|
650
|
+
"Context Menu", "Dropdown Menu", "Disclosure", "Collapsible", "Toggle Group",
|
|
644
651
|
],
|
|
645
652
|
},
|
|
646
653
|
{
|
|
@@ -29,6 +29,7 @@ export * from "./Drawer";
|
|
|
29
29
|
export * from "./DropdownMenu";
|
|
30
30
|
export * from "./EmptyState";
|
|
31
31
|
export * from "./EventChip";
|
|
32
|
+
export * from "./EventListItem";
|
|
32
33
|
export * from "./FileIcon";
|
|
33
34
|
export * from "./FilterButton";
|
|
34
35
|
export * from "./GalleryItem";
|
|
@@ -47,6 +48,7 @@ export * from "./MessageTable";
|
|
|
47
48
|
export * from "./MetricCard";
|
|
48
49
|
export * from "./Modal";
|
|
49
50
|
export * from "./Onboarding";
|
|
51
|
+
export * from "./PageHeader";
|
|
50
52
|
export * from "./Pagination";
|
|
51
53
|
export * from "./ProgressBar";
|
|
52
54
|
export * from "./QuotationBar";
|
|
@@ -97,6 +97,16 @@
|
|
|
97
97
|
--input-w-md: calc(280px * var(--space-scale));
|
|
98
98
|
--input-w-lg: calc(400px * var(--space-scale));
|
|
99
99
|
|
|
100
|
+
/* Breakpoint — 레이아웃 전환 임계점.
|
|
101
|
+
CSS @media의 feature value는 var() 참조가 불가하므로 변수 자체는 참조용/문서용.
|
|
102
|
+
실제 미디어쿼리는 아래 값과 동일한 리터럴을 쓴다. 두 자리 뿐이라 흩어뜨리지 않는다.
|
|
103
|
+
sm = 560px — 모바일/태블릿 경계. "한 손 조작" vs "두 손 조작"
|
|
104
|
+
md = 900px — 태블릿 세로/데스크톱 경계. shell(topbar+sidebar) 축소 전환
|
|
105
|
+
신규 미디어쿼리를 작성할 땐 이 두 값 중 하나로 통일한다. 다른 값이 필요하면
|
|
106
|
+
여기 토큰부터 추가. */
|
|
107
|
+
--bp-sm: 560px;
|
|
108
|
+
--bp-md: 900px;
|
|
109
|
+
|
|
100
110
|
--radius-none: 0;
|
|
101
111
|
--radius-xs: 6px;
|
|
102
112
|
--radius-sm: 8px;
|
|
@@ -308,7 +318,7 @@
|
|
|
308
318
|
|
|
309
319
|
/* interactive / overlay
|
|
310
320
|
--hover: 컴포넌트 hover 시 표면·그림자에 얹는 공통 오버레이.
|
|
311
|
-
라이트=검정 3%, 다크=흰색
|
|
321
|
+
라이트=검정 3%, 다크=흰색 4%. 데이터 테이블·리스트 등 반복 행에서
|
|
312
322
|
"너무 두드러지지 않는" 톤. */
|
|
313
323
|
--hover: oklch(0 0 0 / 0.03);
|
|
314
324
|
/* tooltip: Mode 반전. 라이트에선 다크 bg + 밝은 fg, 다크에선 라이트 bg + 어두운 fg.
|
|
@@ -326,7 +336,7 @@
|
|
|
326
336
|
--shadow-color: 0 0 0;
|
|
327
337
|
|
|
328
338
|
--shadow-xs: 0 1px 2px oklch(var(--shadow-color) / 0.04);
|
|
329
|
-
--shadow-sm: 0
|
|
339
|
+
--shadow-sm: 0 2px 8px oklch(var(--shadow-color) / 0.03), 0 1px 2px oklch(var(--shadow-color) / 0.018);
|
|
330
340
|
--shadow-md: 0 6px 16px -4px oklch(var(--shadow-color) / 0.07),
|
|
331
341
|
0 2px 6px -2px oklch(var(--shadow-color) / 0.04);
|
|
332
342
|
--shadow-lg: 0 12px 20px -6px oklch(var(--shadow-color) / 0.1),
|
|
@@ -498,7 +508,7 @@
|
|
|
498
508
|
--radius-image-circle: var(--radius-2xl);
|
|
499
509
|
|
|
500
510
|
--shadow-xs: 0 1px 2px oklch(var(--shadow-color) / 0.04);
|
|
501
|
-
--shadow-sm: 0
|
|
511
|
+
--shadow-sm: 0 3px 10px -1px oklch(var(--shadow-color) / 0.032), 0 1px 3px oklch(var(--shadow-color) / 0.018);
|
|
502
512
|
--shadow-md: 0 8px 20px -4px oklch(var(--shadow-color) / 0.08), 0 3px 8px -3px oklch(var(--shadow-color) / 0.04);
|
|
503
513
|
--shadow-lg: 0 16px 32px -8px oklch(var(--shadow-color) / 0.14), 0 6px 12px -6px oklch(var(--shadow-color) / 0.06);
|
|
504
514
|
--shadow-xl: 0 32px 60px -14px oklch(var(--shadow-color) / 0.20);
|
|
@@ -515,7 +525,7 @@
|
|
|
515
525
|
--radius-3xl: 36px;
|
|
516
526
|
|
|
517
527
|
--shadow-xs: 0 1px 2px oklch(var(--shadow-color) / 0.04);
|
|
518
|
-
--shadow-sm: 0
|
|
528
|
+
--shadow-sm: 0 2px 8px oklch(var(--shadow-color) / 0.03), 0 1px 2px oklch(var(--shadow-color) / 0.018);
|
|
519
529
|
--shadow-md: 0 6px 16px -4px oklch(var(--shadow-color) / 0.07), 0 2px 6px -2px oklch(var(--shadow-color) / 0.04);
|
|
520
530
|
--shadow-lg: 0 12px 20px -6px oklch(var(--shadow-color) / 0.10), 0 4px 8px -4px oklch(var(--shadow-color) / 0.06);
|
|
521
531
|
--shadow-xl: 0 24px 40px -12px oklch(var(--shadow-color) / 0.16);
|
|
@@ -668,7 +678,7 @@
|
|
|
668
678
|
--radius-3xl: 22px;
|
|
669
679
|
|
|
670
680
|
--shadow-xs: 0 1px 0 oklch(var(--shadow-color) / 0.06);
|
|
671
|
-
--shadow-sm: 0 1px
|
|
681
|
+
--shadow-sm: 0 1px 3px oklch(var(--shadow-color) / 0.04), 0 0 0 1px oklch(var(--shadow-color) / 0.025);
|
|
672
682
|
--shadow-md: 0 2px 4px oklch(var(--shadow-color) / 0.10), 0 0 0 1px oklch(var(--shadow-color) / 0.06);
|
|
673
683
|
--shadow-lg: 0 4px 8px oklch(var(--shadow-color) / 0.12), 0 0 0 1px oklch(var(--shadow-color) / 0.06);
|
|
674
684
|
--shadow-xl: 0 8px 16px oklch(var(--shadow-color) / 0.16), 0 0 0 1px oklch(var(--shadow-color) / 0.06);
|
|
@@ -681,11 +691,14 @@
|
|
|
681
691
|
:root[data-mode="dark"],
|
|
682
692
|
[data-mode="dark"] {
|
|
683
693
|
--bg: oklch(0.20 var(--tone-chroma) var(--tone-hue));
|
|
684
|
-
--bg-subtle: oklch(0.
|
|
685
|
-
--bg-muted: oklch(0.
|
|
694
|
+
--bg-subtle: oklch(0.235 var(--tone-chroma) var(--tone-hue));
|
|
695
|
+
--bg-muted: oklch(0.27 var(--tone-chroma) var(--tone-hue));
|
|
686
696
|
--fg-strong: oklch(1.0 var(--tone-chroma) var(--tone-hue));
|
|
687
697
|
--fg: oklch(0.95 var(--tone-chroma) var(--tone-hue));
|
|
688
|
-
/* 다크: 밝은 잉크(≈0.95)를 낮은 알파로 얹음.
|
|
698
|
+
/* 다크: 밝은 잉크(≈0.95)를 낮은 알파로 얹음.
|
|
699
|
+
fg-muted 알파 0.60은 다크 bg(0.20) 위에서 effective L ≈ 0.65, 콘트라스트 ≈ 3.5:1.
|
|
700
|
+
WCAG AA "normal text" 4.5:1 임계 아래 — 보조 라벨·placeholder 전용.
|
|
701
|
+
중요한 보조 텍스트에는 --fg를 쓸 것. prefers-contrast: more에서 자동 상향됨. */
|
|
689
702
|
--fg-muted: oklch(0.95 var(--tone-chroma) var(--tone-hue) / 0.60);
|
|
690
703
|
--fg-subtle: oklch(0.95 var(--tone-chroma) var(--tone-hue) / 0.40);
|
|
691
704
|
--fg-disabled: oklch(0.95 var(--tone-chroma) var(--tone-hue) / 0.24);
|
|
@@ -695,7 +708,7 @@
|
|
|
695
708
|
|
|
696
709
|
/* primary / status는 :root의 정의를 그대로 사용 — --lightness가 아래에서 재정의되어 자동 반영됨 */
|
|
697
710
|
|
|
698
|
-
--hover: oklch(1 0 0 / 0.
|
|
711
|
+
--hover: oklch(1 0 0 / 0.04);
|
|
699
712
|
/* tooltip은 Mode 반전 — 다크 모드에선 밝은 bg + 어두운 fg. */
|
|
700
713
|
--tooltip-bg: oklch(0.95 var(--tone-chroma) var(--tone-hue));
|
|
701
714
|
--tooltip-fg: oklch(0.18 var(--tone-chroma) var(--tone-hue));
|
|
@@ -742,7 +755,7 @@
|
|
|
742
755
|
--tooltip-fg: oklch(1 0 0);
|
|
743
756
|
|
|
744
757
|
--shadow-xs: 0 1px 2px oklch(var(--shadow-color) / 0.04);
|
|
745
|
-
--shadow-sm: 0
|
|
758
|
+
--shadow-sm: 0 2px 8px oklch(var(--shadow-color) / 0.03), 0 1px 2px oklch(var(--shadow-color) / 0.018);
|
|
746
759
|
--shadow-md: 0 6px 16px -4px oklch(var(--shadow-color) / 0.07),
|
|
747
760
|
0 2px 6px -2px oklch(var(--shadow-color) / 0.04);
|
|
748
761
|
--shadow-lg: 0 12px 20px -6px oklch(var(--shadow-color) / 0.1),
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# CTDS 조립 가이드
|
|
2
|
+
|
|
3
|
+
**벤더에 없는 자리를 조립할 때** 쓰는 메타 알고리즘. 벤더 컴포넌트 문서(`components/*.md`)가 **"있는 컴포넌트를 어떻게 쓰는가"**를 다룬다면, 이 문서는 **"없는 컴포넌트를 어떻게 짜는가"**를 다룬다.
|
|
4
|
+
|
|
5
|
+
> 전제: [[PRINCIPLES]]·[[GUIDE]]를 이미 읽었다. 이 문서는 그 위의 조립 레이어.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 0. 왜 이 문서가 필요한가
|
|
10
|
+
|
|
11
|
+
CTDS 벤더(`web/components/`)는 공통 자리(Button·Input·Modal·Badge 등)만 담는다. **페이지 헤더·섹션 헤더·배너·도메인 리스트 row** 같은 자리는 프로젝트마다 생김새가 다르다 — 벤더로 못 박기 어렵고, 유저·AI가 그때그때 조립할 수밖에 없다.
|
|
12
|
+
|
|
13
|
+
이 "조립" 자리가 CTDS의 **진짜 결과물**이다. 토큰은 토큰대로, 벤더는 벤더대로 지켜져도, **조립 알고리즘이 없으면 매 프로젝트마다 리듬·위계·여백이 흩어진다.**
|
|
14
|
+
|
|
15
|
+
이 문서는 그 흩어짐을 막는 **조립 순서**를 못 박는다.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. 조립 알고리즘 (5단계)
|
|
20
|
+
|
|
21
|
+
벤더에 없는 자리를 짤 때 **반드시 이 순서로** 생각한다. 건너뛰면 수제 하드코딩으로 샌다.
|
|
22
|
+
|
|
23
|
+
### 1단계 — "이게 정말 벤더에 없는가"를 먼저 의심한다
|
|
24
|
+
|
|
25
|
+
조립 전에 벤더를 재확인한다. [[PRINCIPLES]] §5a — 역할이 같은 벤더가 있으면 반드시 쓴다.
|
|
26
|
+
|
|
27
|
+
- `web/components/index.ts` 또는 `prompts/components/` 디렉토리 전수 스캔
|
|
28
|
+
- "비슷한 역할"이 아니라 **"같은 역할"**이면 벤더 사용. 생김새가 조금 다른 건 variant·size로 해결 가능한지 재검토
|
|
29
|
+
- 벤더 기존 variant로 안 되면 → **새 variant/size 신설**이 로컬 조립보다 우선 ([[PRINCIPLES]] §5a)
|
|
30
|
+
- 정말로 벤더에 없는 자리임을 확인한 뒤에만 2단계로
|
|
31
|
+
|
|
32
|
+
**[HARD] 리스트/반복 자리는 1단계에서 반드시 벤더로 확정** — "반복 아이템 N개"가 보이는 순간 Table/ListItem/EventListItem/MenuItem 중 하나를 **반드시** 쓴다. 레시피 조립은 리스트 자리를 커버하지 않는다. ([[PRINCIPLES]] §5a HARD, card-row 레시피 폐기 참고)
|
|
33
|
+
|
|
34
|
+
### 2단계 — "가장 가까운 벤더 anatomy"를 베낀다
|
|
35
|
+
|
|
36
|
+
새 자리의 뼈대를 **같은 성격의 벤더 컴포넌트에서 복제**한다. 새로 짜지 않는다.
|
|
37
|
+
|
|
38
|
+
- "액션+라벨 묶음"이면 Button/Toolbar의 anatomy
|
|
39
|
+
- "표시 라벨"이면 Badge/Chip의 anatomy
|
|
40
|
+
- "텍스트 블록"이면 Callout의 anatomy (단, [[PRINCIPLES]] §5b 준수)
|
|
41
|
+
- "아이콘+텍스트 row"이면 ListItem의 anatomy
|
|
42
|
+
|
|
43
|
+
anatomy = padding·gap·radius·border 전략. 이걸 복제하면 벤더가 축에 반응하는 방식이 자동으로 따라온다.
|
|
44
|
+
|
|
45
|
+
**벤더 사용 시 반드시 확인:**
|
|
46
|
+
- **Prop 이름·타입**은 `web/components/<Name>/<Name>.tsx`의 export type 선언에서 확인 (레시피 예시 그대로 믿지 말 것 — Badge는 `tone`이 아니라 `status`, Stepper는 `value`가 아니라 `activeIndex`)
|
|
47
|
+
- **Icon 이름**은 `web/components/Icon/Icon.tsx`의 phosphor 레지스트리에서 확인 — 레시피/다른 문서에 등장하는 이름이 실제 등재된 이름이라는 보장 없음
|
|
48
|
+
- **`"use client"` 전염**: 쓰는 벤더 중 하나라도 `"use client"`면 조립하는 **page 자체도 `"use client"`**. compound(`Stepper.Item`·`Card.Header` 등)를 Server Component에서 접근하면 **런타임 undefined**(Next 15/React 19 경계 이슈). 현재 Button/Icon/IconButton/Stepper 등 대다수 vendor가 client — 사실상 조립 page는 모두 client로 가는 쪽이 안전
|
|
49
|
+
|
|
50
|
+
### 3단계 — 레시피를 확인한다
|
|
51
|
+
|
|
52
|
+
`prompts/recipes/`에 **자리별 조립 레시피**가 있다. 맞는 레시피가 있으면 그 토큰·구조 그대로 쓴다.
|
|
53
|
+
|
|
54
|
+
- 레시피는 "이 자리에서 어떤 토큰 조합을 쓰는가"를 명시
|
|
55
|
+
- 레시피가 **없는 자리**면 4단계로. 레시피가 **있는데 맞지 않으면** 레시피를 수정하거나 신설(이 문서를 업데이트)
|
|
56
|
+
- 안티패턴 메모리(`antipattern_*`)에 해당하는 자리인지도 확인 — 긍정형 레시피가 없어도 "하지 마"는 있을 수 있다
|
|
57
|
+
|
|
58
|
+
### 4단계 — 토큰만 쓴다
|
|
59
|
+
|
|
60
|
+
조립 코드에 리터럴을 쓰지 않는다. [[PRINCIPLES]] §6.
|
|
61
|
+
|
|
62
|
+
- 색·크기·시간·그림자·폰트 — 전부 `var(--*)` 또는 vendor 컴포넌트 경유
|
|
63
|
+
- 간격은 [[tokens/spacing]]의 "앱 리듬 vs 랜딩 리듬"에서 먼저 리듬 선택 → 그 안에서 스케일
|
|
64
|
+
- `gap`과 `padding/margin`을 양쪽에서 동시에 잡지 않는다 (이중 여백 금지, [[tokens/spacing]] Don't)
|
|
65
|
+
- 20px 미만 `semibold`/`bold` 금지, 섹션 h2/h3 semibold 금지 ([[tokens/typography]])
|
|
66
|
+
|
|
67
|
+
### 5단계 — 조립 검증
|
|
68
|
+
|
|
69
|
+
조립이 끝나면 자가 검증. 항목:
|
|
70
|
+
|
|
71
|
+
**값·토큰**
|
|
72
|
+
- [ ] 토큰·vendor 외에 **리터럴 값**이 있는가 → 없어야 함
|
|
73
|
+
- [ ] `width/height/max-width`에 **1px/0.75em** 같은 리터럴 있는가 → 금지. hairline은 `border-left: 1px solid var(--border)` + `align-self: stretch`로
|
|
74
|
+
- [ ] `border: 1px solid transparent` 가드 패턴 있는가 → 금지 (check_bg_stroke_combo hook). border 안 쓸 땐 선언 자체를 생략
|
|
75
|
+
- [ ] 날짜·시간이 숫자 전용("10. 13", "2026-10-13")인가 → "10월 13일 / 12분 전" 또는 Tooltip에 ISO ([[feedback-date-writing]])
|
|
76
|
+
- [ ] **font-weight 토큰 이름이 정확한가** → `var(--font-weight-medium)` ✓ 아니라 `var(--font-medium)` ✗ 유령 토큰. 조용히 400으로 상속되어 **"얇음" 증상의 숨은 원인** ([[PRINCIPLES]] §6a)
|
|
77
|
+
|
|
78
|
+
**레이아웃·그룹**
|
|
79
|
+
- [ ] 간격을 `margin-top` 또는 `A + B` 선택자로 만들고 있는가 → **그룹 설계가 틀렸음**. wrapper를 추가해 flex column + gap으로 풀어라 (§2a)
|
|
80
|
+
- [ ] 섹션 header에 `margin-bottom`이 있는가 → section 자체를 flex column + gap으로. header는 margin 없음
|
|
81
|
+
- [ ] 섹션 상단에 큰 padding으로 "위 공간" 확보 중인가 → 섹션 사이 분리는 부모 그룹의 gap으로 (§2a)
|
|
82
|
+
- [ ] 조립 코드에 `<section>`/`<article>`/`<nav>` semantic 태그를 썼는가 → **금지**. AppShell의 `.content section`이 `padding-top: 80px` 꽂아 "섹션 머리에 거대한 여백" 함정. `<div>`로 짠다 ([[feedback-no-semantic-tags-in-components]])
|
|
83
|
+
|
|
84
|
+
**리스트·반복 자리 (HARD)**
|
|
85
|
+
- [ ] 반복 아이템 N개를 `<ul>`/`<li>` + 자체 className으로 짰는가 → **즉시 금지** ([[PRINCIPLES]] §5a HARD). 아래 중 하나로 교체:
|
|
86
|
+
- 표 역할 → [[components/table-row]]
|
|
87
|
+
- 단건 반복(알림·활동·작업·자원) → [[components/list-item]]
|
|
88
|
+
- 날짜 중심 이벤트 → [[components/event-list-item]]
|
|
89
|
+
- 메뉴 아이템 → [[components/menu-item]] / [[components/sidebar-item]]
|
|
90
|
+
- [ ] "카드처럼 보이는 리스트"가 필요하다는 이유로 수제 조립 중인가 → ListItem은 **chrome 없음이 공식**. border/bg 두르는 리스트는 (1) 블록 하나면 Callout, (2) 리스트 전체라면 **ListItem에 새 variant 신설**. 수제 조립 금지
|
|
91
|
+
|
|
92
|
+
**리듬·여백**
|
|
93
|
+
- [ ] 부모 gap + 자식 padding 이중 여백이 있는가 → 한쪽만
|
|
94
|
+
- [ ] 콘솔 리듬에서 **섹션 간 gap이 space-6 이상**인가 → `space-4` 고려. 섹션이 border/배경으로 분리돼 있으면 space-6는 "구조 끊어진" 느낌 ([[tokens/spacing]])
|
|
95
|
+
- [ ] 작은 컴포 묶음(버튼↔버튼) gap이 `space-1`인가 → 고정
|
|
96
|
+
- [ ] 아이콘·텍스트가 hairline에 padding 없이 붙었는가 → padding 확보
|
|
97
|
+
- [ ] **멀티라인 블록**(banner·card)에서 padding이 상하 `space-3` 이상인가 → single-line과 같은 padding 쓰면 타이트
|
|
98
|
+
- [ ] **title + sub** 2줄 묶음의 내부 gap이 `space-1` 이상인가 → `space-0.5`는 녹아보임
|
|
99
|
+
|
|
100
|
+
**정렬**
|
|
101
|
+
- [ ] 아이콘 + 여러 줄 텍스트 조합에서 `align-items: center`인가 → **첫 줄과 어긋남**. `flex-start` + 아이콘 `margin-top: space-0.5`로 보정
|
|
102
|
+
- [ ] "라벨 하나만" 블록 위에 뜨거나 혼자 떠서 여백이 깨지는가 → 라벨은 header 자리에 묶거나, 블록 밖으로 승격
|
|
103
|
+
|
|
104
|
+
**색·면**
|
|
105
|
+
- [ ] 배경 + 테두리 동시 적용이 있는가 → 둘 중 하나만 ([[PRINCIPLES]] §5b)
|
|
106
|
+
- [ ] 평상시 border만, hover에서 bg 추가 패턴 쓰는가 → OK (`cardRow` 전략 A). 역으로 평상시 bg + hover border 금지
|
|
107
|
+
|
|
108
|
+
**타이포·숫자**
|
|
109
|
+
- [ ] 숫자 셀을 우측정렬했는가 → 좌측정렬 유지
|
|
110
|
+
- [ ] 가운뎃점(·) 구분자가 있는가 → hairline 세로선으로 교체
|
|
111
|
+
- [ ] 20px 미만 글자에 `semibold` 썼는가 → medium 이하 ([[tokens/typography]])
|
|
112
|
+
- [ ] **섹션 타이틀(h2)이 text-xl 이상**인가 → 콘솔 리듬에서는 `text-lg` 기본. medium 가중치만으론 크면 "크고 얇은" 느낌으로 뜸
|
|
113
|
+
|
|
114
|
+
**아이콘**
|
|
115
|
+
- [ ] 아이콘을 인라인 `<svg>`로 쐈는가 → Icon 컴포넌트 경유
|
|
116
|
+
|
|
117
|
+
이 체크리스트는 메모리에 쌓인 피드백의 **긍정형 자동화** — 조립 시점에 매번 돌린다. 특히 1~3번은 **hook이 자동 차단**하므로 미리 검증.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 2. "조립 자리"로 흔히 등장하는 패턴
|
|
122
|
+
|
|
123
|
+
벤더에 없는 자리 중 반복적으로 등장하는 것들. **각 패턴은 `recipes/` 아래에 레시피가 있다** (없으면 신설).
|
|
124
|
+
|
|
125
|
+
- **페이지 헤더** — 큰 타이틀 + 서브텍스트 + (옵션) 우측 액션 → [[recipes/page-header]]
|
|
126
|
+
- **섹션 헤더** — 중간 타이틀 + (옵션) 설명 1줄 + (옵션) 우측 액션/카운트 → [[recipes/section-header]]
|
|
127
|
+
- **메타 라인** — 라벨 여러 개가 가로로 나열, 사이 구분자 필요 → [[recipes/meta-line]]
|
|
128
|
+
- **카드 리스트 row** → ~~레시피 아님~~. [[components/list-item]] 벤더 ([[PRINCIPLES]] §5a HARD)
|
|
129
|
+
- **폼 푸터** — Spacer + outline(취소) + primary(제출) → [[recipes/form-footer]]
|
|
130
|
+
- **페이지 상단 안내 배너** — 톤 있는 가로 바, 좌측 아이콘+텍스트 + 우측 액션 → [[recipes/page-banner]]
|
|
131
|
+
- **흐름 속 참고 각주** — 아이콘 + 1~2줄 muted 텍스트, 톤·액션 없음 → vendor [[components/hint]] (레시피 아님. "아이콘-텍스트 라인 맞추기" 함정 반복 때문에 벤더로 확정)
|
|
132
|
+
|
|
133
|
+
이 목록 자체가 "지금 CTDS가 자주 조립해야 하는 자리"의 정식 지도다. 새 패턴이 세 번 반복되면 이 목록에 추가하고 레시피를 쓴다.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 2a. 레이아웃 1원칙 — "관련있는 것끼리 묶는다"
|
|
138
|
+
|
|
139
|
+
**레이아웃 설계의 가장 중요한 결정은 "그룹"이다.** 어떤 요소가 어떤 요소와 한 묶음인지 먼저 정하고, 그 다음 각 그룹을 flex column으로 만들고, gap으로 간격을 준다. padding/margin으로 간격을 벌리는 접근은 **틀렸다**.
|
|
140
|
+
|
|
141
|
+
**원칙:**
|
|
142
|
+
1. 역할이 가까운 요소들을 **같은 flex 컨테이너**로 묶는다 (한 그룹 = 한 wrapper)
|
|
143
|
+
2. 그룹 안은 **좁은 gap** (`space-2` ~ `space-3`), 그룹끼리는 **넓은 gap** (`space-6` ~ `space-10`)
|
|
144
|
+
3. **모든 간격은 flex gap**. `margin-top`/`margin-bottom` 트릭 금지. `+` 선택자로 "다음 요소에만 추가 공간" 트릭도 금지 — gap으로 못 풀면 **그룹을 다시 짜라**
|
|
145
|
+
4. 섹션 header의 `margin-bottom` 금지. 섹션 자체를 flex column + gap으로 만들어 header와 body를 자연스럽게 묶는다
|
|
146
|
+
|
|
147
|
+
**전형 (3-tier 리듬):**
|
|
148
|
+
```
|
|
149
|
+
page (gap: space-10) ← 그룹 사이 넓게
|
|
150
|
+
├─ group A: 소개 (gap: space-3) ← 같은 그룹은 좁게
|
|
151
|
+
│ ├─ banner
|
|
152
|
+
│ ├─ page-header
|
|
153
|
+
│ └─ inline hint
|
|
154
|
+
├─ group B: Stepper (gap: space-2)
|
|
155
|
+
│ ├─ eyebrow
|
|
156
|
+
│ └─ Stepper
|
|
157
|
+
└─ group C: 섹션들 (gap: space-6) ← 중간 호흡
|
|
158
|
+
├─ section (gap: space-2) ← 섹션 안은 좁게
|
|
159
|
+
│ ├─ section-header
|
|
160
|
+
│ └─ card-list (gap: space-2)
|
|
161
|
+
└─ section (gap: space-2)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**금지:**
|
|
165
|
+
- [HARD] `.page { gap: X }` 하나로 모든 자식 블록 간격 담당 — 역할 다른 블록이 "통짜"로 뜸
|
|
166
|
+
- [HARD] `A + B { margin-top: ... }`로 특정 자리만 벌리기 — 그룹 설계가 틀렸다는 신호. wrapper를 추가해서 gap으로 풀어라
|
|
167
|
+
- [HARD] 섹션 header `margin-bottom` — section을 flex column + gap으로. 섹션 자체가 header↔body를 묶음
|
|
168
|
+
- [HARD] 섹션 head 쪽에 큰 padding으로 "위 공간 확보" — padding은 "안쪽" 공간, 섹션 사이 분리는 부모 그룹의 gap으로
|
|
169
|
+
|
|
170
|
+
**갭 선택 — 핵심 원칙: "가까운 관계일수록 더 좁게"**
|
|
171
|
+
|
|
172
|
+
갭의 크기는 **관계의 가까움에 반비례**한다. 두 요소가 "가장 가까운 관계"면 가장 좁은 gap, "성격이 다른 묶음"이면 가장 넓은 gap. **인접한 두 레벨은 반드시 한 단계 이상 차이**가 나야 위계가 생긴다 — 같으면 뭉쳐 보임.
|
|
173
|
+
|
|
174
|
+
**관계 → 갭 매핑 (앱/콘솔 리듬, 실측 기준):**
|
|
175
|
+
|
|
176
|
+
| 관계 | 갭 | 예시 |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| **리스트 안 반복 아이템** (가장 가까움) | `space-1` | cardList의 cardRow들 |
|
|
179
|
+
| **그룹 안 미니 묶음** | `space-2` | 타이틀+설명, eyebrow+컨트롤, Stepper eyebrow+Stepper |
|
|
180
|
+
| **같은 그룹 안 header ↔ body** | `space-3` | section header ↔ cardList, pageHeader ↔ inlineHint |
|
|
181
|
+
| **같은 그룹 안 성격 다른 요소** | `space-6` | 메타 banner ↔ 본문 headerBlock |
|
|
182
|
+
| **섹션 ↔ 섹션 (반복 작업 단위)** | `space-20` (80px) | "작업 계획" ↔ "코드 변경안 실행 요청" |
|
|
183
|
+
| **그룹 ↔ 그룹 (성격 다른 묶음)** | `space-20` (80px) | introGroup ↔ stepperGroup ↔ sectionsGroup |
|
|
184
|
+
|
|
185
|
+
**철칙: 인접 레벨 간 반드시 한 단계 이상 차이**
|
|
186
|
+
- 예: 리스트 아이템 gap `space-1` → 섹션 header↔body gap `space-3` (한 단계 뛰어넘음)
|
|
187
|
+
- 예: 섹션 header↔body `space-3` → 섹션 ↔ 섹션 `space-20` (큰 점프)
|
|
188
|
+
- **같은 값 금지**: 아이템 간 `space-2` + 섹션 header↔body `space-2` → "타이틀과 아이템이 같은 레벨"로 뭉쳐 보임
|
|
189
|
+
|
|
190
|
+
> ⚠ 처음엔 "섹션 반복은 space-6, 그룹끼리는 space-10"으로 잡았지만 실측에서 둘 다 **좁았음**. 콘솔 리듬이라도 **"독립된 작업 단위"끼리는 space-20 수준의 호흡**이 자연스러움. "space-6은 섹션 내부 블록 단위"로만 쓸 것.
|
|
191
|
+
|
|
192
|
+
## 2b. 카드로 감싸는 자리 — "균일 padding 지킬 수 있을 때만"
|
|
193
|
+
|
|
194
|
+
조립 자리에 **카드(border + radius + bg)**를 두르면 안쪽은 **상하좌우 균일 padding**이 공식 ([[tokens/spacing]] "카드·패널 내부 padding"). 지키지 못하면 카드 자체를 포기하는 게 낫다.
|
|
195
|
+
|
|
196
|
+
**카드 포기 시그널:**
|
|
197
|
+
- 카드 안에 **라벨/eyebrow가 혼자** 떠서 균일 padding을 깬다 → 라벨을 밖(PageHeader eyebrow)으로 승격 또는 생략, 카드 폐기
|
|
198
|
+
- 카드 안 요소들이 **각자 다른 수평 정렬**(가운데 정렬 Stepper + 오른쪽 정렬 라벨) → 균일 padding으론 못 담음. 풀어 놓거나 요소 하나만 남김
|
|
199
|
+
- 카드가 "그냥 공간 묶음용"일 뿐 **분리 신호가 없다** → border·bg 자체가 노이즈. 페이지 흐름에 그대로 두고 gap만
|
|
200
|
+
|
|
201
|
+
## 3. 조립 시 자주 틀리는 자리
|
|
202
|
+
|
|
203
|
+
조립할 때 사람·AI 모두 자주 미끄러지는 자리. 안티패턴 메모리와 쌍을 이룬다.
|
|
204
|
+
|
|
205
|
+
- **"페이지 맨 위에 Tabs만"** — Tabs 위에 PageHeader(h1/breadcrumb)가 먼저 와야 한다 ([antipattern-tabs-without-title])
|
|
206
|
+
- **"워크스페이스 스위처를 수제 Button+Dropdown으로 조립"** — 전용 IdentityButton + DropdownMenu 사용 ([antipattern-workspace-switcher])
|
|
207
|
+
- **"Topbar leading에만 요소 몰림"** — leading/center/trailing 3구역 균형. leading·trailing 짝 필수 ([[components/topbar]])
|
|
208
|
+
- **"Sidebar 2단 쉘을 SidebarSplit으로"** — 바깥 flex로 Rail+Sidebar 직접 조립 ([feedback-sidebar-scroll])
|
|
209
|
+
- **"안내 영역을 outline Callout으로"** — 색 테두리 + 색 배경 조합은 영구 금지 ([feedback-no-outlined-callout]). muted 인라인 텍스트 한 줄로 대체
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## 관련
|
|
214
|
+
|
|
215
|
+
- [[PRINCIPLES]] — 전역 원칙. 특히 §1(값/판단 분리), §5a(컴포넌트 우선), §5b(배경 또는 테두리), §6(하드코딩 금지)
|
|
216
|
+
- [[GUIDE]] — 섹션 문서 작성 가이드. ASSEMBLY는 그 위의 **조립 레이어**
|
|
217
|
+
- [[components/_variants]] — 역할 × variant 매핑. 2단계(가장 가까운 벤더 anatomy)에서 참조
|
|
218
|
+
- [[tokens/spacing]] — 앱 리듬 vs 랜딩 리듬. 4단계(토큰) 참조
|
|
219
|
+
- `prompts/recipes/` — 자리별 조립 레시피 모음. 3단계 참조
|