@conscience-technology/ctds 0.4.0 → 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/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/PageHeader/PageHeader.module.css +109 -0
- package/assets/components/PageHeader/PageHeader.tsx +112 -0
- package/assets/components/PageHeader/index.ts +2 -0
- package/assets/prompts/components/hint.md +57 -0
- package/assets/prompts/components/kanban.md +167 -0
- package/assets/prompts/components/page-header.md +155 -0
- 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/package.json +1 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/* Root ─────────────────────────────────────────────────
|
|
2
|
+
grid-area로 slot 배치 — DOM 순서와 무관하게 Title+Actions가 같은 줄에
|
|
3
|
+
서고 Eyebrow/Lead/Meta는 그 위아래로 자동 정렬된다. 이 트릭이 compound
|
|
4
|
+
API의 '자식 생략 가능' 요구와 맞물려서 가장 안정적. 빈 slot은 grid가
|
|
5
|
+
공간을 안 먹음(자식이 없는 행은 암묵적으로 0).
|
|
6
|
+
|
|
7
|
+
세로 간격(eyebrow→title→lead→meta)은 row-gap으로.
|
|
8
|
+
PageHeader → 다음 섹션 간격은 **상위 wrapper gap이 담당** — 벤더가
|
|
9
|
+
margin-bottom으로 공급하지 않는다. margin trick 금지, group wrapper + gap만
|
|
10
|
+
([[feedback_page_differential_rhythm]], [[feedback_no_double_spacing]]). */
|
|
11
|
+
.root {
|
|
12
|
+
display: grid;
|
|
13
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
14
|
+
grid-template-areas:
|
|
15
|
+
"eyebrow eyebrow"
|
|
16
|
+
"title actions"
|
|
17
|
+
"lead lead"
|
|
18
|
+
"meta meta";
|
|
19
|
+
row-gap: var(--space-1);
|
|
20
|
+
align-items: start;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Size — 타이포 사이즈만 스왑. 외부 간격은 소비처 상위 gap. */
|
|
24
|
+
.root[data-size="sm"] .title {
|
|
25
|
+
font-size: var(--text-xl);
|
|
26
|
+
line-height: var(--lh-xl);
|
|
27
|
+
}
|
|
28
|
+
.root[data-size="sm"] .lead {
|
|
29
|
+
font-size: var(--text-sm);
|
|
30
|
+
line-height: var(--lh-sm);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.root[data-size="md"] .title {
|
|
34
|
+
font-size: var(--text-2xl);
|
|
35
|
+
line-height: var(--lh-2xl);
|
|
36
|
+
}
|
|
37
|
+
.root[data-size="md"] .lead {
|
|
38
|
+
font-size: var(--text-base);
|
|
39
|
+
line-height: var(--lh-base);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.root[data-size="lg"] .title {
|
|
43
|
+
font-size: var(--text-3xl);
|
|
44
|
+
line-height: var(--lh-3xl);
|
|
45
|
+
}
|
|
46
|
+
.root[data-size="lg"] .lead {
|
|
47
|
+
font-size: var(--text-base);
|
|
48
|
+
line-height: var(--lh-base);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Eyebrow ──────────────────────────────────────────────
|
|
52
|
+
작은 상단 라벨. muted + small, uppercase/wide tracking 금지. */
|
|
53
|
+
.eyebrow {
|
|
54
|
+
grid-area: eyebrow;
|
|
55
|
+
color: var(--fg-subtle);
|
|
56
|
+
font-size: var(--text-sm);
|
|
57
|
+
line-height: var(--lh-sm);
|
|
58
|
+
font-weight: var(--font-weight-medium);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Title ─────────────────────────────────────────────────
|
|
62
|
+
semibold, letter-spacing은 globals의 .page-title 리듬 승계. */
|
|
63
|
+
.title {
|
|
64
|
+
grid-area: title;
|
|
65
|
+
color: var(--fg);
|
|
66
|
+
font-weight: var(--font-weight-semibold);
|
|
67
|
+
letter-spacing: var(--tracking-tighter);
|
|
68
|
+
margin: 0;
|
|
69
|
+
min-width: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Lead ──────────────────────────────────────────────────
|
|
73
|
+
muted 본문. */
|
|
74
|
+
.lead {
|
|
75
|
+
grid-area: lead;
|
|
76
|
+
color: var(--fg-muted);
|
|
77
|
+
margin: 0;
|
|
78
|
+
max-width: 72ch;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Meta ──────────────────────────────────────────────────
|
|
82
|
+
Badge·날짜·author 등. 가로 묶음, gap은 space-2 (작은 컴포넌트 사이 규칙은
|
|
83
|
+
이웃 간 긴밀한 묶음에 적용되고, meta는 서로 다른 '정보 조각'이라 space-2).
|
|
84
|
+
|
|
85
|
+
row-gap(space-1)이 title↔lead에 맞춰 꽤 좁아져 있는데, Meta는 '본문'이 아니라
|
|
86
|
+
'별도 정보 묶음'이라 그 간격이 어색하게 좁다. 윗 slot(lead/title/eyebrow 중
|
|
87
|
+
존재하는 것)과의 사이만 추가 margin-top으로 벌림. */
|
|
88
|
+
.meta {
|
|
89
|
+
grid-area: meta;
|
|
90
|
+
display: flex;
|
|
91
|
+
flex-wrap: wrap;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: var(--space-2);
|
|
94
|
+
color: var(--fg-muted);
|
|
95
|
+
font-size: var(--text-sm);
|
|
96
|
+
line-height: var(--lh-sm);
|
|
97
|
+
margin-top: var(--space-2);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Actions ───────────────────────────────────────────────
|
|
101
|
+
Title과 같은 줄, 오른쪽 끝에 pin.
|
|
102
|
+
자식 사이 gap은 작은 컴포넌트 간 규칙대로 space-1. */
|
|
103
|
+
.actions {
|
|
104
|
+
grid-area: actions;
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
gap: var(--space-1);
|
|
108
|
+
align-self: center;
|
|
109
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../_cn";
|
|
6
|
+
import styles from "./PageHeader.module.css";
|
|
7
|
+
|
|
8
|
+
/* Root ─────────────────────────────────────────────────
|
|
9
|
+
페이지 body 첫 블록의 head. "이 페이지가 뭔지"를 담는 자리.
|
|
10
|
+
Topbar(chrome)와 역할이 다르다 — Topbar는 지금 어디(breadcrumb)·공용 액션,
|
|
11
|
+
PageHeader는 h1·eyebrow·lead·메타·페이지 액션.
|
|
12
|
+
|
|
13
|
+
레이아웃은 수직 스택이지만 Actions가 들어오면 Title과 같은 가로줄로
|
|
14
|
+
자동 분리 (가장 흔한 패턴). Lead/Meta는 Title 아래에 계속 수직. */
|
|
15
|
+
|
|
16
|
+
export type PageHeaderSize = "sm" | "md" | "lg";
|
|
17
|
+
|
|
18
|
+
export interface PageHeaderProps extends HTMLAttributes<HTMLElement> {
|
|
19
|
+
size?: PageHeaderSize;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PageHeaderRoot = forwardRef<HTMLElement, PageHeaderProps>(function PageHeaderRoot(
|
|
23
|
+
{ size = "md", className, children, ...rest },
|
|
24
|
+
ref,
|
|
25
|
+
) {
|
|
26
|
+
return (
|
|
27
|
+
<header
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cn(styles.root, className)}
|
|
30
|
+
data-size={size}
|
|
31
|
+
{...rest}
|
|
32
|
+
>
|
|
33
|
+
{children}
|
|
34
|
+
</header>
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/* Eyebrow — 선택. 작은 상단 라벨("Overview · Billing" 같은 카테고리 표시).
|
|
39
|
+
uppercase + wide tracking 조합은 금지, muted 색 + 작은 사이즈로만. */
|
|
40
|
+
const PageHeaderEyebrow = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
41
|
+
function PageHeaderEyebrow({ className, children, ...rest }, ref) {
|
|
42
|
+
return (
|
|
43
|
+
<div ref={ref} className={cn(styles.eyebrow, className)} {...rest}>
|
|
44
|
+
{children}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
/* Title — 필수. <h1> 자동.
|
|
51
|
+
Actions가 같은 부모 안에 있으면 titleRow에 묶여 가로 분리된다. */
|
|
52
|
+
const PageHeaderTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadingElement>>(
|
|
53
|
+
function PageHeaderTitle({ className, children, ...rest }, ref) {
|
|
54
|
+
return (
|
|
55
|
+
<h1 ref={ref} className={cn(styles.title, className)} {...rest}>
|
|
56
|
+
{children}
|
|
57
|
+
</h1>
|
|
58
|
+
);
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
/* Lead — 선택. muted 본문 1~2문장. em dash·ellipsis 금지. */
|
|
63
|
+
const PageHeaderLead = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
|
|
64
|
+
function PageHeaderLead({ className, children, ...rest }, ref) {
|
|
65
|
+
return (
|
|
66
|
+
<p ref={ref} className={cn(styles.lead, className)} {...rest}>
|
|
67
|
+
{children}
|
|
68
|
+
</p>
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
/* Meta — 선택. Badge·날짜·author 등 작은 메타 조각 가로 묶음.
|
|
74
|
+
gap은 space-2 (meta 토큰 사이는 작은 컴포넌트 사이가 아니라 '정보 묶음' 사이). */
|
|
75
|
+
const PageHeaderMeta = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
76
|
+
function PageHeaderMeta({ className, children, ...rest }, ref) {
|
|
77
|
+
return (
|
|
78
|
+
<div ref={ref} className={cn(styles.meta, className)} {...rest}>
|
|
79
|
+
{children}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
/* Actions — 선택. Title과 같은 줄로 오른쪽 끝에 pin.
|
|
86
|
+
자식 사이 gap은 작은 컴포넌트 간 규칙대로 space-1. */
|
|
87
|
+
const PageHeaderActions = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
88
|
+
function PageHeaderActions({ className, children, ...rest }, ref) {
|
|
89
|
+
return (
|
|
90
|
+
<div ref={ref} className={cn(styles.actions, className)} {...rest}>
|
|
91
|
+
{children}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
type PageHeaderComponent = typeof PageHeaderRoot & {
|
|
98
|
+
Eyebrow: typeof PageHeaderEyebrow;
|
|
99
|
+
Title: typeof PageHeaderTitle;
|
|
100
|
+
Lead: typeof PageHeaderLead;
|
|
101
|
+
Meta: typeof PageHeaderMeta;
|
|
102
|
+
Actions: typeof PageHeaderActions;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const PageHeader = PageHeaderRoot as PageHeaderComponent;
|
|
106
|
+
PageHeader.Eyebrow = PageHeaderEyebrow;
|
|
107
|
+
PageHeader.Title = PageHeaderTitle;
|
|
108
|
+
PageHeader.Lead = PageHeaderLead;
|
|
109
|
+
PageHeader.Meta = PageHeaderMeta;
|
|
110
|
+
PageHeader.Actions = PageHeaderActions;
|
|
111
|
+
|
|
112
|
+
export { PageHeader };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Hint
|
|
2
|
+
|
|
3
|
+
### 원칙
|
|
4
|
+
흐름 속에 스며드는 **참고 한 줄~두 줄**. 아이콘 하나와 muted 텍스트로만 구성된 가장 가벼운 안내. 페이지 폭을 차지하지 않고, 톤도 신호도 띠지 않는다. 자리의 성격은 "이 자리에서 알아야 할 것"의 각주 — [[components/callout]]처럼 액션으로 유도하지 않고, [[recipes/page-banner]]처럼 상태를 외치지 않는다.
|
|
5
|
+
|
|
6
|
+
### Do
|
|
7
|
+
- 아이콘은 **1개 고정**, text는 1~2줄. 길어지면 Callout 또는 별도 블록으로.
|
|
8
|
+
- 톤 색 금지 — `fg-muted` 하나만. tone·variant 축 없음.
|
|
9
|
+
- 아이콘 수직 정렬은 **첫 줄 캡 하이트 중앙** — flex align-items 트릭 대신 `display: inline` + `vertical-align`로 line box 안에 흘려넣기 (내부 구현 책임).
|
|
10
|
+
- 폼 필드 helper text는 [[components/form-field]]의 자체 helper를 쓴다. Hint는 **블록 단위 안내** — 폼 밖, 섹션 안 설명.
|
|
11
|
+
|
|
12
|
+
### Don't
|
|
13
|
+
- [HARD] 액션 버튼을 넣지 않는다 — 액션이 필요하면 [[components/callout]].
|
|
14
|
+
- [HARD] 톤 신호(warning·destructive·success 배경)를 띠지 않는다 — 신호가 필요하면 [[components/callout]] 또는 [[recipes/page-banner]].
|
|
15
|
+
- [HARD] 배경 + 테두리 금지 ([[PRINCIPLES]] §5b). Hint는 **배경·테두리 둘 다 없음**이 기본.
|
|
16
|
+
- [SOFT] 3줄 이상 텍스트 금지 — 길어지면 섹션 자체가 "안내 블록"이 돼야 함. Hint는 각주 성격 유지.
|
|
17
|
+
- [SOFT] 리스트·강조 같은 서식 금지. 평문만.
|
|
18
|
+
|
|
19
|
+
### Fallback
|
|
20
|
+
아이콘이 애매하면 `info` 또는 `question`. 자리가 코드/기술 설명이면 `brackets-square`.
|
|
21
|
+
|
|
22
|
+
### 관련
|
|
23
|
+
- [[components/callout]] — 톤·액션 있는 안내 블록. Hint는 액션 없음.
|
|
24
|
+
- [[recipes/page-banner]] — 페이지 폭 상태 바. Hint는 흐름 속 각주.
|
|
25
|
+
- [[components/form-field]] — 폼 helper text 전용 자리.
|
|
26
|
+
- [[tokens/color]] — fg-muted
|
|
27
|
+
- [[tokens/typography]] — text-sm 기본, line-height 1.6
|
|
28
|
+
|
|
29
|
+
### Props 시그니처
|
|
30
|
+
```tsx
|
|
31
|
+
<Hint
|
|
32
|
+
icon={IconName} // default: "info"
|
|
33
|
+
size="sm | md" // default: "sm" (앱 리듬 기본)
|
|
34
|
+
>{children}</Hint>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Variant 매트릭스
|
|
38
|
+
variant 축 **없음**. Hint는 "참고 각주" 성격 하나만 — 톤·강도 다 제거해서 가장 조용함.
|
|
39
|
+
|
|
40
|
+
| size | font-size | line-height | icon size | 자리 |
|
|
41
|
+
|---|---|---|---|---|
|
|
42
|
+
| `sm` | `--text-sm` | 1.6 | 16 | 섹션 안 각주 (기본) |
|
|
43
|
+
| `md` | `--text-base` | 1.6 | 20 | 페이지 폭 참고 블록 |
|
|
44
|
+
|
|
45
|
+
### 조합 예시
|
|
46
|
+
```tsx
|
|
47
|
+
<Hint icon="brackets-square">
|
|
48
|
+
문서 등록은 추출·정리까지 자동 처리합니다. AI 작업 계획을 요청하고 승인한 뒤 실행을 요청하면,
|
|
49
|
+
연결된 실행기가 코드 변경안과 테스트 결과를 작성합니다.
|
|
50
|
+
</Hint>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 구현 노트 (벤더 코드에만 적용)
|
|
54
|
+
아이콘 수직 정렬이 **반복 함정**이라 명시:
|
|
55
|
+
- flex `align-items: center/flex-start` + `margin-top` 보정 조합은 **금지** — line-height 바뀔 때마다 어긋남
|
|
56
|
+
- 내부 구현은 **`display: inline` + `vertical-align: text-bottom`** (또는 유사 line-box 정렬) 조합으로 자동 정렬
|
|
57
|
+
- line-height는 `1.6` 고정 (muted 다줄 가독성 — [[tokens/spacing]] "텍스트 gap·line-height" 기본 1.5보다 한 눈금 높음)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Kanban
|
|
2
|
+
|
|
3
|
+
### 원칙
|
|
4
|
+
워크플로우를 **상태(컬럼)와 아이템(카드)의 2차원 격자**로 보여주는 자리. 가로축은 진행 단계(todo·in-progress·review·done 등), 세로축은 각 단계 안의 큐. 3-레벨 compound로 분리되어 보드 폭·컬럼 뼈대·카드 내용이 각자 축을 가진다.
|
|
5
|
+
|
|
6
|
+
- **Kanban (board)** — 가로 트랙. 컬럼 width·간격·스크롤 여부만 담당.
|
|
7
|
+
- **Kanban.Column** — 한 상태의 세로 스택. chrome(배경 vs 테두리) × layout(한 블록 vs 두 블록) × accent(상태색 위치) × tone(6종)으로 조립.
|
|
8
|
+
- **Kanban.Card** — 아이템 1장. slot(Cover/Header/Title/Description/Meta/Footer) 조합으로 자유롭게.
|
|
9
|
+
|
|
10
|
+
DnD는 **이 컴포넌트가 품지 않는다** — dnd-kit은 호출부가 바깥에서 감싸는 opt-in 패턴. 시각이 먼저, 상호작용은 선택.
|
|
11
|
+
|
|
12
|
+
### Do
|
|
13
|
+
- 컬럼은 **같은 chrome · 같은 layout으로 통일**한다. 한 보드 안에 line·filled·plain, wrap·stack을 섞지 않는다 — depth 혼란.
|
|
14
|
+
- 상태 색은 `tone` + `accent="dot"`(헤더 title 앞 점) 또는 `chrome="filled"`(bg·fg가 tone 색계열로 동시 변함)로 공급. 컬럼 전체를 진한 bg로 칠하지 않는다 ([[feedback_bg_or_stroke_only]]).
|
|
15
|
+
- `layout="wrap"`(기본)은 header와 카드 스택이 **한 쉘 안에** — 컬럼 하나가 하나의 "그릇". 가장 흔한 칸반 모습.
|
|
16
|
+
- `layout="stack"`은 **header만 chrome**을 가지고 그 아래 카드 스택은 chrome 없이 떨어져 쌓이는 "얹어진" 레이아웃. 카드가 그릇에 갇히지 않고 자유롭게 숨 쉬는 느낌. header에 action이 많거나 카드 영역을 더 넓게 쓰고 싶을 때.
|
|
17
|
+
- 카드는 Column 안에 **자유 조합**: 가장 작은 카드는 `Title`만, 가장 큰 카드는 `Cover` + `Header` + `Title` + `Description` + `Meta` + `Footer` 전부.
|
|
18
|
+
- 보드가 뷰포트보다 넓으면 `scroll` 기본 — 세로 wrap하지 않는다.
|
|
19
|
+
- Column의 count·action은 **헤더 우측**에 자동 배치됨. `action`은 ReactNode라 IconButton 여러 개(`+` · 설정 등)를 Fragment로 넘겨도 됨.
|
|
20
|
+
- `layout="stack"`에선 **컬럼별로 chrome을 섞어 쓰기** 자연스러움 — 특정 컬럼만 filled로 강조하고 나머지는 line으로 조용히 두는 식. `layout="wrap"`에선 chrome 통일.
|
|
21
|
+
|
|
22
|
+
### Don't
|
|
23
|
+
- [HARD] 카드 안에 다시 Card chrome(추가 border/shadow) 쌓지 않는다 — "컬럼 안 카드 안 카드"는 depth 깨짐.
|
|
24
|
+
- [HARD] 컬럼에 "안 연한 배경 + 테두리" 조합 금지 — chrome은 `line`(테두리만) · `filled`(배경만) · `plain`(둘 다 없음) 중 하나.
|
|
25
|
+
- [HARD] 컬럼 title을 UPPERCASE로 강제하지 않는다 — [[feedback_no_uppercase]].
|
|
26
|
+
- [HARD] 상태 색을 **진한 bg로 칠하지 않는다** — tone 색은 chrome=filled일 때 subtle bg(약 8% opacity) + 같은 색 fg 조합으로만. 신호가 더 조용해야 하면 chrome=line/plain + accent="dot".
|
|
27
|
+
- `layout="stack"`에서 body(카드 스택)에 chrome(배경·테두리)을 추가하지 않는다 — stack의 핵심은 "카드가 그릇에서 풀려남". chrome 축은 header에만 영향.
|
|
28
|
+
- 보드 전체를 `<section>`으로 감싸지 않는다 — [[feedback_no_semantic_tags_in_components]].
|
|
29
|
+
|
|
30
|
+
### Fallback
|
|
31
|
+
- 애매하면 `chrome="line"` + `layout="wrap"` + `accent="none"`. 가장 조용한 조합.
|
|
32
|
+
- 상태 신호를 강하게 주고 싶으면 `chrome="filled"` + `tone=…` (bg·fg 둘 다 그 색계열로).
|
|
33
|
+
- header에 action(검색·필터·추가 버튼 등)이 많아져 복잡해지면 `layout="stack"`로 분리.
|
|
34
|
+
|
|
35
|
+
### 관련
|
|
36
|
+
- [[components/list-item]] — 세로 1열 리스트. 칸반 카드는 리스트 아이템을 그리드로 재구성한 것.
|
|
37
|
+
- [[components/metric-card]] — 카드 chrome 규칙(그림자 금지, line 기본) 공유.
|
|
38
|
+
- [[components/badge]] — 카드 Header의 상태·우선순위 라벨.
|
|
39
|
+
- [[components/avatar]] — 카드 Footer의 담당자.
|
|
40
|
+
- [[components/icon-button]] — 컬럼 action, 카드 메뉴.
|
|
41
|
+
- [[tokens/color]] — `--bg` · `--bg-muted` · `--border` · `--primary` · `--success` · `--warning` · `--destructive` · `--info`.
|
|
42
|
+
- [[feedback_bg_or_stroke_only]] — 컬럼 chrome 설계의 핵심.
|
|
43
|
+
- [[feedback_no_hardcoded_values]] — columnWidth도 토큰/계산값 권장.
|
|
44
|
+
|
|
45
|
+
### Props 시그니처
|
|
46
|
+
```tsx
|
|
47
|
+
<Kanban
|
|
48
|
+
columnWidth={string} // CSS 길이. 기본 var(--space-80) = 320px
|
|
49
|
+
gap={string} // 컬럼 사이 간격. 기본 var(--space-3)
|
|
50
|
+
scroll={boolean} // 가로 스크롤 on/off. 기본 true
|
|
51
|
+
>
|
|
52
|
+
<Kanban.Column
|
|
53
|
+
title={ReactNode}
|
|
54
|
+
count={number | string}
|
|
55
|
+
action={ReactNode}
|
|
56
|
+
footer={ReactNode}
|
|
57
|
+
chrome="line | filled | plain" // 기본 line
|
|
58
|
+
layout="wrap | stack" // 기본 wrap
|
|
59
|
+
accent="none | dot" // 기본 none
|
|
60
|
+
tone="neutral | primary | success | warning | danger | info"
|
|
61
|
+
>
|
|
62
|
+
<Kanban.Card href={string}>
|
|
63
|
+
<Kanban.Card.Cover>…</Kanban.Card.Cover>
|
|
64
|
+
<Kanban.Card.Header>…</Kanban.Card.Header>
|
|
65
|
+
<Kanban.Card.Title>…</Kanban.Card.Title>
|
|
66
|
+
<Kanban.Card.Description>…</Kanban.Card.Description>
|
|
67
|
+
<Kanban.Card.Meta>…</Kanban.Card.Meta>
|
|
68
|
+
<Kanban.Card.Footer>…</Kanban.Card.Footer>
|
|
69
|
+
</Kanban.Card>
|
|
70
|
+
</Kanban.Column>
|
|
71
|
+
</Kanban>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### chrome 매트릭스
|
|
75
|
+
| chrome | 배경 | 테두리 | 쓰임 |
|
|
76
|
+
|---|---|---|---|
|
|
77
|
+
| `line` (기본) | 투명 | 1px border | 조용한 보드. 카드가 주연. |
|
|
78
|
+
| `filled` | tone-subtle(없으면 bg-muted) | 없음 | 컬럼 자체가 "그릇"임을 강조. tone 하나가 bg·header fg 둘 다 결정(같은 색을 opacity로만 조절). |
|
|
79
|
+
| `plain` | 투명 | 없음 | chrome 없이 scroll만. 미니 보드·임베드. |
|
|
80
|
+
|
|
81
|
+
### layout 매트릭스
|
|
82
|
+
| layout | 모양 | 쓰임 |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `wrap` (기본) | header와 카드 스택이 한 쉘 안에. 컬럼 하나 = 한 그릇. | 일반 칸반. 가장 흔함. |
|
|
85
|
+
| `stack` | header만 chrome. 그 아래 카드 스택은 chrome 없이 떨어져 쌓임. | 카드가 그릇에서 풀려 숨쉬는 느낌. header 중심 보드. |
|
|
86
|
+
|
|
87
|
+
### accent 매트릭스
|
|
88
|
+
| accent | 신호 위치 | 쓰임 |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `none` | 없음 | 상태 색 신호 불필요 |
|
|
91
|
+
| `dot` | 헤더 title 앞 | 조용한 색 신호. 다수 컬럼 구분. |
|
|
92
|
+
|
|
93
|
+
### 조합 예시
|
|
94
|
+
|
|
95
|
+
**가장 작은 카드 — 제목만**
|
|
96
|
+
```tsx
|
|
97
|
+
<Kanban>
|
|
98
|
+
<Kanban.Column title="To Do" count={3}>
|
|
99
|
+
<Kanban.Card>
|
|
100
|
+
<Kanban.Card.Title>디자인 토큰 정리</Kanban.Card.Title>
|
|
101
|
+
</Kanban.Card>
|
|
102
|
+
</Kanban.Column>
|
|
103
|
+
</Kanban>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**표준 — 제목 + 메타 + 담당자**
|
|
107
|
+
```tsx
|
|
108
|
+
<Kanban.Card href={`/tasks/${t.id}`}>
|
|
109
|
+
<Kanban.Card.Header>
|
|
110
|
+
<Badge variant="soft" status="warning">검토 필요</Badge>
|
|
111
|
+
</Kanban.Card.Header>
|
|
112
|
+
<Kanban.Card.Title>사이드바 리팩터 PR 리뷰</Kanban.Card.Title>
|
|
113
|
+
<Kanban.Card.Meta>
|
|
114
|
+
<Icon name="clock" size={14} /> 금요일 마감
|
|
115
|
+
</Kanban.Card.Meta>
|
|
116
|
+
<Kanban.Card.Footer>
|
|
117
|
+
<AvatarGroup size="xs"><Avatar name="박지훈"/><Avatar name="이수민"/></AvatarGroup>
|
|
118
|
+
<span>3 댓글</span>
|
|
119
|
+
</Kanban.Card.Footer>
|
|
120
|
+
</Kanban.Card>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**얹어진 header — layout=stack + chrome=filled**
|
|
124
|
+
```tsx
|
|
125
|
+
<Kanban>
|
|
126
|
+
<Kanban.Column
|
|
127
|
+
title="진행 중"
|
|
128
|
+
count={5}
|
|
129
|
+
action={<IconButton variant="ghost" size="md" icon={<Icon name="plus" />} aria-label="추가" />}
|
|
130
|
+
chrome="filled"
|
|
131
|
+
layout="stack"
|
|
132
|
+
tone="info"
|
|
133
|
+
>
|
|
134
|
+
{cards.map((c) => (…))}
|
|
135
|
+
</Kanban.Column>
|
|
136
|
+
</Kanban>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**색 신호 — chrome=filled + tone 하나로**
|
|
140
|
+
```tsx
|
|
141
|
+
<Kanban>
|
|
142
|
+
<Kanban.Column title="To Do" chrome="filled" tone="neutral" count={5} />
|
|
143
|
+
<Kanban.Column title="In Progress" chrome="filled" tone="info" count={3} />
|
|
144
|
+
<Kanban.Column title="Review" chrome="filled" tone="warning" count={2} />
|
|
145
|
+
<Kanban.Column title="Done" chrome="filled" tone="success" count={12} />
|
|
146
|
+
</Kanban>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**DnD opt-in — dnd-kit로 바깥 감싸기**
|
|
150
|
+
```tsx
|
|
151
|
+
import { DndContext } from "@dnd-kit/core";
|
|
152
|
+
import { SortableContext, useSortable } from "@dnd-kit/sortable";
|
|
153
|
+
|
|
154
|
+
function SortableCard({ id, children }) {
|
|
155
|
+
const { attributes, listeners, setNodeRef, transform } = useSortable({ id });
|
|
156
|
+
return (
|
|
157
|
+
<Kanban.Card
|
|
158
|
+
ref={setNodeRef}
|
|
159
|
+
style={{ transform: CSS.Transform.toString(transform) }}
|
|
160
|
+
{...attributes}
|
|
161
|
+
{...listeners}
|
|
162
|
+
>
|
|
163
|
+
{children}
|
|
164
|
+
</Kanban.Card>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
```
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Page Header
|
|
2
|
+
|
|
3
|
+
### 원칙
|
|
4
|
+
페이지 body의 **첫 블록**. "이 페이지가 뭔지"를 담는 자리. `h1`·eyebrow·lead·메타·페이지 액션이 여기 모인다. shell의 chrome인 [[components/topbar]]와는 역할이 다르다 — Topbar는 '지금 어디(breadcrumb)·공용 액션'을 담는 **얇은 축**이고, PageHeader는 '이 페이지의 content head'다.
|
|
5
|
+
|
|
6
|
+
레이아웃은 CSS Grid 2행 2열. Title과 Actions는 **같은 가로줄**, Eyebrow·Lead·Meta는 그 위아래로 수직. Actions가 없으면 Title이 혼자 그 줄을 차지한다. 자식 slot은 자유롭게 생략 가능 — 빈 slot은 자리를 먹지 않는다.
|
|
7
|
+
|
|
8
|
+
### Do
|
|
9
|
+
- 페이지당 **하나만** 둔다. `h1`은 PageHeader.Title이 담당하므로 중복 금지.
|
|
10
|
+
- body의 **첫 섹션 바로 앞**에 둔다. Topbar 바로 아래, 또는 shell 없는 페이지라면 콘텐츠 영역 최상단.
|
|
11
|
+
- Title + Lead 조합이 기본. 더 담고 싶을 때만 Eyebrow·Meta·Actions을 추가한다.
|
|
12
|
+
- Actions는 Title과 짝. 보통 primary + outline 두 개 ([[feedback_button_variant_defaults]]). overflow·share 같은 보조 액션은 ghost [[components/icon-button]]을 섞어 쓴다 — Button은 왼쪽, **IconButton은 더 오른쪽 끝**에 둔다(라벨이 있는 쪽이 선, 아이콘 전용이 뒤). IconButton은 **한 단계 크게**(Button이 md면 `size="lg"`) — Button 옆에 두면 정사각형이 좁아 보여서 균형을 맞춰야 한다.
|
|
13
|
+
- Meta 안의 [[components/badge]]는 `variant="soft"` 연한 톤이 기본 — 본문 리듬에 묻어가도록. solid는 상태를 강조해야 할 때만.
|
|
14
|
+
- size는 기본 `md`. 설정·서브페이지는 `sm`, 랜딩·docs 인덱스는 `lg`.
|
|
15
|
+
|
|
16
|
+
### Don't
|
|
17
|
+
- Breadcrumb slot을 두지 않는다 — "지금 어디"는 [[components/topbar]]의 몫. shell이 없는 페이지에서도 Topbar를 두고 그 안에 Breadcrumb을 넣는다.
|
|
18
|
+
- Topbar **안에** 넣지 않는다 — Topbar의 "얇은 축" 약속이 깨진다 ([[components/topbar]]).
|
|
19
|
+
- `<section>` **안에** 넣지 않는다 — 전역 `.content section`이 `padding-top: 80px`를 꽂는다. PageHeader는 section 바깥, body 최상단에 둔다 ([[feedback_no_semantic_tags_in_components]]).
|
|
20
|
+
- 색 배경 + 색 테두리 조합으로 꾸미지 않는다 — 그건 [[components/callout]]의 자리 ([[feedback_bg_or_stroke_only]]).
|
|
21
|
+
- Eyebrow에 uppercase + wide letter-spacing 조합 금지 ([[feedback_no_uppercase]]).
|
|
22
|
+
- Lead에 em dash·ellipsis 금지 ([[feedback_no_em_dash]]).
|
|
23
|
+
- 페이지에 두 개 쓰지 않는다. 하위 섹션 head는 `<h2>`로.
|
|
24
|
+
- 데이터 요약(KPI·숫자·차트)을 PageHeader에 끼우지 않는다 — body 첫 섹션의 [[components/metric-card]] 자리.
|
|
25
|
+
|
|
26
|
+
### Fallback
|
|
27
|
+
size 애매하면 `md`. 글로벌 유틸 `.page-title`(4xl)보다 두 단계 작은 `2xl` — PageHeader가 챙기는 수직 여백/eyebrow/meta와 어우러져 "과하지 않은 head"가 되도록 톤 다운한 값이다.
|
|
28
|
+
Title만으로 충분하면 Lead 생략 OK. Lead를 쓸 거면 1~2문장으로 짧게.
|
|
29
|
+
|
|
30
|
+
### 관련
|
|
31
|
+
- [[components/topbar]] — chrome 축. Breadcrumb·공용 액션은 Topbar의 자리, 페이지 head는 PageHeader의 자리.
|
|
32
|
+
- [[components/breadcrumb]] — Topbar 안에서만 쓴다. PageHeader slot에는 없다.
|
|
33
|
+
- [[components/callout]] — "지금 공지해야 할 큰 콘텐츠"는 PageHeader가 아니라 Callout.
|
|
34
|
+
- [[components/badge]] — PageHeader.Meta 안에서 상태 라벨로.
|
|
35
|
+
- [[components/button]] — PageHeader.Actions 안에서. primary + outline 짝이 기본.
|
|
36
|
+
- [[tokens/spacing]] — size별 아래 margin과 row-gap을 결정.
|
|
37
|
+
- [[tokens/typography]] — Title letter-spacing은 -0.03em, size별 font-size는 토큰 경유.
|
|
38
|
+
|
|
39
|
+
### Props 시그니처
|
|
40
|
+
```tsx
|
|
41
|
+
<PageHeader size="sm | md | lg">
|
|
42
|
+
<PageHeader.Eyebrow>Overview</PageHeader.Eyebrow>
|
|
43
|
+
<PageHeader.Title>Page title</PageHeader.Title>
|
|
44
|
+
<PageHeader.Lead>한두 문장 설명.</PageHeader.Lead>
|
|
45
|
+
<PageHeader.Meta>
|
|
46
|
+
<Badge>draft</Badge>
|
|
47
|
+
<span>updated oct 14</span>
|
|
48
|
+
</PageHeader.Meta>
|
|
49
|
+
<PageHeader.Actions>
|
|
50
|
+
<Button variant="outline">share</Button>
|
|
51
|
+
<Button variant="primary">publish</Button>
|
|
52
|
+
</PageHeader.Actions>
|
|
53
|
+
</PageHeader>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| 요소 | 역할 |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `PageHeader` | grid 컨테이너. slot 자리와 세로 리듬을 쥔다 |
|
|
59
|
+
| `PageHeader.Eyebrow` | 선택. 작은 상단 라벨. muted + small |
|
|
60
|
+
| `PageHeader.Title` | 필수. `<h1>` 자동, size별 font-size |
|
|
61
|
+
| `PageHeader.Lead` | 선택. muted 본문, max-width 72ch |
|
|
62
|
+
| `PageHeader.Meta` | 선택. Badge·날짜 등 가로 묶음 |
|
|
63
|
+
| `PageHeader.Actions` | 선택. Title과 같은 줄 오른쪽 pin |
|
|
64
|
+
|
|
65
|
+
### Size 매트릭스
|
|
66
|
+
|
|
67
|
+
| size | Title | Lead | 용도 |
|
|
68
|
+
|---|---|---|---|
|
|
69
|
+
| `sm` | `--text-xl` | `--text-sm` | 설정·서브페이지 head |
|
|
70
|
+
| `md` (기본) | `--text-2xl` | `--text-base` | 일반 페이지 head |
|
|
71
|
+
| `lg` | `--text-3xl` | `--text-base` | 랜딩·docs 인덱스 |
|
|
72
|
+
|
|
73
|
+
**외부 간격**: PageHeader → 다음 섹션 간격은 **상위 wrapper gap이 담당**. 벤더가 margin-bottom으로 공급하지 않음 — margin trick 금지, `display: flex; flex-direction: column; gap: var(--space-6)` 식 group wrapper + gap만 사용 ([[feedback_page_differential_rhythm]], [[feedback_no_double_spacing]]).
|
|
74
|
+
|
|
75
|
+
### 금지 조합
|
|
76
|
+
- Breadcrumb을 PageHeader slot으로 노출 — Topbar만의 자리.
|
|
77
|
+
- `<section>` 안에 삽입 — 전역 padding 충돌.
|
|
78
|
+
- 색 배경 + 색 border — Callout 자리 침범.
|
|
79
|
+
- Title 없이 Lead/Meta만 두기 — head 역할이 성립 안 함.
|
|
80
|
+
- 페이지에 두 번 — `h1` 중복.
|
|
81
|
+
- Actions 안에 세 개 이상 Button — primary + outline 짝이 기본, 더 필요하면 overflow 메뉴로.
|
|
82
|
+
|
|
83
|
+
### 조합 예시
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
// 1) 기본 — Title + Lead만
|
|
87
|
+
<PageHeader>
|
|
88
|
+
<PageHeader.Title>Billing</PageHeader.Title>
|
|
89
|
+
<PageHeader.Lead>구독 플랜과 결제 수단을 관리합니다.</PageHeader.Lead>
|
|
90
|
+
</PageHeader>
|
|
91
|
+
|
|
92
|
+
// 2) 액션 짝 — Title | Actions 같은 줄 자동 분리
|
|
93
|
+
<PageHeader>
|
|
94
|
+
<PageHeader.Title>Projects</PageHeader.Title>
|
|
95
|
+
<PageHeader.Lead>진행 중인 프로젝트와 아카이브를 모아 봅니다.</PageHeader.Lead>
|
|
96
|
+
<PageHeader.Actions>
|
|
97
|
+
<Button variant="outline">import</Button>
|
|
98
|
+
<Button variant="primary">new project</Button>
|
|
99
|
+
</PageHeader.Actions>
|
|
100
|
+
</PageHeader>
|
|
101
|
+
|
|
102
|
+
// 2-b) IconButton + primary — Button이 왼쪽, IconButton들이 더 오른쪽 끝.
|
|
103
|
+
// IconButton은 한 단계 크게(size="lg") — Button md와 시각적 균형.
|
|
104
|
+
<PageHeader>
|
|
105
|
+
<PageHeader.Title>Documents</PageHeader.Title>
|
|
106
|
+
<PageHeader.Lead>팀 문서와 공유 링크를 관리합니다.</PageHeader.Lead>
|
|
107
|
+
<PageHeader.Actions>
|
|
108
|
+
<Button variant="primary">new doc</Button>
|
|
109
|
+
<IconButton size="lg" icon={<Icon name="share-network" />} aria-label="share" />
|
|
110
|
+
<IconButton size="lg" icon={<Icon name="dots-three" />} aria-label="more" />
|
|
111
|
+
</PageHeader.Actions>
|
|
112
|
+
</PageHeader>
|
|
113
|
+
|
|
114
|
+
// 3) 메타 담긴 상세 head — Badge·날짜
|
|
115
|
+
<PageHeader>
|
|
116
|
+
<PageHeader.Eyebrow>Spec</PageHeader.Eyebrow>
|
|
117
|
+
<PageHeader.Title>Checkout flow v2</PageHeader.Title>
|
|
118
|
+
<PageHeader.Lead>결제 단계의 폼 밀도와 오류 복구를 개선합니다.</PageHeader.Lead>
|
|
119
|
+
<PageHeader.Meta>
|
|
120
|
+
<Badge variant="soft" status="warning">draft</Badge>
|
|
121
|
+
<span>updated oct 14</span>
|
|
122
|
+
<span>heeyoung</span>
|
|
123
|
+
</PageHeader.Meta>
|
|
124
|
+
<PageHeader.Actions>
|
|
125
|
+
<Button variant="outline">share</Button>
|
|
126
|
+
<Button variant="primary">publish</Button>
|
|
127
|
+
</PageHeader.Actions>
|
|
128
|
+
</PageHeader>
|
|
129
|
+
|
|
130
|
+
// 4) 설정 서브페이지 — size=sm
|
|
131
|
+
<PageHeader size="sm">
|
|
132
|
+
<PageHeader.Title>API keys</PageHeader.Title>
|
|
133
|
+
<PageHeader.Lead>개발자 토큰을 발급하고 폐기합니다.</PageHeader.Lead>
|
|
134
|
+
</PageHeader>
|
|
135
|
+
|
|
136
|
+
// 5) 랜딩·docs 인덱스 — size=lg
|
|
137
|
+
<PageHeader size="lg">
|
|
138
|
+
<PageHeader.Eyebrow>CTDS</PageHeader.Eyebrow>
|
|
139
|
+
<PageHeader.Title>디자인 시스템 하나로 끝</PageHeader.Title>
|
|
140
|
+
<PageHeader.Lead>8축 토큰 스왑으로 브랜드·밀도·형태를 즉시 바꿉니다.</PageHeader.Lead>
|
|
141
|
+
<PageHeader.Actions>
|
|
142
|
+
<Button variant="outline">문서 보기</Button>
|
|
143
|
+
<Button variant="primary">시작하기</Button>
|
|
144
|
+
</PageHeader.Actions>
|
|
145
|
+
</PageHeader>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 조립 — 상위 shell과의 공식
|
|
149
|
+
- shell 안 페이지: Topbar(size `md`) 바로 아래, body 콘텐츠 영역의 **첫 블록**.
|
|
150
|
+
- Topbar의 Breadcrumb과 PageHeader의 Title은 **다른 자리**다. "지금 어디(breadcrumb)" + "이 페이지(title)"는 쌍으로 들어가 되 **중복되지 않는다**.
|
|
151
|
+
- 아래 간격은 **상위 wrapper gap**이 쥔다. PageHeader는 margin 공급 안 함, 다음 섹션에서 `padding-top`을 또 걸지 않는다 ([[feedback_no_double_spacing]], [[feedback_page_differential_rhythm]]).
|
|
152
|
+
|
|
153
|
+
### Known Gaps
|
|
154
|
+
- 긴 Title의 반응형 축약 규칙은 없다. 자유롭게 줄바꿈. 필요하면 호출처가 `max-width`를 override.
|
|
155
|
+
- 다국어/RTL은 grid 자체가 flow 따라감 (`margin-inline`·`justify-items`로 대응).
|