@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.
@@ -0,0 +1,339 @@
1
+ /*
2
+ * Kanban — 보드 / 컬럼 / 카드 3-레벨.
3
+ *
4
+ * CSS 변수 조절점:
5
+ * --kanban-column-width 컬럼 1장 width
6
+ * --kanban-column-gap 컬럼 사이 가로 간격
7
+ * --kanban-column-pad 컬럼 내부 padding
8
+ * --kanban-card-pad 카드 내부 padding
9
+ * --kanban-card-gap 카드 안 slot 세로 gap
10
+ * --kanban-stack-gap 한 컬럼 안 카드 세로 gap
11
+ *
12
+ * layout 축:
13
+ * wrap — 컬럼 자체가 chrome(배경/테두리)을 가지고 header·body가 안에 함께 들어감.
14
+ * stack — 컬럼 자체엔 chrome 없고 header와 body가 각자 자체 chrome을 가진
15
+ * 두 블록으로 세로 분리. header가 chrome 바깥 위에 "얹어진" 모습.
16
+ */
17
+
18
+ /* ──────────────────────────────────────────────────────
19
+ Board
20
+ ──────────────────────────────────────────────────── */
21
+
22
+ .board {
23
+ /* Kanban 컬럼 전용 폭 — 공용 space 스케일에 등가 토큰 없음, 로컬 상수로 선언. */
24
+ --kanban-column-width: 320px; /* lint-hardcoded: ignore */
25
+ --kanban-column-gap: var(--space-3);
26
+ --kanban-column-pad: var(--space-3);
27
+ --kanban-stack-gap: var(--space-2);
28
+
29
+ display: flex;
30
+ gap: var(--kanban-column-gap);
31
+ align-items: stretch;
32
+ min-width: 0;
33
+ }
34
+
35
+ .board.scroll {
36
+ overflow-x: auto;
37
+ overflow-y: hidden;
38
+ padding-bottom: var(--space-1);
39
+ }
40
+
41
+ /* scroll=false: 가로 스크롤 끄되 wrap하지 않음. 칸반은 무조건 로우. */
42
+ .board.noscroll {
43
+ overflow: visible;
44
+ }
45
+
46
+ /* ──────────────────────────────────────────────────────
47
+ Column — 공통 뼈대
48
+ ──────────────────────────────────────────────────── */
49
+
50
+ .column {
51
+ position: relative;
52
+ flex: 0 0 var(--kanban-column-width);
53
+ width: var(--kanban-column-width);
54
+ max-width: 100%;
55
+ min-width: 0;
56
+ display: flex;
57
+ flex-direction: column;
58
+ color: var(--fg);
59
+ }
60
+
61
+ /* scroll=false면 각 컬럼이 남는 공간을 균등 분할. 로우 유지. */
62
+ .board.noscroll .column {
63
+ flex: 1 1 var(--kanban-column-width);
64
+ }
65
+
66
+ /* ──────────────────────────────────────────────────────
67
+ layout=wrap — 컬럼 자체가 chrome 가짐. header/body 공통 쉘.
68
+ ──────────────────────────────────────────────────── */
69
+
70
+ .layout-wrap {
71
+ gap: var(--space-2);
72
+ padding: var(--kanban-column-pad);
73
+ border-radius: var(--radius-lg);
74
+ transition: var(--transition-colors);
75
+ }
76
+
77
+ /* chrome 축 — layout=wrap. [[feedback_bg_or_stroke_only]]: 배경만 or 테두리만. */
78
+ .layout-wrap.chrome-line {
79
+ background: transparent;
80
+ border: 1px solid var(--border);
81
+ }
82
+ .layout-wrap.chrome-filled {
83
+ background: var(--column-fill, var(--bg-muted));
84
+ }
85
+ .layout-wrap.chrome-plain {
86
+ background: transparent;
87
+ padding: 0;
88
+ }
89
+
90
+ /* tone별 fill 색 — chrome=filled일 때 적용됨.
91
+ neutral은 중립 bg-muted(기본값), 나머지는 base color 4% opacity.
92
+ 글로벌 *-subtle(8%)보다 더 연하게 — 컬럼은 "면"이라 subtle보다 조용해야 함. */
93
+ .column[data-tone="primary"] { --column-fill: color-mix(in oklab, var(--primary) 4%, transparent); }
94
+ .column[data-tone="success"] { --column-fill: color-mix(in oklab, var(--success) 4%, transparent); }
95
+ .column[data-tone="warning"] { --column-fill: color-mix(in oklab, var(--warning) 4%, transparent); }
96
+ .column[data-tone="danger"] { --column-fill: color-mix(in oklab, var(--destructive) 4%, transparent); }
97
+ .column[data-tone="info"] { --column-fill: color-mix(in oklab, var(--info) 4%, transparent); }
98
+
99
+ /* chrome=filled일 때 header 안 텍스트·아이콘도 tone 색계열로 (fill 색 full opacity).
100
+ neutral은 기본 fg-strong 유지.
101
+ action 안 IconButton까지 색이 번지도록 자손 svg·button까지 상속. */
102
+ .chrome-filled[data-tone="primary"] .columnHeader,
103
+ .chrome-filled[data-tone="primary"] .columnHeader * { color: var(--primary); }
104
+ .chrome-filled[data-tone="success"] .columnHeader,
105
+ .chrome-filled[data-tone="success"] .columnHeader * { color: var(--success); }
106
+ .chrome-filled[data-tone="warning"] .columnHeader,
107
+ .chrome-filled[data-tone="warning"] .columnHeader * { color: var(--warning); }
108
+ .chrome-filled[data-tone="danger"] .columnHeader,
109
+ .chrome-filled[data-tone="danger"] .columnHeader * { color: var(--destructive); }
110
+ .chrome-filled[data-tone="info"] .columnHeader,
111
+ .chrome-filled[data-tone="info"] .columnHeader * { color: var(--info); }
112
+
113
+ /* ──────────────────────────────────────────────────────
114
+ layout=stack — header만 chrome. body(카드 스택)는 chrome 없음.
115
+ header가 그릇 바깥 위에 "얹어진" 모습 — 카드들은 그 아래 분리된 공간.
116
+ ──────────────────────────────────────────────────── */
117
+
118
+ .layout-stack {
119
+ gap: var(--space-2);
120
+ background: transparent;
121
+ padding: 0;
122
+ }
123
+
124
+ /* stack: header만 chrome 보유 */
125
+ .layout-stack .columnHeader {
126
+ padding: var(--space-2) var(--space-3);
127
+ border-radius: var(--radius-md);
128
+ transition: var(--transition-colors);
129
+ position: relative;
130
+ }
131
+
132
+ /* stack: body는 chrome 없음. 카드 스택만. */
133
+ .layout-stack .columnBody {
134
+ padding: 0;
135
+ }
136
+
137
+ /* chrome 축 — layout=stack: header에만 적용 */
138
+ .layout-stack.chrome-line .columnHeader {
139
+ background: transparent;
140
+ border: 1px solid var(--border);
141
+ }
142
+ .layout-stack.chrome-filled .columnHeader {
143
+ background: var(--column-fill, var(--bg-muted));
144
+ }
145
+ .layout-stack.chrome-plain .columnHeader {
146
+ padding: 0 var(--space-1);
147
+ }
148
+
149
+ /* ──────────────────────────────────────────────────────
150
+ Column header (공통)
151
+ ──────────────────────────────────────────────────── */
152
+
153
+ .columnHeader {
154
+ display: flex;
155
+ align-items: center;
156
+ gap: var(--space-2);
157
+ min-width: 0;
158
+ padding-inline: var(--space-1);
159
+ }
160
+
161
+ .columnTitle {
162
+ font-size: var(--text-base);
163
+ font-weight: var(--font-weight-medium);
164
+ color: var(--fg-strong, var(--fg));
165
+ line-height: var(--lh-base);
166
+ min-width: 0;
167
+ overflow: hidden;
168
+ text-overflow: ellipsis;
169
+ white-space: nowrap;
170
+ }
171
+
172
+ .columnCount {
173
+ font-size: var(--text-xs);
174
+ color: var(--fg-muted);
175
+ font-variant-numeric: tabular-nums;
176
+ flex-shrink: 0;
177
+ margin-left: calc(var(--space-2) * -1 + var(--space-1));
178
+ margin-right: auto;
179
+ }
180
+
181
+ .columnAction {
182
+ display: inline-flex;
183
+ align-items: center;
184
+ gap: var(--space-1);
185
+ flex-shrink: 0;
186
+ color: var(--fg-muted);
187
+ }
188
+
189
+ /* ──────────────────────────────────────────────────────
190
+ Accent (tone 색 지점)
191
+ ──────────────────────────────────────────────────── */
192
+
193
+ .accentMark {
194
+ flex-shrink: 0;
195
+ background: var(--column-accent, var(--fg-muted));
196
+ }
197
+
198
+ .accentMark[data-accent="dot"] {
199
+ width: var(--space-2);
200
+ height: var(--space-2);
201
+ border-radius: var(--radius-full);
202
+ }
203
+
204
+ .column[data-tone="neutral"] { --column-accent: var(--fg-muted); }
205
+ .column[data-tone="primary"] { --column-accent: var(--primary); }
206
+ .column[data-tone="success"] { --column-accent: var(--success); }
207
+ .column[data-tone="warning"] { --column-accent: var(--warning); }
208
+ .column[data-tone="danger"] { --column-accent: var(--destructive); }
209
+ .column[data-tone="info"] { --column-accent: var(--info); }
210
+
211
+ /* ──────────────────────────────────────────────────────
212
+ Column body (카드 스택)
213
+ ──────────────────────────────────────────────────── */
214
+
215
+ .columnBody {
216
+ display: flex;
217
+ flex-direction: column;
218
+ gap: var(--kanban-stack-gap);
219
+ min-height: var(--space-8);
220
+ border-radius: var(--radius-md);
221
+ transition: var(--transition-colors);
222
+ }
223
+
224
+
225
+ .columnFooter {
226
+ padding-top: var(--space-1);
227
+ color: var(--fg-muted);
228
+ }
229
+
230
+ /* ──────────────────────────────────────────────────────
231
+ Card
232
+ ──────────────────────────────────────────────────── */
233
+
234
+ .card {
235
+ --kanban-card-pad: var(--space-3);
236
+ --kanban-card-gap: var(--space-2);
237
+
238
+ display: flex;
239
+ flex-direction: column;
240
+ gap: var(--kanban-card-gap);
241
+ padding: var(--kanban-card-pad);
242
+ background: color-mix(in oklab, var(--bg) 80%, transparent);
243
+ border-radius: var(--radius-md);
244
+ color: var(--fg);
245
+ text-decoration: none;
246
+ min-width: 0;
247
+ transition: var(--transition-colors), transform var(--duration-base) var(--ease-out),
248
+ box-shadow var(--duration-base) var(--ease-out);
249
+ }
250
+
251
+ .card:hover {
252
+ background: var(--bg);
253
+ border-color: var(--border-strong);
254
+ transform: translateY(calc(var(--space-0_5) * -1));
255
+ box-shadow: var(--shadow-md);
256
+ }
257
+
258
+ .cardInteractive {
259
+ cursor: pointer;
260
+ }
261
+ .cardInteractive:focus-visible {
262
+ outline: none;
263
+ box-shadow: var(--ring-shadow);
264
+ }
265
+
266
+ /* ── Card slots ────────────────────────────────────── */
267
+
268
+ .cover {
269
+ margin: calc(var(--kanban-card-pad) * -1) calc(var(--kanban-card-pad) * -1) 0;
270
+ border-top-left-radius: inherit;
271
+ border-top-right-radius: inherit;
272
+ overflow: hidden;
273
+ display: block;
274
+ }
275
+ .cover > img,
276
+ .cover > video {
277
+ display: block;
278
+ width: 100%;
279
+ height: auto;
280
+ }
281
+
282
+ .cardHeader {
283
+ display: flex;
284
+ align-items: center;
285
+ gap: var(--space-2);
286
+ min-width: 0;
287
+ color: var(--fg-muted);
288
+ }
289
+
290
+ .cardTitle {
291
+ font-size: var(--text-sm);
292
+ line-height: var(--lh-sm);
293
+ color: var(--fg-strong, var(--fg));
294
+ font-weight: var(--font-weight-medium);
295
+ min-width: 0;
296
+ display: flex;
297
+ flex-wrap: wrap;
298
+ align-items: center;
299
+ gap: var(--space-2);
300
+ }
301
+
302
+ .cardDescription {
303
+ margin: 0;
304
+ font-size: var(--text-sm);
305
+ line-height: var(--lh-sm);
306
+ color: var(--fg-muted);
307
+ font-weight: var(--font-weight-regular);
308
+ display: -webkit-box;
309
+ -webkit-line-clamp: 3;
310
+ -webkit-box-orient: vertical;
311
+ overflow: hidden;
312
+ }
313
+
314
+ /* title 바로 뒤의 description — 둘은 한 묶음으로 간주해 gap 좁힘.
315
+ card의 기본 gap(space-2)에서 space-1만큼 당겨 space-1(=4px)로 유지. */
316
+ .cardTitle + .cardDescription {
317
+ margin-top: calc(var(--space-1) * -1);
318
+ }
319
+
320
+ .cardMeta {
321
+ display: flex;
322
+ align-items: center;
323
+ flex-wrap: wrap;
324
+ gap: var(--space-2);
325
+ font-size: var(--text-xs);
326
+ line-height: var(--lh-sm);
327
+ color: var(--fg-subtle);
328
+ font-variant-numeric: tabular-nums;
329
+ }
330
+
331
+ .cardFooter {
332
+ display: flex;
333
+ align-items: center;
334
+ justify-content: space-between;
335
+ gap: var(--space-2);
336
+ color: var(--fg-muted);
337
+ font-size: var(--text-xs);
338
+ line-height: var(--lh-sm);
339
+ }
@@ -0,0 +1,371 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Kanban — 가로 트랙(Board) + 컬럼(Column) + 카드(Card)의 3-레벨 compound.
5
+ *
6
+ * 레벨별 축:
7
+ * Kanban (board)
8
+ * columnWidth — 컬럼 1장 width. 기본 var(--kanban-column-width) = 320px
9
+ * gap — 컬럼 사이 가로 간격. 기본 space-3
10
+ * scroll — 가로 스크롤 on/off. 기본 true. false면 flex-wrap으로 반응형.
11
+ *
12
+ * Kanban.Column
13
+ * chrome — line (border) | filled (bg-muted) | plain (chrome 없음)
14
+ * accent — none | dot | bar. 상태 색 신호 위치.
15
+ * tone — neutral | primary | success | warning | danger | info
16
+ * * layout — stack (기본) | wrap
17
+ * · stack(권장): header만 자체 chrome을 가지고 카드 스택은
18
+ * chrome 없이 그 아래 자유롭게 쌓이는 "얹어진" 형태.
19
+ * · wrap(비권장): header와 카드가 한 쉘 안에 함께 들어있는
20
+ * "감싸는" 형태. 전통적 칸반이지만 카드가 그릇에 갇혀 답답함.
21
+ *
22
+ * Kanban.Card
23
+ * content은 Card.Cover · Card.Header · Card.Title · Card.Description
24
+ * · Card.Meta · Card.Footer slot으로 자유 조립.
25
+ * interactive — href/onClick 자동 hover (ListItem과 동일)
26
+ *
27
+ * DnD는 호출부가 바깥에서 dnd-kit로 감싸는 opt-in. 시각 먼저, 상호작용은 선택.
28
+ */
29
+
30
+ import {
31
+ forwardRef,
32
+ type AnchorHTMLAttributes,
33
+ type HTMLAttributes,
34
+ type ReactNode,
35
+ type Ref,
36
+ } from "react";
37
+
38
+ import styles from "./Kanban.module.css";
39
+
40
+ /* ──────────────────────────────────────────────────────
41
+ Kanban (board)
42
+ ──────────────────────────────────────────────────── */
43
+
44
+ export type KanbanProps = Omit<HTMLAttributes<HTMLDivElement>, "children"> & {
45
+ /** 컬럼 1장 width. CSS 길이 문자열(e.g. "280px", "320px"). */
46
+ columnWidth?: string;
47
+ /** 컬럼 사이 가로 간격. CSS 길이. 기본 var(--space-3). */
48
+ gap?: string;
49
+ /** 가로 스크롤. 기본 true. false면 flex-wrap으로 반응형. */
50
+ scroll?: boolean;
51
+ children?: ReactNode;
52
+ };
53
+
54
+ const KanbanRoot = forwardRef<HTMLDivElement, KanbanProps>(function Kanban(
55
+ { columnWidth, gap, scroll = true, className, style, children, ...rest },
56
+ ref,
57
+ ) {
58
+ const rootClass = [
59
+ styles.board,
60
+ scroll ? styles.scroll : styles.noscroll,
61
+ className ?? "",
62
+ ]
63
+ .filter(Boolean)
64
+ .join(" ");
65
+
66
+ const cssVars: Record<string, string> = {};
67
+ if (columnWidth) cssVars["--kanban-column-width"] = columnWidth;
68
+ if (gap) cssVars["--kanban-column-gap"] = gap;
69
+
70
+ return (
71
+ <div
72
+ ref={ref}
73
+ className={rootClass}
74
+ style={{ ...cssVars, ...style }}
75
+ {...rest}
76
+ >
77
+ {children}
78
+ </div>
79
+ );
80
+ });
81
+ KanbanRoot.displayName = "Kanban";
82
+
83
+ /* ──────────────────────────────────────────────────────
84
+ Column
85
+ ──────────────────────────────────────────────────── */
86
+
87
+ export type KanbanColumnChrome = "line" | "filled" | "plain";
88
+ export type KanbanColumnAccent = "none" | "dot";
89
+ export type KanbanColumnTone =
90
+ | "neutral"
91
+ | "primary"
92
+ | "success"
93
+ | "warning"
94
+ | "danger"
95
+ | "info";
96
+ export type KanbanColumnLayout = "wrap" | "stack";
97
+
98
+ export type KanbanColumnProps = Omit<HTMLAttributes<HTMLElement>, "title"> & {
99
+ title?: ReactNode;
100
+ count?: number | string;
101
+ action?: ReactNode;
102
+ footer?: ReactNode;
103
+ chrome?: KanbanColumnChrome;
104
+ accent?: KanbanColumnAccent;
105
+ tone?: KanbanColumnTone;
106
+ layout?: KanbanColumnLayout;
107
+ children?: ReactNode;
108
+ };
109
+
110
+ const Column = forwardRef<HTMLElement, KanbanColumnProps>(function KanbanColumn(
111
+ {
112
+ title,
113
+ count,
114
+ action,
115
+ footer,
116
+ chrome = "line",
117
+ accent = "none",
118
+ tone = "neutral",
119
+ layout = "stack",
120
+ className,
121
+ children,
122
+ ...rest
123
+ },
124
+ ref,
125
+ ) {
126
+ const cls = [
127
+ styles.column,
128
+ styles[`layout-${layout}`],
129
+ styles[`chrome-${chrome}`],
130
+ styles[`accent-${accent}`],
131
+ className ?? "",
132
+ ]
133
+ .filter(Boolean)
134
+ .join(" ");
135
+
136
+ const showHeader =
137
+ title !== undefined ||
138
+ count !== undefined ||
139
+ accent !== "none" ||
140
+ action !== undefined;
141
+
142
+ return (
143
+ <div
144
+ ref={ref as Ref<HTMLDivElement>}
145
+ className={cls}
146
+ data-tone={tone}
147
+ {...(rest as HTMLAttributes<HTMLDivElement>)}
148
+ >
149
+ {showHeader && (
150
+ <div className={styles.columnHeader}>
151
+ {accent !== "none" && (
152
+ <span className={styles.accentMark} aria-hidden data-accent={accent} />
153
+ )}
154
+ {title !== undefined && (
155
+ <span className={styles.columnTitle}>{title}</span>
156
+ )}
157
+ {count !== undefined && (
158
+ <span className={styles.columnCount}>{count}</span>
159
+ )}
160
+ {action !== undefined && (
161
+ <span className={styles.columnAction}>{action}</span>
162
+ )}
163
+ </div>
164
+ )}
165
+ <div className={styles.columnBody}>{children}</div>
166
+ {footer !== undefined && (
167
+ <div className={styles.columnFooter}>{footer}</div>
168
+ )}
169
+ </div>
170
+ );
171
+ });
172
+ Column.displayName = "Kanban.Column";
173
+
174
+ /* ──────────────────────────────────────────────────────
175
+ Card
176
+ ──────────────────────────────────────────────────── */
177
+
178
+ type CardCommon = {
179
+ children?: ReactNode;
180
+ };
181
+
182
+ type CardDivProps = CardCommon &
183
+ Omit<HTMLAttributes<HTMLDivElement>, keyof CardCommon> & { href?: undefined };
184
+
185
+ type CardAnchorProps = CardCommon &
186
+ Omit<AnchorHTMLAttributes<HTMLAnchorElement>, keyof CardCommon> & {
187
+ href: string;
188
+ };
189
+
190
+ export type KanbanCardProps = CardDivProps | CardAnchorProps;
191
+
192
+ const Card = forwardRef<HTMLElement, KanbanCardProps>(function KanbanCard(
193
+ props,
194
+ ref,
195
+ ) {
196
+ const {
197
+ className,
198
+ children,
199
+ ...rest
200
+ } = props as CardCommon & { className?: string } & Record<string, unknown>;
201
+
202
+ const isLink = typeof (props as CardAnchorProps).href === "string";
203
+ const isInteractive =
204
+ isLink ||
205
+ typeof (props as HTMLAttributes<HTMLElement>).onClick === "function";
206
+
207
+ const cls = [
208
+ styles.card,
209
+ isInteractive ? styles.cardInteractive : "",
210
+ className ?? "",
211
+ ]
212
+ .filter(Boolean)
213
+ .join(" ");
214
+
215
+ if (isLink) {
216
+ return (
217
+ <a
218
+ ref={ref as Ref<HTMLAnchorElement>}
219
+ className={cls}
220
+ {...(rest as AnchorHTMLAttributes<HTMLAnchorElement>)}
221
+ href={(props as CardAnchorProps).href}
222
+ >
223
+ {children}
224
+ </a>
225
+ );
226
+ }
227
+
228
+ return (
229
+ <div
230
+ ref={ref as Ref<HTMLDivElement>}
231
+ className={cls}
232
+ {...(rest as HTMLAttributes<HTMLDivElement>)}
233
+ >
234
+ {children}
235
+ </div>
236
+ );
237
+ });
238
+ Card.displayName = "Kanban.Card";
239
+
240
+ /* ── Card slots ─────────────────────────────────────── */
241
+
242
+ type DivProps = HTMLAttributes<HTMLDivElement>;
243
+
244
+ const CardCover = forwardRef<HTMLDivElement, DivProps>(function KanbanCardCover(
245
+ { className, children, ...rest },
246
+ ref,
247
+ ) {
248
+ return (
249
+ <div
250
+ ref={ref}
251
+ className={[styles.cover, className ?? ""].filter(Boolean).join(" ")}
252
+ {...rest}
253
+ >
254
+ {children}
255
+ </div>
256
+ );
257
+ });
258
+ CardCover.displayName = "Kanban.Card.Cover";
259
+
260
+ const CardHeader = forwardRef<HTMLDivElement, DivProps>(function KanbanCardHeader(
261
+ { className, children, ...rest },
262
+ ref,
263
+ ) {
264
+ return (
265
+ <div
266
+ ref={ref}
267
+ className={[styles.cardHeader, className ?? ""].filter(Boolean).join(" ")}
268
+ {...rest}
269
+ >
270
+ {children}
271
+ </div>
272
+ );
273
+ });
274
+ CardHeader.displayName = "Kanban.Card.Header";
275
+
276
+ const CardTitle = forwardRef<HTMLDivElement, DivProps>(function KanbanCardTitle(
277
+ { className, children, ...rest },
278
+ ref,
279
+ ) {
280
+ return (
281
+ <div
282
+ ref={ref}
283
+ className={[styles.cardTitle, className ?? ""].filter(Boolean).join(" ")}
284
+ {...rest}
285
+ >
286
+ {children}
287
+ </div>
288
+ );
289
+ });
290
+ CardTitle.displayName = "Kanban.Card.Title";
291
+
292
+ const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
293
+ function KanbanCardDescription({ className, children, ...rest }, ref) {
294
+ return (
295
+ <p
296
+ ref={ref}
297
+ className={[styles.cardDescription, className ?? ""]
298
+ .filter(Boolean)
299
+ .join(" ")}
300
+ {...rest}
301
+ >
302
+ {children}
303
+ </p>
304
+ );
305
+ },
306
+ );
307
+ CardDescription.displayName = "Kanban.Card.Description";
308
+
309
+ const CardMeta = forwardRef<HTMLDivElement, DivProps>(function KanbanCardMeta(
310
+ { className, children, ...rest },
311
+ ref,
312
+ ) {
313
+ return (
314
+ <div
315
+ ref={ref}
316
+ className={[styles.cardMeta, className ?? ""].filter(Boolean).join(" ")}
317
+ {...rest}
318
+ >
319
+ {children}
320
+ </div>
321
+ );
322
+ });
323
+ CardMeta.displayName = "Kanban.Card.Meta";
324
+
325
+ const CardFooter = forwardRef<HTMLDivElement, DivProps>(function KanbanCardFooter(
326
+ { className, children, ...rest },
327
+ ref,
328
+ ) {
329
+ return (
330
+ <div
331
+ ref={ref}
332
+ className={[styles.cardFooter, className ?? ""].filter(Boolean).join(" ")}
333
+ {...rest}
334
+ >
335
+ {children}
336
+ </div>
337
+ );
338
+ });
339
+ CardFooter.displayName = "Kanban.Card.Footer";
340
+
341
+ /* ──────────────────────────────────────────────────────
342
+ compound export
343
+ ──────────────────────────────────────────────────── */
344
+
345
+ type CardCompound = typeof Card & {
346
+ Cover: typeof CardCover;
347
+ Header: typeof CardHeader;
348
+ Title: typeof CardTitle;
349
+ Description: typeof CardDescription;
350
+ Meta: typeof CardMeta;
351
+ Footer: typeof CardFooter;
352
+ };
353
+
354
+ const KanbanCard = Card as CardCompound;
355
+ KanbanCard.Cover = CardCover;
356
+ KanbanCard.Header = CardHeader;
357
+ KanbanCard.Title = CardTitle;
358
+ KanbanCard.Description = CardDescription;
359
+ KanbanCard.Meta = CardMeta;
360
+ KanbanCard.Footer = CardFooter;
361
+
362
+ type KanbanCompound = typeof KanbanRoot & {
363
+ Column: typeof Column;
364
+ Card: typeof KanbanCard;
365
+ };
366
+
367
+ const Kanban = KanbanRoot as KanbanCompound;
368
+ Kanban.Column = Column;
369
+ Kanban.Card = KanbanCard;
370
+
371
+ export { Kanban };