@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-
|
|
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-
|
|
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(--
|
|
75
|
-
padding:
|
|
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
|
|
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:
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
<
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
package/src/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
}
|