pm-canvas-viewer 0.1.0 → 0.1.1
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/bin/cli.js +90 -89
- package/dist/frontend/assets/index-DuoTxNrI.css +1 -0
- package/dist/frontend/assets/index-wIsEd6Hm.js +353 -0
- package/dist/frontend/assets/{main-DeCTtllz.js → main-BVykNxDI.js} +1 -1
- package/dist/frontend/index.html +2 -2
- package/lib/history.js +103 -4
- package/lib/prompts.js +8 -33
- package/lib/scanner.js +286 -49
- package/package.json +2 -2
- package/server/api/anchors.js +86 -17
- package/server/api/files.js +100 -44
- package/server/index.js +52 -13
- package/server/itemRegistry.js +38 -0
- package/dist/frontend/assets/index-CDTMa6Xk.js +0 -287
- package/dist/frontend/assets/index-idGrS1dS.css +0 -1
package/bin/cli.js
CHANGED
|
@@ -1,84 +1,65 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import { resolve } from 'path';
|
|
4
|
-
import { existsSync } from 'fs';
|
|
3
|
+
import { resolve, basename } from 'path';
|
|
4
|
+
import { existsSync, statSync } from 'fs';
|
|
5
5
|
import open from 'open';
|
|
6
6
|
import { createServer } from '../server/index.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
7
|
+
import { scanForItems } from '../lib/scanner.js';
|
|
8
|
+
import { recordOpenRoot, getRecentRoots } from '../lib/history.js';
|
|
9
|
+
import {
|
|
10
|
+
findAvailablePort, isPortFree, getPortOccupant, killPort,
|
|
11
|
+
listRunningInstances, registerInstance,
|
|
12
|
+
} from '../lib/port.js';
|
|
13
|
+
import { fuzzySelectFromHistory, handlePortConflict } from '../lib/prompts.js';
|
|
14
|
+
import registry from '../server/itemRegistry.js';
|
|
11
15
|
|
|
12
16
|
const program = new Command();
|
|
13
17
|
|
|
14
18
|
program
|
|
15
19
|
.name('canvas')
|
|
16
|
-
.description('Local
|
|
20
|
+
.description('Local viewer + doc editor for HTML prototypes & pm-canvas boards')
|
|
17
21
|
.version('0.1.0');
|
|
18
22
|
|
|
19
|
-
//
|
|
23
|
+
// canvas — 从历史里挑一个根目录打开
|
|
20
24
|
program
|
|
21
25
|
.action(async () => {
|
|
22
|
-
const entries = await
|
|
26
|
+
const entries = await getRecentRoots();
|
|
23
27
|
const selected = await fuzzySelectFromHistory(entries);
|
|
24
28
|
if (!selected) process.exit(0);
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
console.error(` Canvas no longer exists: ${selected.path}`);
|
|
30
|
+
if (!existsSync(selected.rootPath)) {
|
|
31
|
+
console.error(` Root no longer exists: ${selected.rootPath}`);
|
|
29
32
|
process.exit(1);
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
await
|
|
35
|
+
await startServerForRoot(selected.rootPath);
|
|
33
36
|
});
|
|
34
37
|
|
|
35
|
-
// canvas open [path] —
|
|
38
|
+
// canvas open [path] — 打开指定文件夹(或当前目录)
|
|
36
39
|
program
|
|
37
40
|
.command('open [path]')
|
|
38
|
-
.description('
|
|
41
|
+
.description('Open a directory as viewer root (scans for items)')
|
|
39
42
|
.option('-p, --port <port>', 'Server port', '4800')
|
|
40
43
|
.action(async (targetPath, opts) => {
|
|
41
44
|
portOverride = opts.port ? parseInt(opts.port) : null;
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
const fullPath = resolve(targetPath);
|
|
46
|
+
const target = targetPath ? resolve(targetPath) : process.cwd();
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// Direct canvas path
|
|
52
|
-
const meta = await tryReadCanvasMeta(fullPath);
|
|
53
|
-
if (meta) {
|
|
54
|
-
await startServer(meta.path, meta);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Not a canvas — scan it
|
|
59
|
-
console.log(' Scanning for canvases...');
|
|
60
|
-
const found = await scanForCanvases(fullPath, 4);
|
|
61
|
-
const history = await getRecentCanvases();
|
|
62
|
-
const selected = await fuzzySelectCanvas(found, history);
|
|
63
|
-
if (!selected) process.exit(0);
|
|
64
|
-
await startServer(selected.path, selected);
|
|
65
|
-
} else {
|
|
66
|
-
// No path — scan current directory
|
|
67
|
-
const cwd = process.cwd();
|
|
68
|
-
const cwdMeta = await tryReadCanvasMeta(cwd);
|
|
69
|
-
|
|
70
|
-
if (cwdMeta) {
|
|
71
|
-
await startServer(cwdMeta.path, cwdMeta);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
48
|
+
if (!existsSync(target)) {
|
|
49
|
+
console.error(` Path not found: ${target}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
74
52
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
53
|
+
// 拒绝单文件路径 — viewer 定位是"HTML + docs/"组合,必须是文件夹
|
|
54
|
+
const st = statSync(target);
|
|
55
|
+
if (!st.isDirectory()) {
|
|
56
|
+
console.error(` viewer 必须打开文件夹,不接受单文件路径`);
|
|
57
|
+
console.error(` Got: ${target}`);
|
|
58
|
+
console.error(` Hint: 用浏览器直接打开 HTML 文件即可,viewer 用于"HTML + docs 文档"组合查看`);
|
|
59
|
+
process.exit(1);
|
|
81
60
|
}
|
|
61
|
+
|
|
62
|
+
await startServerForRoot(target);
|
|
82
63
|
});
|
|
83
64
|
|
|
84
65
|
program
|
|
@@ -90,7 +71,6 @@ program
|
|
|
90
71
|
console.log(' No running canvas instances.');
|
|
91
72
|
return;
|
|
92
73
|
}
|
|
93
|
-
|
|
94
74
|
console.log(`\n Running instances:\n`);
|
|
95
75
|
for (const inst of instances) {
|
|
96
76
|
console.log(` Port ${inst.port} PID ${inst.pid}`);
|
|
@@ -147,52 +127,73 @@ program
|
|
|
147
127
|
|
|
148
128
|
let portOverride = null;
|
|
149
129
|
|
|
150
|
-
async function
|
|
151
|
-
|
|
152
|
-
let
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
case 'reuse':
|
|
165
|
-
const url = `http://localhost:${preferredPort}`;
|
|
166
|
-
console.log(`\n Opening ${url}`);
|
|
167
|
-
await open(url);
|
|
168
|
-
process.exit(0);
|
|
169
|
-
case 'kill':
|
|
170
|
-
killPort(preferredPort);
|
|
171
|
-
await new Promise(r => setTimeout(r, 500));
|
|
172
|
-
port = preferredPort;
|
|
173
|
-
break;
|
|
174
|
-
case 'next':
|
|
175
|
-
port = await findAvailablePort(preferredPort + 1);
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
} else {
|
|
179
|
-
port = await findAvailablePort(preferredPort);
|
|
180
|
-
}
|
|
130
|
+
async function startServerForRoot(rootDir) {
|
|
131
|
+
console.log(' Scanning for items...');
|
|
132
|
+
let scanResult;
|
|
133
|
+
try {
|
|
134
|
+
scanResult = await scanForItems(rootDir);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error(` Scan failed: ${err.message}`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (scanResult.items.length === 0) {
|
|
141
|
+
console.error(` No HTML prototypes or canvas boards found under ${rootDir}`);
|
|
142
|
+
console.error(` Hint: 目录里需要至少 1 个 .html 文件,或含 canvas.json 的子目录`);
|
|
143
|
+
process.exit(1);
|
|
181
144
|
}
|
|
182
145
|
|
|
183
|
-
const
|
|
184
|
-
|
|
146
|
+
const rootName = basename(rootDir);
|
|
147
|
+
registry.setRoot({
|
|
148
|
+
rootDir,
|
|
149
|
+
rootName,
|
|
150
|
+
items: scanResult.items,
|
|
151
|
+
tree: scanResult.tree,
|
|
152
|
+
stats: scanResult.stats,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const port = await resolvePort(portOverride || 4800);
|
|
156
|
+
if (port === null) process.exit(0); // 用户在端口冲突 prompt 中选了 reuse
|
|
157
|
+
|
|
158
|
+
const server = createServer();
|
|
185
159
|
server.listen(port, async () => {
|
|
186
160
|
const url = `http://localhost:${port}`;
|
|
161
|
+
const { itemCount, canvasCount, htmlCount } = scanResult.stats;
|
|
187
162
|
console.log(`\n canvas-viewer`);
|
|
188
|
-
console.log(`
|
|
189
|
-
console.log(`
|
|
163
|
+
console.log(` Root: ${rootDir}`);
|
|
164
|
+
console.log(` Items: ${itemCount} (${canvasCount} canvas · ${htmlCount} html)`);
|
|
190
165
|
console.log(` URL: ${url}\n`);
|
|
191
166
|
open(url);
|
|
192
167
|
|
|
193
|
-
await registerInstance(port,
|
|
194
|
-
await
|
|
168
|
+
await registerInstance(port, rootDir, rootName).catch(() => {});
|
|
169
|
+
await recordOpenRoot(rootDir, { title: rootName, itemCount }).catch(() => {});
|
|
195
170
|
});
|
|
196
171
|
}
|
|
197
172
|
|
|
173
|
+
async function resolvePort(preferredPort) {
|
|
174
|
+
if (await isPortFree(preferredPort)) return preferredPort;
|
|
175
|
+
|
|
176
|
+
const occupant = getPortOccupant(preferredPort);
|
|
177
|
+
if (occupant?.isPmCanvas) {
|
|
178
|
+
const action = await handlePortConflict(preferredPort, occupant);
|
|
179
|
+
if (!action) return null;
|
|
180
|
+
|
|
181
|
+
switch (action) {
|
|
182
|
+
case 'reuse': {
|
|
183
|
+
const url = `http://localhost:${preferredPort}`;
|
|
184
|
+
console.log(`\n Opening ${url}`);
|
|
185
|
+
await open(url);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
case 'kill':
|
|
189
|
+
killPort(preferredPort);
|
|
190
|
+
await new Promise(r => setTimeout(r, 500));
|
|
191
|
+
return preferredPort;
|
|
192
|
+
case 'next':
|
|
193
|
+
return await findAvailablePort(preferredPort + 1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return await findAvailablePort(preferredPort);
|
|
197
|
+
}
|
|
198
|
+
|
|
198
199
|
program.parse();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.sidebar{flex:0 0 248px;width:248px;height:100%;display:flex;flex-direction:column;background:#fafafa;border-right:1px solid #e5e7eb;font-family:-apple-system,PingFang SC,Microsoft YaHei,sans-serif;font-size:13px;color:#1f2937;-webkit-user-select:none;user-select:none;overflow:hidden}.sidebar__section{flex:1 1 50%;min-height:0;overflow-y:auto;padding:6px 0}.sidebar__section.is-only{flex:1 1 100%}.sidebar__divider{flex:0 0 1px;background:#e5e7eb}.sidebar-tree{display:flex;flex-direction:column;padding:2px 0}.sidebar-tree-node,.sidebar-tree-children{display:flex;flex-direction:column}.sidebar-tree-row{display:flex;align-items:center;gap:4px;height:26px;padding-right:8px;cursor:pointer;color:#374151;border-radius:4px;margin:0 6px;transition:background .12s,color .12s}.sidebar-tree-row:hover{background:#eef0f3}.sidebar-tree-row--group{color:#6b7280;font-weight:500}.sidebar-tree-row--item.is-active{background:#e6f0fb;color:#1f2937;box-shadow:inset 2px 0 #4a90d9}.sidebar-tree-row--item.is-active:hover{background:#e0ecf8}.sidebar-tree-row__name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:13px;line-height:1}.sidebar-tree-row__icon{display:inline-flex;align-items:center;flex-shrink:0}.sidebar-tree-caret-wrap{display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;flex-shrink:0}.sidebar-tree-caret{display:inline-flex;align-items:center;justify-content:center;width:12px;height:12px;color:#9ca3af;transform:rotate(90deg);transition:transform .12s}.sidebar-tree-caret.is-collapsed{transform:rotate(0)}.sidebar-tree-caret--empty{display:inline-block;width:12px;height:12px}.sidebar-pages{display:flex;flex-direction:column;padding:2px 0}.sidebar-page-row{display:flex;align-items:center;gap:6px;height:26px;padding:0 10px 0 22px;cursor:pointer;color:#4b5563;border-radius:4px;margin:0 6px;transition:background .12s,color .12s;position:relative}.sidebar-page-row:hover{background:#eef0f3;color:#1f2937}.sidebar-page-row.is-active{background:#e6f0fb;color:#1f2937;box-shadow:inset 2px 0 #4a90d9}.sidebar-page-row.is-active:hover{background:#e0ecf8}.sidebar-page-row__name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:13px;line-height:1}.sidebar-page-icon{flex-shrink:0;color:#9ca3af}.sidebar-page-row.is-active .sidebar-page-icon{color:#4a90d9}.sidebar-page-row.is-dragging{opacity:.4}.sidebar-page-row.is-drop-top{box-shadow:inset 0 2px #4a90d9}.sidebar-page-row.is-drop-bottom{box-shadow:inset 0 -2px #4a90d9}.sidebar-page-row.is-active.is-drop-top{box-shadow:inset 2px 0 #4a90d9,inset 0 2px #4a90d9}.sidebar-page-row.is-active.is-drop-bottom{box-shadow:inset 2px 0 #4a90d9,inset 0 -2px #4a90d9}.sidebar-item-icon{flex-shrink:0}.anchor-marker-wrap{margin:4px 0;-webkit-user-select:none;user-select:none}.anchor-marker-chip{display:inline-flex;align-items:center;gap:5px;padding:5px 6px 5px 10px;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer;-webkit-user-select:none;user-select:none;background:color-mix(in srgb,var(--anchor-color, #4A90D9) 8%,white);border:1.5px solid color-mix(in srgb,var(--anchor-color, #4A90D9) 30%,transparent);color:#374151;transition:background .15s}.anchor-marker-chip:hover{background:color-mix(in srgb,var(--anchor-color, #4A90D9) 15%,white)}.anchor-marker-chip__icon{display:flex;align-items:center;color:var(--anchor-color, #4A90D9);flex-shrink:0}.anchor-marker-chip__label{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.anchor-marker-chip__more{display:flex;align-items:center;justify-content:center;width:22px;height:22px;border:none;background:transparent;border-radius:4px;cursor:pointer;color:#9ca3af;flex-shrink:0;transition:all .1s;padding:0}.anchor-marker-chip__more:hover{background:#0000000f;color:#374151}.toast-container{position:fixed;bottom:24px;left:50%;transform:translate(-50%);z-index:10000;display:flex;flex-direction:column;gap:8px;align-items:center;pointer-events:none}.toast{background:#1f2937eb;color:#fff;padding:8px 16px;border-radius:6px;font-size:13px;font-family:-apple-system,PingFang SC,Microsoft YaHei,sans-serif;box-shadow:0 4px 12px #00000026;animation:toast-slide-in .2s ease;max-width:360px;text-align:center}@keyframes toast-slide-in{0%{transform:translateY(20px);opacity:0}to{transform:translateY(0);opacity:1}}.anchor-overlay{position:absolute;top:0;right:0;bottom:0;left:0;pointer-events:none;overflow:hidden;z-index:5}.anchor-overlay__canvas{position:absolute;top:0;left:0;width:100%;height:100%}.anchor-pin{position:absolute;top:0;left:0;pointer-events:auto;display:inline-flex;align-items:center;gap:5px;padding:5px 4px 5px 10px;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap;background:#fff;border:1.5px solid var(--pin-color, #4A90D9);color:#374151;box-shadow:0 1px 6px #0000001a;transition:box-shadow .15s;z-index:1}.anchor-pin:hover{box-shadow:0 2px 10px #00000029;z-index:2}.anchor-pin--dragging{cursor:grabbing;box-shadow:0 4px 16px #00000038;z-index:10;opacity:.85}.anchor-pin__icon{color:var(--pin-color, #4A90D9);display:flex;align-items:center;flex-shrink:0}.anchor-pin__label{max-width:140px;overflow:hidden;text-overflow:ellipsis}.anchor-pin__more{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;background:transparent;border-radius:4px;cursor:pointer;color:#9ca3af;flex-shrink:0;transition:all .1s}.anchor-pin__more:hover{background:#f3f4f6;color:#374151}.anchor-popover{position:fixed;z-index:200;background:#fff;border:1px solid #e5e7eb;border-radius:8px;box-shadow:0 4px 20px #00000024;padding:4px;min-width:160px}.anchor-popover__item{width:100%;padding:7px 10px;border:none;background:none;text-align:left;font-size:13px;color:#374151;cursor:pointer;border-radius:5px;display:flex;align-items:center;gap:8px}.anchor-popover__item:hover{background:#f3f4f6}.anchor-popover__item--danger{color:#ef4444}.anchor-popover__item--danger:hover{background:#fef2f2}.anchor-popover__item--muted{color:#9ca3af}.anchor-popover__item--muted:hover{background:#f9fafb;color:#6b7280}.anchor-popover__item-icon{display:flex;align-items:center;flex-shrink:0}.anchor-popover__divider{height:1px;background:#f0f0f0;margin:3px 0}.anchor-popover__edit{padding:8px;display:flex;flex-direction:column;gap:8px}.anchor-popover__input{padding:6px 10px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;outline:none;width:100%}.anchor-popover__input:focus{border-color:#3b82f6;box-shadow:0 0 0 2px #3b82f61a}.anchor-popover__edit-actions{display:flex;gap:6px;justify-content:flex-end}.anchor-popover__edit-btn{padding:4px 12px;border-radius:5px;font-size:12px;cursor:pointer;border:1px solid #e5e7eb;background:#fff;color:#4b5563}.anchor-popover__edit-btn:hover{background:#f9fafb}.anchor-popover__edit-btn--primary{background:#3b82f6;color:#fff;border-color:#3b82f6}.anchor-popover__edit-btn--primary:hover{background:#2563eb}.anchor-popover__color-grid{display:flex;gap:8px;flex-wrap:wrap}.anchor-dialog-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:200;background:#0000004d;display:flex;align-items:center;justify-content:center}.anchor-dialog{background:#fff;border-radius:10px;box-shadow:0 8px 32px #0000002e;width:360px;max-height:480px;padding:20px;display:flex;flex-direction:column;gap:14px}.anchor-dialog__header{display:flex;align-items:center;justify-content:space-between}.anchor-dialog__title{font-size:15px;font-weight:600;color:#1f2937}.anchor-dialog__steps{display:flex;gap:6px}.anchor-dialog__step-dot{width:8px;height:8px;border-radius:50%;background:#e5e7eb;transition:background .2s}.anchor-dialog__step-dot--active{background:#3b82f6}.anchor-dialog__field{display:flex;flex-direction:column;gap:6px}.anchor-dialog__field label{font-size:12px;color:#6b7280;font-weight:500}.anchor-dialog__field input{padding:7px 10px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;outline:none}.anchor-dialog__field input:focus{border-color:#3b82f6;box-shadow:0 0 0 2px #3b82f61a}.anchor-dialog__colors{display:flex;gap:8px}.anchor-color-btn{width:26px;height:26px;border-radius:50%;border:2px solid transparent;cursor:pointer;transition:all .12s}.anchor-color-btn:hover{transform:scale(1.15)}.anchor-color-btn--active{border-color:#1f2937;box-shadow:0 0 0 2px #fff,0 0 0 4px #1f2937}.anchor-dialog__file-list,.anchor-dialog__heading-list{display:flex;flex-direction:column;gap:2px;max-height:280px;overflow-y:auto}.anchor-dialog__file-item,.anchor-dialog__heading-item{padding:8px 12px;border:none;background:none;text-align:left;font-size:13px;color:#374151;cursor:pointer;border-radius:6px;display:flex;align-items:center;gap:8px}.anchor-dialog__file-item:hover,.anchor-dialog__heading-item:hover{background:#f3f4f6}.anchor-dialog__file-item--active,.anchor-dialog__heading-item--active{background:#eff6ff;color:#1d4ed8}.anchor-dialog__file-svg{color:#6b7280;flex-shrink:0}.anchor-dialog__file-item--active .anchor-dialog__file-svg{color:#1d4ed8}.anchor-dialog__file-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.anchor-dialog__heading-icon{color:#9ca3af;flex-shrink:0}.anchor-dialog__heading-level{font-size:10px;font-weight:700;color:#9ca3af;min-width:22px}.anchor-dialog__empty{padding:24px;text-align:center;color:silver;font-size:13px}.anchor-dialog__actions{display:flex;gap:8px;justify-content:flex-end}.anchor-dialog__btn{padding:7px 16px;border-radius:6px;font-size:13px;cursor:pointer;border:1px solid #e5e7eb;background:#fff;color:#4b5563;transition:all .12s}.anchor-dialog__btn:hover{background:#f9fafb}.anchor-dialog__btn--primary{background:#3b82f6;color:#fff;border-color:#3b82f6}.anchor-dialog__btn--primary:hover{background:#2563eb}.anchor-dialog__btn--primary:disabled{opacity:.5;cursor:default}@keyframes anchor-flash{0%,to{opacity:1}50%{opacity:.4}}.anchor-pin--highlight,.anchor-marker-chip--highlight{animation:anchor-flash .4s ease-in-out 3}*{margin:0;padding:0;box-sizing:border-box}html,body,#root{height:100%}body{font-family:-apple-system,PingFang SC,Microsoft YaHei,sans-serif;font-size:14px;color:#1f2937;background:#f5f5f5}.app{display:flex;height:100vh;overflow:hidden;position:relative;background:#fff}.app__canvas{flex:1;min-width:0;position:relative}.app__canvas iframe{width:100%;height:100%;border:none;background:#fff}.app__panel{position:relative;z-index:1;flex:0 0 40%;min-width:320px;display:flex;flex-direction:column;overflow:visible;background:#fff;border-left:1px solid #e0e2e5}.panel-toggle{position:absolute;top:50%;transform:translateY(-50%);z-index:20;width:20px;height:48px;border:1px solid #e5e7eb;border-right:none;border-radius:6px 0 0 6px;background:#fff;cursor:pointer;color:#bcc3ce;display:flex;align-items:center;justify-content:center;box-shadow:-2px 0 6px #0000000a;transition:color .15s,background .15s}.panel-toggle:hover{color:#6b7280;background:#f9fafb}.panel-toggle--open{left:-20px;right:auto}.panel-toggle:not(.panel-toggle--open){right:0;left:auto}.panel-resize-handle{position:absolute;left:0;top:0;width:4px;height:100%;cursor:col-resize;z-index:10;background:transparent;transition:background .15s}.panel-resize-handle:hover,.panel-resize-handle:active{background:#3b82f64d}.doc-panel{display:flex;flex-direction:column;height:100%}.doc-panel__tabbar{display:flex;align-items:stretch;border-bottom:1px solid #e5e7eb;background:#fafafa;flex-shrink:0;min-height:42px}.doc-panel__tabs{display:flex;flex:1;overflow-x:auto;gap:0}.doc-panel__tabs::-webkit-scrollbar{height:0}.doc-tab{padding:0 18px;height:42px;border:none;background:transparent;font-size:14px;color:#6b7280;cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:7px;border-bottom:2px solid transparent;transition:all .12s;position:relative}.doc-tab:hover{color:#374151;background:#f0f0f0}.doc-tab--dragging{opacity:.35}.doc-tab--drop-left{box-shadow:inset 2px 0 #3b82f6}.doc-tab--drop-right{box-shadow:inset -2px 0 #3b82f6}.doc-tab--active{color:#1f2937;background:#fff;border-bottom-color:#3b82f6;font-weight:500}.file-icon{flex-shrink:0}.doc-tab__name{max-width:140px;overflow:hidden;text-overflow:ellipsis}.doc-tab__dirty{width:6px;height:6px;border-radius:50%;background:#f59e0b;flex-shrink:0}.doc-tab__delete{display:none;align-items:center;justify-content:center;width:18px;height:18px;border-radius:3px;color:#9ca3af;flex-shrink:0;margin-left:2px;transition:all .12s}.doc-tab:hover .doc-tab__delete{display:flex}.doc-tab__delete:hover{color:#ef4444;background:#fef2f2}.doc-panel__new{position:relative;display:flex;align-items:center;padding:0 8px;border-left:1px solid #f0f0f0}.doc-panel__new-trigger{width:30px;height:30px;border:1px solid #e5e7eb;border-radius:4px;background:#fff;cursor:pointer;color:#9ca3af;display:flex;align-items:center;justify-content:center;transition:all .12s}.doc-panel__new-trigger:hover{color:#3b82f6;border-color:#93c5fd;background:#eff6ff}.doc-panel__new-menu{position:absolute;top:100%;right:4px;margin-top:4px;background:#fff;border:1px solid #e5e7eb;border-radius:6px;box-shadow:0 4px 16px #0000001a;z-index:50;min-width:170px;padding:4px}.doc-panel__new-menu button{width:100%;padding:7px 10px;border:none;background:none;text-align:left;font-size:13px;color:#374151;cursor:pointer;border-radius:4px;display:flex;align-items:center;gap:8px}.doc-panel__new-menu button:hover{background:#f3f4f6}.doc-panel__content{flex:1;overflow:hidden;display:flex;flex-direction:column}.doc-panel__placeholder{padding:60px 20px;text-align:center;color:silver;font-size:13px;line-height:1.8}.doc-panel__placeholder strong{color:#9ca3af}.delete-confirm-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000040;z-index:100;display:flex;align-items:center;justify-content:center}.delete-confirm{background:#fff;border-radius:10px;padding:24px 28px;min-width:320px;box-shadow:0 8px 30px #0000001f}.delete-confirm__msg{font-size:14px;color:#1f2937;margin-bottom:6px}.delete-confirm__warning{font-size:12px;color:#9ca3af;margin-bottom:20px}.delete-confirm__actions{display:flex;gap:8px;justify-content:flex-end}.delete-confirm__btn{padding:7px 18px;border-radius:6px;font-size:13px;cursor:pointer;border:1px solid #e5e7eb;background:#fff;color:#4b5563;transition:all .12s}.delete-confirm__btn:hover{background:#f9fafb}.delete-confirm__btn--danger{background:#ef4444;color:#fff;border-color:#ef4444}.delete-confirm__btn--danger:hover{background:#dc2626}.new-file-dialog{padding:32px 24px}.new-file-dialog__title{font-size:14px;font-weight:600;margin-bottom:14px;color:#374151}.new-file-dialog__input-row{display:flex;align-items:center;gap:4px;margin-bottom:8px}.new-file-dialog__input{flex:1;padding:8px 10px;border:1px solid #e5e7eb;border-radius:6px;font-size:14px;outline:none;transition:border .15s}.new-file-dialog__input:focus{border-color:#3b82f6;box-shadow:0 0 0 2px #3b82f61a}.new-file-dialog__ext{font-size:13px;color:#9ca3af}.new-file-dialog__error{font-size:12px;color:#ef4444;margin-bottom:8px}.new-file-dialog__actions{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}.new-file-dialog__btn{padding:7px 16px;border-radius:6px;font-size:13px;cursor:pointer;border:1px solid #e5e7eb;background:#fff;color:#4b5563;transition:all .12s}.new-file-dialog__btn--create{background:#3b82f6;color:#fff;border-color:#3b82f6}.new-file-dialog__btn--create:hover{background:#2563eb}.new-file-dialog__btn--cancel:hover{background:#f9fafb}.dirty-badge{font-size:11px;color:#f59e0b;padding:2px 8px;background:#fffbeb;border-radius:4px;font-weight:500}.md-editor{display:flex;flex-direction:column;height:100%}.md-editor__toolbar{display:flex;align-items:center;justify-content:space-between;padding:4px 12px;border-bottom:1px solid #f0f0f0;flex-shrink:0;background:#fafafa;min-height:36px;gap:8px}.md-editor__toolbar-left{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.md-editor__save{padding:5px 14px;border:1px solid #e5e7eb;border-radius:5px;background:#fff;font-size:13px;color:#6b7280;cursor:pointer;transition:all .12s;flex-shrink:0}.md-editor__save:hover{border-color:#3b82f6;color:#3b82f6}.md-editor__save:disabled{opacity:.5;cursor:default}.md-editor__editor{flex:1;overflow:auto}.md-editor__error{padding:60px 20px;text-align:center;color:#ef4444;font-size:13px}.md-toolbar{display:flex;align-items:center;gap:2px;flex-wrap:nowrap;overflow-x:auto}.md-toolbar::-webkit-scrollbar{height:0}.md-toolbar-btn{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;padding:0 5px;border:none;border-radius:4px;background:transparent;color:#6b7280;font-size:12px;font-weight:500;cursor:pointer;transition:all .1s;white-space:nowrap}.md-toolbar-btn:hover{background:#f3f4f6;color:#374151}.md-toolbar-btn--active{background:#e0e7ff;color:#3b5bdb}.md-toolbar-btn code{font-size:11px;font-family:inherit}.md-toolbar-divider{width:1px;height:16px;background:#e5e7eb;margin:0 3px;flex-shrink:0}.md-editor__editor .tiptap{outline:none;padding:16px 20px;min-height:100%;font-size:14px;line-height:1.7;color:#1f2937}.md-editor__editor .tiptap>*:first-child{margin-top:0}.md-editor__editor .tiptap h1{font-size:1.5em;font-weight:700;margin:1.4em 0 .6em;color:#111827;line-height:1.3}.md-editor__editor .tiptap h2{font-size:1.25em;font-weight:700;margin:1.2em 0 .5em;color:#1f2937;line-height:1.35}.md-editor__editor .tiptap h3{font-size:1.1em;font-weight:600;margin:1em 0 .4em;color:#374151;line-height:1.4}.md-editor__editor .tiptap p{margin:.5em 0}.md-editor__editor .tiptap strong{font-weight:600}.md-editor__editor .tiptap a{color:#2563eb;text-decoration:underline;text-underline-offset:2px}.md-editor__editor .tiptap code{font-family:JetBrains Mono,SF Mono,Fira Code,monospace;font-size:.88em;background:#f3f4f6;padding:1px 5px;border-radius:3px;color:#d6336c}.md-editor__editor .tiptap pre{background:#f8f9fa;color:#1f2937;border:1px solid #e5e7eb;border-radius:6px;padding:14px 16px;margin:.8em 0;overflow-x:auto;font-size:13px;line-height:1.6}.md-editor__editor .tiptap pre code{background:none;padding:0;border-radius:0;color:inherit;font-size:inherit}.md-editor__editor .tiptap blockquote{border-left:3px solid #d1d5db;padding-left:14px;margin:.8em 0;color:#6b7280}.md-editor__editor .tiptap hr{border:none;border-top:1px solid #e5e7eb;margin:1.2em 0}.md-editor__editor .tiptap ul,.md-editor__editor .tiptap ol{padding-left:1.5em;margin:.4em 0}.md-editor__editor .tiptap li{margin:.15em 0}.md-editor__editor .tiptap li p{margin:0}.md-editor__editor .tiptap table{border-collapse:collapse;width:100%;margin:.8em 0;font-size:13px}.md-editor__editor .tiptap th,.md-editor__editor .tiptap td{border:1px solid #d1d5db;padding:6px 10px;text-align:left;vertical-align:top;min-width:60px;position:relative}.md-editor__editor .tiptap th{background:#f9fafb;font-weight:600;color:#374151}.md-editor__editor .tiptap td{background:#fff}.md-editor__editor .tiptap th.selectedCell,.md-editor__editor .tiptap td.selectedCell{background:#dbeafe}.md-editor__editor .tiptap .column-resize-handle{position:absolute;right:-2px;top:0;bottom:0;width:4px;cursor:col-resize;background:#3b82f666}.md-editor__editor .tiptap .tableWrapper{overflow-x:auto;margin:.8em 0}.md-editor__editor .tiptap p.is-editor-empty:first-child:before{content:"Start writing...";float:left;color:silver;pointer-events:none;height:0}.excalidraw-editor{display:flex;flex-direction:column;height:100%}.excalidraw-editor__toolbar{display:flex;align-items:center;justify-content:space-between;padding:4px 12px;border-bottom:1px solid #f0f0f0;flex-shrink:0;background:#fafafa;min-height:30px;gap:8px}.excalidraw-editor__toolbar-left{display:flex;align-items:center;gap:8px}.excalidraw-editor__filename{font-size:11px;color:silver;font-family:JetBrains Mono,SF Mono,monospace}.excalidraw-editor__save{padding:5px 14px;border:1px solid #e5e7eb;border-radius:5px;background:#fff;font-size:13px;color:#6b7280;cursor:pointer;transition:all .12s}.excalidraw-editor__save:hover{border-color:#3b82f6;color:#3b82f6}.excalidraw-editor__save:disabled{opacity:.5;cursor:default}.excalidraw-editor__canvas{flex:1;position:relative;overflow:hidden}.excalidraw-loading{display:flex;align-items:center;justify-content:center;height:100%;color:silver;font-size:13px}.drawio-editor{display:flex;flex-direction:column;height:100%}.drawio-editor__toolbar{display:flex;align-items:center;justify-content:space-between;padding:4px 12px;border-bottom:1px solid #f0f0f0;flex-shrink:0;background:#fafafa;min-height:30px;gap:8px}.drawio-editor__toolbar-left{display:flex;align-items:center;gap:8px}.drawio-editor__filename{font-size:11px;color:silver;font-family:JetBrains Mono,SF Mono,monospace}.drawio-editor__save{padding:5px 14px;border:1px solid #e5e7eb;border-radius:5px;background:#fff;font-size:13px;color:#6b7280;cursor:pointer;transition:all .12s}.drawio-editor__save:hover{border-color:#3b82f6;color:#3b82f6}.drawio-editor__save:disabled{opacity:.5;cursor:default}.drawio-editor__canvas{flex:1;position:relative;overflow:hidden}.drawio-loading{display:flex;align-items:center;justify-content:center;height:100%;color:silver;font-size:13px}
|