@conscience-technology/ctds 0.1.14 → 0.2.0
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/AppFrame/AppFrame.module.css +6 -0
- package/assets/components/{AttachmentChip/AttachmentChip.module.css → AttachmentItem/AttachmentItem.module.css} +5 -8
- package/assets/components/{AttachmentChip/AttachmentChip.tsx → AttachmentItem/AttachmentItem.tsx} +21 -15
- package/assets/components/AttachmentItem/index.ts +6 -0
- package/assets/components/Button/Button.module.css +11 -2
- package/assets/components/ChatInput/ChatInput.tsx +2 -2
- package/assets/components/DatePicker/Calendar.tsx +19 -1
- package/assets/components/Drawer/Drawer.tsx +120 -39
- package/assets/components/Drawer/index.ts +9 -1
- package/assets/components/{MessageAttachment/MessageAttachment.module.css → GeneratedImage/GeneratedImage.module.css} +3 -2
- package/assets/components/{MessageAttachment/MessageAttachment.tsx → GeneratedImage/GeneratedImage.tsx} +8 -8
- package/assets/components/GeneratedImage/index.ts +5 -0
- package/assets/components/Icon/Icon.tsx +4 -1
- package/assets/components/Modal/Modal.tsx +114 -33
- package/assets/components/Modal/index.ts +12 -2
- package/assets/components/Spinner/lottieManifest.ts +8 -8
- package/assets/components/Switch/Switch.tsx +6 -0
- package/assets/components/TableHeader/TableHeader.module.css +29 -20
- package/assets/components/TableRow/TableRow.module.css +7 -5
- package/assets/components/TableRowEdit/TableRowEdit.module.css +10 -3
- package/assets/components/TableRowEdit/TableRowEdit.tsx +7 -2
- package/assets/components/Toast/Toast.tsx +19 -0
- package/assets/components/_data.ts +39 -16
- package/assets/components/app-shell.css +17 -0
- package/assets/components/index.ts +77 -0
- package/assets/components/reset.css +63 -0
- package/assets/components/themeBootScript.ts +40 -0
- package/assets/components/tokens.css +68 -57
- package/assets/prompts/GUIDE.md +1 -1
- package/assets/prompts/PRINCIPLES.md +9 -0
- package/assets/prompts/axes/cascade.md +49 -0
- package/assets/prompts/axes/mode.md +3 -1
- package/assets/prompts/components/_variants.md +84 -0
- package/assets/prompts/components/{attachment-chip.md → attachment-item.md} +12 -10
- package/assets/prompts/components/attachment-thumb.md +2 -2
- package/assets/prompts/components/avatar-upload.md +53 -0
- package/assets/prompts/components/bubble.md +2 -2
- package/assets/prompts/components/chat-input.md +7 -8
- package/assets/prompts/components/code-block.md +2 -2
- package/assets/prompts/components/disclosure.md +47 -0
- package/assets/prompts/components/file-icon.md +3 -3
- package/assets/prompts/components/generated-image.md +38 -0
- package/assets/prompts/components/message-quote.md +2 -2
- package/assets/prompts/components/message-sources.md +5 -4
- package/assets/prompts/components/message-table.md +9 -6
- package/assets/prompts/components/onboarding.md +43 -0
- package/assets/prompts/components/quotation-bar.md +1 -1
- package/assets/prompts/components/sidebar-split.md +70 -0
- package/assets/prompts/components/sidebar.md +2 -0
- package/assets/prompts/components/time-picker.md +69 -0
- package/assets/prompts/components/toolbar.md +69 -0
- package/assets/prompts/components/user-message.md +2 -2
- package/package.json +80 -3
- package/src/commands/init.js +53 -23
- package/src/utils/deps.js +23 -17
- package/src/utils/patch.js +155 -0
- package/assets/components/AttachmentChip/index.ts +0 -6
- package/assets/components/Calendar/Calendar.module.css +0 -163
- package/assets/components/Calendar/Calendar.tsx +0 -323
- package/assets/components/Calendar/index.ts +0 -2
- package/assets/components/MessageAttachment/index.ts +0 -5
- package/assets/prompts/components/calendar.md +0 -116
- package/assets/prompts/components/form-field.md +0 -74
- package/assets/prompts/components/message-attachment.md +0 -40
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conscience-technology/ctds",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CTDS — 하이브리드 디자인 시스템 설치 · 온보딩 CLI",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "CTDS — 하이브리드 디자인 시스템 설치 · 온보딩 CLI (shadcn식 코드 생성기)",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"sideEffects": [
|
|
7
|
+
"assets/components/**/*.css",
|
|
8
|
+
"assets/app/**/*.css"
|
|
9
|
+
],
|
|
6
10
|
"bin": {
|
|
7
11
|
"ctds": "./bin/ctds.js"
|
|
8
12
|
},
|
|
@@ -13,6 +17,13 @@
|
|
|
13
17
|
"templates",
|
|
14
18
|
"README.md"
|
|
15
19
|
],
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": "./bin/ctds.js"
|
|
23
|
+
},
|
|
24
|
+
"./assets/*": "./assets/*",
|
|
25
|
+
"./templates/*": "./templates/*"
|
|
26
|
+
},
|
|
16
27
|
"scripts": {
|
|
17
28
|
"bundle-assets": "node ./scripts/bundle-assets.mjs",
|
|
18
29
|
"prepublishOnly": "npm run bundle-assets"
|
|
@@ -23,7 +34,11 @@
|
|
|
23
34
|
"keywords": [
|
|
24
35
|
"ctds",
|
|
25
36
|
"design-system",
|
|
26
|
-
"cli"
|
|
37
|
+
"cli",
|
|
38
|
+
"shadcn",
|
|
39
|
+
"nextjs",
|
|
40
|
+
"react",
|
|
41
|
+
"tokens"
|
|
27
42
|
],
|
|
28
43
|
"license": "MIT",
|
|
29
44
|
"publishConfig": {
|
|
@@ -33,5 +48,67 @@
|
|
|
33
48
|
"type": "git",
|
|
34
49
|
"url": "git+https://github.com/Conscience-Technology/CTDS.git",
|
|
35
50
|
"directory": "packages/ctds-cli"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"react": ">=18.2.0",
|
|
54
|
+
"react-dom": ">=18.2.0",
|
|
55
|
+
"next": ">=14.0.0",
|
|
56
|
+
"@phosphor-icons/react": "^2.1.10",
|
|
57
|
+
"solar-icon-set": "^2.0.1",
|
|
58
|
+
"lucide-react": "^1.47.0",
|
|
59
|
+
"@radix-ui/react-popover": "^1.1.23",
|
|
60
|
+
"@radix-ui/react-select": "^2.3.7",
|
|
61
|
+
"@radix-ui/react-tooltip": "^1.2.16",
|
|
62
|
+
"@radix-ui/react-context-menu": "^2.3.7",
|
|
63
|
+
"@radix-ui/react-dialog": "^1.2.0",
|
|
64
|
+
"@radix-ui/react-toast": "^1.2.24",
|
|
65
|
+
"@radix-ui/react-slot": "^1.3.3",
|
|
66
|
+
"@dnd-kit/core": "^6.3.1",
|
|
67
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
68
|
+
"@dnd-kit/utilities": "^3.2.2"
|
|
69
|
+
},
|
|
70
|
+
"peerDependenciesMeta": {
|
|
71
|
+
"next": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"@phosphor-icons/react": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"solar-icon-set": {
|
|
78
|
+
"optional": true
|
|
79
|
+
},
|
|
80
|
+
"lucide-react": {
|
|
81
|
+
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"@radix-ui/react-popover": {
|
|
84
|
+
"optional": true
|
|
85
|
+
},
|
|
86
|
+
"@radix-ui/react-select": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"@radix-ui/react-tooltip": {
|
|
90
|
+
"optional": true
|
|
91
|
+
},
|
|
92
|
+
"@radix-ui/react-context-menu": {
|
|
93
|
+
"optional": true
|
|
94
|
+
},
|
|
95
|
+
"@radix-ui/react-dialog": {
|
|
96
|
+
"optional": true
|
|
97
|
+
},
|
|
98
|
+
"@radix-ui/react-toast": {
|
|
99
|
+
"optional": true
|
|
100
|
+
},
|
|
101
|
+
"@radix-ui/react-slot": {
|
|
102
|
+
"optional": true
|
|
103
|
+
},
|
|
104
|
+
"@dnd-kit/core": {
|
|
105
|
+
"optional": true
|
|
106
|
+
},
|
|
107
|
+
"@dnd-kit/sortable": {
|
|
108
|
+
"optional": true
|
|
109
|
+
},
|
|
110
|
+
"@dnd-kit/utilities": {
|
|
111
|
+
"optional": true
|
|
112
|
+
}
|
|
36
113
|
}
|
|
37
114
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -23,6 +23,7 @@ import { checkMissingDeps, renderInstallCmd } from "../utils/deps.js";
|
|
|
23
23
|
import { c, step, success, warn, info, kv } from "../utils/log.js";
|
|
24
24
|
import { computeFilesMap, writeMeta } from "../utils/meta.js";
|
|
25
25
|
import { hasDevScript, startDevAndOpen } from "../utils/launch.js";
|
|
26
|
+
import { patchGlobalsCss, patchLayoutTsx } from "../utils/patch.js";
|
|
26
27
|
|
|
27
28
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
29
|
const CLI_PKG_PATH = join(__dirname, "..", "..", "package.json");
|
|
@@ -126,45 +127,74 @@ export async function run(args) {
|
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
|
|
129
|
-
// 3)
|
|
130
|
+
// 3) 자동 패치 — globals.css import + layout.tsx ThemeController wrapper
|
|
130
131
|
console.log("");
|
|
131
132
|
console.log(c.bold("설치 완료"));
|
|
132
133
|
console.log(c.dim("—").repeat(40));
|
|
133
134
|
success("자동으로 복사·생성된 파일은 위 리스트와 같습니다.");
|
|
134
135
|
console.log("");
|
|
135
136
|
|
|
136
|
-
|
|
137
|
-
console.log("");
|
|
137
|
+
const manualFallbacks = [];
|
|
138
138
|
|
|
139
139
|
// 3-1) globals.css — tokens.css import (폰트 + CTDS 토큰 + 축 셀렉터 전부 포함)
|
|
140
140
|
const globalsRel = project.useSrcDir ? "src/app/globals.css" : "app/globals.css";
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
141
|
+
const globalsAbs = resolve(cwd, globalsRel);
|
|
142
|
+
const globalsResult = patchGlobalsCss(globalsAbs, { dry });
|
|
143
|
+
if (globalsResult.status === "applied") {
|
|
144
|
+
step(dry ? `[dry] 패치` : `패치`, `${globalsRel} — @import 추가`);
|
|
145
|
+
} else if (globalsResult.status === "skipped") {
|
|
146
|
+
info(`${globalsRel} — ${globalsResult.reason}`);
|
|
147
|
+
} else {
|
|
148
|
+
warn(`${globalsRel} 자동 패치 실패: ${globalsResult.reason}`);
|
|
149
|
+
manualFallbacks.push({
|
|
150
|
+
title: `${c.cyan(globalsRel)} 최상단에 다음 한 줄을 추가:`,
|
|
151
|
+
lang: "css",
|
|
152
|
+
body: `@import "../components/ctds/tokens.css";`,
|
|
153
|
+
note: "tokens.css가 폰트 로드 + CTDS 토큰 + 축 셀렉터 전부 담당합니다.",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
148
156
|
|
|
149
157
|
// 3-2) layout.tsx wrapper
|
|
150
158
|
const layoutRel = project.useSrcDir ? "src/app/layout.tsx" : "app/layout.tsx";
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
159
|
+
const layoutAbs = resolve(cwd, layoutRel);
|
|
160
|
+
const layoutResult = patchLayoutTsx(layoutAbs, { dry });
|
|
161
|
+
if (layoutResult.status === "applied") {
|
|
162
|
+
step(dry ? `[dry] 패치` : `패치`, `${layoutRel} — <body>를 ThemeController로 감쌈`);
|
|
163
|
+
} else if (layoutResult.status === "skipped") {
|
|
164
|
+
info(`${layoutRel} — ${layoutResult.reason}`);
|
|
165
|
+
} else {
|
|
166
|
+
warn(`${layoutRel} 자동 패치 실패: ${layoutResult.reason}`);
|
|
167
|
+
manualFallbacks.push({
|
|
168
|
+
title: `${c.cyan(layoutRel)} 의 <html>·<body>를 아래 형태로 수정:`,
|
|
169
|
+
lang: "tsx",
|
|
170
|
+
body: renderLayoutSnippet(project),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
156
173
|
|
|
157
174
|
// 3-3) 의존성 체크
|
|
158
175
|
const { missing } = checkMissingDeps(cwd);
|
|
159
176
|
if (missing.length > 0) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
177
|
+
manualFallbacks.push({
|
|
178
|
+
title: "다음 패키지를 설치해주세요 (CTDS 컴포넌트가 필요):",
|
|
179
|
+
lang: "bash",
|
|
180
|
+
body: renderInstallCmd(missing),
|
|
181
|
+
});
|
|
165
182
|
} else {
|
|
166
183
|
success("필요한 의존성이 모두 설치돼 있어요.");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (manualFallbacks.length > 0) {
|
|
187
|
+
console.log("");
|
|
188
|
+
console.log(c.bold("수동 설정이 남았어요:"));
|
|
167
189
|
console.log("");
|
|
190
|
+
manualFallbacks.forEach((m, idx) => {
|
|
191
|
+
info(`${c.bold(`${idx + 1})`)} ${m.title}`);
|
|
192
|
+
console.log(c.dim("```" + m.lang));
|
|
193
|
+
console.log(m.body);
|
|
194
|
+
console.log(c.dim("```"));
|
|
195
|
+
if (m.note) console.log(c.dim(" " + m.note));
|
|
196
|
+
console.log("");
|
|
197
|
+
});
|
|
168
198
|
}
|
|
169
199
|
|
|
170
200
|
// 4) 다음 단계
|
|
@@ -172,9 +202,9 @@ export async function run(args) {
|
|
|
172
202
|
console.log(c.dim("—").repeat(40));
|
|
173
203
|
console.log("");
|
|
174
204
|
|
|
175
|
-
// 수동 설정이 남은 경우(globals.css import / layout.tsx wrapper /
|
|
176
|
-
// 서버가 안 뜨거나 뜨더라도 화면이 깨져서 혼란만 커짐.
|
|
177
|
-
const needsManual =
|
|
205
|
+
// 수동 설정이 남은 경우(globals.css import 자동 패치 실패 / layout.tsx wrapper 실패 /
|
|
206
|
+
// 의존성 설치 필요) 자동 실행 보류. 서버가 안 뜨거나 뜨더라도 화면이 깨져서 혼란만 커짐.
|
|
207
|
+
const needsManual = manualFallbacks.length > 0;
|
|
178
208
|
|
|
179
209
|
if (dry || noOpen || needsManual || !hasDevScript(cwd)) {
|
|
180
210
|
console.log(` ${c.cyan("npm run dev")}`);
|
package/src/utils/deps.js
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CTDS 런타임 의존성 체크 + 설치 명령 생성.
|
|
3
3
|
* 사용자 플젝 package.json을 읽어 빠진 패키지만 안내.
|
|
4
|
+
*
|
|
5
|
+
* Single source of truth: ctds-cli/package.json의 peerDependencies.
|
|
6
|
+
* react/react-dom/next는 소비자가 당연히 가진다고 가정하고 체크에서 제외.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
-
import {
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
const CLI_PKG_PATH = resolve(__dirname, "../../package.json");
|
|
16
|
+
|
|
17
|
+
/** 소비자 레포가 당연히 가진 호스트 환경 패키지. install 안내 대상에서 제외. */
|
|
18
|
+
const HOST_DEPS = new Set(["react", "react-dom", "next"]);
|
|
19
|
+
|
|
20
|
+
/** CTDS 컴포넌트가 실제로 import하는 외부 패키지. CLI 자체 package.json에서 읽어온다. */
|
|
21
|
+
export const CTDS_RUNTIME_DEPS = (() => {
|
|
22
|
+
const pkg = JSON.parse(readFileSync(CLI_PKG_PATH, "utf8"));
|
|
23
|
+
const peers = pkg.peerDependencies ?? {};
|
|
24
|
+
const runtime = {};
|
|
25
|
+
for (const [name, range] of Object.entries(peers)) {
|
|
26
|
+
if (HOST_DEPS.has(name)) continue;
|
|
27
|
+
runtime[name] = range;
|
|
28
|
+
}
|
|
29
|
+
return runtime;
|
|
30
|
+
})();
|
|
25
31
|
|
|
26
32
|
/**
|
|
27
33
|
* 사용자 플젝의 package.json을 읽어 CTDS가 필요로 하는 deps 중 빠진 것 리스트.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ctds init이 소비자 플젝의 globals.css · layout.tsx를 자동 패치하는 유틸.
|
|
3
|
+
*
|
|
4
|
+
* 설계:
|
|
5
|
+
* - 각 패치는 멱등(idempotent): 이미 적용돼 있으면 skip 리턴, 재실행해도 안전.
|
|
6
|
+
* - 실패 시 fallback 수동 안내로 넘긴다. 억지로 깨뜨리지 않음.
|
|
7
|
+
* - 변경 전 .bak 백업. 실패하면 되돌릴 수 있게.
|
|
8
|
+
* - --dry 모드에선 쓰지 않고 "하게 될 일" 리포트만.
|
|
9
|
+
*
|
|
10
|
+
* 리턴 형태 (공통):
|
|
11
|
+
* { status: "applied" | "skipped" | "failed", reason: string, before?, after? }
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, copyFileSync } from "node:fs";
|
|
15
|
+
|
|
16
|
+
const GLOBALS_IMPORT = `@import "../components/ctds/tokens.css";`;
|
|
17
|
+
const GLOBALS_IMPORT_RE = /@import\s+["']\.\.\/components\/ctds\/tokens\.css["']\s*;?/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* globals.css 최상단에 CTDS tokens.css import를 추가.
|
|
21
|
+
* 이미 있으면 skip. 다른 import 라인은 건드리지 않음.
|
|
22
|
+
*/
|
|
23
|
+
export function patchGlobalsCss(filePath, { dry = false } = {}) {
|
|
24
|
+
if (!existsSync(filePath)) {
|
|
25
|
+
return { status: "failed", reason: `파일 없음: ${filePath}` };
|
|
26
|
+
}
|
|
27
|
+
const before = readFileSync(filePath, "utf8");
|
|
28
|
+
if (GLOBALS_IMPORT_RE.test(before)) {
|
|
29
|
+
return { status: "skipped", reason: "CTDS tokens.css import가 이미 있어요." };
|
|
30
|
+
}
|
|
31
|
+
const after = GLOBALS_IMPORT + "\n" + before;
|
|
32
|
+
if (dry) {
|
|
33
|
+
return { status: "applied", reason: "(dry) 최상단에 @import 추가 예정", before, after };
|
|
34
|
+
}
|
|
35
|
+
copyFileSync(filePath, filePath + ".bak");
|
|
36
|
+
writeFileSync(filePath, after, "utf8");
|
|
37
|
+
return { status: "applied", reason: "최상단에 @import 추가", before, after };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const THEME_CONTROLLER_IMPORT_RE =
|
|
41
|
+
/import\s*\{\s*ThemeController\s*\}\s*from\s*["'][^"']+ThemeController["']\s*;?/;
|
|
42
|
+
const THEME_BOOT_IMPORT_RE =
|
|
43
|
+
/import\s*\{\s*themeBootScript\s*\}\s*from\s*["'][^"']+themeBootScript["']\s*;?/;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* layout.tsx 자동 패치:
|
|
47
|
+
* 1) ThemeController import + <body>를 <ThemeController>로 wrap
|
|
48
|
+
* 2) themeBootScript import + <head> 안 blocking <script> 삽입 (FOUC 방지)
|
|
49
|
+
* 3) <html>에 suppressHydrationWarning 추가 (boot script가 attr을 미리 세팅하므로)
|
|
50
|
+
*
|
|
51
|
+
* 각 단계는 멱등 — 이미 적용돼 있으면 그 단계만 skip. 둘 다 skip이면 전체 skip 리턴.
|
|
52
|
+
* <body> 매칭 실패 같은 치명 조건에선 전체 fail — fallback 수동 안내.
|
|
53
|
+
*/
|
|
54
|
+
export function patchLayoutTsx(
|
|
55
|
+
filePath,
|
|
56
|
+
{
|
|
57
|
+
dry = false,
|
|
58
|
+
importPath = "../components/ctds/ThemeController",
|
|
59
|
+
bootImportPath = "../components/ctds/themeBootScript",
|
|
60
|
+
} = {},
|
|
61
|
+
) {
|
|
62
|
+
if (!existsSync(filePath)) {
|
|
63
|
+
return { status: "failed", reason: `파일 없음: ${filePath}` };
|
|
64
|
+
}
|
|
65
|
+
const before = readFileSync(filePath, "utf8");
|
|
66
|
+
|
|
67
|
+
const hasController =
|
|
68
|
+
THEME_CONTROLLER_IMPORT_RE.test(before) || /<ThemeController[\s>]/.test(before);
|
|
69
|
+
const hasBoot =
|
|
70
|
+
THEME_BOOT_IMPORT_RE.test(before) || /themeBootScript\s*\(/.test(before);
|
|
71
|
+
|
|
72
|
+
if (hasController && hasBoot) {
|
|
73
|
+
return { status: "skipped", reason: "ThemeController + boot script가 이미 wiring 돼 있어요." };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let next = before;
|
|
77
|
+
const applied = [];
|
|
78
|
+
|
|
79
|
+
// 1) ThemeController wrap
|
|
80
|
+
if (!hasController) {
|
|
81
|
+
const bodyRe = /<body(\s[^>]*)?>([\s\S]*?)<\/body>/;
|
|
82
|
+
const match = next.match(bodyRe);
|
|
83
|
+
if (!match) {
|
|
84
|
+
return { status: "failed", reason: "<body> 태그를 찾지 못했어요 — 수동 설정 필요." };
|
|
85
|
+
}
|
|
86
|
+
const secondBodyIdx = next.indexOf("<body", match.index + 5);
|
|
87
|
+
if (secondBodyIdx !== -1) {
|
|
88
|
+
return { status: "failed", reason: "<body>가 둘 이상이에요 — 수동 설정 필요." };
|
|
89
|
+
}
|
|
90
|
+
const [, attrs = "", inner] = match;
|
|
91
|
+
const wrapped =
|
|
92
|
+
`<body${attrs}>\n` +
|
|
93
|
+
` <ThemeController>${inner}</ThemeController>\n` +
|
|
94
|
+
` </body>`;
|
|
95
|
+
next = insertAfterLastImport(next, `import { ThemeController } from "${importPath}";`);
|
|
96
|
+
next = next.replace(bodyRe, wrapped);
|
|
97
|
+
applied.push("ThemeController wrap");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 2) themeBootScript import + <head> 안 <script> 삽입
|
|
101
|
+
if (!hasBoot) {
|
|
102
|
+
next = insertAfterLastImport(next, `import { themeBootScript } from "${bootImportPath}";`);
|
|
103
|
+
const scriptTag = `<script dangerouslySetInnerHTML={{ __html: themeBootScript() }} />`;
|
|
104
|
+
// <head>...</head> 쌍 매칭 우선. 없으면 self-closed <head />를 쌍 형태로 치환.
|
|
105
|
+
const headPairRe = /<head(\s[^>]*)?>([\s\S]*?)<\/head>/;
|
|
106
|
+
const headSelfRe = /<head(\s[^>]*)?\/>/;
|
|
107
|
+
if (headPairRe.test(next)) {
|
|
108
|
+
next = next.replace(headPairRe, (_m, attrs = "", inner) => {
|
|
109
|
+
const a = attrs ? ` ${attrs.trim()}` : "";
|
|
110
|
+
return `<head${a}>${inner}\n ${scriptTag}\n </head>`;
|
|
111
|
+
});
|
|
112
|
+
} else if (headSelfRe.test(next)) {
|
|
113
|
+
next = next.replace(headSelfRe, (_m, attrs = "") => {
|
|
114
|
+
const a = attrs ? ` ${attrs.trim()}` : "";
|
|
115
|
+
return `<head${a}>\n ${scriptTag}\n </head>`;
|
|
116
|
+
});
|
|
117
|
+
} else {
|
|
118
|
+
// <head>가 아예 없으면 <html ...> 바로 다음에 <head>를 신규 삽입.
|
|
119
|
+
const htmlOpenRe = /<html(\s[^>]*)?>/;
|
|
120
|
+
if (!htmlOpenRe.test(next)) {
|
|
121
|
+
return { status: "failed", reason: "<html> 태그를 찾지 못했어요 — 수동 설정 필요." };
|
|
122
|
+
}
|
|
123
|
+
next = next.replace(htmlOpenRe, (m) => `${m}\n <head>\n ${scriptTag}\n </head>`);
|
|
124
|
+
}
|
|
125
|
+
applied.push("boot script 삽입");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 3) <html>에 suppressHydrationWarning 추가 (boot script가 attr을 재세팅하므로 필요)
|
|
129
|
+
if (!/suppressHydrationWarning/.test(next)) {
|
|
130
|
+
next = next.replace(/<html(\s[^>]*)?>/, (m, attrs = "") => {
|
|
131
|
+
return `<html${attrs} suppressHydrationWarning>`;
|
|
132
|
+
});
|
|
133
|
+
applied.push("suppressHydrationWarning");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (next === before) {
|
|
137
|
+
return { status: "skipped", reason: "변경 사항 없음." };
|
|
138
|
+
}
|
|
139
|
+
if (dry) {
|
|
140
|
+
return { status: "applied", reason: `(dry) ${applied.join(" + ")}`, before, after: next };
|
|
141
|
+
}
|
|
142
|
+
copyFileSync(filePath, filePath + ".bak");
|
|
143
|
+
writeFileSync(filePath, next, "utf8");
|
|
144
|
+
return { status: "applied", reason: applied.join(" + "), before, after: next };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 파일 상단의 마지막 import 바로 아래에 line 삽입. import가 없으면 파일 최상단. */
|
|
148
|
+
function insertAfterLastImport(source, line) {
|
|
149
|
+
const lastImportMatch = [...source.matchAll(/^import\s[^\n]+\n/gm)].pop();
|
|
150
|
+
if (lastImportMatch) {
|
|
151
|
+
const insertAt = lastImportMatch.index + lastImportMatch[0].length;
|
|
152
|
+
return source.slice(0, insertAt) + line + "\n" + source.slice(insertAt);
|
|
153
|
+
}
|
|
154
|
+
return line + "\n\n" + source;
|
|
155
|
+
}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
/* Calendar ───────────────────────────────────────────────────
|
|
2
|
-
페이지에 인라인으로 들어가는 월간 캘린더. DatePicker와 분리.
|
|
3
|
-
레이아웃은 flex column + 그리드 flex-1로 뷰포트 높이에 반응.
|
|
4
|
-
스크롤 2-레이어 규약: cellEvents는 overflow:hidden 유지 — 넘치면
|
|
5
|
-
hiddenCount로 "+N" 뱃지가 대체한다 (내부 스크롤 금지). */
|
|
6
|
-
.root {
|
|
7
|
-
display: flex;
|
|
8
|
-
flex-direction: column;
|
|
9
|
-
min-height: 0;
|
|
10
|
-
min-width: 0;
|
|
11
|
-
background: var(--bg);
|
|
12
|
-
color: var(--fg);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/* Toolbar ──────────────────────────────────────────────────── */
|
|
16
|
-
.toolbar {
|
|
17
|
-
position: relative;
|
|
18
|
-
display: flex;
|
|
19
|
-
align-items: center;
|
|
20
|
-
justify-content: space-between;
|
|
21
|
-
padding-block: var(--space-2);
|
|
22
|
-
gap: var(--space-3);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.toolbarSide {
|
|
26
|
-
display: flex;
|
|
27
|
-
align-items: center;
|
|
28
|
-
gap: var(--space-1);
|
|
29
|
-
min-width: 0;
|
|
30
|
-
flex: 1 1 0;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
.toolbarSide[data-align="end"] {
|
|
34
|
-
justify-content: flex-end;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.monthNav {
|
|
38
|
-
position: absolute;
|
|
39
|
-
left: 50%;
|
|
40
|
-
transform: translateX(-50%);
|
|
41
|
-
display: flex;
|
|
42
|
-
align-items: center;
|
|
43
|
-
gap: var(--space-1);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.monthTitle {
|
|
47
|
-
min-width: calc(var(--space-2) * 14);
|
|
48
|
-
text-align: center;
|
|
49
|
-
font-family: var(--font-normal);
|
|
50
|
-
font-size: var(--text-base);
|
|
51
|
-
font-weight: var(--font-weight-medium);
|
|
52
|
-
color: var(--fg);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/* "오늘"은 현재 달이 아닐 때만 등장. 사라져도 toolbar 높이가 흔들리지 않도록
|
|
56
|
-
자리를 유지하고 visibility만 토글. */
|
|
57
|
-
.todaySlot {
|
|
58
|
-
visibility: hidden;
|
|
59
|
-
}
|
|
60
|
-
.todaySlot[data-visible] {
|
|
61
|
-
visibility: visible;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/* Weekdays ─────────────────────────────────────────────────── */
|
|
65
|
-
.weekdays {
|
|
66
|
-
display: grid;
|
|
67
|
-
gap: 0;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.weekday {
|
|
71
|
-
padding-block: var(--space-2);
|
|
72
|
-
text-align: center;
|
|
73
|
-
font-family: var(--label-font);
|
|
74
|
-
font-size: var(--text-xs);
|
|
75
|
-
color: var(--fg-muted);
|
|
76
|
-
letter-spacing: var(--label-tracking);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
.weekday[data-tone="sat"] { color: var(--info); }
|
|
80
|
-
.weekday[data-tone="sun"] { color: var(--destructive); }
|
|
81
|
-
|
|
82
|
-
/* Month grid ────────────────────────────────────────────────
|
|
83
|
-
보더는 그리드가 전체 둘레(top/left)를 그리고, 각 셀이 right/bottom만 그린다.
|
|
84
|
-
이렇게 하면 열 수(5열/7열)가 바뀌어도 왼쪽 세로선이 안정적으로 유지됨. */
|
|
85
|
-
.grid {
|
|
86
|
-
display: grid;
|
|
87
|
-
flex: 1 1 0;
|
|
88
|
-
min-height: 0;
|
|
89
|
-
border-top: var(--hairline) solid var(--border);
|
|
90
|
-
border-left: var(--hairline) solid var(--border);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/* Cell ───────────────────────────────────────────────────── */
|
|
94
|
-
.cell {
|
|
95
|
-
position: relative;
|
|
96
|
-
display: flex;
|
|
97
|
-
flex-direction: column;
|
|
98
|
-
gap: var(--space-1);
|
|
99
|
-
min-width: 0;
|
|
100
|
-
min-height: 0;
|
|
101
|
-
padding: var(--space-2);
|
|
102
|
-
border-right: var(--hairline) solid var(--border);
|
|
103
|
-
border-bottom: var(--hairline) solid var(--border);
|
|
104
|
-
overflow: hidden;
|
|
105
|
-
background: var(--bg);
|
|
106
|
-
cursor: pointer;
|
|
107
|
-
transition: var(--transition-colors);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
.cell:hover:not([data-selected]) {
|
|
111
|
-
background: var(--hover);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
.cell:focus-visible {
|
|
115
|
-
outline: var(--ring-width) solid var(--ring);
|
|
116
|
-
outline-offset: calc(-1 * var(--ring-width));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/* selected 셀 — inset ring. active 상태 위 hover가 border/bg를 덮지 않도록
|
|
120
|
-
ring만 사용 (feedback_hover_over_active 규약). */
|
|
121
|
-
.cell[data-selected] {
|
|
122
|
-
z-index: var(--z-raise);
|
|
123
|
-
box-shadow: inset 0 0 0 var(--ring-width) var(--primary);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
.cellHead {
|
|
127
|
-
display: flex;
|
|
128
|
-
align-items: center;
|
|
129
|
-
gap: var(--space-1);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
.dayNum {
|
|
133
|
-
font-family: var(--font-normal);
|
|
134
|
-
font-size: var(--text-sm);
|
|
135
|
-
color: var(--fg);
|
|
136
|
-
line-height: 1;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
.dayNum[data-tone="muted"] { color: var(--fg-disabled); }
|
|
140
|
-
.dayNum[data-tone="sat"] { color: var(--info); }
|
|
141
|
-
.dayNum[data-tone="sun"] { color: var(--destructive); }
|
|
142
|
-
|
|
143
|
-
.dayNum[data-selected],
|
|
144
|
-
.cell[data-today] .dayNum:not([data-tone="muted"]) {
|
|
145
|
-
color: var(--primary);
|
|
146
|
-
font-weight: var(--font-weight-medium);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
.cellBadges {
|
|
150
|
-
margin-left: auto;
|
|
151
|
-
display: inline-flex;
|
|
152
|
-
align-items: center;
|
|
153
|
-
gap: var(--space-1);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
.cellEvents {
|
|
157
|
-
display: flex;
|
|
158
|
-
flex-direction: column;
|
|
159
|
-
gap: var(--space-1);
|
|
160
|
-
flex: 1 1 0;
|
|
161
|
-
min-height: 0;
|
|
162
|
-
overflow: hidden;
|
|
163
|
-
}
|