loopgen 0.1.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/LICENSE +21 -0
- package/README.md +414 -0
- package/dist/adapters/agents-md.js +77 -0
- package/dist/adapters/claude.js +88 -0
- package/dist/adapters/codex.js +97 -0
- package/dist/adapters/cursor.js +41 -0
- package/dist/adapters/local-model.js +172 -0
- package/dist/adapters/windsurf.js +41 -0
- package/dist/cli.js +211 -0
- package/dist/core/adapters.js +162 -0
- package/dist/core/diff.js +29 -0
- package/dist/core/fs-plan.js +12 -0
- package/dist/core/generator.js +226 -0
- package/dist/core/scanner.js +304 -0
- package/dist/core/templates.js +624 -0
- package/dist/core/types.js +1 -0
- package/dist/server.js +241 -0
- package/dist-web/assets/index-BrxUKxHo.css +1 -0
- package/dist-web/assets/index-CIWs8r78.js +13 -0
- package/dist-web/index.html +13 -0
- package/examples/demo-webapp/.github/workflows/ci.yml +20 -0
- package/examples/demo-webapp/README.md +13 -0
- package/examples/demo-webapp/package-lock.json +1246 -0
- package/examples/demo-webapp/package.json +15 -0
- package/examples/demo-webapp/src/checkout.test.ts +13 -0
- package/examples/demo-webapp/src/checkout.ts +9 -0
- package/package.json +67 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promises as fs } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { applyGeneratedFiles } from "./core/fs-plan.js";
|
|
8
|
+
import { demoProjectRoot, generateLoopProject } from "./core/generator.js";
|
|
9
|
+
import { scanProject } from "./core/scanner.js";
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
export async function startLoopgenServer(options) {
|
|
12
|
+
const webDir = options.webDir ?? defaultWebDir();
|
|
13
|
+
const server = createServer(async (request, response) => {
|
|
14
|
+
try {
|
|
15
|
+
if (!request.url) {
|
|
16
|
+
send(response, 404, "Not found");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const url = new URL(request.url, `http://${options.host}:${options.port}`);
|
|
20
|
+
if (url.pathname.startsWith("/api/")) {
|
|
21
|
+
await handleApi(request, response, url, options.projectRoot);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
await serveStatic(response, webDir, url.pathname);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
sendJson(response, 500, {
|
|
28
|
+
error: error instanceof Error ? error.message : String(error)
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
await new Promise((resolve) => server.listen(options.port, options.host, resolve));
|
|
33
|
+
return {
|
|
34
|
+
server,
|
|
35
|
+
url: `http://${options.host}:${options.port}`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function handleApi(request, response, url, defaultRoot) {
|
|
39
|
+
if (request.method === "GET" && url.pathname === "/api/scan") {
|
|
40
|
+
const experienceMode = parseExperienceMode(url.searchParams.get("experienceMode"));
|
|
41
|
+
const root = experienceMode === "demo" ? demoProjectRoot() : url.searchParams.get("path") || defaultRoot;
|
|
42
|
+
sendJson(response, 200, await scanProject(root));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (request.method === "POST" && url.pathname === "/api/choose-folder") {
|
|
46
|
+
sendJson(response, 200, await chooseLocalFolder());
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (request.method === "POST" && url.pathname === "/api/preview") {
|
|
50
|
+
const body = await readJsonBody(request);
|
|
51
|
+
const result = await generateLoopProject(toGenerationOptions(body, defaultRoot));
|
|
52
|
+
sendJson(response, 200, result);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (request.method === "POST" && url.pathname === "/api/apply") {
|
|
56
|
+
const body = await readJsonBody(request);
|
|
57
|
+
if (body.confirm !== true) {
|
|
58
|
+
sendJson(response, 400, { error: "Apply requires confirm: true." });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (body.experienceMode === "demo") {
|
|
62
|
+
sendJson(response, 400, { error: "Demo mode is preview-only. Switch to Use my project before applying files." });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const result = await generateLoopProject(toGenerationOptions(body, defaultRoot));
|
|
66
|
+
const written = await applyGeneratedFiles(result.scan.root, result.files);
|
|
67
|
+
sendJson(response, 200, { written, warnings: result.warnings });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
sendJson(response, 404, { error: "Unknown API route." });
|
|
71
|
+
}
|
|
72
|
+
async function chooseLocalFolder() {
|
|
73
|
+
if (process.platform === "darwin") {
|
|
74
|
+
return chooseFolderWithAppleScript();
|
|
75
|
+
}
|
|
76
|
+
if (process.platform === "win32") {
|
|
77
|
+
return chooseFolderWithPowerShell();
|
|
78
|
+
}
|
|
79
|
+
return chooseFolderWithLinuxPicker();
|
|
80
|
+
}
|
|
81
|
+
async function chooseFolderWithAppleScript() {
|
|
82
|
+
try {
|
|
83
|
+
const { stdout } = await execFileAsync("osascript", [
|
|
84
|
+
"-e",
|
|
85
|
+
'POSIX path of (choose folder with prompt "Select a project folder for loopgen")'
|
|
86
|
+
]);
|
|
87
|
+
return normalizeFolderPickerOutput(stdout);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return folderPickerError(error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function chooseFolderWithPowerShell() {
|
|
94
|
+
try {
|
|
95
|
+
const { stdout } = await execFileAsync("powershell.exe", [
|
|
96
|
+
"-NoProfile",
|
|
97
|
+
"-STA",
|
|
98
|
+
"-Command",
|
|
99
|
+
[
|
|
100
|
+
"Add-Type -AssemblyName System.Windows.Forms",
|
|
101
|
+
"$dialog = New-Object System.Windows.Forms.FolderBrowserDialog",
|
|
102
|
+
"$dialog.Description = 'Select a project folder for loopgen'",
|
|
103
|
+
"if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $dialog.SelectedPath }"
|
|
104
|
+
].join("; ")
|
|
105
|
+
]);
|
|
106
|
+
return normalizeFolderPickerOutput(stdout);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
return folderPickerError(error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function chooseFolderWithLinuxPicker() {
|
|
113
|
+
for (const command of [
|
|
114
|
+
{ bin: "zenity", args: ["--file-selection", "--directory", "--title=Select a project folder for loopgen"] },
|
|
115
|
+
{ bin: "kdialog", args: ["--getexistingdirectory", ".", "Select a project folder for loopgen"] }
|
|
116
|
+
]) {
|
|
117
|
+
try {
|
|
118
|
+
const { stdout } = await execFileAsync(command.bin, command.args);
|
|
119
|
+
return normalizeFolderPickerOutput(stdout);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
const result = folderPickerError(error);
|
|
123
|
+
if (result.canceled)
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
unsupported: true,
|
|
129
|
+
message: "No supported folder picker was found. Install zenity or kdialog, or paste the project path manually."
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function normalizeFolderPickerOutput(stdout) {
|
|
133
|
+
const selectedPath = stdout.trim();
|
|
134
|
+
return selectedPath ? { path: selectedPath } : { canceled: true };
|
|
135
|
+
}
|
|
136
|
+
function folderPickerError(error) {
|
|
137
|
+
const details = error && typeof error === "object" && "stderr" in error ? String(error.stderr ?? "") : "";
|
|
138
|
+
if (/User canceled|cancelled|canceled/i.test(details)) {
|
|
139
|
+
return { canceled: true };
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
unsupported: true,
|
|
143
|
+
message: details.trim() || (error instanceof Error ? error.message : String(error))
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function toGenerationOptions(body, defaultRoot) {
|
|
147
|
+
return {
|
|
148
|
+
projectRoot: typeof body.projectRoot === "string" && body.projectRoot.length > 0 ? body.projectRoot : defaultRoot,
|
|
149
|
+
experienceMode: parseExperienceMode(body.experienceMode),
|
|
150
|
+
selectedTemplates: stringArray(body.selectedTemplates),
|
|
151
|
+
adapters: stringArray(body.adapters),
|
|
152
|
+
adapterConfigs: adapterConfigMap(body.adapterConfigs),
|
|
153
|
+
audienceFilter: typeof body.audienceFilter === "string" ? body.audienceFilter : undefined,
|
|
154
|
+
categoryFilter: typeof body.categoryFilter === "string" ? body.categoryFilter : undefined,
|
|
155
|
+
triggerCadence: typeof body.triggerCadence === "string" ? body.triggerCadence : undefined,
|
|
156
|
+
acceptanceCriteria: typeof body.acceptanceCriteria === "string" ? body.acceptanceCriteria : undefined,
|
|
157
|
+
allowPrCreation: Boolean(body.allowPrCreation),
|
|
158
|
+
allowedCommands: stringArray(body.allowedCommands),
|
|
159
|
+
maxIterations: typeof body.maxIterations === "number" ? body.maxIterations : undefined
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function parseExperienceMode(value) {
|
|
163
|
+
return value === "demo" ? "demo" : "project";
|
|
164
|
+
}
|
|
165
|
+
function stringArray(value) {
|
|
166
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : undefined;
|
|
167
|
+
}
|
|
168
|
+
function adapterConfigMap(value) {
|
|
169
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
170
|
+
return undefined;
|
|
171
|
+
const entries = Object.entries(value).flatMap(([adapterId, rawConfig]) => {
|
|
172
|
+
const config = adapterConfig(rawConfig);
|
|
173
|
+
return config ? [[adapterId, config]] : [];
|
|
174
|
+
});
|
|
175
|
+
return Object.fromEntries(entries);
|
|
176
|
+
}
|
|
177
|
+
function adapterConfig(value) {
|
|
178
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
179
|
+
return undefined;
|
|
180
|
+
const raw = value;
|
|
181
|
+
return {
|
|
182
|
+
preset: typeof raw.preset === "string" ? raw.preset : undefined,
|
|
183
|
+
baseUrl: typeof raw.baseUrl === "string" ? raw.baseUrl : undefined,
|
|
184
|
+
model: typeof raw.model === "string" ? raw.model : undefined,
|
|
185
|
+
apiKeyEnv: typeof raw.apiKeyEnv === "string" ? raw.apiKeyEnv : undefined
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async function serveStatic(response, webDir, requestPath) {
|
|
189
|
+
const normalizedPath = requestPath === "/" ? "/index.html" : requestPath;
|
|
190
|
+
const filePath = path.join(webDir, normalizedPath);
|
|
191
|
+
const safePath = path.resolve(filePath);
|
|
192
|
+
const safeRoot = path.resolve(webDir);
|
|
193
|
+
if (!safePath.startsWith(safeRoot)) {
|
|
194
|
+
send(response, 403, "Forbidden");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
let content = await fs.readFile(safePath).catch(() => undefined);
|
|
198
|
+
let finalPath = safePath;
|
|
199
|
+
if (!content) {
|
|
200
|
+
finalPath = path.join(webDir, "index.html");
|
|
201
|
+
content = await fs.readFile(finalPath).catch(() => undefined);
|
|
202
|
+
}
|
|
203
|
+
if (!content) {
|
|
204
|
+
send(response, 503, "Web assets are missing. Run `npm run build` first.");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
response.writeHead(200, { "Content-Type": contentType(finalPath) });
|
|
208
|
+
response.end(content);
|
|
209
|
+
}
|
|
210
|
+
function contentType(filePath) {
|
|
211
|
+
if (filePath.endsWith(".html"))
|
|
212
|
+
return "text/html; charset=utf-8";
|
|
213
|
+
if (filePath.endsWith(".js"))
|
|
214
|
+
return "text/javascript; charset=utf-8";
|
|
215
|
+
if (filePath.endsWith(".css"))
|
|
216
|
+
return "text/css; charset=utf-8";
|
|
217
|
+
if (filePath.endsWith(".svg"))
|
|
218
|
+
return "image/svg+xml";
|
|
219
|
+
return "application/octet-stream";
|
|
220
|
+
}
|
|
221
|
+
async function readJsonBody(request) {
|
|
222
|
+
const chunks = [];
|
|
223
|
+
for await (const chunk of request) {
|
|
224
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
225
|
+
}
|
|
226
|
+
if (chunks.length === 0)
|
|
227
|
+
return {};
|
|
228
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
229
|
+
}
|
|
230
|
+
function sendJson(response, status, body) {
|
|
231
|
+
response.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
232
|
+
response.end(JSON.stringify(body));
|
|
233
|
+
}
|
|
234
|
+
function send(response, status, body) {
|
|
235
|
+
response.writeHead(status, { "Content-Type": "text/plain; charset=utf-8" });
|
|
236
|
+
response.end(body);
|
|
237
|
+
}
|
|
238
|
+
function defaultWebDir() {
|
|
239
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
240
|
+
return path.resolve(currentDir, "..", "dist-web");
|
|
241
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;color:#181f21;font-synthesis:none;text-rendering:optimizelegibility;-webkit-font-smoothing:antialiased;--bg:#f4f6f6;--surface:#fff;--surface-2:#f8faf9;--surface-3:#eef4f3;--line:#dbe2e1;--line-strong:#c5d0ce;--muted:#5e6b6e;--text:#181f21;--teal:#008d89;--teal-dark:#00736f;--teal-soft:#e8f7f5;--green:#147a35;--green-soft:#e9f6ed;--amber:#ad6500;--amber-soft:#fff4df;--red:#b73332;--red-soft:#fff0ef;--shadow:0 18px 42px #12262b14;--mono:"SFMono-Regular", Consolas, "Liberation Mono", monospace;background:#f4f6f6;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;color:#e7edec;--bg:#0f1413;--surface:#161d1c;--surface-2:#1b2322;--surface-3:#202a29;--line:#2a3433;--line-strong:#3a4644;--muted:#9aa7a8;--text:#e7edec;--teal:#2fb6b0;--teal-dark:#23938e;--teal-soft:#10302e;--green:#3fae62;--green-soft:#11271a;--amber:#e0992f;--amber-soft:#2e2410;--red:#f06b6a;--red-soft:#2e1716;--shadow:0 18px 42px #00000073;background:#0f1413}}*{box-sizing:border-box}:focus-visible{outline:2px solid var(--teal);outline-offset:2px;border-radius:4px}body{background:var(--bg);min-width:320px;min-height:100vh;margin:0}button,input,textarea,select{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.58}.app-shell{background:var(--bg);grid-template-columns:248px minmax(0,1fr);min-height:100vh;display:grid}.sidebar{border-right:1px solid var(--line);background:var(--surface);flex-direction:column;height:100vh;display:flex;position:sticky;top:0}.brand{border-bottom:1px solid var(--line);align-items:center;gap:13px;height:92px;padding:0 24px;font-size:24px;display:flex}.brand-mark{color:#fff;background:var(--teal);border-radius:7px;place-items:center;width:30px;height:30px;display:grid}.project-card{border:1px solid var(--line);background:var(--surface-2);color:var(--muted);border-radius:8px;grid-template-columns:22px 1fr;align-items:center;gap:10px;margin:18px 18px 10px;padding:12px;display:grid}.project-card strong,.project-card span{min-width:0;display:block}.project-card span{text-transform:uppercase;color:var(--muted);font-size:11px}.project-card strong{color:var(--text);text-overflow:ellipsis;white-space:nowrap;margin-top:3px;font-size:14px;overflow:hidden}.side-nav{gap:5px;padding:8px 12px;display:grid}.side-nav-item{color:#344144;text-align:left;background:0 0;border:0;border-radius:7px;align-items:center;gap:10px;width:100%;min-height:40px;padding:0 11px;font-size:14px;display:flex}.side-nav-item:hover{background:var(--surface-3)}.side-nav-item.active{background:var(--teal-soft);color:var(--teal-dark);font-weight:680}.sidebar-footer{border-top:1px solid var(--line);min-height:64px;color:var(--muted);align-items:center;gap:9px;margin-top:auto;padding:0 24px;font-size:13px;display:flex}.daemon-dot{background:var(--green);width:8px;height:8px;box-shadow:0 0 0 3px var(--green-soft);border-radius:50%}.workspace{min-width:0}.workspace-page{align-content:start;gap:18px;min-height:100vh;padding:26px 28px;display:grid}.view-header{border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:20px;min-height:118px;display:flex}.view-header svg{color:var(--teal)}.summary-grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;display:grid}.summary-card{border:1px solid var(--line);background:var(--surface);border-radius:8px;gap:7px;min-height:86px;padding:14px;display:grid}.summary-card span{color:var(--muted);text-transform:uppercase;font-size:12px;font-weight:680}.summary-card strong{overflow-wrap:anywhere;min-width:0;color:var(--text);font-size:20px}.content-grid{grid-template-columns:minmax(620px,1fr) minmax(388px,33vw);min-height:100vh;display:grid}.main-column{align-content:start;gap:18px;min-width:0;padding:26px 28px;display:grid}.tool-panel{border:1px solid var(--line);background:var(--surface);border-radius:8px;padding:18px}.mode-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:14px;display:grid}.mode-button{border:1px solid var(--line);color:#263235;text-align:left;background:#fff;border-left:3px solid #0000;border-radius:8px;grid-template-columns:26px minmax(0,1fr) 22px;align-items:start;gap:12px;min-width:0;min-height:92px;padding:14px 12px;display:grid}.mode-button:hover{background:var(--surface-2)}.mode-button.active{border-left-color:var(--teal);background:#fbfefe}.mode-button>svg{color:var(--teal)}.mode-button span{gap:5px;min-width:0;display:grid}.mode-button strong{font-size:14px}.mode-button small{color:var(--muted);font-size:12px;line-height:1.35}.mode-check{border:1px solid var(--line-strong);color:#fff;background:var(--teal);border-radius:5px;place-items:center;width:21px;height:21px;display:grid!important}.mode-button:not(.active) .mode-check{background:#fff}.panel-kicker{color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:7px;font-size:11px;font-weight:720}.scan-bar,.section-title{justify-content:space-between;align-items:flex-start;gap:18px;display:flex}h1,h2{color:var(--text);letter-spacing:0;margin:0;font-weight:740}h1{font-size:24px;line-height:1.18}h2{font-size:20px;line-height:1.25}p{color:var(--muted);margin:5px 0 0;font-size:13px;line-height:1.45}.count-badge,.status-pill{border:1px solid var(--line);color:#3d4b4e;white-space:nowrap;background:#fff;border-radius:7px;align-items:center;gap:8px;min-height:30px;padding:6px 10px;font-size:12px;font-weight:640;display:inline-flex}.status-pill.success{color:var(--green);background:var(--green-soft);border-color:#b9d8c2}.status-pill.warning{color:var(--amber);background:var(--amber-soft);border-color:#ead0a7}.status-pill.error{color:var(--red);background:var(--red-soft);border-color:#ecc0bf}.path-row{grid-template-columns:minmax(0,1fr) auto auto;align-items:end;gap:12px;margin-top:16px;display:grid}.path-row label,.behavior-grid label{color:#3f4d50;gap:7px;font-size:12px;font-weight:640;display:grid}.path-row input,.settings-grid input,.behavior-grid textarea,.behavior-grid select,.filter-row select,.adapter-config input,.adapter-config select{border:1px solid var(--line);width:100%;color:var(--text);background:#fff;border-radius:7px;padding:10px 11px;font-size:13px;line-height:1.35}.path-row input:disabled,.settings-grid input:disabled{color:#58676a;background:var(--surface-3)}.behavior-grid textarea{resize:vertical;font-family:var(--mono)}.settings-grid{grid-template-columns:minmax(0,1fr) auto auto auto;align-items:end;gap:12px;margin-top:14px;display:grid}.settings-grid label{color:#3f4d50;gap:7px;font-size:12px;font-weight:640;display:grid}.history-list{gap:8px;margin:14px 0 0;padding:0;list-style:none;display:grid}.history-entry{border:1px solid var(--line);background:#fff;border-radius:8px;grid-template-columns:12px minmax(0,1fr) auto;align-items:start;gap:12px;min-height:64px;padding:12px;display:grid}.history-entry strong,.settings-adapter strong{color:var(--text);font-size:14px}.history-entry p{overflow-wrap:anywhere}.history-entry time{color:var(--muted);white-space:nowrap;font-size:12px}.history-dot{background:var(--green);width:9px;height:9px;box-shadow:0 0 0 3px var(--green-soft);border-radius:50%;margin-top:5px}.history-entry.warning .history-dot{background:var(--amber);box-shadow:0 0 0 3px var(--amber-soft)}.history-entry.error .history-dot{background:var(--red);box-shadow:0 0 0 3px var(--red-soft)}.empty-state{border:1px dashed var(--line-strong);min-height:220px;color:var(--muted);text-align:center;border-radius:8px;align-content:center;place-items:center;gap:8px;margin-top:14px;display:grid}.empty-state strong{color:var(--text)}.empty-state span{max-width:280px;font-size:13px;line-height:1.4}.settings-adapter-list{gap:8px;margin-top:14px;display:grid}.settings-adapter{border:1px solid var(--line);background:#fff;border-radius:8px;grid-template-columns:26px minmax(0,1fr) auto;align-items:center;gap:12px;min-height:56px;padding:10px 12px;display:grid}.settings-adapter svg{color:var(--teal)}.settings-adapter div{gap:2px;min-width:0;display:grid}.settings-adapter span{overflow-wrap:anywhere;min-width:0;color:var(--muted);font-size:12px}.scan-layout{grid-template-columns:minmax(340px,1fr) minmax(220px,280px);align-items:start;gap:28px;margin-top:16px;display:grid}.scan-table{border:1px solid var(--line);background:var(--surface);border-radius:7px;margin:0;overflow:hidden}.scan-table div{border-bottom:1px solid var(--line);grid-template-columns:minmax(130px,36%) minmax(0,1fr);min-height:34px;display:grid}.scan-table div:last-child{border-bottom:0}.scan-table dt,.scan-table dd{overflow-wrap:anywhere;min-width:0;margin:0;padding:9px 12px;font-size:13px;line-height:1.3}.scan-table dt{color:#536164;background:var(--surface-2);border-right:1px solid var(--line)}.scan-table dd{color:#253236}.metrics-list{gap:11px;display:grid}.metric{color:#3f4d50;grid-template-columns:22px 1fr auto;align-items:center;gap:10px;font-size:13px;display:grid}.metric svg{color:#657477}.metric strong{color:var(--text);font-weight:720}.template-list,.adapter-list{gap:8px;margin-top:14px;display:grid}.filter-row{grid-template-columns:minmax(150px,.7fr) minmax(150px,.7fr) auto;align-items:end;gap:10px;margin-top:14px;display:grid}.filter-row label{color:#3f4d50;gap:7px;font-size:12px;font-weight:640;display:grid}.compact-empty{min-height:150px}.template-row{border:1px solid var(--line);color:#263235;background:#fff;border-left-width:3px;border-left-color:#0000;border-radius:8px;grid-template-columns:26px 28px minmax(0,1fr) auto;align-items:start;gap:13px;min-height:108px;padding:10px 12px 10px 10px;display:grid;position:relative}.template-row:hover{background:var(--surface-2)}.template-row.selected{border-left-color:var(--teal);background:#fbfefe}.template-row input,.adapter-checkbox input{opacity:0;pointer-events:none;position:absolute}.checkbox-mark{color:#fff;background:#fff;border:1px solid #9ca9ab;border-radius:5px;place-items:center;width:21px;height:21px;display:grid}.template-row input:checked+.checkbox-mark,.adapter-checkbox input:checked+.checkbox-mark{border-color:var(--teal);background:var(--teal)}.template-copy,.adapter-copy{gap:3px;min-width:0;display:grid}.template-title-line{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;display:flex}.template-copy strong,.adapter-copy strong{font-size:14px;font-weight:720}.template-copy small,.adapter-copy small{color:var(--muted);font-size:12px;line-height:1.3}.template-title-line small,.template-meta small{border:1px solid var(--line);background:var(--surface-2);color:#4a595c;border-radius:999px;width:fit-content;max-width:100%;padding:2px 6px;font-size:11px;font-weight:650}.template-meta{flex-wrap:wrap;gap:6px;min-width:0;display:flex}.template-outcome{overflow-wrap:anywhere}.recommend{white-space:nowrap;align-items:center;gap:7px;font-size:12px;display:inline-flex}.recommend:before{content:"";background:currentColor;border-radius:50%;width:8px;height:8px}.recommend.recommended{color:var(--green)}.recommend.optional{color:var(--amber)}.adapter-card{border:1px solid var(--line);background:#fff;border-left:3px solid #0000;border-radius:8px;overflow:hidden}.adapter-card.selected{border-left-color:var(--teal)}.adapter-card.expanded{box-shadow:0 10px 24px #12262b0f}.adapter-row{grid-template-columns:38px minmax(0,1fr);align-items:stretch;min-height:62px;display:grid}.adapter-checkbox{border-right:1px solid var(--line);place-items:center;display:grid;position:relative}.adapter-toggle{width:100%;min-width:0;color:var(--text);text-align:left;background:0 0;border:0;grid-template-columns:28px minmax(0,1fr) auto;align-items:center;gap:12px;padding:10px 12px;display:grid}.adapter-toggle:hover{background:var(--surface-2)}.adapter-copy>span{align-items:center;gap:8px;min-width:0;display:flex}.adapter-copy em{border:1px solid var(--line);color:var(--muted);border-radius:999px;padding:2px 6px;font-size:11px;font-style:normal;font-weight:650}.adapter-chevron{color:var(--muted);transition:transform .16s}.adapter-toggle[aria-expanded=true] .adapter-chevron{transform:rotate(180deg)}.adapter-panel{border-top:1px solid var(--line);background:var(--surface-2);padding:14px}.adapter-detail-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.detail-item{border:1px solid var(--line);background:#fff;border-radius:7px;align-content:start;gap:6px;min-width:0;padding:10px;display:grid}.detail-item.wide{grid-column:1/-1}.detail-item span{color:var(--muted);letter-spacing:.04em;text-transform:uppercase;font-size:11px;font-weight:720}.detail-item strong{color:#263235;font-size:13px;line-height:1.35}code{font-family:var(--mono);font-size:12px}.detail-item code,.file-chip-row code,.command-chips code{overflow-wrap:anywhere;border:1px solid var(--line);color:#283638;background:#f7f9f9;border-radius:5px;width:fit-content;max-width:100%;padding:3px 6px}.file-chip-row{flex-wrap:wrap;gap:6px;display:flex}.file-chip-row small{background:var(--teal-soft);color:var(--teal-dark);border-radius:999px;padding:4px 7px;font-size:12px;font-weight:650}.success-text{color:var(--green)!important}.safety-note ul{color:#4f5d60;margin:0;padding-left:18px;font-size:12px;line-height:1.45}.adapter-config-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.adapter-config-grid label{color:#3f4d50;text-transform:none;letter-spacing:0;gap:7px;min-width:0;font-size:12px;font-weight:640;display:grid}.behavior-grid{grid-template-columns:minmax(160px,.45fr) minmax(260px,1fr);gap:14px;margin-top:14px;display:grid}.wide-field{grid-column:1/-1}.command-chips{color:#3f4d50;gap:7px;font-size:12px;font-weight:640;display:grid}.command-chips div{border:1px solid var(--line);background:#fff;border-radius:7px;flex-wrap:wrap;align-items:center;gap:7px;min-height:40px;padding:8px 9px;display:flex}.command-chips em{color:var(--muted);font-style:normal;font-weight:500}.check-line{color:#3f4d50;align-items:center;gap:9px;min-height:34px;font-size:13px;display:flex!important}.preview-panel{border-left:1px solid var(--line);background:var(--surface);grid-template-rows:auto auto minmax(0,1fr) auto;min-width:0;height:100vh;display:grid;position:sticky;top:0}.preview-header{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;min-height:124px;padding:24px 22px 18px;display:flex}.preview-header svg{color:var(--teal)}.preview-stats{border-bottom:1px solid var(--line);grid-template-columns:repeat(3,minmax(0,1fr));display:grid}.preview-stats span{color:var(--muted);text-transform:uppercase;border-right:1px solid var(--line);gap:2px;padding:12px 18px;font-size:11px;display:grid}.preview-stats span:last-child{border-right:0}.preview-stats strong{color:var(--text);font-size:18px;font-weight:740}.preview-stats .warn-stat strong{color:var(--amber)}.diff-view{font-family:var(--mono);background:#fbfcfc;margin:0;padding:16px 0;font-size:12px;line-height:1.5;overflow:auto}.diff-view code{white-space:pre;min-width:max-content;padding:0 18px;display:block}.diff-add{color:#14713a;background:#eff9f2}.diff-remove{color:#a83232;background:#fff3f2}.diff-meta,.diff-hunk{color:#5c6a6d;background:#f0f4f4}.diff-context{color:#253236}.empty-diff{color:#5e6c70;text-align:center;align-content:center;place-items:center;gap:9px;min-height:360px;padding:34px;display:grid}.empty-diff strong{color:#273436}.empty-diff span{max-width:260px;font-size:13px;line-height:1.4}.preview-footer{border-top:1px solid var(--line);background:#fff;gap:15px;padding:16px 18px 18px;display:grid}.status-summary{color:var(--green);align-items:flex-start;gap:10px;display:flex}.status-summary.error{color:var(--red)}.status-summary.warning{color:var(--amber)}.status-summary div{gap:3px;display:grid}.status-summary strong{color:#203034;font-size:14px}.status-summary span{color:var(--muted);font-size:12px;line-height:1.35}.action-row{grid-template-columns:1fr 1fr;gap:10px;display:grid}.primary-button,.secondary-button{border-radius:7px;justify-content:center;align-items:center;gap:9px;min-height:44px;padding:0 16px;font-size:14px;font-weight:680;display:inline-flex}.compact-button{min-height:38px}.primary-button{border:1px solid var(--teal-dark);background:var(--teal);color:#fff;box-shadow:var(--shadow)}.primary-button:hover{background:var(--teal-dark)}.secondary-button{border:1px solid var(--line-strong);color:#253236;background:#fff}.secondary-button:hover{background:#f3f7f7}.spin{animation:.9s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}@media (width<=1120px){.app-shell{grid-template-columns:1fr}.sidebar{height:auto;position:static}.brand{height:70px}.project-card,.sidebar-footer{display:none}.side-nav{border-bottom:1px solid var(--line);grid-template-columns:repeat(3,minmax(0,1fr));padding:10px 16px}.side-nav-item{justify-content:center}.content-grid{grid-template-columns:1fr}.preview-panel{border-left:0;border-top:1px solid var(--line);height:auto;min-height:560px;position:static}}@media (width<=760px){.workspace-page{gap:14px;padding:16px}.view-header{min-height:auto;padding-bottom:16px}.summary-grid,.settings-grid{grid-template-columns:1fr}.history-entry,.settings-adapter{grid-template-columns:12px minmax(0,1fr)}.history-entry time,.settings-adapter code{grid-column:2}.main-column{gap:14px;padding:16px}.tool-panel{padding:15px}.scan-bar,.section-title,.path-row,.scan-layout,.mode-grid,.filter-row,.behavior-grid{grid-template-columns:1fr}.path-row{align-items:stretch}.template-row{grid-template-columns:26px 24px minmax(0,1fr)}.recommend{display:none}.adapter-detail-grid,.adapter-config-grid{grid-template-columns:1fr}.adapter-row{grid-template-columns:36px minmax(0,1fr)}.adapter-copy>span{flex-wrap:wrap}.preview-header{min-height:auto}.action-row{grid-template-columns:1fr}.primary-button,.secondary-button{width:100%}}
|