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 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 { scanForCanvases, tryReadCanvasMeta } from '../lib/scanner.js';
8
- import { recordOpen, getRecentCanvases } from '../lib/history.js';
9
- import { findAvailablePort, isPortFree, getPortOccupant, killPort, listRunningInstances, registerInstance } from '../lib/port.js';
10
- import { fuzzySelectCanvas, fuzzySelectFromHistory, handlePortConflict } from '../lib/prompts.js';
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 canvas viewer + doc editor')
20
+ .description('Local viewer + doc editor for HTML prototypes & pm-canvas boards')
17
21
  .version('0.1.0');
18
22
 
19
- // Default command: canvas — interactive fuzzy search over history
23
+ // canvas — 从历史里挑一个根目录打开
20
24
  program
21
25
  .action(async () => {
22
- const entries = await getRecentCanvases();
26
+ const entries = await getRecentRoots();
23
27
  const selected = await fuzzySelectFromHistory(entries);
24
28
  if (!selected) process.exit(0);
25
29
 
26
- const meta = await tryReadCanvasMeta(selected.path);
27
- if (!meta) {
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 startServer(selected.path, meta);
35
+ await startServerForRoot(selected.rootPath);
33
36
  });
34
37
 
35
- // canvas open [path] — scan current directory or open a direct path
38
+ // canvas open [path] — 打开指定文件夹(或当前目录)
36
39
  program
37
40
  .command('open [path]')
38
- .description('Scan current directory for canvases, or open a path directly')
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
- if (targetPath) {
44
- const fullPath = resolve(targetPath);
46
+ const target = targetPath ? resolve(targetPath) : process.cwd();
45
47
 
46
- if (!existsSync(fullPath)) {
47
- console.error(` Directory not found: ${fullPath}`);
48
- process.exit(1);
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
- console.log(' Scanning for canvases...');
76
- const found = await scanForCanvases(cwd, 4);
77
- const history = await getRecentCanvases();
78
- const selected = await fuzzySelectCanvas(found, history);
79
- if (!selected) process.exit(0);
80
- await startServer(selected.path, selected);
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 startServer(canvasDir, meta) {
151
- const preferredPort = portOverride || 4800;
152
- let port;
153
-
154
- if (await isPortFree(preferredPort)) {
155
- port = preferredPort;
156
- } else {
157
- const occupant = getPortOccupant(preferredPort);
158
-
159
- if (occupant?.isPmCanvas) {
160
- const action = await handlePortConflict(preferredPort, occupant);
161
- if (!action) process.exit(0);
162
-
163
- switch (action) {
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 server = createServer(canvasDir);
184
- const title = meta?.title || canvasDir.split('/').pop();
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(` Canvas: ${title}`);
189
- console.log(` Path: ${canvasDir}`);
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, canvasDir, title).catch(() => {});
194
- await recordOpen(canvasDir, meta?.title).catch(() => {});
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}