@mat3ra/esse 2026.4.29-0 → 2026.5.18-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/package.json +1 -1
- package/src/html/app.js +309 -0
- package/src/html/index.html +54 -0
- package/src/html/style.css +265 -0
package/package.json
CHANGED
package/src/html/app.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ── Monaco bootstrap ──────────────────────────────────────────────────────────
|
|
4
|
+
let monacoEditor = null;
|
|
5
|
+
let pendingContent = null;
|
|
6
|
+
|
|
7
|
+
require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs' } });
|
|
8
|
+
require(['vs/editor/editor.main'], function () {
|
|
9
|
+
monacoEditor = monaco.editor.create(document.getElementById('monaco-editor'), {
|
|
10
|
+
value: '',
|
|
11
|
+
language: 'json',
|
|
12
|
+
theme: 'vs-dark',
|
|
13
|
+
readOnly: true,
|
|
14
|
+
minimap: { enabled: true },
|
|
15
|
+
scrollBeyondLastLine: false,
|
|
16
|
+
fontSize: 13,
|
|
17
|
+
lineNumbers: 'on',
|
|
18
|
+
wordWrap: 'off',
|
|
19
|
+
automaticLayout: true,
|
|
20
|
+
renderLineHighlight: 'all',
|
|
21
|
+
});
|
|
22
|
+
if (pendingContent !== null) {
|
|
23
|
+
applyContent(pendingContent);
|
|
24
|
+
pendingContent = null;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function applyContent(text) {
|
|
29
|
+
if (!monacoEditor) { pendingContent = text; return; }
|
|
30
|
+
document.getElementById('welcome').style.display = 'none';
|
|
31
|
+
document.getElementById('monaco-editor').style.display = 'block';
|
|
32
|
+
monacoEditor.setValue(text);
|
|
33
|
+
monacoEditor.revealLine(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── State ─────────────────────────────────────────────────────────────────────
|
|
37
|
+
let allFiles = [];
|
|
38
|
+
let expandedSet = new Set(); // set of folder paths that are expanded
|
|
39
|
+
let selectedFile = null; // currently selected file path
|
|
40
|
+
|
|
41
|
+
// ── Build nested tree object from flat path list ──────────────────────────────
|
|
42
|
+
function pathsToTree(paths) {
|
|
43
|
+
const root = {};
|
|
44
|
+
paths.forEach(p => {
|
|
45
|
+
const parts = p.split('/');
|
|
46
|
+
let node = root;
|
|
47
|
+
for (let i = 0; i < parts.length; i++) {
|
|
48
|
+
const part = parts[i];
|
|
49
|
+
if (i === parts.length - 1) {
|
|
50
|
+
if (!node.__files) node.__files = [];
|
|
51
|
+
node.__files.push({ name: part, path: p });
|
|
52
|
+
} else {
|
|
53
|
+
if (!node[part]) node[part] = {};
|
|
54
|
+
node = node[part];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return root;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Render tree (lazy — children only rendered when folder is opened) ─────────
|
|
62
|
+
function renderTree(container, node, depth, folderPath) {
|
|
63
|
+
// Folders first (alphabetical)
|
|
64
|
+
const folderKeys = Object.keys(node).filter(k => k !== '__files').sort();
|
|
65
|
+
folderKeys.forEach(key => {
|
|
66
|
+
const fp = folderPath ? `${folderPath}/${key}` : key;
|
|
67
|
+
const isOpen = expandedSet.has(fp);
|
|
68
|
+
|
|
69
|
+
const row = document.createElement('div');
|
|
70
|
+
row.className = 't-item folder';
|
|
71
|
+
row.style.paddingLeft = `${depth * 12 + 4}px`;
|
|
72
|
+
row.dataset.fp = fp;
|
|
73
|
+
|
|
74
|
+
row.innerHTML =
|
|
75
|
+
`<span class="t-chevron">${isOpen ? '▾' : '▸'}</span>` +
|
|
76
|
+
`<span class="t-icon" style="color:var(--icon-folder)">📁</span>` +
|
|
77
|
+
`<span class="t-label">${escHtml(key)}</span>`;
|
|
78
|
+
|
|
79
|
+
const children = document.createElement('div');
|
|
80
|
+
children.className = 't-children' + (isOpen ? ' open' : '');
|
|
81
|
+
children.dataset.rendered = isOpen ? 'true' : 'false';
|
|
82
|
+
|
|
83
|
+
row.addEventListener('click', e => {
|
|
84
|
+
e.stopPropagation();
|
|
85
|
+
const opening = !children.classList.contains('open');
|
|
86
|
+
children.classList.toggle('open', opening);
|
|
87
|
+
row.querySelector('.t-chevron').textContent = opening ? '▾' : '▸';
|
|
88
|
+
if (opening) {
|
|
89
|
+
expandedSet.add(fp);
|
|
90
|
+
if (children.dataset.rendered === 'false') {
|
|
91
|
+
renderTree(children, node[key], depth + 1, fp);
|
|
92
|
+
children.dataset.rendered = 'true';
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
expandedSet.delete(fp);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (isOpen && children.dataset.rendered === 'false') {
|
|
100
|
+
renderTree(children, node[key], depth + 1, fp);
|
|
101
|
+
children.dataset.rendered = 'true';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
container.appendChild(row);
|
|
105
|
+
container.appendChild(children);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Then files
|
|
109
|
+
if (node.__files) {
|
|
110
|
+
node.__files
|
|
111
|
+
.slice()
|
|
112
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
113
|
+
.forEach(({ name, path }) => {
|
|
114
|
+
const row = document.createElement('div');
|
|
115
|
+
row.className = 't-item file' + (path === selectedFile ? ' selected' : '');
|
|
116
|
+
row.style.paddingLeft = `${depth * 12 + 22}px`;
|
|
117
|
+
row.dataset.path = path;
|
|
118
|
+
|
|
119
|
+
row.innerHTML =
|
|
120
|
+
`<span class="t-icon">📄</span>` +
|
|
121
|
+
`<span class="t-label">${escHtml(name)}</span>`;
|
|
122
|
+
|
|
123
|
+
row.addEventListener('click', () => openFile(path));
|
|
124
|
+
container.appendChild(row);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Render flat search results ────────────────────────────────────────────────
|
|
130
|
+
function renderSearch(container, matches, query) {
|
|
131
|
+
if (matches.length === 0) {
|
|
132
|
+
container.innerHTML = '<div class="tree-msg">No results found.</div>';
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
document.getElementById('status-count').textContent = `${matches.length} result${matches.length !== 1 ? 's' : ''}`;
|
|
136
|
+
|
|
137
|
+
matches.forEach(path => {
|
|
138
|
+
const fileName = path.split('/').pop();
|
|
139
|
+
const dirPart = path.slice(0, path.length - fileName.length - 1);
|
|
140
|
+
const row = document.createElement('div');
|
|
141
|
+
row.className = 't-item file flat-result' + (path === selectedFile ? ' selected' : '');
|
|
142
|
+
row.style.paddingLeft = '8px';
|
|
143
|
+
row.dataset.path = path;
|
|
144
|
+
|
|
145
|
+
const hl = s => hlMatch(s, query);
|
|
146
|
+
|
|
147
|
+
row.innerHTML =
|
|
148
|
+
`<span class="t-icon" style="flex-shrink:0">📄</span>` +
|
|
149
|
+
`<span class="t-label">` +
|
|
150
|
+
`<span class="t-filename">${hl(fileName)}</span>` +
|
|
151
|
+
`<span class="t-filepath">${hl(dirPart)}</span>` +
|
|
152
|
+
`</span>`;
|
|
153
|
+
|
|
154
|
+
row.addEventListener('click', () => openFile(path));
|
|
155
|
+
container.appendChild(row);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Highlight query match in text ─────────────────────────────────────────────
|
|
160
|
+
function hlMatch(text, query) {
|
|
161
|
+
if (!query) return escHtml(text);
|
|
162
|
+
const idx = text.toLowerCase().indexOf(query.toLowerCase());
|
|
163
|
+
if (idx === -1) return escHtml(text);
|
|
164
|
+
return escHtml(text.slice(0, idx)) +
|
|
165
|
+
`<mark>${escHtml(text.slice(idx, idx + query.length))}</mark>` +
|
|
166
|
+
escHtml(text.slice(idx + query.length));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function escHtml(s) {
|
|
170
|
+
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Pre-expand folders up to a given depth ────────────────────────────────────
|
|
174
|
+
function preExpand(node, depth, folderPath, maxDepth) {
|
|
175
|
+
if (depth >= maxDepth) return;
|
|
176
|
+
Object.keys(node)
|
|
177
|
+
.filter(k => k !== '__files')
|
|
178
|
+
.forEach(key => {
|
|
179
|
+
const fp = folderPath ? `${folderPath}/${key}` : key;
|
|
180
|
+
expandedSet.add(fp);
|
|
181
|
+
preExpand(node[key], depth + 1, fp, maxDepth);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Rebuild the whole tree pane ───────────────────────────────────────────────
|
|
186
|
+
function rebuildTree(query) {
|
|
187
|
+
const container = document.getElementById('file-tree');
|
|
188
|
+
container.innerHTML = '';
|
|
189
|
+
|
|
190
|
+
if (!query) {
|
|
191
|
+
document.getElementById('status-count').textContent =
|
|
192
|
+
`${allFiles.length} file${allFiles.length !== 1 ? 's' : ''}`;
|
|
193
|
+
const tree = pathsToTree(allFiles);
|
|
194
|
+
renderTree(container, tree, 0, '');
|
|
195
|
+
} else {
|
|
196
|
+
const q = query.toLowerCase();
|
|
197
|
+
const matches = allFiles.filter(f => f.toLowerCase().includes(q));
|
|
198
|
+
renderSearch(container, matches, query);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Open / display a file ─────────────────────────────────────────────────────
|
|
203
|
+
function openFile(path) {
|
|
204
|
+
selectedFile = path;
|
|
205
|
+
|
|
206
|
+
// Highlight in tree (works for both tree and search views)
|
|
207
|
+
document.querySelectorAll('.t-item.selected').forEach(el => el.classList.remove('selected'));
|
|
208
|
+
const el = document.querySelector(`.t-item.file[data-path="${CSS.escape(path)}"]`);
|
|
209
|
+
if (el) el.classList.add('selected');
|
|
210
|
+
|
|
211
|
+
// Tab
|
|
212
|
+
const fname = path.split('/').pop();
|
|
213
|
+
document.getElementById('tabbar').innerHTML =
|
|
214
|
+
`<div class="tab active"><span>📄</span><span>${escHtml(fname)}</span></div>`;
|
|
215
|
+
|
|
216
|
+
// Breadcrumb
|
|
217
|
+
const parts = path.split('/');
|
|
218
|
+
document.getElementById('breadcrumb').innerHTML =
|
|
219
|
+
parts.map((p, i) =>
|
|
220
|
+
i < parts.length - 1
|
|
221
|
+
? `<span>${escHtml(p)}</span><span class="bc-sep">›</span>`
|
|
222
|
+
: `<span style="color:var(--text-primary)">${escHtml(p)}</span>`
|
|
223
|
+
).join('');
|
|
224
|
+
|
|
225
|
+
// Status
|
|
226
|
+
document.getElementById('status-path').textContent = path;
|
|
227
|
+
|
|
228
|
+
// Update URL hash for deep-linking
|
|
229
|
+
history.replaceState(null, '', '#' + path);
|
|
230
|
+
|
|
231
|
+
// Fetch
|
|
232
|
+
fetch(path)
|
|
233
|
+
.then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
|
234
|
+
.then(data => applyContent(JSON.stringify(data, null, 2)))
|
|
235
|
+
.catch(err => applyContent(`// Error loading file\n// ${err.message}`));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── Expand all ancestor folders for a given file path ────────────────────────
|
|
239
|
+
function expandToFile(path) {
|
|
240
|
+
const parts = path.split('/');
|
|
241
|
+
let folderPath = '';
|
|
242
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
243
|
+
folderPath = folderPath ? `${folderPath}/${parts[i]}` : parts[i];
|
|
244
|
+
expandedSet.add(folderPath);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── Search handler ────────────────────────────────────────────────────────────
|
|
249
|
+
let searchTimer = null;
|
|
250
|
+
document.getElementById('search-input').addEventListener('input', function () {
|
|
251
|
+
clearTimeout(searchTimer);
|
|
252
|
+
searchTimer = setTimeout(() => rebuildTree(this.value.trim()), 150);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ── Resizer ───────────────────────────────────────────────────────────────────
|
|
256
|
+
(function () {
|
|
257
|
+
const resizer = document.getElementById('resizer');
|
|
258
|
+
const sidebar = document.getElementById('sidebar');
|
|
259
|
+
let dragging = false, startX = 0, startW = 0;
|
|
260
|
+
|
|
261
|
+
resizer.addEventListener('mousedown', e => {
|
|
262
|
+
dragging = true; startX = e.clientX; startW = sidebar.offsetWidth;
|
|
263
|
+
resizer.classList.add('active');
|
|
264
|
+
document.body.style.cursor = 'col-resize';
|
|
265
|
+
document.body.style.userSelect = 'none';
|
|
266
|
+
e.preventDefault();
|
|
267
|
+
});
|
|
268
|
+
document.addEventListener('mousemove', e => {
|
|
269
|
+
if (!dragging) return;
|
|
270
|
+
const w = Math.max(120, Math.min(window.innerWidth * 0.6, startW + e.clientX - startX));
|
|
271
|
+
sidebar.style.width = w + 'px';
|
|
272
|
+
if (monacoEditor) monacoEditor.layout();
|
|
273
|
+
});
|
|
274
|
+
document.addEventListener('mouseup', () => {
|
|
275
|
+
if (!dragging) return;
|
|
276
|
+
dragging = false;
|
|
277
|
+
resizer.classList.remove('active');
|
|
278
|
+
document.body.style.cursor = '';
|
|
279
|
+
document.body.style.userSelect = '';
|
|
280
|
+
});
|
|
281
|
+
}());
|
|
282
|
+
|
|
283
|
+
// ── Init ──────────────────────────────────────────────────────────────────────
|
|
284
|
+
fetch('files.json')
|
|
285
|
+
.then(r => r.json())
|
|
286
|
+
.then(files => {
|
|
287
|
+
allFiles = files;
|
|
288
|
+
// Pre-expand to second degree by default
|
|
289
|
+
const tree = pathsToTree(allFiles);
|
|
290
|
+
preExpand(tree, 0, '', 2);
|
|
291
|
+
// Handle deep-link hash
|
|
292
|
+
const hash = window.location.hash.slice(1);
|
|
293
|
+
if (hash && allFiles.includes(hash)) {
|
|
294
|
+
expandToFile(hash);
|
|
295
|
+
}
|
|
296
|
+
rebuildTree('');
|
|
297
|
+
if (hash && allFiles.includes(hash)) {
|
|
298
|
+
openFile(hash);
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
.catch(err => {
|
|
302
|
+
document.getElementById('file-tree').innerHTML =
|
|
303
|
+
`<div class="tree-msg" style="color:#f88">Failed to load file index: ${err.message}</div>`;
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
window.addEventListener('hashchange', () => {
|
|
307
|
+
const hash = window.location.hash.slice(1);
|
|
308
|
+
if (hash && allFiles.includes(hash)) openFile(hash);
|
|
309
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>ESSEntial Source of Schemas and Examples (ESSE)</title>
|
|
7
|
+
<link rel="stylesheet" href="style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
|
|
11
|
+
<div id="titlebar">
|
|
12
|
+
<span class="app-name">ESSEntial Source of Schemas and Examples (ESSE)</span>
|
|
13
|
+
<span>—</span>
|
|
14
|
+
<span>schemas.mat3ra.com</span>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div id="workspace">
|
|
18
|
+
|
|
19
|
+
<div id="sidebar">
|
|
20
|
+
<div id="sidebar-title">Explorer</div>
|
|
21
|
+
<div id="search-wrap">
|
|
22
|
+
<input id="search-input" type="search" placeholder="Search schemas…" autocomplete="off" spellcheck="false">
|
|
23
|
+
</div>
|
|
24
|
+
<div id="file-tree">
|
|
25
|
+
<div class="tree-msg">Loading…</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div id="resizer"></div>
|
|
30
|
+
|
|
31
|
+
<div id="editor-area">
|
|
32
|
+
<div id="tabbar"></div>
|
|
33
|
+
<div id="breadcrumb">ESSEntial Source of Schemas and Examples (ESSE)</div>
|
|
34
|
+
<div id="editor-wrap">
|
|
35
|
+
<div id="welcome">
|
|
36
|
+
<h2>ESSEntial Source of Schemas and Examples (ESSE)</h2>
|
|
37
|
+
<p>Select a schema from the explorer on the left, or start typing to search.</p>
|
|
38
|
+
</div>
|
|
39
|
+
<div id="monaco-editor"></div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div id="statusbar">
|
|
46
|
+
<span id="status-path">No file selected</span>
|
|
47
|
+
<span id="status-count"></span>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- Monaco Editor (loaded from CDN) -->
|
|
51
|
+
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs/loader.min.js"></script>
|
|
52
|
+
<script src="app.js"></script>
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg-editor: #1e1e1e;
|
|
3
|
+
--bg-sidebar: #252526;
|
|
4
|
+
--bg-inactive: #2d2d2d;
|
|
5
|
+
--bg-hover: #2a2d2e;
|
|
6
|
+
--bg-selected: #094771;
|
|
7
|
+
--text-primary: #d4d4d4;
|
|
8
|
+
--text-secondary: #abb2bf;
|
|
9
|
+
--text-muted: #858585;
|
|
10
|
+
--border: #3e3e3e;
|
|
11
|
+
--accent: #0078d4;
|
|
12
|
+
--icon-folder: #dcb67a;
|
|
13
|
+
--icon-json: #9cdcfe;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|
20
|
+
background: var(--bg-editor);
|
|
21
|
+
color: var(--text-primary);
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
height: 100vh;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* ── Title bar ───────────────────────────────────────────── */
|
|
29
|
+
#titlebar {
|
|
30
|
+
height: 30px;
|
|
31
|
+
background: #323233;
|
|
32
|
+
border-bottom: 1px solid #111;
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
padding: 0 14px;
|
|
36
|
+
flex-shrink: 0;
|
|
37
|
+
font-size: 13px;
|
|
38
|
+
color: var(--text-muted);
|
|
39
|
+
gap: 8px;
|
|
40
|
+
}
|
|
41
|
+
#titlebar .app-name { color: var(--text-primary); font-weight: 500; }
|
|
42
|
+
|
|
43
|
+
/* ── Layout ──────────────────────────────────────────────── */
|
|
44
|
+
#workspace {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex: 1;
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ── Sidebar ─────────────────────────────────────────────── */
|
|
51
|
+
#sidebar {
|
|
52
|
+
width: 280px;
|
|
53
|
+
min-width: 120px;
|
|
54
|
+
max-width: 60vw;
|
|
55
|
+
background: var(--bg-sidebar);
|
|
56
|
+
border-right: 1px solid var(--border);
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-direction: column;
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
flex-shrink: 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#sidebar-title {
|
|
64
|
+
padding: 10px 12px 6px;
|
|
65
|
+
font-size: 11px;
|
|
66
|
+
font-weight: 700;
|
|
67
|
+
text-transform: uppercase;
|
|
68
|
+
letter-spacing: 0.1em;
|
|
69
|
+
color: var(--text-muted);
|
|
70
|
+
flex-shrink: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#search-wrap {
|
|
74
|
+
padding: 0 8px 8px;
|
|
75
|
+
flex-shrink: 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#search-input {
|
|
79
|
+
width: 100%;
|
|
80
|
+
background: #3c3c3c;
|
|
81
|
+
border: 1px solid #555;
|
|
82
|
+
border-radius: 3px;
|
|
83
|
+
color: var(--text-primary);
|
|
84
|
+
font-size: 13px;
|
|
85
|
+
padding: 5px 8px 5px 30px;
|
|
86
|
+
outline: none;
|
|
87
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23858585'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z'/%3E%3C/svg%3E");
|
|
88
|
+
background-repeat: no-repeat;
|
|
89
|
+
background-position: 8px 50%;
|
|
90
|
+
background-size: 14px 14px;
|
|
91
|
+
}
|
|
92
|
+
#search-input:focus { border-color: var(--accent); }
|
|
93
|
+
#search-input::placeholder { color: var(--text-muted); }
|
|
94
|
+
|
|
95
|
+
#file-tree {
|
|
96
|
+
flex: 1;
|
|
97
|
+
overflow-y: auto;
|
|
98
|
+
font-size: 13px;
|
|
99
|
+
line-height: 1;
|
|
100
|
+
}
|
|
101
|
+
#file-tree::-webkit-scrollbar { width: 6px; }
|
|
102
|
+
#file-tree::-webkit-scrollbar-thumb { background: #424242; border-radius: 3px; }
|
|
103
|
+
#file-tree::-webkit-scrollbar-track { background: transparent; }
|
|
104
|
+
|
|
105
|
+
/* ── Tree items ──────────────────────────────────────────── */
|
|
106
|
+
.t-item {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
height: 22px;
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
white-space: nowrap;
|
|
112
|
+
padding-right: 8px;
|
|
113
|
+
gap: 3px;
|
|
114
|
+
}
|
|
115
|
+
.t-item:hover { background: var(--bg-hover); }
|
|
116
|
+
.t-item.selected { background: var(--bg-selected); }
|
|
117
|
+
|
|
118
|
+
.t-chevron {
|
|
119
|
+
flex-shrink: 0;
|
|
120
|
+
width: 16px;
|
|
121
|
+
text-align: center;
|
|
122
|
+
font-size: 9px;
|
|
123
|
+
color: var(--text-muted);
|
|
124
|
+
transition: transform 0.1s;
|
|
125
|
+
}
|
|
126
|
+
.t-icon { flex-shrink: 0; font-size: 14px; width: 18px; text-align: center; }
|
|
127
|
+
.t-label { flex: 1; overflow: hidden; text-overflow: ellipsis; }
|
|
128
|
+
|
|
129
|
+
.t-item.folder .t-label { color: var(--text-primary); }
|
|
130
|
+
.t-item.file .t-label { color: var(--icon-json); }
|
|
131
|
+
|
|
132
|
+
.t-children { display: none; }
|
|
133
|
+
.t-children.open { display: block; }
|
|
134
|
+
|
|
135
|
+
/* search result: show full path */
|
|
136
|
+
.t-item.flat-result .t-label {
|
|
137
|
+
display: flex;
|
|
138
|
+
flex-direction: column;
|
|
139
|
+
line-height: 1.3;
|
|
140
|
+
padding: 2px 0;
|
|
141
|
+
height: auto;
|
|
142
|
+
}
|
|
143
|
+
.t-item.flat-result { height: auto; min-height: 22px; align-items: flex-start; padding-top: 3px; padding-bottom: 3px; }
|
|
144
|
+
.t-item.flat-result .t-filename { color: var(--icon-json); font-weight: 500; }
|
|
145
|
+
.t-item.flat-result .t-filepath { font-size: 11px; color: var(--text-muted); }
|
|
146
|
+
.t-item.flat-result mark { background: transparent; color: #e6b422; font-weight: 700; }
|
|
147
|
+
|
|
148
|
+
/* no-results / loading */
|
|
149
|
+
.tree-msg {
|
|
150
|
+
padding: 12px;
|
|
151
|
+
font-size: 12px;
|
|
152
|
+
color: var(--text-muted);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ── Resizer ─────────────────────────────────────────────── */
|
|
156
|
+
#resizer {
|
|
157
|
+
width: 4px;
|
|
158
|
+
background: transparent;
|
|
159
|
+
cursor: col-resize;
|
|
160
|
+
flex-shrink: 0;
|
|
161
|
+
transition: background 0.15s;
|
|
162
|
+
}
|
|
163
|
+
#resizer:hover, #resizer.active { background: var(--accent); }
|
|
164
|
+
|
|
165
|
+
/* ── Editor area ─────────────────────────────────────────── */
|
|
166
|
+
#editor-area {
|
|
167
|
+
flex: 1;
|
|
168
|
+
display: flex;
|
|
169
|
+
flex-direction: column;
|
|
170
|
+
overflow: hidden;
|
|
171
|
+
min-width: 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* Tab bar */
|
|
175
|
+
#tabbar {
|
|
176
|
+
height: 35px;
|
|
177
|
+
background: var(--bg-sidebar);
|
|
178
|
+
border-bottom: 1px solid var(--border);
|
|
179
|
+
display: flex;
|
|
180
|
+
align-items: flex-end;
|
|
181
|
+
flex-shrink: 0;
|
|
182
|
+
overflow: hidden;
|
|
183
|
+
}
|
|
184
|
+
.tab {
|
|
185
|
+
height: 35px;
|
|
186
|
+
padding: 0 12px;
|
|
187
|
+
font-size: 13px;
|
|
188
|
+
display: flex;
|
|
189
|
+
align-items: center;
|
|
190
|
+
gap: 5px;
|
|
191
|
+
background: var(--bg-inactive);
|
|
192
|
+
border-right: 1px solid var(--border);
|
|
193
|
+
white-space: nowrap;
|
|
194
|
+
overflow: hidden;
|
|
195
|
+
text-overflow: ellipsis;
|
|
196
|
+
color: var(--text-muted);
|
|
197
|
+
max-width: 240px;
|
|
198
|
+
}
|
|
199
|
+
.tab.active {
|
|
200
|
+
background: var(--bg-editor);
|
|
201
|
+
color: var(--text-primary);
|
|
202
|
+
border-top: 1px solid var(--accent);
|
|
203
|
+
height: 36px;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* Breadcrumb */
|
|
207
|
+
#breadcrumb {
|
|
208
|
+
height: 22px;
|
|
209
|
+
padding: 0 14px;
|
|
210
|
+
font-size: 12px;
|
|
211
|
+
color: var(--text-muted);
|
|
212
|
+
background: var(--bg-editor);
|
|
213
|
+
border-bottom: 1px solid var(--border);
|
|
214
|
+
display: flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
flex-shrink: 0;
|
|
217
|
+
overflow: hidden;
|
|
218
|
+
gap: 2px;
|
|
219
|
+
}
|
|
220
|
+
.bc-sep { opacity: 0.4; margin: 0 2px; }
|
|
221
|
+
|
|
222
|
+
/* Editor container */
|
|
223
|
+
#editor-wrap {
|
|
224
|
+
flex: 1;
|
|
225
|
+
position: relative;
|
|
226
|
+
overflow: hidden;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#monaco-editor {
|
|
230
|
+
position: absolute;
|
|
231
|
+
inset: 0;
|
|
232
|
+
display: none;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* Welcome */
|
|
236
|
+
#welcome {
|
|
237
|
+
position: absolute;
|
|
238
|
+
inset: 0;
|
|
239
|
+
display: flex;
|
|
240
|
+
flex-direction: column;
|
|
241
|
+
align-items: center;
|
|
242
|
+
justify-content: center;
|
|
243
|
+
gap: 10px;
|
|
244
|
+
color: var(--text-muted);
|
|
245
|
+
}
|
|
246
|
+
#welcome h2 {
|
|
247
|
+
font-size: 22px;
|
|
248
|
+
font-weight: 300;
|
|
249
|
+
color: var(--text-secondary);
|
|
250
|
+
}
|
|
251
|
+
#welcome p { font-size: 13px; }
|
|
252
|
+
|
|
253
|
+
/* Status bar */
|
|
254
|
+
#statusbar {
|
|
255
|
+
height: 22px;
|
|
256
|
+
background: var(--accent);
|
|
257
|
+
color: #fff;
|
|
258
|
+
display: flex;
|
|
259
|
+
align-items: center;
|
|
260
|
+
padding: 0 14px;
|
|
261
|
+
font-size: 12px;
|
|
262
|
+
flex-shrink: 0;
|
|
263
|
+
gap: 16px;
|
|
264
|
+
}
|
|
265
|
+
#status-path { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|