@conscience-technology/ctds 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/components/ChatInput/ChatInput.module.css +12 -4
- package/assets/components/ChatInput/ChatInput.tsx +11 -10
- package/assets/components/TableHeader/TableHeader.module.css +12 -0
- package/assets/components/TableHeader/TableHeader.tsx +22 -3
- package/assets/components/TableRow/TableRow.module.css +15 -0
- package/assets/components/TableRow/TableRow.tsx +24 -2
- package/assets/components/_data.ts +1 -1
- package/assets/prompts/components/menu-item.md +2 -1
- package/assets/prompts/components/sidebar.md +59 -35
- package/assets/skill/SKILL.md +1 -0
- package/package.json +1 -1
|
@@ -124,27 +124,35 @@
|
|
|
124
124
|
align-items: center;
|
|
125
125
|
justify-content: space-between;
|
|
126
126
|
gap: var(--space-1);
|
|
127
|
-
padding: 0 var(--space-
|
|
127
|
+
padding: 0 var(--space-2) var(--space-2);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
/* SingleLine
|
|
130
|
+
/* SingleLine — Shift+Enter로 자라면 textarea 상단으로 확장, 좌/우 버튼은 하단 고정.
|
|
131
|
+
MultiLine과 동일 max-height(≈8줄)에서 스크롤. */
|
|
131
132
|
.shape-single .body {
|
|
132
133
|
flex-direction: row;
|
|
133
|
-
align-items:
|
|
134
|
+
align-items: flex-end; /* 버튼들 하단 고정 */
|
|
134
135
|
gap: var(--space-0_5);
|
|
135
136
|
padding: var(--space-2) var(--space-2);
|
|
136
137
|
}
|
|
137
138
|
.singleField {
|
|
138
139
|
flex: 1 1 auto;
|
|
139
140
|
min-width: 0;
|
|
140
|
-
padding:
|
|
141
|
+
padding: calc((var(--space-8) - var(--text-sm) * 1.5) / 2) var(--space-1);
|
|
141
142
|
background: transparent;
|
|
142
143
|
border: 0;
|
|
143
144
|
outline: none;
|
|
145
|
+
resize: none;
|
|
144
146
|
color: var(--fg-strong);
|
|
145
147
|
font: inherit;
|
|
146
148
|
font-size: var(--text-sm);
|
|
147
149
|
line-height: 1.5;
|
|
150
|
+
/* MultiLine과 동일 상한 — 넘으면 textarea 자체 스크롤 */
|
|
151
|
+
max-height: calc(var(--text-sm) * 1.5 * 8);
|
|
152
|
+
overflow-y: auto;
|
|
153
|
+
}
|
|
154
|
+
.singleField::placeholder {
|
|
155
|
+
color: var(--fg-muted);
|
|
148
156
|
}
|
|
149
157
|
.singleField::placeholder {
|
|
150
158
|
color: var(--fg-muted);
|
|
@@ -108,22 +108,22 @@ export const ChatInput = forwardRef<HTMLDivElement, ChatInputProps>(
|
|
|
108
108
|
const canSend = !disabled && (value.trim().length > 0 || attachments.length > 0);
|
|
109
109
|
const showStop = isStreaming && !!onStop;
|
|
110
110
|
|
|
111
|
-
/* autoResize
|
|
112
|
-
|
|
113
|
-
룰: 스크롤이 wrapper에 생기므로 wrapper padding이 "트랙 역할" (padding 안쪽까지 흘러 border에서 잘림). */
|
|
111
|
+
/* autoResize — multi/single 둘 다. textarea는 content에 맞춰 자람.
|
|
112
|
+
Shift+Enter로 줄바꿈 → textarea 즉시 확장. max 넘으면 CSS max-height가 scroll 생성. */
|
|
114
113
|
useLayoutEffect(() => {
|
|
115
|
-
if (shape !== "multi") return;
|
|
116
114
|
const el = taRef.current;
|
|
117
115
|
if (!el) return;
|
|
118
116
|
el.style.height = "auto";
|
|
119
|
-
|
|
117
|
+
// scrollHeight로 설정하되 CSS max-height가 override하도록 직접 min
|
|
118
|
+
const nextH = el.scrollHeight;
|
|
119
|
+
el.style.height = `${nextH}px`;
|
|
120
120
|
}, [shape, value]);
|
|
121
121
|
|
|
122
122
|
const handleKeyDown = useCallback(
|
|
123
123
|
(e: KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
|
124
124
|
if (e.key !== "Enter") return;
|
|
125
|
-
if (e.shiftKey) return; //
|
|
126
|
-
if (
|
|
125
|
+
if (e.shiftKey) return; // Shift+Enter: multi/single 모두 줄바꿈 허용
|
|
126
|
+
if (e.nativeEvent.isComposing) return; // 한글 조합 중 전송 방지
|
|
127
127
|
e.preventDefault();
|
|
128
128
|
if (showStop) {
|
|
129
129
|
onStop?.();
|
|
@@ -131,7 +131,7 @@ export const ChatInput = forwardRef<HTMLDivElement, ChatInputProps>(
|
|
|
131
131
|
onSend();
|
|
132
132
|
}
|
|
133
133
|
},
|
|
134
|
-
[
|
|
134
|
+
[canSend, showStop, onSend, onStop]
|
|
135
135
|
);
|
|
136
136
|
|
|
137
137
|
const handleSendClick = useCallback(() => {
|
|
@@ -277,14 +277,15 @@ export const ChatInput = forwardRef<HTMLDivElement, ChatInputProps>(
|
|
|
277
277
|
{shape === "single" ? (
|
|
278
278
|
<>
|
|
279
279
|
{AttachButton}
|
|
280
|
-
<
|
|
281
|
-
|
|
280
|
+
<textarea
|
|
281
|
+
ref={taRef}
|
|
282
282
|
className={styles.singleField}
|
|
283
283
|
value={value}
|
|
284
284
|
onChange={(e) => onChange(e.target.value)}
|
|
285
285
|
onKeyDown={handleKeyDown}
|
|
286
286
|
placeholder={placeholder}
|
|
287
287
|
disabled={disabled}
|
|
288
|
+
rows={1}
|
|
288
289
|
aria-label={placeholder}
|
|
289
290
|
/>
|
|
290
291
|
{ModelAndSend}
|
|
@@ -92,6 +92,18 @@
|
|
|
92
92
|
background: var(--bg);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/* pinned 경계 — hairline으로 "고정 영역" 분리. TableRow와 짝. */
|
|
96
|
+
.cellSlot[data-pinned="left"] {
|
|
97
|
+
border-right: var(--hairline) solid var(--border);
|
|
98
|
+
}
|
|
99
|
+
.cellSlot[data-pinned="right"] {
|
|
100
|
+
border-left: var(--hairline) solid var(--border);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.selectCell {
|
|
104
|
+
border-right: var(--hairline) solid var(--border);
|
|
105
|
+
}
|
|
106
|
+
|
|
95
107
|
/* Cell (columnheader) ─────────────────────────────────────── */
|
|
96
108
|
/* width:100%로 슬롯 폭을 그대로 채워 라벨/tick/handle 정렬이 슬롯 경계에 맞춰 계산됨.
|
|
97
109
|
padding-inline은 hover/drag hit 영역이 콘텐츠에 바짝 붙지 않도록 필수.
|
|
@@ -394,6 +394,25 @@ const TableHeader = forwardRef<HTMLDivElement, TableHeaderProps>(function TableH
|
|
|
394
394
|
.map((k) => columnMap.get(k))
|
|
395
395
|
.filter((c): c is TableHeaderColumn => !!c);
|
|
396
396
|
|
|
397
|
+
/* selectCell이 렌더되면 그 실제 폭을 측정해 좌측 pinned columns의 left offset에
|
|
398
|
+
더해준다. CSS 토큰(--iconbtn-h-lg / --control-h-sm) + space-scale 반영이 자동으로
|
|
399
|
+
흡수되므로 하드코딩이나 CSS 변수 파싱 없이 처리 가능. */
|
|
400
|
+
const selectCellRef = useRef<HTMLDivElement>(null);
|
|
401
|
+
const [selectCellWidth, setSelectCellWidth] = useState(0);
|
|
402
|
+
useEffect(() => {
|
|
403
|
+
if (!selectable) {
|
|
404
|
+
setSelectCellWidth(0);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const el = selectCellRef.current;
|
|
408
|
+
if (!el) return;
|
|
409
|
+
const update = () => setSelectCellWidth(el.getBoundingClientRect().width);
|
|
410
|
+
update();
|
|
411
|
+
const ro = new ResizeObserver(update);
|
|
412
|
+
ro.observe(el);
|
|
413
|
+
return () => ro.disconnect();
|
|
414
|
+
}, [selectable]);
|
|
415
|
+
|
|
397
416
|
/* Sort ─────────────────────────────────────────────────── */
|
|
398
417
|
const toggleSort = useCallback(
|
|
399
418
|
(key: string) => {
|
|
@@ -497,7 +516,7 @@ const TableHeader = forwardRef<HTMLDivElement, TableHeaderProps>(function TableH
|
|
|
497
516
|
const cells = (
|
|
498
517
|
<>
|
|
499
518
|
{selectable && (
|
|
500
|
-
<div className={styles.selectCell} role="columnheader">
|
|
519
|
+
<div ref={selectCellRef} className={styles.selectCell} role="columnheader">
|
|
501
520
|
<Checkbox
|
|
502
521
|
checked={selectAllChecked}
|
|
503
522
|
onChange={(e) => onSelectAllChange?.(e.currentTarget.checked)}
|
|
@@ -524,7 +543,7 @@ const TableHeader = forwardRef<HTMLDivElement, TableHeaderProps>(function TableH
|
|
|
524
543
|
className={styles.cellSlot}
|
|
525
544
|
style={{
|
|
526
545
|
...cellSlotStyle(col, widths[col.key]),
|
|
527
|
-
...pinnedStyle(col, order, columnMap, widths),
|
|
546
|
+
...pinnedStyle(col, order, columnMap, widths, selectCellWidth),
|
|
528
547
|
}}
|
|
529
548
|
>
|
|
530
549
|
<Cell
|
|
@@ -551,7 +570,7 @@ const TableHeader = forwardRef<HTMLDivElement, TableHeaderProps>(function TableH
|
|
|
551
570
|
className={styles.cellSlot}
|
|
552
571
|
style={{
|
|
553
572
|
...cellSlotStyle(col, widths[col.key]),
|
|
554
|
-
...pinnedStyle(col, order, columnMap, widths),
|
|
573
|
+
...pinnedStyle(col, order, columnMap, widths, selectCellWidth),
|
|
555
574
|
}}
|
|
556
575
|
>
|
|
557
576
|
<Cell
|
|
@@ -145,6 +145,21 @@ a.root:not(.disabled):focus-visible,
|
|
|
145
145
|
contain: none;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/* pinned 경계 — scroll shadow 대신 상시 hairline으로 "여기까지 고정 영역"을 분명히.
|
|
149
|
+
좌측 pinned → 우측 경계에 세로 hairline. 우측 pinned → 좌측 경계. */
|
|
150
|
+
.cellSlot[data-pinned="left"] {
|
|
151
|
+
border-right: var(--hairline) solid var(--border);
|
|
152
|
+
}
|
|
153
|
+
.cellSlot[data-pinned="right"] {
|
|
154
|
+
border-left: var(--hairline) solid var(--border);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* selectable + pinned 조합일 때 selectCell이 "실질적 좌측 pinned의 선두" 역할이라
|
|
158
|
+
selectCell에도 우측 hairline을 그어 경계 톤 일치. */
|
|
159
|
+
.selectCell {
|
|
160
|
+
border-right: var(--hairline) solid var(--border);
|
|
161
|
+
}
|
|
162
|
+
|
|
148
163
|
/* ── Cell ───────────────────────────────────────────────
|
|
149
164
|
Header의 .cell과 1:1 동일. slot 안쪽 width:100%로 가득 채우는 콘텐츠 그릇.
|
|
150
165
|
display:inline-flex 는 Header 그대로 따라감 — flex vs inline-flex 차이가
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
forwardRef,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
5
8
|
type AnchorHTMLAttributes,
|
|
6
9
|
type CSSProperties,
|
|
7
10
|
type HTMLAttributes,
|
|
@@ -143,6 +146,25 @@ function TableRowInner<T>(
|
|
|
143
146
|
const orderedKeys = resolveOrder(columns, order);
|
|
144
147
|
const columnMap = new Map(columns.map((c) => [c.key, c]));
|
|
145
148
|
|
|
149
|
+
/* selectCell 폭을 측정해 좌측 pinned column의 left offset에 반영.
|
|
150
|
+
Header와 동일한 로직 — selectCell이 sticky left:0이라 pinned column들이 그 뒤에
|
|
151
|
+
위치하려면 폭만큼 밀어야 함. */
|
|
152
|
+
const selectCellRef = useRef<HTMLDivElement>(null);
|
|
153
|
+
const [selectCellWidth, setSelectCellWidth] = useState(0);
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (!selectable) {
|
|
156
|
+
setSelectCellWidth(0);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const el = selectCellRef.current;
|
|
160
|
+
if (!el) return;
|
|
161
|
+
const update = () => setSelectCellWidth(el.getBoundingClientRect().width);
|
|
162
|
+
update();
|
|
163
|
+
const ro = new ResizeObserver(update);
|
|
164
|
+
ro.observe(el);
|
|
165
|
+
return () => ro.disconnect();
|
|
166
|
+
}, [selectable]);
|
|
167
|
+
|
|
146
168
|
const rootClass = [
|
|
147
169
|
styles.root,
|
|
148
170
|
variant === "plate" ? styles.plate : styles.line,
|
|
@@ -163,7 +185,7 @@ function TableRowInner<T>(
|
|
|
163
185
|
const cells = (
|
|
164
186
|
<>
|
|
165
187
|
{selectable && (
|
|
166
|
-
<div className={styles.selectCell} onClick={stop}>
|
|
188
|
+
<div ref={selectCellRef} className={styles.selectCell} onClick={stop}>
|
|
167
189
|
<Checkbox
|
|
168
190
|
checked={selected}
|
|
169
191
|
disabled={disabled}
|
|
@@ -184,7 +206,7 @@ function TableRowInner<T>(
|
|
|
184
206
|
minWidth: toCssSize(col.minWidth) ?? cssWidth,
|
|
185
207
|
flex: col.flex ? "1 1 0" : undefined,
|
|
186
208
|
justifyContent: alignToJustify(col.align),
|
|
187
|
-
...pinnedStyle(col, orderedKeys, columnMap, widths ?? {}),
|
|
209
|
+
...pinnedStyle(col, orderedKeys, columnMap, widths ?? {}, selectCellWidth),
|
|
188
210
|
};
|
|
189
211
|
const content = col.render ? col.render(data) : (data as Record<string, ReactNode>)?.[col.key];
|
|
190
212
|
return (
|
|
@@ -19,7 +19,7 @@ root는 `<div role="menuitem">`이다 — `leadingActions`/`trailingActions`에
|
|
|
19
19
|
### Don't
|
|
20
20
|
- MenuItem 안에 두 개 이상의 “주된 액션”을 두지 않는다 — 아이템 전체가 하나의 selection. 부가 액션(즐겨찾기 토글·⋯메뉴)만 slot에 얹는다.
|
|
21
21
|
- 라벨을 두 줄 이상 넣지 않는다. 설명이 필요하면 [[components/command-palette]] description 슬롯이나 [[components/tooltip]].
|
|
22
|
-
- 페이지 이동에 root click 시맨틱 금지. 라우팅 아이템이면 `asChild`로 `<Link>`/`<a>`에 위임.
|
|
22
|
+
- 페이지 이동에 root click 시맨틱 금지. 라우팅 아이템이면 `asChild`로 `<Link>`/`<a>`에 위임. **단, `asChild`일 땐 라벨 `.label` 래퍼가 적용되지 않는다** — 자식(`<Link>`) 자체가 라벨 역할이라 truncation·color·weight가 자동 상속되지 않음. 긴 라벨이면 `<Link>` 쪽에 truncate 유틸을 걸거나, 짧게 유지하는 설계로.
|
|
23
23
|
- height를 CSS로 덮어쓰지 않는다. `size` prop 경유 (Density 축이 스왑).
|
|
24
24
|
- hover-only 액션을 “시각적으로 숨김”으로만 처리하지 않는다 — `opacity: 0` + `pointer-events: none`이 짝. 안 그러면 안 보이는 버튼이 클릭됨.
|
|
25
25
|
|
|
@@ -81,6 +81,7 @@ type MenuItemAction =
|
|
|
81
81
|
checked={boolean} // leadingActions 맨 앞에 checkmark 자동 삽입
|
|
82
82
|
active={boolean} // 현재 페이지/선택 아이템 강조
|
|
83
83
|
disabled={boolean}
|
|
84
|
+
collapsed={boolean} // icon-only. leading 하나만 남기고 label/trailing/shortcut 전부 숨김. (Sidebar가 자동 주입)
|
|
84
85
|
asChild={boolean} // Radix Slot — <Link>/<a>/DropdownMenu.Item 등에 위임
|
|
85
86
|
onClick={(e) => void}
|
|
86
87
|
/>
|
|
@@ -1,23 +1,38 @@
|
|
|
1
1
|
# Sidebar
|
|
2
2
|
|
|
3
3
|
### 원칙
|
|
4
|
-
좌측 세로 nav. **라우팅 전용** — Tabs/필터는 쓸 곳이 아님.
|
|
4
|
+
좌측 세로 nav. **라우팅 전용** — Tabs/필터는 쓸 곳이 아님. Header(고정) + Body(스크롤) + Footer(고정) 3영역 세로 쉘에, Brand + Section + Item + Separator + Profile을 compound로 조합. `collapsed`로 icon-only 축소. Item은 [[components/menu-item]]을 그대로 어댑트 — 시각·상태 규칙 공유.
|
|
5
|
+
|
|
6
|
+
### 3영역 구조 (중요)
|
|
7
|
+
Sidebar는 **Header / Body / Footer 세 영역으로 강제 분리**된다. 역할 혼동이 레이아웃 사고의 1순위 원인.
|
|
8
|
+
|
|
9
|
+
| 영역 | 스크롤 | 레이아웃 | 담는 것 |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| `Sidebar.Header` | ❌ 고정 | **가로 flex 행** (brand symbol + wordmark 나란히) | `Sidebar.Brand` 전용 |
|
|
12
|
+
| `Sidebar.Body` | ✅ 세로 스크롤 | 세로 flex 열 | `Sidebar.Section` · `Sidebar.Separator` 전부 여기 |
|
|
13
|
+
| `Sidebar.Footer` | ❌ 고정 | 가로 flex 행 | `Sidebar.Profile` 또는 보조 액션 |
|
|
14
|
+
|
|
15
|
+
Header는 **가로 행**이기 때문에 Section을 넣으면 nav 전체가 가로 1줄로 눌려 겹친다. Section·Item·Separator는 반드시 `Sidebar.Body` 안에.
|
|
5
16
|
|
|
6
17
|
### Do
|
|
7
|
-
-
|
|
18
|
+
- Section·Separator는 반드시 **`Sidebar.Body` 안에** 둔다 — Body만 스크롤되는 영역.
|
|
19
|
+
- 라우트 이동은 Item에 `asChild` + Next `<Link>`. 단, `asChild`일 땐 라벨 truncation이 `<Link>` 쪽 책임이 된다 ([[components/menu-item]] Slot 동작) — 긴 라벨이면 Link에 `truncate` 유틸 또는 `title` 속성을 걸 것.
|
|
8
20
|
- 현재 라우트는 `active`로 명시 (MenuItem의 active 시각 그대로).
|
|
9
21
|
- Brand는 Home 링크 자리로. `symbol`(항상 노출) + `wordmark`(expanded만).
|
|
10
22
|
- Section 묶음은 `title`로 그룹. collapsed일 땐 title 자동 숨김.
|
|
11
23
|
- `collapsed` 상태에서는 `aria-label`과 `title`을 자동으로 걸어 접근성 유지.
|
|
24
|
+
- 콘텐츠 영역과의 **경계선은 [[components/app-frame]] `variant="outlined"`** 가 그린다 — Sidebar 자체는 border를 그리지 않음. 사이드바가 녹아 보이면 AppFrame variant부터 확인.
|
|
12
25
|
|
|
13
26
|
### Don't
|
|
27
|
+
- **Header에 Section·Item·nav를 넣지 않는다** — Header는 가로 flex 행이라 세로 nav가 깔리면 전부 눌려 겹친다. Brand 전용.
|
|
14
28
|
- 페이지 안 뷰 전환에 쓰지 않는다 — 그건 [[components/tabs]].
|
|
15
29
|
- Item 안에 variant로 다른 톤을 쓰지 않는다. active 외의 강조 금지.
|
|
16
30
|
- Sidebar 자체에 padding/width를 CSS로 덮지 않는다. `--sidebar-w*` 토큰 경유.
|
|
31
|
+
- `.root`에 padding을 꽂지 않는다 — Body가 스크롤 컨테이너라 스크롤바가 안쪽으로 밀려 가장자리에 안 붙는다. 좌우 여백은 Header/Section/Footer가 각자 담당([[project_scroll_rule]]).
|
|
17
32
|
- Brand symbol·wordmark 크기 override 금지. `--sidebar-brand-symbol-max`·`--sidebar-brand-wordmark-max` 상한 안에서만.
|
|
18
33
|
|
|
19
34
|
### Fallback
|
|
20
|
-
collapsed 결정 애매하면 `false`. Section title 애매하면 생략.
|
|
35
|
+
collapsed 결정 애매하면 `false`. Section title 애매하면 생략. 경계선 애매하면 AppFrame `variant="outlined"`.
|
|
21
36
|
|
|
22
37
|
### 관련
|
|
23
38
|
- [[components/menu-item]] — Item 시각·상태 전부 재사용
|
|
@@ -29,26 +44,31 @@ collapsed 결정 애매하면 `false`. Section title 애매하면 생략.
|
|
|
29
44
|
### Props 시그니처
|
|
30
45
|
```tsx
|
|
31
46
|
<Sidebar collapsed={boolean}> // default: false
|
|
47
|
+
{/* Header — 고정, 가로 행. Brand 전용. */}
|
|
32
48
|
<Sidebar.Header>
|
|
33
49
|
<Sidebar.Brand symbol={<Icon />} wordmark={<Logo /> | "CTDS"} />
|
|
34
50
|
</Sidebar.Header>
|
|
35
51
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
{/* Body — 세로 스크롤 트랙. Section·Separator 전부 여기. 여러 개 두면 각자 독립 스크롤. */}
|
|
53
|
+
<Sidebar.Body>
|
|
54
|
+
<Sidebar.Section title="워크스페이스">
|
|
55
|
+
<Sidebar.Item
|
|
56
|
+
icon={<Icon name="house" />}
|
|
57
|
+
label="대시보드"
|
|
58
|
+
active={boolean}
|
|
59
|
+
trailing={ReactNode} // 배지·카운트
|
|
60
|
+
shortcut="⌘1"
|
|
61
|
+
hasSubmenu={boolean}
|
|
62
|
+
disabled={boolean}
|
|
63
|
+
asChild={boolean}
|
|
64
|
+
onClick={(e) => void}
|
|
65
|
+
/>
|
|
66
|
+
</Sidebar.Section>
|
|
67
|
+
|
|
68
|
+
<Sidebar.Separator />
|
|
69
|
+
</Sidebar.Body>
|
|
70
|
+
|
|
71
|
+
{/* Footer — 고정, 하단. Profile 또는 보조 액션. */}
|
|
52
72
|
<Sidebar.Footer>
|
|
53
73
|
<Sidebar.Profile
|
|
54
74
|
name="희영"
|
|
@@ -69,26 +89,28 @@ collapsed 결정 애매하면 `false`. Section title 애매하면 생략.
|
|
|
69
89
|
|
|
70
90
|
### 조합 예시
|
|
71
91
|
```tsx
|
|
72
|
-
// 기본 — 섹션 2개 + Profile
|
|
92
|
+
// 기본 — 섹션 2개 + Profile. Section·Separator는 Body 안.
|
|
73
93
|
<Sidebar>
|
|
74
94
|
<Sidebar.Header>
|
|
75
95
|
<Sidebar.Brand symbol={<Icon name="rocket-launch" />} wordmark="CTDS" />
|
|
76
96
|
</Sidebar.Header>
|
|
77
97
|
|
|
78
|
-
<Sidebar.
|
|
79
|
-
<Sidebar.
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
98
|
+
<Sidebar.Body>
|
|
99
|
+
<Sidebar.Section title="워크스페이스">
|
|
100
|
+
<Sidebar.Item asChild icon={<Icon name="house" />} label="대시보드" active>
|
|
101
|
+
<Link href="/">대시보드</Link>
|
|
102
|
+
</Sidebar.Item>
|
|
103
|
+
<Sidebar.Item asChild icon={<Icon name="chart-line" />} label="분석">
|
|
104
|
+
<Link href="/analytics">분석</Link>
|
|
105
|
+
</Sidebar.Item>
|
|
106
|
+
</Sidebar.Section>
|
|
86
107
|
|
|
87
|
-
|
|
108
|
+
<Sidebar.Separator />
|
|
88
109
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
110
|
+
<Sidebar.Section title="설정">
|
|
111
|
+
<Sidebar.Item icon={<Icon name="gear" />} label="환경설정" shortcut="⌘," />
|
|
112
|
+
</Sidebar.Section>
|
|
113
|
+
</Sidebar.Body>
|
|
92
114
|
|
|
93
115
|
<Sidebar.Footer>
|
|
94
116
|
<Sidebar.Profile name="희영" sub="admin@ctds.dev" avatar={{ status: "online" }} />
|
|
@@ -100,8 +122,10 @@ collapsed 결정 애매하면 `false`. Section title 애매하면 생략.
|
|
|
100
122
|
<Sidebar.Header>
|
|
101
123
|
<Sidebar.Brand symbol={<Icon name="rocket-launch" />} />
|
|
102
124
|
</Sidebar.Header>
|
|
103
|
-
<Sidebar.
|
|
104
|
-
<Sidebar.
|
|
105
|
-
|
|
125
|
+
<Sidebar.Body>
|
|
126
|
+
<Sidebar.Section>
|
|
127
|
+
<Sidebar.Item icon={<Icon name="house" />} label="대시보드" active />
|
|
128
|
+
</Sidebar.Section>
|
|
129
|
+
</Sidebar.Body>
|
|
106
130
|
</Sidebar>
|
|
107
131
|
```
|
package/assets/skill/SKILL.md
CHANGED
|
@@ -29,6 +29,7 @@ Layer 1(shadcn 방식 컴포넌트 코드) + Layer 2(AI 프롬프트 컨텍스
|
|
|
29
29
|
|---|---|
|
|
30
30
|
| 컴포넌트 신규 제작·variant/prop 수정 (`web/components/<Name>/` 또는 `prompts/components/<name>.md`) | `ctds-component` |
|
|
31
31
|
| 페이지·대시보드·레이아웃 조합 (`web/app/**/page.tsx`) | `ctds-page` |
|
|
32
|
+
| **와이어프레임/기존 페이지를 CTDS로 번역·교체** (사용자가 "CTDS로 바꿔줘/교체해줘/일괄 변경" 요청, 또는 와이어프레임 코드·이미지를 입력) | `ctds-wireframe` |
|
|
32
33
|
| 테마 프리셋 생성·수정 (`prompts/themes/*` 또는 `web/app/theme-demo/*`) | `ctds-theme` |
|
|
33
34
|
| 코드 검증 (하드코딩·유령 토큰·메모리 규칙·문서-코드 동기화) | `ctds-validate` |
|
|
34
35
|
| 3D 아이콘 일러스트 자산 생성 | `ctds-illust` |
|