@conscience-technology/ctds 0.4.5 → 0.4.7
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/Icon/Icon.tsx +76 -2
- package/assets/components/TableHeader/TableHeader.module.css +5 -3
- package/assets/components/TableRowEdit/TableRowEdit.module.css +14 -1
- package/assets/components/TableRowEdit/TableRowEdit.tsx +6 -4
- package/assets/prompts/ASSEMBLY.md +1 -0
- package/assets/prompts/components/tab-strip.md +13 -0
- package/assets/skills/ctds-page/SKILL.md +2 -0
- package/assets/skills/ctds-wireframe/SKILL.md +5 -4
- package/package.json +1 -1
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* (예: solar에 자동차 아이콘이 없음 → truck/car가 같은 Truck으로 수렴).
|
|
11
11
|
* 이건 라이브러리 한계라는 사실을 받아들이는 거지, 설계 결함이 아님.
|
|
12
12
|
* - 매핑 자체가 아예 없는 경우에만 family 전용 placeholder("빗금 사각형")를 렌더.
|
|
13
|
+
* - 일부 editor UI 아이콘(text-align, flex align, bold/italic 등)은 solar/lucide에
|
|
14
|
+
* 형상이 명확한 등가물이 없어 축과 무관하게 phosphor로 고정 노출한다
|
|
15
|
+
* (forcePhosphorFamily). 유저에겐 조용히 처리.
|
|
13
16
|
*
|
|
14
17
|
* weight(regular/fill) 반응:
|
|
15
18
|
* phosphor → weight prop으로 반응
|
|
@@ -222,6 +225,26 @@ import {
|
|
|
222
225
|
Flower as PhFlower,
|
|
223
226
|
Leaf as PhLeaf,
|
|
224
227
|
Tree as PhTree,
|
|
228
|
+
// Editor UI — forced to phosphor regardless of family axis (see forcePhosphorFamily)
|
|
229
|
+
TextAlignLeft as PhTextAlignLeft,
|
|
230
|
+
TextAlignCenter as PhTextAlignCenter,
|
|
231
|
+
TextAlignRight as PhTextAlignRight,
|
|
232
|
+
TextAlignJustify as PhTextAlignJustify,
|
|
233
|
+
TextB as PhTextB,
|
|
234
|
+
TextItalic as PhTextItalic,
|
|
235
|
+
TextUnderline as PhTextUnderline,
|
|
236
|
+
TextStrikethrough as PhTextStrikethrough,
|
|
237
|
+
TextIndent as PhTextIndent,
|
|
238
|
+
TextOutdent as PhTextOutdent,
|
|
239
|
+
AlignLeft as PhAlignLeft,
|
|
240
|
+
AlignCenterHorizontal as PhAlignCenterHorizontal,
|
|
241
|
+
AlignRight as PhAlignRight,
|
|
242
|
+
AlignTop as PhAlignTop,
|
|
243
|
+
AlignCenterVertical as PhAlignCenterVertical,
|
|
244
|
+
AlignBottom as PhAlignBottom,
|
|
245
|
+
Code as PhCode,
|
|
246
|
+
CodeBlock as PhCodeBlock,
|
|
247
|
+
Quotes as PhQuotes,
|
|
225
248
|
} from "@phosphor-icons/react";
|
|
226
249
|
|
|
227
250
|
// Solar (Linear = regular, Bold = fill) ───────────────────────────
|
|
@@ -836,6 +859,26 @@ const phosphorSet: Partial<Record<IconName, AnyComp>> = {
|
|
|
836
859
|
flower: PhFlower,
|
|
837
860
|
leaf: PhLeaf,
|
|
838
861
|
tree: PhTree,
|
|
862
|
+
// Editor UI — forced phosphor (see forcePhosphorFamily)
|
|
863
|
+
"text-align-left": PhTextAlignLeft,
|
|
864
|
+
"text-align-center": PhTextAlignCenter,
|
|
865
|
+
"text-align-right": PhTextAlignRight,
|
|
866
|
+
"text-align-justify": PhTextAlignJustify,
|
|
867
|
+
"text-b": PhTextB,
|
|
868
|
+
"text-italic": PhTextItalic,
|
|
869
|
+
"text-underline": PhTextUnderline,
|
|
870
|
+
"text-strikethrough": PhTextStrikethrough,
|
|
871
|
+
"text-indent": PhTextIndent,
|
|
872
|
+
"text-outdent": PhTextOutdent,
|
|
873
|
+
"align-left": PhAlignLeft,
|
|
874
|
+
"align-center-horizontal": PhAlignCenterHorizontal,
|
|
875
|
+
"align-right": PhAlignRight,
|
|
876
|
+
"align-top": PhAlignTop,
|
|
877
|
+
"align-center-vertical": PhAlignCenterVertical,
|
|
878
|
+
"align-bottom": PhAlignBottom,
|
|
879
|
+
code: PhCode,
|
|
880
|
+
"code-block": PhCodeBlock,
|
|
881
|
+
quotes: PhQuotes,
|
|
839
882
|
};
|
|
840
883
|
|
|
841
884
|
/* solar 세트 — phosphor 이름 → solar의 가장 자연스러운 대응.
|
|
@@ -858,7 +901,7 @@ const solarSet: Partial<Record<IconName, SolarPair>> = {
|
|
|
858
901
|
sliders: { regular: SoSlidersL, fill: SoSlidersB },
|
|
859
902
|
trash: { regular: SoTrashL, fill: SoTrashB },
|
|
860
903
|
"arrow-right": { regular: SoArrowRightL, fill: SoArrowRightB },
|
|
861
|
-
x: { regular:
|
|
904
|
+
x: { regular: PhX, fill: PhX },
|
|
862
905
|
sun: { regular: SoSunL, fill: SoSunB },
|
|
863
906
|
moon: { regular: SoMoonL, fill: SoMoonB },
|
|
864
907
|
"caret-down": { regular: SoCaretDownL, fill: SoCaretDownB },
|
|
@@ -1310,6 +1353,36 @@ function ArrowsDownUpIcon({
|
|
|
1310
1353
|
);
|
|
1311
1354
|
}
|
|
1312
1355
|
|
|
1356
|
+
/* 축과 무관하게 phosphor로 고정 노출할 아이콘.
|
|
1357
|
+
solar/lucide에 형상이 명확한 등가물이 없거나 기존 매핑이 의미상 틀린 자리. */
|
|
1358
|
+
const forcePhosphorFamily = new Set<IconName>([
|
|
1359
|
+
"text-align-left",
|
|
1360
|
+
"text-align-center",
|
|
1361
|
+
"text-align-right",
|
|
1362
|
+
"text-align-justify",
|
|
1363
|
+
"text-b",
|
|
1364
|
+
"text-italic",
|
|
1365
|
+
"text-underline",
|
|
1366
|
+
"text-strikethrough",
|
|
1367
|
+
"text-indent",
|
|
1368
|
+
"text-outdent",
|
|
1369
|
+
"align-left",
|
|
1370
|
+
"align-center-horizontal",
|
|
1371
|
+
"align-right",
|
|
1372
|
+
"align-top",
|
|
1373
|
+
"align-center-vertical",
|
|
1374
|
+
"align-bottom",
|
|
1375
|
+
"code",
|
|
1376
|
+
"code-block",
|
|
1377
|
+
"quotes",
|
|
1378
|
+
// 기존 매핑이 solar/lucide에서 의미상 틀린 자리
|
|
1379
|
+
"square-half",
|
|
1380
|
+
"toggle-left",
|
|
1381
|
+
"toggle-right",
|
|
1382
|
+
"line-segments",
|
|
1383
|
+
"textbox",
|
|
1384
|
+
]);
|
|
1385
|
+
|
|
1313
1386
|
export interface IconProps extends React.SVGAttributes<SVGSVGElement> {
|
|
1314
1387
|
name: IconName;
|
|
1315
1388
|
/** 축 기본값(html data-icon-family) 오버라이드 */
|
|
@@ -1332,7 +1405,8 @@ export function Icon({
|
|
|
1332
1405
|
...rest
|
|
1333
1406
|
}: IconProps) {
|
|
1334
1407
|
const axis = useIconAxis();
|
|
1335
|
-
const
|
|
1408
|
+
const requestedFamily = familyProp ?? axis.family;
|
|
1409
|
+
const family = forcePhosphorFamily.has(name) ? "phosphor" : requestedFamily;
|
|
1336
1410
|
const weight = weightProp ?? axis.weight;
|
|
1337
1411
|
|
|
1338
1412
|
/* family와 무관하게 늘 같은 모양으로 나와야 하는 커스텀 아이콘 분기. */
|
|
@@ -212,12 +212,14 @@
|
|
|
212
212
|
display: none;
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
/* pinned 경계 hairline과 tick(cellSlot 좌측 세로선) 겹침 방지.
|
|
215
|
+
/* pinned/select 경계 hairline과 tick(cellSlot 좌측 세로선) 겹침 방지.
|
|
216
216
|
- pinned-left 셀의 바로 다음 cellSlot: pinned의 border-right와 다음 셀의
|
|
217
217
|
tick이 바로 붙어 두 줄처럼 보이는 현상 제거.
|
|
218
|
-
- pinned-right 셀 자신: border-left와 자기 tick이 같은 지점에 겹침.
|
|
218
|
+
- pinned-right 셀 자신: border-left와 자기 tick이 같은 지점에 겹침.
|
|
219
|
+
- selectCell 바로 다음 cellSlot: selectCell의 border-right와 겹침 제거. */
|
|
219
220
|
.cellSlot[data-pinned="left"] + .cellSlot .tick,
|
|
220
|
-
.cellSlot[data-pinned="right"] .tick
|
|
221
|
+
.cellSlot[data-pinned="right"] .tick,
|
|
222
|
+
.selectCell + .cellSlot .tick {
|
|
221
223
|
display: none;
|
|
222
224
|
}
|
|
223
225
|
|
|
@@ -51,7 +51,11 @@
|
|
|
51
51
|
padding-inline-end: 0;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/* ── 세로 구분선 ──────────────────────────────────────────
|
|
54
|
+
/* ── 세로 구분선 ──────────────────────────────────────────
|
|
55
|
+
pinned/select 경계는 cellSlot/selectCell box-shadow가 담당. JS에서 skip하지만
|
|
56
|
+
렌더 지연·외부 조립 등 어떤 경로로든 data-divider가 붙어도 CSS로 보강:
|
|
57
|
+
selectCell 바로 다음 cellSlot, pinned-left 바로 다음 cellSlot, pinned-right
|
|
58
|
+
자신의 셀 ::before는 숨겨 두 줄 겹침 방지. */
|
|
55
59
|
.editCell[data-divider]::before,
|
|
56
60
|
.readonlyCell[data-divider]::before {
|
|
57
61
|
content: "";
|
|
@@ -63,6 +67,15 @@
|
|
|
63
67
|
pointer-events: none;
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
[data-edit-cell="select"] + * .editCell::before,
|
|
71
|
+
[data-edit-cell="select"] + * .readonlyCell::before,
|
|
72
|
+
[data-pinned="left"] + * .editCell::before,
|
|
73
|
+
[data-pinned="left"] + * .readonlyCell::before,
|
|
74
|
+
[data-pinned="right"] .editCell::before,
|
|
75
|
+
[data-pinned="right"] .readonlyCell::before {
|
|
76
|
+
display: none;
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
|
|
67
80
|
/* ── Edit cell ────────────────────────────────────────────
|
|
68
81
|
클릭/포커스 타겟. 활성 상태엔 input/select가 셀을 꽉 채움. */
|
|
@@ -179,12 +179,14 @@ function TableRowEditInner<T>(
|
|
|
179
179
|
/* 세로 구분선은 editable 여부와 무관하게 모든 셀 사이에 균일하게.
|
|
180
180
|
엑셀 톤 "셀 그리드" 신호는 일관성이 핵심 — read-only 셀만 선이 빠지면
|
|
181
181
|
리듬이 깨져 보임. 첫 데이터 셀은 왼쪽에 선 없음(테이블 외곽).
|
|
182
|
-
|
|
183
|
-
pinned
|
|
184
|
-
|
|
182
|
+
pinned/select 경계는 cellSlot/selectCell box-shadow가 담당 —
|
|
183
|
+
pinned-right 셀 자신, pinned-left 바로 다음 셀, selectable일 때
|
|
184
|
+
첫 데이터 셀의 왼쪽 divider는 그으면 두 줄로 겹쳐 보임. */
|
|
185
185
|
const prevCol = idx > 0 ? columnMap.get(orderedKeys[idx - 1]) : undefined;
|
|
186
186
|
const skipDivider =
|
|
187
|
-
col.pinned === "right" ||
|
|
187
|
+
col.pinned === "right" ||
|
|
188
|
+
prevCol?.pinned === "left" ||
|
|
189
|
+
(idx === 0 && selectable);
|
|
188
190
|
const showLeftDivider = (idx > 0 || selectable) && !skipDivider;
|
|
189
191
|
return (
|
|
190
192
|
<div
|
|
@@ -214,6 +214,7 @@ page (gap: space-10) ← 그룹 사이 넓게
|
|
|
214
214
|
조립할 때 사람·AI 모두 자주 미끄러지는 자리. 안티패턴 메모리와 쌍을 이룬다.
|
|
215
215
|
|
|
216
216
|
- **"페이지 맨 위에 Tabs만"** — Tabs 위에 PageHeader(h1/breadcrumb)가 먼저 와야 한다 ([antipattern-tabs-without-title])
|
|
217
|
+
- **"브라우저·노션처럼 다중 탭" 요청에 Tabs로 조립** — Tabs는 섹션 전환 전용(한 화면 안 뷰 토글). "열려있는 문서 N개 중 하나 선택"(각 탭 = 라우트, 추가/닫기)은 전부 [[components/tab-strip]]. Chrome·Figma·VSCode·노션 탭 UI 요청은 즉시 TabStrip으로. 수제 조립·Tabs 변형 금지
|
|
217
218
|
- **"워크스페이스 스위처를 수제 Button+Dropdown으로 조립"** — 전용 IdentityButton + DropdownMenu 사용 ([antipattern-workspace-switcher])
|
|
218
219
|
- **"Topbar leading에만 요소 몰림"** — leading/center/trailing 3구역 균형. leading·trailing 짝 필수 ([[components/topbar]])
|
|
219
220
|
- **"Sidebar 2단 쉘을 SidebarSplit으로"** — 바깥 flex로 Rail+Sidebar 직접 조립 ([feedback-sidebar-scroll])
|
|
@@ -7,6 +7,19 @@
|
|
|
7
7
|
|
|
8
8
|
활성 표시는 **pill·underline이 아니라 bg-muted 배경 상시 유지** — hover 색(bg-muted)이 활성 탭에서 lock된 꼴. "hover되는 자리가 현재 자리"라는 직관.
|
|
9
9
|
|
|
10
|
+
### 발동 트리거 — 다중 탭 UI를 원할 때
|
|
11
|
+
사용자가 **"브라우저처럼 탭 열기"**, **"노션처럼 다중 탭"**, **"Figma/VSCode처럼 파일 탭"**, **"Chrome 탭 UI"**, **"탭으로 여러 문서/페이지 열기"** 등을 요구하면 **TabStrip이 정답**. 수제 조립·`<Tabs>` 전용화 하지 말 것. 신호 체크:
|
|
12
|
+
- 사용자가 **"열어두기"**·**"탭 추가"**·**"X 버튼으로 닫기"** 언급 → TabStrip (각 탭 = 열린 문서)
|
|
13
|
+
- 사용자가 **"섹션 전환"**·**"뷰 바꾸기"**·**"필터 토글"** 언급 → [[components/tabs]] (한 화면 안 뷰 전환)
|
|
14
|
+
- 둘 다 필요하면 **TabStrip(상단, 문서 탭) → PageHeader → Tabs(그 안 섹션)** 순으로 중첩. 한 화면에 TabStrip도 되고 Tabs도 되는 자리면 **TabStrip이 바깥**.
|
|
15
|
+
|
|
16
|
+
소비처 세팅은 보통 **라우팅 상태 + 열린 문서 배열**:
|
|
17
|
+
```tsx
|
|
18
|
+
const [docs, setDocs] = useState<Doc[]>(openDocs);
|
|
19
|
+
const [activeId, setActiveId] = useState<string>(openDocs[0].id);
|
|
20
|
+
// closeDoc, addDoc 핸들러는 소비처가 관리 (TabStrip은 상태 안 가짐).
|
|
21
|
+
```
|
|
22
|
+
|
|
10
23
|
### Do
|
|
11
24
|
- **각 탭 = 독립 문서/라우트**. 라우팅이나 외부 상태 저장소에 반영 (`?doc=portfolio` 또는 workspace store).
|
|
12
25
|
- **leadingIcon에 자유롭게** — [[components/icon]], `<img>`(파일 썸네일), `<svg>`(브랜드 로고) 전부 ReactNode로 받는다. Figma 파일 탭의 작은 썸네일, VSCode 파일 아이콘 패턴.
|
|
@@ -33,6 +33,8 @@ description: CTDS 페이지·대시보드·레이아웃을 조합할 때 발동.
|
|
|
33
33
|
| 메타 라벨 가로 나열 | `prompts/recipes/meta-line.md` |
|
|
34
34
|
| 폼·모달 하단 액션 | `prompts/recipes/form-footer.md` |
|
|
35
35
|
| 페이지 폭 안내 바 | `prompts/recipes/page-banner.md` |
|
|
36
|
+
| 다중 탭 UI (브라우저·노션·Figma·VSCode식, 각 탭 = 열린 문서/라우트, 추가·닫기) | `prompts/components/tab-strip.md` — Tabs 아님 |
|
|
37
|
+
| 섹션/뷰 전환 탭 (한 화면 안에서 뷰만 토글, URL `?tab=…`) | `prompts/components/tabs.md` — TabStrip 아님 |
|
|
36
38
|
| 리스트 | `prompts/components/list-item.md` |
|
|
37
39
|
| 테이블 | `prompts/components/{table-row,table-row-edit,table-row-select,table-header}.md` |
|
|
38
40
|
| 폼 | `prompts/components/{form-field,input,select,checkbox,radio,switch}.md` |
|
|
@@ -73,10 +73,11 @@ description: 와이어프레임·목업·기존 페이지를 CTDS로 번역·교
|
|
|
73
73
|
|
|
74
74
|
1. **CTDS 벤더 컴포넌트가 그대로 맞음** → 그대로 import해서 사용. 끝.
|
|
75
75
|
2. **살짝 다름** (라벨 하나 더, trailing 영역 하나 추가, 특정 variant 미지원 등) → **로컬 파생 컴포넌트** 생성:
|
|
76
|
-
- 가장 비슷한 CTDS 벤더 컴포넌트 코드(JSX + CSS Module)를 **그대로 복사해서
|
|
76
|
+
- 가장 비슷한 CTDS 벤더 컴포넌트 코드(JSX + CSS Module)를 **그대로 복사해서 로컬 경로에 다른 이름으로 둠**. 벤더가 설치된 경로(`components/ctds/` 또는 `src/components/ctds/`) **바깥**이어야 함.
|
|
77
|
+
- 로컬 경로는 소비 프로젝트 관례에 맞춤 — Next.js app router 플젝이면 라우트 옆 `_components/`·`_parts/`, 공용은 `components/local/`·`src/components/local/` 등. 관례가 애매하면 사용자에게 한 번 묻고 그 뒤로는 그 자리 재사용.
|
|
77
78
|
- 거기서 **디자인만 살짝** 조정. 토큰·height·padding·gap·radius 기본값은 **건드리지 말고 유지** — 조정은 추가 요소/위치/variant 수준.
|
|
78
|
-
- **CTDS 벤더
|
|
79
|
-
3. **벤더에 비슷한 것조차 없음** → `prompts/recipes/<자리>.md` 반드시 확인 ([[ASSEMBLY]]). 레시피에도 없으면 **수제가 아니라 사용자에게 보고** — "이 자리 벤더/레시피 둘 다 없음, 어떻게 할지" 물어본다. 임의로 div+span 수제 조립 금지.
|
|
79
|
+
- **CTDS 벤더(설치된 `components/ctds/` 전체 + 레포 작업 중이면 `packages/ctds-cli/assets/`)는 절대 수정 금지** ([[feedback_no_vendor_modification]]).
|
|
80
|
+
3. **벤더에 비슷한 것조차 없음** → `prompts/recipes/<자리>.md` 반드시 확인 ([[ASSEMBLY]]). 레시피 위치는 레포 루트 `prompts/recipes/` 또는 `ctds init`으로 설치된 소비 프로젝트 루트 `prompts/recipes/` 둘 다 유효. 레시피에도 없으면 **수제가 아니라 사용자에게 보고** — "이 자리 벤더/레시피 둘 다 없음, 어떻게 할지" 물어본다. 임의로 div+span 수제 조립 금지.
|
|
80
81
|
|
|
81
82
|
**왜 로컬 파생이 수제보다 통일성이 좋은가**:
|
|
82
83
|
- 수제 조립은 토큰 선택부터 틀어진다. padding `12px`·gap `8px` 같은 직관에서 뽑은 값들이 CTDS space scale(--space-2=8, --space-3=12 등)과 미묘하게 어긋나거나, 심지어 하드코딩됨 ([[feedback_no_hardcoded_values]]).
|
|
@@ -85,7 +86,7 @@ description: 와이어프레임·목업·기존 페이지를 CTDS로 번역·교
|
|
|
85
86
|
|
|
86
87
|
**로컬 파생 결정 시 확인**:
|
|
87
88
|
- 어떤 벤더 컴포넌트에서 베낄지 (파일 경로 명시)
|
|
88
|
-
- 어디에 둘지 (
|
|
89
|
+
- 어디에 둘지 (소비 프로젝트 관례의 로컬 폴더 — 벤더 경로 바깥)
|
|
89
90
|
- 무엇을 바꿀지 (추가 slot, 삭제한 variant, 추가 라벨 — "디자인 기본값 건드리지 않음" 명시)
|
|
90
91
|
- 베끼면서 토큰 하드코딩 금지, 기존 토큰 그대로 유지
|
|
91
92
|
|