@cadcrawl/cad-browser 0.3.0 → 0.4.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.
@@ -0,0 +1 @@
1
+ :root{color:#252623;background:#f3f2ee;font-family:Segoe UI Variable Text,Segoe UI,sans-serif;font-size:14px;font-synthesis:none;--paper: #fbfaf7;--panel: #f5f4ef;--sidebar-bg: #efeee9;--ink: #252623;--muted: #71736d;--faint: #9a9c96;--line: #deddd7;--line-soft: #e9e8e2;--accent: #2f6657;--accent-soft: #e4eee9;--sidebar: 250px;--inspector: 360px}*{box-sizing:border-box}html,body,#root{min-width:1024px;min-height:100%;margin:0}body{overflow:hidden}button,input,select{font:inherit}button,select{color:inherit}button:focus-visible,input:focus-visible,select:focus-visible,article:focus-visible{outline:2px solid rgba(47,102,87,.45);outline-offset:2px}.app-shell{height:100vh;display:grid;grid-template:56px calc(100vh - 56px) / var(--sidebar) minmax(480px,1fr) var(--inspector);background:var(--panel)}.app-shell.sidebar-collapsed{grid-template-columns:0 minmax(480px,1fr) var(--inspector)}.topbar{grid-column:1 / -1;z-index:10;display:grid;grid-template-columns:minmax(270px,1fr) minmax(360px,620px) minmax(270px,1fr);align-items:center;padding:0 14px;background:#fbfaf7fa;border-bottom:1px solid var(--line)}.brand,.top-actions,.search,.status-pill{display:flex;align-items:center}.brand{gap:9px;font-size:14px;font-weight:650}.brand-mark{display:grid;place-items:center;width:30px;height:30px;color:#fff;background:#2b2d29;border-radius:7px}.project-chip{max-width:180px;padding:4px 8px;overflow:hidden;border-radius:5px;background:#efede7;color:var(--muted);font-size:12px;font-weight:500;text-overflow:ellipsis;white-space:nowrap}.search{height:36px;gap:9px;padding:0 10px;color:var(--muted);background:#efeee9;border:1px solid transparent;border-radius:7px}.search:focus-within{background:#fff;border-color:#bbbdb6;box-shadow:0 0 0 3px #2f665714}.search input{flex:1;min-width:0;border:0;outline:0;background:transparent;color:var(--ink);font-size:13px}.search input::placeholder{color:#92948e}.search button{display:grid;padding:2px;border:0;background:transparent;cursor:pointer}.search kbd,.shortcut-list kbd{padding:2px 6px;border:1px solid #d8d7d1;border-bottom-color:#c8c7c1;border-radius:4px;background:#f8f7f3;color:#777972;font-family:inherit;font-size:10px;box-shadow:0 1px #d6d5cf}.top-actions{justify-content:flex-end;gap:9px}.icon-button{display:grid;place-items:center;width:32px;height:32px;padding:0;border:1px solid var(--line);border-radius:6px;background:var(--paper);cursor:pointer}.icon-button:hover{background:#efeee9}.icon-button.quiet{border-color:transparent;background:transparent;color:var(--muted)}.status-pill{gap:7px;padding:5px 9px;border:1px solid var(--line);border-radius:999px;color:var(--muted);font-size:11px}.status-dot{width:6px;height:6px;border-radius:50%;background:#4f8968;box-shadow:0 0 0 3px #e2eee6}.sidebar{min-width:0;display:grid;grid-template-rows:46px 1fr 48px;overflow:hidden;background:var(--sidebar-bg);border-right:1px solid var(--line)}.sidebar-collapsed .sidebar{opacity:0;pointer-events:none}.sidebar-head{display:flex;align-items:center;justify-content:space-between;padding:0 10px 0 15px;color:var(--muted);font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase}.tree-scroll{padding:2px 7px 14px;overflow:auto}.tree-row{width:100%;height:31px;display:flex;align-items:center;padding-left:calc(5px + var(--depth) * 15px);border-radius:5px;color:#54564f;font-size:12px}.tree-row:hover{background:#ffffffa6}.tree-row.active{background:#dcddd7;color:#242521;font-weight:600}.tree-toggle{flex:0 0 22px;height:27px;display:grid;place-items:center;padding:0;border:0;border-radius:4px;background:transparent;color:#898b84;cursor:pointer}.tree-toggle:hover{background:#484a4414;color:#4f514b}.tree-folder{min-width:0;flex:1;height:31px;display:flex;align-items:center;gap:7px;padding:0 7px 0 1px;border:0;background:transparent;color:inherit;font-weight:inherit;text-align:left;cursor:pointer}.tree-label{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-count{color:#999b95;font-size:10px;font-variant-numeric:tabular-nums}.sidebar-footer{display:flex;align-items:center;gap:15px;padding:0 15px;border-top:1px solid var(--line);color:#8b8d87;font-size:10px}.sidebar-footer strong{color:#555750}.sidebar-reveal{position:fixed;z-index:15;left:12px;top:68px;display:grid;place-items:center;width:34px;height:34px;padding:0;border:1px solid var(--line);border-radius:7px;background:var(--paper);box-shadow:0 5px 18px #1e201c1f;cursor:pointer}.workspace{min-width:0;overflow:auto;background:var(--paper)}.content-head{position:sticky;z-index:4;top:0;min-height:108px;display:flex;align-items:flex-end;justify-content:space-between;gap:24px;padding:20px 26px 18px;background:#fbfaf7f5;border-bottom:1px solid var(--line-soft);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}.content-title{min-width:0}.breadcrumbs{display:flex;align-items:center;gap:4px;margin-bottom:8px;color:var(--faint);font-size:11px}.breadcrumbs button{max-width:170px;padding:0;overflow:hidden;border:0;background:transparent;color:inherit;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}.breadcrumbs button:hover{color:var(--ink)}.content-head h1{margin:0;overflow:hidden;color:#292a26;font-family:Georgia,Times New Roman,serif;font-size:25px;font-weight:500;letter-spacing:-.035em;text-overflow:ellipsis;white-space:nowrap}.content-head p{margin:5px 0 0;color:var(--muted);font-size:12px}.browser-toolbar{display:flex;align-items:center;gap:7px}.dropdown,.sort-control,.grid-scale,.view-switcher{height:32px;display:flex;align-items:center;border:1px solid var(--line);border-radius:6px;background:#f3f2ed;color:#686a64}.dropdown{position:relative}.dropdown-trigger{height:30px;display:flex;align-items:center;gap:6px;padding:0 8px;border:0;border-radius:5px;background:transparent;color:#5c5e58;font-size:11px;cursor:pointer}.dropdown-trigger>span{white-space:nowrap}.dropdown.open .dropdown-trigger,.dropdown-trigger:hover{background:#fff;color:var(--ink)}.dropdown-menu{position:absolute;z-index:20;top:calc(100% + 5px);left:0;min-width:100%;padding:5px;border:1px solid #d6d7d1;border-radius:7px;background:#fff;box-shadow:0 10px 28px #1e201c24}.dropdown-menu button{width:100%;min-width:130px;height:30px;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:0 8px;border:0;border-radius:5px;background:transparent;color:#555750;font-size:11px;text-align:left;cursor:pointer}.dropdown-menu button:hover{background:#f1f2ee;color:var(--ink)}.dropdown-menu button.selected{color:#28584b;font-weight:650}.sort-control .dropdown{border:0;background:transparent}.sort-control .dropdown-trigger{padding-left:8px}.sort-control>button,.grid-scale button,.view-switcher button{display:grid;place-items:center;height:30px;padding:0;border:0;border-radius:5px;background:transparent;color:#74766f;cursor:pointer}.sort-control>button{width:29px;border-left:1px solid var(--line);border-radius:0 5px 5px 0}.grid-scale button{width:27px}.grid-scale button:hover:not(:disabled),.view-switcher button:hover,.sort-control>button:hover{background:#fff;color:var(--ink)}.grid-scale button:disabled{opacity:.28;cursor:default}.grid-scale span{min-width:25px;color:#62645e;font-size:10px;text-align:center;font-variant-numeric:tabular-nums}.grid-scale span:after{content:" col";color:#999b95}.view-switcher{padding:1px}.view-switcher button{width:29px;height:28px}.view-switcher button.active{background:#fff;color:var(--ink);box-shadow:0 1px 3px #1e201c1f}.file-grid{display:grid;grid-template-columns:repeat(var(--grid-columns, 4),minmax(0,1fr));gap:15px;padding:20px 26px 40px}.file-card{min-width:0;overflow:hidden;border:1px solid var(--line-soft);border-radius:8px;background:#fff;cursor:default;opacity:0;transform:translateY(4px);animation:reveal .25s ease forwards;animation-delay:var(--delay);transition:border-color .14s,background-color .14s}@keyframes reveal{to{opacity:1;transform:translateY(0)}}.file-card:hover{border-color:#bfc1ba;background:#fdfdfa}.file-card.selected{border-color:#4f7b6c;background:#fbfdfc}.thumb{position:relative;aspect-ratio:4 / 3;display:grid;place-items:center;overflow:hidden;background:#f4f3ef;border-bottom:1px solid var(--line-soft)}.thumb img{width:100%;height:100%;object-fit:contain}.placeholder{width:100%;height:100%;display:grid;place-content:center;justify-items:center;gap:8px;color:#878a83}.placeholder span{font-family:Georgia,Times New Roman,serif;font-size:11px;font-style:italic;font-weight:400}.placeholder.kind-cad{color:#557568;background:#edf2ef}.placeholder.kind-pdf{color:#9a5b49;background:#f4ece8}.placeholder.kind-text{color:#66705e;background:#eef0e9}.placeholder.kind-image{color:#636d82;background:#eceff3}.type-badge{position:absolute;left:8px;top:8px;padding:3px 6px;border:1px solid rgba(40,42,37,.12);border-radius:4px;background:#ffffffe6;color:#64665f;font-size:9px;font-weight:700;letter-spacing:.055em;text-transform:uppercase;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.processing{position:absolute;inset:auto 8px 8px;display:flex;align-items:center;gap:6px;padding:6px 8px;border-radius:5px;background:#242621d6;color:#fff;font-size:10px}.processing svg,.analysis-note svg,.loading-screen>svg{animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.file-copy{padding:10px 11px 11px}.file-name{overflow:hidden;color:#31322e;font-size:12px;font-weight:620;text-overflow:ellipsis;white-space:nowrap}.file-copy span{display:block;margin-top:4px;overflow:hidden;color:var(--muted);font-size:10px;font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap}.file-grid.density-compact{gap:8px;padding-inline:18px}.file-grid.density-compact .file-copy{padding:7px}.file-grid.density-compact .file-name{font-size:10px}.file-grid.density-compact .file-copy span{font-size:8px}.file-grid.density-compact .type-badge{left:5px;top:5px;padding:2px 4px;font-size:7px}.file-grid.density-micro{gap:5px;padding-inline:13px}.file-grid.density-micro .file-card{border-radius:5px}.file-grid.density-micro .file-copy{padding:5px 6px 6px}.file-grid.density-micro .file-name{font-size:8px}.file-grid.density-micro .file-copy span{display:none}.file-grid.density-micro .type-badge{left:3px;top:3px;padding:1px 3px;border-radius:3px;font-size:6px}.file-list{padding:13px 26px 36px}.file-list .file-card{display:grid;grid-template-columns:88px minmax(0,1fr);margin-bottom:7px}.file-list .thumb{height:68px;aspect-ratio:auto;border:0;border-right:1px solid var(--line-soft)}.file-list .placeholder svg{width:23px}.file-list .placeholder span,.file-list .type-badge{display:none}.file-list .file-copy{align-self:center}.inspector{min-width:0;overflow-y:auto;background:#f8f7f3;border-left:1px solid var(--line)}.empty-inspector{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:36px;color:var(--muted);text-align:center}.inspector-empty-mark{display:grid;place-items:center;width:44px;height:44px;border:1px solid var(--line);border-radius:10px;background:#fff;color:#787a74}.empty-inspector h2{margin:15px 0 5px;color:var(--ink);font-family:Georgia,Times New Roman,serif;font-size:18px;font-weight:400}.empty-inspector p{max-width:250px;margin:0;font-size:12px;line-height:1.55}.shortcut-list{width:210px;display:grid;grid-template-columns:1fr auto;align-items:center;gap:8px 12px;margin-top:22px;color:#7e807a;font-size:11px;text-align:left}.inspector-head{min-height:61px;display:grid;grid-template-columns:36px minmax(0,1fr) 32px;align-items:center;gap:10px;padding:11px 11px 11px 14px;border-bottom:1px solid var(--line-soft)}.inspector-title{min-width:0}.inspector-title strong,.inspector-title span{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.inspector-title strong{color:#2b2c28;font-size:13px;font-weight:650}.inspector-title span{margin-top:3px;color:var(--muted);font-size:11px}.type-icon{display:grid;place-items:center;width:36px;height:36px;border-radius:8px;background:#e8e7e1;color:#656760}.type-icon.kind-cad{background:#e2ece7;color:#46705f}.type-icon.kind-pdf{background:#f0e3dd;color:#9a513d}.type-icon.kind-text{background:#e8ece2;color:#5e6c53}.inspector-preview{position:relative;width:calc(100% - 28px);aspect-ratio:4 / 3;display:grid;place-items:center;margin:14px;padding:0;overflow:hidden;border:1px solid var(--line);border-radius:8px;background:#f0efea;cursor:zoom-in}.inspector-preview:disabled{cursor:default}.inspector-preview img{width:100%;height:100%;object-fit:contain}.inspector-actions{display:grid;grid-template-columns:1.3fr 1fr 34px;gap:7px;margin:-1px 14px 14px}.inspector-actions button{height:34px;display:flex;align-items:center;justify-content:center;gap:7px;padding:0 9px;border:1px solid #cccec7;border-radius:6px;background:#fff;color:#4c4e48;font-size:11px;font-weight:600;cursor:pointer}.inspector-actions button:hover{border-color:#aeb1a9;background:#fdfdfb}.inspector-actions .primary-action{border-color:#315f52;background:#315f52;color:#fff}.inspector-actions .primary-action:hover{background:#274f44}.analysis-note,.error-note{margin:0 14px 13px;padding:10px 11px;border-radius:6px;font-size:12px;line-height:1.45}.analysis-note{display:flex;align-items:center;gap:8px;background:#e5eee9;color:#4e695e}.error-note{background:#f2e3df;color:#884534}.inspector-section{padding:17px 18px 18px;border-top:1px solid var(--line-soft)}.inspector-section h3{margin:0 0 12px;color:#656861;font-size:11px;font-weight:700;letter-spacing:.065em;text-transform:uppercase}.property,.vector{min-height:32px;display:flex;align-items:center;justify-content:space-between;gap:14px;font-size:13px}.property span,.vector span{color:var(--muted)}.property strong{overflow:hidden;color:#292a27;font-weight:600;text-align:right;text-overflow:ellipsis}.vector code{color:#4f514b;font-family:Cascadia Mono,Consolas,monospace;font-size:11px}.dimension-strip{display:grid;grid-template-columns:repeat(3,1fr);gap:7px;margin-bottom:9px}.dimension-strip div{min-width:0;padding:10px;border-radius:6px;background:#ebeae5}.dimension-strip span,.dimension-strip strong{display:block}.dimension-strip span{margin-bottom:4px;color:#898b85;font-size:10px;font-weight:700}.dimension-strip strong{overflow:hidden;color:#292a27;font-size:13px;font-variant-numeric:tabular-nums;text-overflow:ellipsis}.swatches{display:grid;gap:8px}.swatch{display:grid;grid-template-columns:22px 1fr auto;align-items:center;gap:9px;font-size:12px}.swatch>span{width:22px;height:22px;border:1px solid rgba(0,0,0,.14);border-radius:5px}.swatch code,.model-tree{font-family:Cascadia Mono,Consolas,monospace}.swatch code{font-size:11px}.swatch small{color:var(--muted);font-size:11px}.material-name{margin-bottom:7px;font-size:12px;font-weight:600}.text-preview{max-height:180px;overflow:auto;margin-top:8px;padding:11px;border-radius:6px;background:#eeede8;color:#4e504a;font-family:Georgia,Times New Roman,serif;font-size:13px;line-height:1.6;white-space:pre-wrap}.plain-text-preview{max-height:340px;margin:10px 0 0;overflow:auto;padding:12px;border-radius:6px;background:#ebeae5;color:#41433e;font-family:Cascadia Mono,Consolas,monospace;font-size:11px;line-height:1.65;white-space:pre-wrap;overflow-wrap:anywhere}.text-limit-note{margin-top:8px;color:#8a6b43;font-size:11px}.model-tree{max-height:240px;margin:0;overflow:auto;padding:11px;border-radius:6px;background:#ebeae5;color:#474944;font-size:11px;line-height:1.65;white-space:pre}.inspector-footer{padding:14px 18px 20px;border-top:1px solid var(--line-soft)}.inspector-footer button{display:flex;align-items:center;gap:7px;padding:0;border:0;background:transparent;color:#60635c;font-size:11px;cursor:pointer}.inspector-footer button:hover{color:var(--ink)}.modal-backdrop{position:fixed;z-index:30;top:0;right:0;bottom:0;left:0;display:grid;place-items:center;padding:38px;background:#1e201c94;-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);animation:fade-in .14s ease}@keyframes fade-in{0%{opacity:0}}.preview-modal{width:min(1100px,91vw);height:min(820px,89vh);display:grid;grid-template-rows:58px 1fr;overflow:hidden;border:1px solid rgba(255,255,255,.2);border-radius:10px;background:#f8f7f3;box-shadow:0 28px 80px #14151257}.modal-head{display:flex;align-items:center;justify-content:space-between;padding:0 12px 0 18px;border-bottom:1px solid var(--line)}.modal-head strong,.modal-head span{display:block}.modal-head strong{font-size:13px}.modal-head span{margin-top:3px;color:var(--muted);font-size:11px}.modal-canvas{min-height:0;display:grid;place-items:center;padding:24px;background:#e9e8e3}.modal-canvas img{max-width:100%;max-height:100%;object-fit:contain;box-shadow:0 10px 34px #23242029}.empty-state{grid-column:1 / -1;min-height:300px;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--muted);text-align:center}.empty-state h2{margin:12px 0 4px;color:var(--ink);font-family:Georgia,Times New Roman,serif;font-size:18px;font-weight:400}.empty-state p{max-width:390px;margin:0;font-size:12px;line-height:1.5}.loading-screen{height:100vh;display:flex;align-items:center;justify-content:center;gap:10px;background:var(--paper);color:var(--muted);font-size:13px}.toast{position:fixed;z-index:40;left:50%;bottom:22px;display:flex;align-items:center;gap:8px;padding:9px 12px;border:1px solid rgba(255,255,255,.13);border-radius:7px;background:#252722f0;color:#fff;font-size:12px;box-shadow:0 10px 30px #191a1738;transform:translate(-50%)}.toast.error{background:#773226f5}@media(max-width:1220px){:root{--sidebar: 220px;--inspector: 330px}.content-head,.file-grid,.file-list{padding-inline:18px}.browser-toolbar{gap:5px}}
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="theme-color" content="#f5f3ee" />
7
7
  <title>CAD Browser</title>
8
- <script type="module" crossorigin src="/assets/index-HFIcPO3j.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-C1XW2UmF.css">
8
+ <script type="module" crossorigin src="/assets/index-B37D4sOF.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-CQQ62v1M.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cadcrawl/cad-browser",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Local engineering file browser with CAD and PDF previews powered by cad-toolbox.",
5
5
  "type": "module",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
package/src/analyzer.js CHANGED
@@ -1,8 +1,11 @@
1
+ import fs from 'node:fs/promises';
1
2
  import { inspectFile } from '@cadcrawl/cad-toolbox/src/inspect.js';
2
3
  import { CAD_VIEW_DIRECTIONS } from '@cadcrawl/cad-toolbox/src/render-options.js';
3
4
  import { renderStem } from './cache.js';
4
5
 
5
6
  export async function analyzeFile(absolutePath, relativePath, cache, kind) {
7
+ if (kind === 'text') return analyzeTextFile(absolutePath);
8
+
6
9
  const fields = new Set(['metadata', 'renders']);
7
10
  if (kind === 'cad' && /\.(step|stp)$/i.test(relativePath)) fields.add('tree');
8
11
  if (kind === 'pdf') fields.add('text');
@@ -29,3 +32,34 @@ export async function analyzeFile(absolutePath, relativePath, cache, kind) {
29
32
  previewPath: result.renders?.[0] ?? null,
30
33
  };
31
34
  }
35
+
36
+ async function analyzeTextFile(absolutePath) {
37
+ const stat = await fs.stat(absolutePath);
38
+ const maximumBytes = 256 * 1024;
39
+ const bytesToRead = Math.min(stat.size, maximumBytes);
40
+ const handle = await fs.open(absolutePath, 'r');
41
+ let text;
42
+ try {
43
+ const buffer = Buffer.alloc(bytesToRead);
44
+ await handle.read(buffer, 0, bytesToRead, 0);
45
+ text = buffer.toString('utf8').replace(/^\uFEFF/, '');
46
+ } finally {
47
+ await handle.close();
48
+ }
49
+
50
+ const lines = text.length === 0 ? 0 : text.split(/\r\n|\r|\n/).length;
51
+ const words = text.trim() ? text.trim().split(/\s+/u).length : 0;
52
+ return {
53
+ metadata: {
54
+ text: {
55
+ lines,
56
+ words,
57
+ characters: text.length,
58
+ truncated: stat.size > maximumBytes,
59
+ },
60
+ },
61
+ tree: null,
62
+ text,
63
+ previewPath: null,
64
+ };
65
+ }
package/src/file-types.js CHANGED
@@ -1,15 +1,18 @@
1
1
  export const CAD_EXTENSIONS = new Set(['.step', '.stp', '.stl', '.3mf']);
2
2
  export const PDF_EXTENSIONS = new Set(['.pdf']);
3
3
  export const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp', '.svg']);
4
+ export const TEXT_EXTENSIONS = new Set(['.md', '.txt']);
4
5
 
5
6
  export function classifyExtension(extension) {
6
7
  const normalized = extension.toLowerCase();
7
8
  if (CAD_EXTENSIONS.has(normalized)) return 'cad';
8
9
  if (PDF_EXTENSIONS.has(normalized)) return 'pdf';
9
10
  if (IMAGE_EXTENSIONS.has(normalized)) return 'image';
11
+ if (TEXT_EXTENSIONS.has(normalized)) return 'text';
10
12
  return 'file';
11
13
  }
12
14
 
13
15
  export function canAnalyze(extension) {
14
- return CAD_EXTENSIONS.has(extension.toLowerCase()) || PDF_EXTENSIONS.has(extension.toLowerCase());
16
+ const normalized = extension.toLowerCase();
17
+ return CAD_EXTENSIONS.has(normalized) || PDF_EXTENSIONS.has(normalized) || TEXT_EXTENSIONS.has(normalized);
15
18
  }
package/src/main.jsx CHANGED
@@ -10,7 +10,7 @@ import './styles.css';
10
10
 
11
11
  const number = new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 });
12
12
  const date = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeStyle: 'short' });
13
- const TYPE_FILTERS = ['all', 'cad', 'pdf', 'image', 'file'];
13
+ const TYPE_FILTERS = ['all', 'cad', 'pdf', 'text', 'image', 'file'];
14
14
  const SORT_OPTIONS = [
15
15
  ['name', 'Name'],
16
16
  ['modified', 'Modified'],
@@ -159,7 +159,7 @@ function App() {
159
159
  ref={searchRef}
160
160
  value={query}
161
161
  onChange={(event) => setQuery(event.target.value)}
162
- placeholder="Search files, paths, and PDF text"
162
+ placeholder="Search files, paths, and document text"
163
163
  aria-label="Search project"
164
164
  />
165
165
  {query && <button onClick={() => setQuery('')} aria-label="Clear search"><X size={14} /></button>}
@@ -181,7 +181,7 @@ function App() {
181
181
  </div>
182
182
  <div className="sidebar-footer">
183
183
  <div><strong>{project.counts.total}</strong> files</div>
184
- <div><strong>{project.counts.engineering}</strong> previewable</div>
184
+ <div><strong>{project.counts.engineering}</strong> indexed</div>
185
185
  </div>
186
186
  </aside>
187
187
 
@@ -335,12 +335,21 @@ function TreeNode({ node, activeFolder, onFolder, rootName, depth = 0 }) {
335
335
  const label = depth === 0 ? rootName : node.name;
336
336
  return (
337
337
  <div>
338
- <button className={`tree-row ${activeFolder === node.path ? 'active' : ''}`} style={{ '--depth': depth }} onClick={() => { setOpen(!open); onFolder(node.path); }}>
339
- <span className="tree-toggle">{open ? <ChevronDown size={14} /> : <ChevronRight size={14} />}</span>
340
- {open ? <FolderOpen size={16} /> : <Folder size={16} />}
341
- <span className="tree-label">{label}</span>
342
- <span className="tree-count">{node.fileCount || countNodeFiles(node)}</span>
343
- </button>
338
+ <div className={`tree-row ${activeFolder === node.path ? 'active' : ''}`} style={{ '--depth': depth }}>
339
+ <button
340
+ className="tree-toggle"
341
+ onClick={() => setOpen((value) => !value)}
342
+ aria-label={`${open ? 'Collapse' : 'Expand'} ${label}`}
343
+ aria-expanded={open}
344
+ >
345
+ {open ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
346
+ </button>
347
+ <button className="tree-folder" onClick={() => onFolder(node.path)} title={`Open ${label}`}>
348
+ {open ? <FolderOpen size={16} /> : <Folder size={16} />}
349
+ <span className="tree-label">{label}</span>
350
+ <span className="tree-count">{node.fileCount || countNodeFiles(node)}</span>
351
+ </button>
352
+ </div>
344
353
  {open && node.children.filter((child) => child.type === 'directory').map((child) => (
345
354
  <TreeNode key={child.path} node={child} activeFolder={activeFolder} onFolder={onFolder} rootName={rootName} depth={depth + 1} />
346
355
  ))}
@@ -372,14 +381,19 @@ function FileCard({ file, index, view, selected, onSelect, onOpen }) {
372
381
  }
373
382
 
374
383
  function FilePlaceholder({ file }) {
375
- const Icon = file.kind === 'pdf' ? FileText : file.kind === 'image' ? FileImage : file.kind === 'cad' ? Boxes : File;
376
- return <div className={`placeholder kind-${file.kind}`}><Icon size={38} strokeWidth={1.35} /><span>{file.kind === 'cad' ? 'CAD model' : file.kind === 'pdf' ? 'Drawing' : 'File'}</span></div>;
384
+ const Icon = file.kind === 'pdf' || file.kind === 'text' ? FileText : file.kind === 'image' ? FileImage : file.kind === 'cad' ? Boxes : File;
385
+ const label = file.kind === 'cad' ? 'CAD model' : file.kind === 'pdf' ? 'Drawing' : file.kind === 'text' ? 'Text document' : 'File';
386
+ return <div className={`placeholder kind-${file.kind}`}><Icon size={38} strokeWidth={1.35} /><span>{label}</span></div>;
377
387
  }
378
388
 
379
389
  function FileSummary({ file, compact }) {
380
390
  const size = file.analysis?.metadata?.bounding_box?.size;
381
391
  if (size) return <span>{size.map(formatDimension).join(' × ')}</span>;
382
392
  if (file.kind === 'pdf' && file.analysis?.metadata?.pages) return <span>{file.analysis.metadata.pages} {file.analysis.metadata.pages === 1 ? 'page' : 'pages'}</span>;
393
+ if (file.kind === 'text' && file.analysis?.metadata?.text) {
394
+ const text = file.analysis.metadata.text;
395
+ return <span>{number.format(text.lines)} lines · {number.format(text.words)} words</span>;
396
+ }
383
397
  if (!compact) return <span>{formatBytes(file.size)} · {date.format(new Date(file.modifiedAt))}</span>;
384
398
  return <span>{formatBytes(file.size)}</span>;
385
399
  }
@@ -451,6 +465,15 @@ function Inspector({ file, onClose, onPreview, onOpen, onReveal, onCopy, onReana
451
465
  {file.analysis?.text && <div className="text-preview">{file.analysis.text}</div>}
452
466
  </InspectorSection>
453
467
  )}
468
+ {metadata?.text && (
469
+ <InspectorSection title="Text">
470
+ <Property label="Lines" value={number.format(metadata.text.lines)} />
471
+ <Property label="Words" value={number.format(metadata.text.words)} />
472
+ <Property label="Characters" value={number.format(metadata.text.characters)} />
473
+ {metadata.text.truncated && <div className="text-limit-note">Showing the first 256 KB.</div>}
474
+ {file.analysis?.text && <pre className="plain-text-preview">{file.analysis.text}</pre>}
475
+ </InspectorSection>
476
+ )}
454
477
  {file.analysis?.tree && <InspectorSection title="Model structure"><pre className="model-tree">{file.analysis.tree}</pre></InspectorSection>}
455
478
 
456
479
  <div className="inspector-footer">
@@ -493,7 +516,7 @@ function PreviewModal({ file, onClose }) {
493
516
  }
494
517
 
495
518
  function TypeIcon({ kind }) {
496
- const Icon = kind === 'pdf' ? FileText : kind === 'image' ? FileImage : kind === 'cad' ? Boxes : File;
519
+ const Icon = kind === 'pdf' || kind === 'text' ? FileText : kind === 'image' ? FileImage : kind === 'cad' ? Boxes : File;
497
520
  return <span className={`type-icon kind-${kind}`}><Icon size={19} strokeWidth={1.7} /></span>;
498
521
  }
499
522
  function EmptyState({ query }) {
@@ -123,5 +123,5 @@ function countKinds(files) {
123
123
  counts[file.kind] = (counts[file.kind] ?? 0) + 1;
124
124
  if (file.analyzable) counts.engineering += 1;
125
125
  return counts;
126
- }, { total: 0, engineering: 0, cad: 0, pdf: 0, image: 0, file: 0 });
126
+ }, { total: 0, engineering: 0, cad: 0, pdf: 0, image: 0, text: 0, file: 0 });
127
127
  }
package/src/styles.css CHANGED
@@ -140,19 +140,41 @@ button:focus-visible, input:focus-visible, select:focus-visible, article:focus-v
140
140
  height: 31px;
141
141
  display: flex;
142
142
  align-items: center;
143
- gap: 7px;
144
- padding: 0 7px 0 calc(5px + var(--depth) * 15px);
145
- border: 0;
143
+ padding-left: calc(5px + var(--depth) * 15px);
146
144
  border-radius: 5px;
147
- background: transparent;
148
145
  color: #54564f;
149
146
  font-size: 12px;
150
- text-align: left;
151
- cursor: pointer;
152
147
  }
153
148
  .tree-row:hover { background: rgba(255,255,255,.65); }
154
149
  .tree-row.active { background: #dcddd7; color: #242521; font-weight: 600; }
155
- .tree-toggle { display: grid; width: 14px; color: #898b84; }
150
+ .tree-toggle {
151
+ flex: 0 0 22px;
152
+ height: 27px;
153
+ display: grid;
154
+ place-items: center;
155
+ padding: 0;
156
+ border: 0;
157
+ border-radius: 4px;
158
+ background: transparent;
159
+ color: #898b84;
160
+ cursor: pointer;
161
+ }
162
+ .tree-toggle:hover { background: rgba(72,74,68,.08); color: #4f514b; }
163
+ .tree-folder {
164
+ min-width: 0;
165
+ flex: 1;
166
+ height: 31px;
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 7px;
170
+ padding: 0 7px 0 1px;
171
+ border: 0;
172
+ background: transparent;
173
+ color: inherit;
174
+ font-weight: inherit;
175
+ text-align: left;
176
+ cursor: pointer;
177
+ }
156
178
  .tree-label { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
157
179
  .tree-count { color: #999b95; font-size: 10px; font-variant-numeric: tabular-nums; }
158
180
  .sidebar-footer { display: flex; align-items: center; gap: 15px; padding: 0 15px; border-top: 1px solid var(--line); color: #8b8d87; font-size: 10px; }
@@ -306,6 +328,7 @@ button:focus-visible, input:focus-visible, select:focus-visible, article:focus-v
306
328
  .placeholder span { font-family: Georgia, "Times New Roman", serif; font-size: 11px; font-style: italic; font-weight: 400; }
307
329
  .placeholder.kind-cad { color: #557568; background: #edf2ef; }
308
330
  .placeholder.kind-pdf { color: #9a5b49; background: #f4ece8; }
331
+ .placeholder.kind-text { color: #66705e; background: #eef0e9; }
309
332
  .placeholder.kind-image { color: #636d82; background: #eceff3; }
310
333
  .type-badge {
311
334
  position: absolute;
@@ -362,6 +385,7 @@ button:focus-visible, input:focus-visible, select:focus-visible, article:focus-v
362
385
  .type-icon { display: grid; place-items: center; width: 36px; height: 36px; border-radius: 8px; background: #e8e7e1; color: #656760; }
363
386
  .type-icon.kind-cad { background: #e2ece7; color: #46705f; }
364
387
  .type-icon.kind-pdf { background: #f0e3dd; color: #9a513d; }
388
+ .type-icon.kind-text { background: #e8ece2; color: #5e6c53; }
365
389
  .inspector-preview { position: relative; width: calc(100% - 28px); aspect-ratio: 4 / 3; display: grid; place-items: center; margin: 14px; padding: 0; overflow: hidden; border: 1px solid var(--line); border-radius: 8px; background: #f0efea; cursor: zoom-in; }
366
390
  .inspector-preview:disabled { cursor: default; }
367
391
  .inspector-preview img { width: 100%; height: 100%; object-fit: contain; }
@@ -406,6 +430,21 @@ button:focus-visible, input:focus-visible, select:focus-visible, article:focus-v
406
430
  .swatch small { color: var(--muted); font-size: 11px; }
407
431
  .material-name { margin-bottom: 7px; font-size: 12px; font-weight: 600; }
408
432
  .text-preview { max-height: 180px; overflow: auto; margin-top: 8px; padding: 11px; border-radius: 6px; background: #eeede8; color: #4e504a; font-family: Georgia, "Times New Roman", serif; font-size: 13px; line-height: 1.6; white-space: pre-wrap; }
433
+ .plain-text-preview {
434
+ max-height: 340px;
435
+ margin: 10px 0 0;
436
+ overflow: auto;
437
+ padding: 12px;
438
+ border-radius: 6px;
439
+ background: #ebeae5;
440
+ color: #41433e;
441
+ font-family: "Cascadia Mono", Consolas, monospace;
442
+ font-size: 11px;
443
+ line-height: 1.65;
444
+ white-space: pre-wrap;
445
+ overflow-wrap: anywhere;
446
+ }
447
+ .text-limit-note { margin-top: 8px; color: #8a6b43; font-size: 11px; }
409
448
  .model-tree { max-height: 240px; margin: 0; overflow: auto; padding: 11px; border-radius: 6px; background: #ebeae5; color: #474944; font-size: 11px; line-height: 1.65; white-space: pre; }
410
449
  .inspector-footer { padding: 14px 18px 20px; border-top: 1px solid var(--line-soft); }
411
450
  .inspector-footer button { display: flex; align-items: center; gap: 7px; padding: 0; border: 0; background: transparent; color: #60635c; font-size: 11px; cursor: pointer; }
@@ -1 +0,0 @@
1
- :root{color:#252623;background:#f3f2ee;font-family:Segoe UI Variable Text,Segoe UI,sans-serif;font-size:14px;font-synthesis:none;--paper: #fbfaf7;--panel: #f5f4ef;--sidebar-bg: #efeee9;--ink: #252623;--muted: #71736d;--faint: #9a9c96;--line: #deddd7;--line-soft: #e9e8e2;--accent: #2f6657;--accent-soft: #e4eee9;--sidebar: 250px;--inspector: 360px}*{box-sizing:border-box}html,body,#root{min-width:1024px;min-height:100%;margin:0}body{overflow:hidden}button,input,select{font:inherit}button,select{color:inherit}button:focus-visible,input:focus-visible,select:focus-visible,article:focus-visible{outline:2px solid rgba(47,102,87,.45);outline-offset:2px}.app-shell{height:100vh;display:grid;grid-template:56px calc(100vh - 56px) / var(--sidebar) minmax(480px,1fr) var(--inspector);background:var(--panel)}.app-shell.sidebar-collapsed{grid-template-columns:0 minmax(480px,1fr) var(--inspector)}.topbar{grid-column:1 / -1;z-index:10;display:grid;grid-template-columns:minmax(270px,1fr) minmax(360px,620px) minmax(270px,1fr);align-items:center;padding:0 14px;background:#fbfaf7fa;border-bottom:1px solid var(--line)}.brand,.top-actions,.search,.status-pill{display:flex;align-items:center}.brand{gap:9px;font-size:14px;font-weight:650}.brand-mark{display:grid;place-items:center;width:30px;height:30px;color:#fff;background:#2b2d29;border-radius:7px}.project-chip{max-width:180px;padding:4px 8px;overflow:hidden;border-radius:5px;background:#efede7;color:var(--muted);font-size:12px;font-weight:500;text-overflow:ellipsis;white-space:nowrap}.search{height:36px;gap:9px;padding:0 10px;color:var(--muted);background:#efeee9;border:1px solid transparent;border-radius:7px}.search:focus-within{background:#fff;border-color:#bbbdb6;box-shadow:0 0 0 3px #2f665714}.search input{flex:1;min-width:0;border:0;outline:0;background:transparent;color:var(--ink);font-size:13px}.search input::placeholder{color:#92948e}.search button{display:grid;padding:2px;border:0;background:transparent;cursor:pointer}.search kbd,.shortcut-list kbd{padding:2px 6px;border:1px solid #d8d7d1;border-bottom-color:#c8c7c1;border-radius:4px;background:#f8f7f3;color:#777972;font-family:inherit;font-size:10px;box-shadow:0 1px #d6d5cf}.top-actions{justify-content:flex-end;gap:9px}.icon-button{display:grid;place-items:center;width:32px;height:32px;padding:0;border:1px solid var(--line);border-radius:6px;background:var(--paper);cursor:pointer}.icon-button:hover{background:#efeee9}.icon-button.quiet{border-color:transparent;background:transparent;color:var(--muted)}.status-pill{gap:7px;padding:5px 9px;border:1px solid var(--line);border-radius:999px;color:var(--muted);font-size:11px}.status-dot{width:6px;height:6px;border-radius:50%;background:#4f8968;box-shadow:0 0 0 3px #e2eee6}.sidebar{min-width:0;display:grid;grid-template-rows:46px 1fr 48px;overflow:hidden;background:var(--sidebar-bg);border-right:1px solid var(--line)}.sidebar-collapsed .sidebar{opacity:0;pointer-events:none}.sidebar-head{display:flex;align-items:center;justify-content:space-between;padding:0 10px 0 15px;color:var(--muted);font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase}.tree-scroll{padding:2px 7px 14px;overflow:auto}.tree-row{width:100%;height:31px;display:flex;align-items:center;gap:7px;padding:0 7px 0 calc(5px + var(--depth) * 15px);border:0;border-radius:5px;background:transparent;color:#54564f;font-size:12px;text-align:left;cursor:pointer}.tree-row:hover{background:#ffffffa6}.tree-row.active{background:#dcddd7;color:#242521;font-weight:600}.tree-toggle{display:grid;width:14px;color:#898b84}.tree-label{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-count{color:#999b95;font-size:10px;font-variant-numeric:tabular-nums}.sidebar-footer{display:flex;align-items:center;gap:15px;padding:0 15px;border-top:1px solid var(--line);color:#8b8d87;font-size:10px}.sidebar-footer strong{color:#555750}.sidebar-reveal{position:fixed;z-index:15;left:12px;top:68px;display:grid;place-items:center;width:34px;height:34px;padding:0;border:1px solid var(--line);border-radius:7px;background:var(--paper);box-shadow:0 5px 18px #1e201c1f;cursor:pointer}.workspace{min-width:0;overflow:auto;background:var(--paper)}.content-head{position:sticky;z-index:4;top:0;min-height:108px;display:flex;align-items:flex-end;justify-content:space-between;gap:24px;padding:20px 26px 18px;background:#fbfaf7f5;border-bottom:1px solid var(--line-soft);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}.content-title{min-width:0}.breadcrumbs{display:flex;align-items:center;gap:4px;margin-bottom:8px;color:var(--faint);font-size:11px}.breadcrumbs button{max-width:170px;padding:0;overflow:hidden;border:0;background:transparent;color:inherit;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}.breadcrumbs button:hover{color:var(--ink)}.content-head h1{margin:0;overflow:hidden;color:#292a26;font-family:Georgia,Times New Roman,serif;font-size:25px;font-weight:500;letter-spacing:-.035em;text-overflow:ellipsis;white-space:nowrap}.content-head p{margin:5px 0 0;color:var(--muted);font-size:12px}.browser-toolbar{display:flex;align-items:center;gap:7px}.dropdown,.sort-control,.grid-scale,.view-switcher{height:32px;display:flex;align-items:center;border:1px solid var(--line);border-radius:6px;background:#f3f2ed;color:#686a64}.dropdown{position:relative}.dropdown-trigger{height:30px;display:flex;align-items:center;gap:6px;padding:0 8px;border:0;border-radius:5px;background:transparent;color:#5c5e58;font-size:11px;cursor:pointer}.dropdown-trigger>span{white-space:nowrap}.dropdown.open .dropdown-trigger,.dropdown-trigger:hover{background:#fff;color:var(--ink)}.dropdown-menu{position:absolute;z-index:20;top:calc(100% + 5px);left:0;min-width:100%;padding:5px;border:1px solid #d6d7d1;border-radius:7px;background:#fff;box-shadow:0 10px 28px #1e201c24}.dropdown-menu button{width:100%;min-width:130px;height:30px;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:0 8px;border:0;border-radius:5px;background:transparent;color:#555750;font-size:11px;text-align:left;cursor:pointer}.dropdown-menu button:hover{background:#f1f2ee;color:var(--ink)}.dropdown-menu button.selected{color:#28584b;font-weight:650}.sort-control .dropdown{border:0;background:transparent}.sort-control .dropdown-trigger{padding-left:8px}.sort-control>button,.grid-scale button,.view-switcher button{display:grid;place-items:center;height:30px;padding:0;border:0;border-radius:5px;background:transparent;color:#74766f;cursor:pointer}.sort-control>button{width:29px;border-left:1px solid var(--line);border-radius:0 5px 5px 0}.grid-scale button{width:27px}.grid-scale button:hover:not(:disabled),.view-switcher button:hover,.sort-control>button:hover{background:#fff;color:var(--ink)}.grid-scale button:disabled{opacity:.28;cursor:default}.grid-scale span{min-width:25px;color:#62645e;font-size:10px;text-align:center;font-variant-numeric:tabular-nums}.grid-scale span:after{content:" col";color:#999b95}.view-switcher{padding:1px}.view-switcher button{width:29px;height:28px}.view-switcher button.active{background:#fff;color:var(--ink);box-shadow:0 1px 3px #1e201c1f}.file-grid{display:grid;grid-template-columns:repeat(var(--grid-columns, 4),minmax(0,1fr));gap:15px;padding:20px 26px 40px}.file-card{min-width:0;overflow:hidden;border:1px solid var(--line-soft);border-radius:8px;background:#fff;cursor:default;opacity:0;transform:translateY(4px);animation:reveal .25s ease forwards;animation-delay:var(--delay);transition:border-color .14s,background-color .14s}@keyframes reveal{to{opacity:1;transform:translateY(0)}}.file-card:hover{border-color:#bfc1ba;background:#fdfdfa}.file-card.selected{border-color:#4f7b6c;background:#fbfdfc}.thumb{position:relative;aspect-ratio:4 / 3;display:grid;place-items:center;overflow:hidden;background:#f4f3ef;border-bottom:1px solid var(--line-soft)}.thumb img{width:100%;height:100%;object-fit:contain}.placeholder{width:100%;height:100%;display:grid;place-content:center;justify-items:center;gap:8px;color:#878a83}.placeholder span{font-family:Georgia,Times New Roman,serif;font-size:11px;font-style:italic;font-weight:400}.placeholder.kind-cad{color:#557568;background:#edf2ef}.placeholder.kind-pdf{color:#9a5b49;background:#f4ece8}.placeholder.kind-image{color:#636d82;background:#eceff3}.type-badge{position:absolute;left:8px;top:8px;padding:3px 6px;border:1px solid rgba(40,42,37,.12);border-radius:4px;background:#ffffffe6;color:#64665f;font-size:9px;font-weight:700;letter-spacing:.055em;text-transform:uppercase;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.processing{position:absolute;inset:auto 8px 8px;display:flex;align-items:center;gap:6px;padding:6px 8px;border-radius:5px;background:#242621d6;color:#fff;font-size:10px}.processing svg,.analysis-note svg,.loading-screen>svg{animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.file-copy{padding:10px 11px 11px}.file-name{overflow:hidden;color:#31322e;font-size:12px;font-weight:620;text-overflow:ellipsis;white-space:nowrap}.file-copy span{display:block;margin-top:4px;overflow:hidden;color:var(--muted);font-size:10px;font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap}.file-grid.density-compact{gap:8px;padding-inline:18px}.file-grid.density-compact .file-copy{padding:7px}.file-grid.density-compact .file-name{font-size:10px}.file-grid.density-compact .file-copy span{font-size:8px}.file-grid.density-compact .type-badge{left:5px;top:5px;padding:2px 4px;font-size:7px}.file-grid.density-micro{gap:5px;padding-inline:13px}.file-grid.density-micro .file-card{border-radius:5px}.file-grid.density-micro .file-copy{padding:5px 6px 6px}.file-grid.density-micro .file-name{font-size:8px}.file-grid.density-micro .file-copy span{display:none}.file-grid.density-micro .type-badge{left:3px;top:3px;padding:1px 3px;border-radius:3px;font-size:6px}.file-list{padding:13px 26px 36px}.file-list .file-card{display:grid;grid-template-columns:88px minmax(0,1fr);margin-bottom:7px}.file-list .thumb{height:68px;aspect-ratio:auto;border:0;border-right:1px solid var(--line-soft)}.file-list .placeholder svg{width:23px}.file-list .placeholder span,.file-list .type-badge{display:none}.file-list .file-copy{align-self:center}.inspector{min-width:0;overflow-y:auto;background:#f8f7f3;border-left:1px solid var(--line)}.empty-inspector{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:36px;color:var(--muted);text-align:center}.inspector-empty-mark{display:grid;place-items:center;width:44px;height:44px;border:1px solid var(--line);border-radius:10px;background:#fff;color:#787a74}.empty-inspector h2{margin:15px 0 5px;color:var(--ink);font-family:Georgia,Times New Roman,serif;font-size:18px;font-weight:400}.empty-inspector p{max-width:250px;margin:0;font-size:12px;line-height:1.55}.shortcut-list{width:210px;display:grid;grid-template-columns:1fr auto;align-items:center;gap:8px 12px;margin-top:22px;color:#7e807a;font-size:11px;text-align:left}.inspector-head{min-height:61px;display:grid;grid-template-columns:36px minmax(0,1fr) 32px;align-items:center;gap:10px;padding:11px 11px 11px 14px;border-bottom:1px solid var(--line-soft)}.inspector-title{min-width:0}.inspector-title strong,.inspector-title span{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.inspector-title strong{color:#2b2c28;font-size:13px;font-weight:650}.inspector-title span{margin-top:3px;color:var(--muted);font-size:11px}.type-icon{display:grid;place-items:center;width:36px;height:36px;border-radius:8px;background:#e8e7e1;color:#656760}.type-icon.kind-cad{background:#e2ece7;color:#46705f}.type-icon.kind-pdf{background:#f0e3dd;color:#9a513d}.inspector-preview{position:relative;width:calc(100% - 28px);aspect-ratio:4 / 3;display:grid;place-items:center;margin:14px;padding:0;overflow:hidden;border:1px solid var(--line);border-radius:8px;background:#f0efea;cursor:zoom-in}.inspector-preview:disabled{cursor:default}.inspector-preview img{width:100%;height:100%;object-fit:contain}.inspector-actions{display:grid;grid-template-columns:1.3fr 1fr 34px;gap:7px;margin:-1px 14px 14px}.inspector-actions button{height:34px;display:flex;align-items:center;justify-content:center;gap:7px;padding:0 9px;border:1px solid #cccec7;border-radius:6px;background:#fff;color:#4c4e48;font-size:11px;font-weight:600;cursor:pointer}.inspector-actions button:hover{border-color:#aeb1a9;background:#fdfdfb}.inspector-actions .primary-action{border-color:#315f52;background:#315f52;color:#fff}.inspector-actions .primary-action:hover{background:#274f44}.analysis-note,.error-note{margin:0 14px 13px;padding:10px 11px;border-radius:6px;font-size:12px;line-height:1.45}.analysis-note{display:flex;align-items:center;gap:8px;background:#e5eee9;color:#4e695e}.error-note{background:#f2e3df;color:#884534}.inspector-section{padding:17px 18px 18px;border-top:1px solid var(--line-soft)}.inspector-section h3{margin:0 0 12px;color:#656861;font-size:11px;font-weight:700;letter-spacing:.065em;text-transform:uppercase}.property,.vector{min-height:32px;display:flex;align-items:center;justify-content:space-between;gap:14px;font-size:13px}.property span,.vector span{color:var(--muted)}.property strong{overflow:hidden;color:#292a27;font-weight:600;text-align:right;text-overflow:ellipsis}.vector code{color:#4f514b;font-family:Cascadia Mono,Consolas,monospace;font-size:11px}.dimension-strip{display:grid;grid-template-columns:repeat(3,1fr);gap:7px;margin-bottom:9px}.dimension-strip div{min-width:0;padding:10px;border-radius:6px;background:#ebeae5}.dimension-strip span,.dimension-strip strong{display:block}.dimension-strip span{margin-bottom:4px;color:#898b85;font-size:10px;font-weight:700}.dimension-strip strong{overflow:hidden;color:#292a27;font-size:13px;font-variant-numeric:tabular-nums;text-overflow:ellipsis}.swatches{display:grid;gap:8px}.swatch{display:grid;grid-template-columns:22px 1fr auto;align-items:center;gap:9px;font-size:12px}.swatch>span{width:22px;height:22px;border:1px solid rgba(0,0,0,.14);border-radius:5px}.swatch code,.model-tree{font-family:Cascadia Mono,Consolas,monospace}.swatch code{font-size:11px}.swatch small{color:var(--muted);font-size:11px}.material-name{margin-bottom:7px;font-size:12px;font-weight:600}.text-preview{max-height:180px;overflow:auto;margin-top:8px;padding:11px;border-radius:6px;background:#eeede8;color:#4e504a;font-family:Georgia,Times New Roman,serif;font-size:13px;line-height:1.6;white-space:pre-wrap}.model-tree{max-height:240px;margin:0;overflow:auto;padding:11px;border-radius:6px;background:#ebeae5;color:#474944;font-size:11px;line-height:1.65;white-space:pre}.inspector-footer{padding:14px 18px 20px;border-top:1px solid var(--line-soft)}.inspector-footer button{display:flex;align-items:center;gap:7px;padding:0;border:0;background:transparent;color:#60635c;font-size:11px;cursor:pointer}.inspector-footer button:hover{color:var(--ink)}.modal-backdrop{position:fixed;z-index:30;top:0;right:0;bottom:0;left:0;display:grid;place-items:center;padding:38px;background:#1e201c94;-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);animation:fade-in .14s ease}@keyframes fade-in{0%{opacity:0}}.preview-modal{width:min(1100px,91vw);height:min(820px,89vh);display:grid;grid-template-rows:58px 1fr;overflow:hidden;border:1px solid rgba(255,255,255,.2);border-radius:10px;background:#f8f7f3;box-shadow:0 28px 80px #14151257}.modal-head{display:flex;align-items:center;justify-content:space-between;padding:0 12px 0 18px;border-bottom:1px solid var(--line)}.modal-head strong,.modal-head span{display:block}.modal-head strong{font-size:13px}.modal-head span{margin-top:3px;color:var(--muted);font-size:11px}.modal-canvas{min-height:0;display:grid;place-items:center;padding:24px;background:#e9e8e3}.modal-canvas img{max-width:100%;max-height:100%;object-fit:contain;box-shadow:0 10px 34px #23242029}.empty-state{grid-column:1 / -1;min-height:300px;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--muted);text-align:center}.empty-state h2{margin:12px 0 4px;color:var(--ink);font-family:Georgia,Times New Roman,serif;font-size:18px;font-weight:400}.empty-state p{max-width:390px;margin:0;font-size:12px;line-height:1.5}.loading-screen{height:100vh;display:flex;align-items:center;justify-content:center;gap:10px;background:var(--paper);color:var(--muted);font-size:13px}.toast{position:fixed;z-index:40;left:50%;bottom:22px;display:flex;align-items:center;gap:8px;padding:9px 12px;border:1px solid rgba(255,255,255,.13);border-radius:7px;background:#252722f0;color:#fff;font-size:12px;box-shadow:0 10px 30px #191a1738;transform:translate(-50%)}.toast.error{background:#773226f5}@media(max-width:1220px){:root{--sidebar: 220px;--inspector: 330px}.content-head,.file-grid,.file-list{padding-inline:18px}.browser-toolbar{gap:5px}}