@conscience-technology/ctds 0.1.0 → 0.1.2

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.
@@ -6,13 +6,24 @@
6
6
  display: flex;
7
7
  flex-direction: column;
8
8
  gap: var(--space-2);
9
- padding: var(--space-3) var(--space-3);
9
+ padding: var(--space-2) var(--space-3);
10
10
  background: var(--bg);
11
11
  border: var(--hairline) solid var(--border);
12
12
  border-radius: var(--radius-sm);
13
13
  transition: var(--transition-colors), var(--transition-ring);
14
14
  }
15
15
 
16
+ /* MultiLine — root 상하 padding은 좁게. textarea 내부 padding이 숨 쉴 공간 책임.
17
+ 룰: 긴 글 스크롤 시 textarea 안의 bottom padding 영역까지 흘러 들어가 자연스레 잘림. */
18
+ .shape-multi {
19
+ padding: var(--space-2) var(--space-3) var(--space-2);
20
+ }
21
+
22
+ /* SingleLine은 상하 대칭 유지 */
23
+ .shape-single {
24
+ padding: var(--space-2) var(--space-3);
25
+ }
26
+
16
27
  /* radius variant — Shape 축에 반응 */
17
28
  .radius-square { border-radius: var(--radius-xs); }
18
29
  .radius-rounded { border-radius: var(--radius-sm); }
@@ -64,15 +75,26 @@
64
75
  .multiStack {
65
76
  display: flex;
66
77
  flex-direction: column;
67
- gap: var(--space-2);
78
+ gap: var(--space-1);
68
79
  width: 100%;
69
80
  min-width: 0;
70
81
  }
82
+ /* multiScroll — 바깥 스크롤 컨테이너. padding은 안쪽 트랙 역할.
83
+ 룰: 긴 글이 들어오면 textarea가 자람 → 이 wrapper가 max-height로 넘침 →
84
+ 스크롤 시 텍스트가 padding 영역을 통과해 wrapper border에서 자연스레 잘림. */
85
+ .multiScroll {
86
+ overflow-y: auto;
87
+ max-height: calc(var(--text-base) * 1.5 * 8); /* ~8 lines */
88
+ padding: var(--space-1) var(--space-1);
89
+ }
90
+
91
+ /* textarea 자체는 flat — padding/scroll 없음. 콘텐츠 높이로만 자람. */
71
92
  .multiField {
93
+ display: block;
72
94
  width: 100%;
73
95
  min-width: 0;
74
- min-height: var(--space-6);
75
- padding: var(--space-1) var(--space-1);
96
+ min-height: calc(var(--text-base) * 1.5);
97
+ padding: 0;
76
98
  background: transparent;
77
99
  border: 0;
78
100
  outline: none;
@@ -81,7 +103,7 @@
81
103
  font: inherit;
82
104
  font-size: var(--text-base);
83
105
  line-height: 1.5;
84
- overflow-y: hidden;
106
+ overflow: hidden;
85
107
  }
86
108
  .multiField::placeholder {
87
109
  color: var(--fg-muted);
@@ -103,7 +125,7 @@
103
125
  .singleField {
104
126
  flex: 1 1 auto;
105
127
  min-width: 0;
106
- padding: var(--space-1) var(--space-1);
128
+ padding: 0 var(--space-1);
107
129
  background: transparent;
108
130
  border: 0;
109
131
  outline: none;
@@ -102,27 +102,22 @@ export const ChatInput = forwardRef<HTMLDivElement, ChatInputProps>(
102
102
  } = props;
103
103
 
104
104
  const taRef = useRef<HTMLTextAreaElement>(null);
105
+ const scrollRef = useRef<HTMLDivElement>(null);
105
106
  const fileRef = useRef<HTMLInputElement>(null);
106
107
 
107
108
  const canSend = !disabled && (value.trim().length > 0 || attachments.length > 0);
108
109
  const showStop = isStreaming && !!onStop;
109
110
 
110
- /* autoResize for multi */
111
+ /* autoResize for multi — textarea는 content에 맞춰 자람(scroll 아님).
112
+ 외부 scroll wrapper가 max-height를 책임 → 넘치면 wrapper가 스크롤.
113
+ 룰: 스크롤이 wrapper에 생기므로 wrapper padding이 "트랙 역할" (padding 안쪽까지 흘러 border에서 잘림). */
111
114
  useLayoutEffect(() => {
112
115
  if (shape !== "multi") return;
113
116
  const el = taRef.current;
114
117
  if (!el) return;
115
- const cs = getComputedStyle(el);
116
- const lh = parseFloat(cs.lineHeight);
117
- const pt = parseFloat(cs.paddingTop);
118
- const pb = parseFloat(cs.paddingBottom);
119
- const chrome = pt + pb;
120
- const maxH = lh * maxRows + chrome;
121
118
  el.style.height = "auto";
122
- const next = Math.min(el.scrollHeight, maxH);
123
- el.style.height = `${next}px`;
124
- el.style.overflowY = el.scrollHeight > maxH ? "auto" : "hidden";
125
- }, [shape, value, maxRows]);
119
+ el.style.height = `${el.scrollHeight}px`;
120
+ }, [shape, value]);
126
121
 
127
122
  const handleKeyDown = useCallback(
128
123
  (e: KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>) => {
@@ -296,17 +291,19 @@ export const ChatInput = forwardRef<HTMLDivElement, ChatInputProps>(
296
291
  </>
297
292
  ) : (
298
293
  <div className={styles.multiStack}>
299
- <textarea
300
- ref={taRef}
301
- className={styles.multiField}
302
- value={value}
303
- onChange={(e) => onChange(e.target.value)}
304
- onKeyDown={handleKeyDown}
305
- placeholder={placeholder}
306
- disabled={disabled}
307
- rows={1}
308
- aria-label={placeholder}
309
- />
294
+ <div className={styles.multiScroll}>
295
+ <textarea
296
+ ref={taRef}
297
+ className={styles.multiField}
298
+ value={value}
299
+ onChange={(e) => onChange(e.target.value)}
300
+ onKeyDown={handleKeyDown}
301
+ placeholder={placeholder}
302
+ disabled={disabled}
303
+ rows={1}
304
+ aria-label={placeholder}
305
+ />
306
+ </div>
310
307
  <div className={styles.footer}>
311
308
  {AttachButton}
312
309
  {ModelAndSend}
@@ -581,7 +581,6 @@ export const componentSlugs: Record<string, string> = {
581
581
  Drawer: "feedback/drawer",
582
582
  Toast: "feedback/toast",
583
583
  "Table Header": "data/table-header",
584
- "Table Row": "data/table-row",
585
584
  "Table Row Select": "data/table-row-select",
586
585
  "Table Row Edit": "data/table-row-edit",
587
586
  "List Item": "data/list-item",
@@ -593,7 +592,6 @@ export const componentSlugs: Record<string, string> = {
593
592
  "Attachment Thumb": "chat/attachment-thumb",
594
593
  "Attachment Chip": "chat/attachment-chip",
595
594
  "Quotation Bar": "chat/quotation-bar",
596
- "Chat Input": "chat/chat-input",
597
595
  };
598
596
 
599
597
  export const components: { id: string; title: string; items: string[] }[] = [
package/bin/ctds.js CHANGED
@@ -69,6 +69,7 @@ Examples:
69
69
  ctds init
70
70
  ctds init --dry 아무것도 쓰지 않고 어떤 파일이 생성될지만 출력
71
71
  ctds init --force 기존 파일이 있어도 덮어쓰기
72
+ ctds init --no-open 설치 후 dev 서버 자동 실행 + 브라우저 오픈 건너뛰기
72
73
  ctds update 최신으로 업데이트 (충돌 파일만 prompt)
73
74
  ctds update --dry 어떤 파일이 바뀔지, 충돌이 몇 개인지만 보기
74
75
  ctds update --force 수정 감지 무시, 전부 덮어쓰기 (git 커밋 후에만)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conscience-technology/ctds",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "CTDS — 하이브리드 디자인 시스템 설치 · 온보딩 CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,8 +9,9 @@
9
9
  * (layout.tsx wrapper, globals.css import — 자동 패치는 깨지기 쉬워서 안내만)
10
10
  *
11
11
  * 플래그:
12
- * --dry 실제 쓰기 없이 어떤 파일이 생성될지만 출력
13
- * --force 기존 파일 있어도 덮어쓰기
12
+ * --dry 실제 쓰기 없이 어떤 파일이 생성될지만 출력
13
+ * --force 기존 파일 있어도 덮어쓰기
14
+ * --no-open 설치 후 dev 서버 자동 실행 + 브라우저 오픈 건너뛰기
14
15
  */
15
16
 
16
17
  import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync } from "node:fs";
@@ -21,6 +22,7 @@ import { getAssetSources } from "../utils/assets.js";
21
22
  import { checkMissingDeps, renderInstallCmd } from "../utils/deps.js";
22
23
  import { c, step, success, warn, info, kv } from "../utils/log.js";
23
24
  import { computeFilesMap, writeMeta } from "../utils/meta.js";
25
+ import { hasDevScript, startDevAndOpen } from "../utils/launch.js";
24
26
 
25
27
  const __dirname = dirname(fileURLToPath(import.meta.url));
26
28
  const CLI_PKG_PATH = join(__dirname, "..", "..", "package.json");
@@ -35,6 +37,7 @@ function readCliVersion() {
35
37
  export async function run(args) {
36
38
  const dry = args.includes("--dry");
37
39
  const force = args.includes("--force");
40
+ const noOpen = args.includes("--no-open");
38
41
 
39
42
  console.log("");
40
43
  console.log(c.bold("CTDS 설치"));
@@ -168,13 +171,40 @@ export async function run(args) {
168
171
  console.log(c.bold("다음 단계"));
169
172
  console.log(c.dim("—").repeat(40));
170
173
  console.log("");
171
- console.log(` ${c.cyan("npm run dev")}`);
172
- console.log(` → ${c.cyan("http://localhost:3000/ctds")} 접속`);
173
- console.log(` → 온보딩 돌리고 "적용"하면 ${c.bold("ctds.config.json")}·${c.bold("DESIGN.md")}가 생성됩니다.`);
174
- console.log("");
175
- console.log(c.dim("이 두 파일을 커밋하면 AI 에이전트(Claude Code 등)가 참조해"));
176
- console.log(c.dim("프로젝트의 축에 맞게 컴포넌트·페이지를 생성합니다."));
174
+
175
+ // 수동 설정이 남은 경우(globals.css import / layout.tsx wrapper / 의존성) 자동 실행 보류.
176
+ // 서버가 안 뜨거나 뜨더라도 화면이 깨져서 혼란만 커짐.
177
+ const needsManual = missing.length > 0;
178
+
179
+ if (dry || noOpen || needsManual || !hasDevScript(cwd)) {
180
+ console.log(` ${c.cyan("npm run dev")}`);
181
+ console.log(` → ${c.cyan("http://localhost:3000/ctds")} 접속`);
182
+ console.log(` → 온보딩 돌리고 "적용"하면 ${c.bold("ctds.config.json")}·${c.bold("DESIGN.md")}가 생성됩니다.`);
183
+ console.log("");
184
+ console.log(c.dim("이 두 파일을 커밋하면 AI 에이전트(Claude Code 등)가 참조해"));
185
+ console.log(c.dim("프로젝트의 축에 맞게 컴포넌트·페이지를 생성합니다."));
186
+ console.log("");
187
+ if (needsManual) {
188
+ info("위 수동 설정(의존성 설치 등) 끝낸 뒤 `npm run dev`로 띄워주세요.");
189
+ console.log("");
190
+ }
191
+ return;
192
+ }
193
+
194
+ // 자동: dev 서버 띄우고 /, /ctds 두 탭 오픈.
195
+ info("dev 서버를 띄우고 홈 + /ctds 온보딩을 자동으로 엽니다.");
196
+ console.log(c.dim(" (끄려면 Ctrl+C — 다음부터 수동으로 돌리려면 `ctds init --no-open`)"));
177
197
  console.log("");
198
+
199
+ startDevAndOpen(cwd, {
200
+ port: 3000,
201
+ urls: ["/", "/ctds"],
202
+ onReady: () => {
203
+ console.log("");
204
+ success("브라우저를 열었어요. 온보딩 돌리고 \"적용\"하면 ctds.config.json + DESIGN.md가 생성됩니다.");
205
+ console.log("");
206
+ },
207
+ });
178
208
  }
179
209
 
180
210
  function renderCtdsRoute(project) {
@@ -0,0 +1,94 @@
1
+ /**
2
+ * dev 서버 실행 + 브라우저 오픈 유틸.
3
+ * 외부 의존성 없이 node 내장만 사용.
4
+ */
5
+
6
+ import { spawn } from "node:child_process";
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { get as httpGet } from "node:http";
10
+
11
+ /** package.json의 scripts.dev 존재 확인 */
12
+ export function hasDevScript(cwd) {
13
+ const pkgPath = join(cwd, "package.json");
14
+ if (!existsSync(pkgPath)) return false;
15
+ try {
16
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
17
+ return Boolean(pkg.scripts && pkg.scripts.dev);
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ /** 플젝에 쓰이는 패키지 매니저 감지 (lockfile 기반) */
24
+ export function detectPackageManager(cwd) {
25
+ if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
26
+ if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
27
+ if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) return "bun";
28
+ return "npm";
29
+ }
30
+
31
+ /** localhost:port 에 GET / 가 응답할 때까지 polling. 타임아웃 ms 초과 시 reject. */
32
+ export function waitForServer(port, { timeoutMs = 60_000, intervalMs = 500 } = {}) {
33
+ const start = Date.now();
34
+ return new Promise((resolve, reject) => {
35
+ const tick = () => {
36
+ const req = httpGet(
37
+ { host: "127.0.0.1", port, path: "/", timeout: 1500 },
38
+ (res) => {
39
+ res.resume();
40
+ resolve();
41
+ },
42
+ );
43
+ req.on("error", () => {
44
+ if (Date.now() - start > timeoutMs) {
45
+ reject(new Error(`dev 서버 타임아웃 (${timeoutMs}ms, port ${port})`));
46
+ return;
47
+ }
48
+ setTimeout(tick, intervalMs);
49
+ });
50
+ req.on("timeout", () => {
51
+ req.destroy();
52
+ });
53
+ };
54
+ tick();
55
+ });
56
+ }
57
+
58
+ /** 플랫폼별 브라우저 오픈 명령 */
59
+ export function openUrl(url) {
60
+ const plat = process.platform;
61
+ const cmd =
62
+ plat === "darwin" ? "open" : plat === "win32" ? "cmd" : "xdg-open";
63
+ const args = plat === "win32" ? ["/c", "start", "", url] : [url];
64
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
65
+ child.unref();
66
+ }
67
+
68
+ /**
69
+ * dev 서버 띄우고(stdio inherit) 백그라운드에서 URL 폴링 후 브라우저 오픈.
70
+ * dev 서버 자체는 포그라운드 — 사용자가 Ctrl+C로 종료.
71
+ */
72
+ export function startDevAndOpen(cwd, { port = 3000, urls = ["/"], onReady } = {}) {
73
+ const pm = detectPackageManager(cwd);
74
+ const child = spawn(pm, ["run", "dev"], {
75
+ cwd,
76
+ stdio: "inherit",
77
+ shell: process.platform === "win32",
78
+ });
79
+
80
+ waitForServer(port, { timeoutMs: 90_000 })
81
+ .then(() => {
82
+ for (const path of urls) {
83
+ openUrl(`http://localhost:${port}${path}`);
84
+ }
85
+ if (onReady) onReady();
86
+ })
87
+ .catch((err) => {
88
+ console.error("");
89
+ console.error(` 브라우저 자동 오픈 실패: ${err.message}`);
90
+ console.error(` 수동으로 접속하세요: ${urls.map((p) => `http://localhost:${port}${p}`).join(" , ")}`);
91
+ });
92
+
93
+ return child;
94
+ }