mbeditor 0.3.8 → 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.
@@ -76,6 +76,35 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
76
76
  if (inputRef.current) inputRef.current.focus();
77
77
  };
78
78
 
79
+ // Priority tier for a file path: lower = shown first.
80
+ // Order: controller > model > helper > concern > view > job > other > noise
81
+ function getFilePriority(path) {
82
+ var p = (path || '').toLowerCase();
83
+ if (p.indexOf('/controllers/') >= 0) return 1;
84
+ if (p.indexOf('/models/') >= 0) return 2;
85
+ if (p.indexOf('/helpers/') >= 0) return 3;
86
+ if (p.indexOf('/concerns/') >= 0) return 4;
87
+ if (p.indexOf('/views/') >= 0) return 5;
88
+ if (p.indexOf('/jobs/') >= 0) return 6;
89
+ // Deprioritise: migrations, schema, compiled assets, vendor, lock files
90
+ if (p.indexOf('/migrate/') >= 0 || p.indexOf('schema.rb') >= 0) return 90;
91
+ if (p.indexOf('/public/') >= 0 || p.indexOf('/vendor/') >= 0) return 91;
92
+ if (p.slice(-7) === '.min.js' || p.slice(-8) === '.min.css' ||
93
+ p.slice(-4) === '.map' || p.slice(-5) === '.lock') return 92;
94
+ return 50;
95
+ }
96
+
97
+ // Match relevance within a priority tier: exact basename > prefix > substring > other.
98
+ function getMatchRelevance(result, q) {
99
+ if (!q) return 3;
100
+ var name = (result.name || (result.path || '').split('/').pop() || '').toLowerCase();
101
+ var lq = q.toLowerCase();
102
+ if (name === lq) return 0;
103
+ if (name.slice(0, lq.length) === lq) return 1;
104
+ if (name.indexOf(lq) >= 0) return 2;
105
+ return 3;
106
+ }
107
+
79
108
  var getQuickOpenIcon = function getQuickOpenIcon(path, name, type) {
80
109
  if (type === 'dir') {
81
110
  return React.createElement('i', { className: 'fas fa-folder quick-open-result-icon quick-open-folder-icon', 'aria-hidden': 'true' });
@@ -99,7 +128,16 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
99
128
  var res = SearchService.searchFiles(query);
100
129
  // Filter by type: always include files; include dirs only when showFolders is on
101
130
  var filtered = showFolders ? res : res.filter(function(r) { return r.type !== 'dir'; });
102
- setResults(filtered.slice(0, 20));
131
+ // Sort: files always beat dirs; within the same tier exact basename > prefix > substring > other.
132
+ // JS sort is stable in modern engines so MiniSearch relevance score order is the tiebreaker
133
+ // when match relevance is equal.
134
+ filtered.sort(function(a, b) {
135
+ var aPriority = getFilePriority(a.path) + (a.type === 'dir' ? 100 : 0);
136
+ var bPriority = getFilePriority(b.path) + (b.type === 'dir' ? 100 : 0);
137
+ if (aPriority !== bPriority) return aPriority - bPriority;
138
+ return getMatchRelevance(a, query) - getMatchRelevance(b, query);
139
+ });
140
+ setResults(filtered.slice(0, 200));
103
141
  setSelectedIndex(0);
104
142
  }, [query, showFolders]);
105
143
 
@@ -18,6 +18,7 @@ var TabBar = function TabBar(_ref) {
18
18
  var onHardenTab = _ref.onHardenTab;
19
19
  var onShowHistory = _ref.onShowHistory;
20
20
  var onRevealInExplorer = _ref.onRevealInExplorer;
21
+ var tabDisplayMode = _ref.tabDisplayMode || 'scroll';
21
22
 
22
23
  var containerRef = useRef(null);
23
24
 
@@ -87,8 +88,8 @@ var TabBar = function TabBar(_ref) {
87
88
  null,
88
89
  React.createElement(
89
90
  'div',
90
- { className: 'tab-bar', ref: containerRef, onWheel: function (e) {
91
- if (containerRef.current) {
91
+ { className: 'tab-bar tab-bar-' + tabDisplayMode, ref: containerRef, onWheel: function (e) {
92
+ if (tabDisplayMode !== 'wrap' && containerRef.current) {
92
93
  containerRef.current.scrollLeft += e.deltaY;
93
94
  }
94
95
  } },
@@ -20,6 +20,7 @@ var EditorStore = (function () {
20
20
  };
21
21
 
22
22
  var _listeners = [];
23
+ var _statusTimer = null;
23
24
 
24
25
  function getState() { return _state; }
25
26
 
@@ -42,6 +43,9 @@ var EditorStore = (function () {
42
43
  // Subscribe to changes in a specific subset of state keys.
43
44
  // The listener is only called when at least one of the watched keys changes
44
45
  // by reference (===), preventing unnecessary re-renders for unrelated updates.
46
+ // IMPORTANT: all state updates MUST produce a new object reference for any
47
+ // nested value (use Object.assign / spread — never mutate in place), otherwise
48
+ // subscribeToSlice will not detect the change.
45
49
  function subscribeToSlice(keys, fn) {
46
50
  var prev = {};
47
51
  keys.forEach(function(k) { prev[k] = _state[k]; });
@@ -57,9 +61,13 @@ var EditorStore = (function () {
57
61
  function setStatus(text, kind) {
58
62
  kind = kind || "info";
59
63
  setState({ statusMessage: { text: text, kind: kind } });
60
- // Auto-clear after 4s for non-error messages
64
+ if (_statusTimer !== null) {
65
+ clearTimeout(_statusTimer);
66
+ _statusTimer = null;
67
+ }
61
68
  if (kind !== "error") {
62
- setTimeout(function () {
69
+ _statusTimer = setTimeout(function () {
70
+ _statusTimer = null;
63
71
  if (_state.statusMessage.text === text) {
64
72
  setState({ statusMessage: { text: "", kind: "info" } });
65
73
  }
@@ -2,6 +2,10 @@
2
2
  // The server uses this header to silence editor logs and guard non-GET requests.
3
3
  axios.defaults.headers.common['X-Mbeditor-Client'] = '1';
4
4
 
5
+ // Cap all API calls at 30 s so a hung Rails server never blocks the UI forever.
6
+ // The ping endpoint overrides this with a tighter 4 s timeout per-request.
7
+ axios.defaults.timeout = 30000;
8
+
5
9
  // Surface pending-migration errors as a dismissible banner instead of silently failing.
6
10
  axios.interceptors.response.use(null, function(error) {
7
11
  if (error.response && error.response.data && error.response.data.pending_migration_error) {
@@ -28,16 +32,12 @@ axios.interceptors.response.use(null, function(error) {
28
32
  });
29
33
 
30
34
  var FileService = (function () {
31
- function basePath() {
32
- return (window.MBEDITOR_BASE_PATH || '/mbeditor').replace(/\/$/, '');
33
- }
34
-
35
35
  function getWorkspace() {
36
- return axios.get(basePath() + '/workspace').then(function(res) { return res.data; });
36
+ return axios.get(window.mbeditorBasePath() + '/workspace').then(function(res) { return res.data; });
37
37
  }
38
38
 
39
39
  function getTree() {
40
- return axios.get(basePath() + '/files').then(function(res) { return res.data; });
40
+ return axios.get(window.mbeditorBasePath() + '/files').then(function(res) { return res.data; });
41
41
  }
42
42
 
43
43
  function getFile(path, options) {
@@ -45,72 +45,72 @@ var FileService = (function () {
45
45
  if (options && options.allowMissing) {
46
46
  params.allow_missing = '1';
47
47
  }
48
- return axios.get(basePath() + '/file', { params: params }).then(function(res) { return res.data; });
48
+ return axios.get(window.mbeditorBasePath() + '/file', { params: params }).then(function(res) { return res.data; });
49
49
  }
50
50
 
51
51
  function saveFile(path, code) {
52
- return axios.post(basePath() + '/file', { path: path, code: code }).then(function(res) { return res.data; });
52
+ return axios.post(window.mbeditorBasePath() + '/file', { path: path, code: code }).then(function(res) { return res.data; });
53
53
  }
54
54
 
55
55
  function createFile(path, code) {
56
- return axios.post(basePath() + '/create_file', { path: path, code: code || '' }).then(function(res) { return res.data; });
56
+ return axios.post(window.mbeditorBasePath() + '/create_file', { path: path, code: code || '' }).then(function(res) { return res.data; });
57
57
  }
58
58
 
59
59
  function createDir(path) {
60
- return axios.post(basePath() + '/create_dir', { path: path }).then(function(res) { return res.data; });
60
+ return axios.post(window.mbeditorBasePath() + '/create_dir', { path: path }).then(function(res) { return res.data; });
61
61
  }
62
62
 
63
63
  function renamePath(path, newPath) {
64
- return axios.patch(basePath() + '/rename', { path: path, new_path: newPath }).then(function(res) { return res.data; });
64
+ return axios.patch(window.mbeditorBasePath() + '/rename', { path: path, new_path: newPath }).then(function(res) { return res.data; });
65
65
  }
66
66
 
67
67
  function deletePath(path) {
68
- return axios.delete(basePath() + '/delete', { data: { path: path } }).then(function(res) { return res.data; });
68
+ return axios.delete(window.mbeditorBasePath() + '/delete', { data: { path: path } }).then(function(res) { return res.data; });
69
69
  }
70
70
 
71
71
  function lintFile(path, code) {
72
- return axios.post(basePath() + '/lint', { path: path, code: code }).then(function(res) { return res.data; });
72
+ return axios.post(window.mbeditorBasePath() + '/lint', { path: path, code: code }).then(function(res) { return res.data; });
73
73
  }
74
74
 
75
75
  function quickFixOffense(path, code, copName) {
76
- return axios.post(basePath() + '/quick_fix', { path: path, code: code, cop_name: copName }).then(function(res) { return res.data; });
76
+ return axios.post(window.mbeditorBasePath() + '/quick_fix', { path: path, code: code, cop_name: copName }).then(function(res) { return res.data; });
77
77
  }
78
78
 
79
79
  function formatFile(path, code) {
80
- return axios.post(basePath() + '/format', { path: path, code: code }).then(function(res) { return res.data; });
80
+ return axios.post(window.mbeditorBasePath() + '/format', { path: path, code: code }).then(function(res) { return res.data; });
81
81
  }
82
82
 
83
83
  function runTests(path) {
84
- return axios.post(basePath() + '/test', { path: path }).then(function(res) { return res.data; });
84
+ return axios.post(window.mbeditorBasePath() + '/test', { path: path }).then(function(res) { return res.data; });
85
85
  }
86
86
 
87
87
  function ping() {
88
- return axios.get(basePath() + '/ping', { timeout: 4000 }).then(function(res) { return res.data; });
88
+ return axios.get(window.mbeditorBasePath() + '/ping', { timeout: 4000 }).then(function(res) { return res.data; });
89
89
  }
90
90
 
91
91
  function getState() {
92
- return axios.get(basePath() + '/state').then(function(res) { return res.data; });
92
+ return axios.get(window.mbeditorBasePath() + '/state').then(function(res) { return res.data; });
93
93
  }
94
94
 
95
95
  function saveState(state) {
96
- return axios.post(basePath() + '/state', { state: state }).then(function(res) { return res.data; });
96
+ return axios.post(window.mbeditorBasePath() + '/state', { state: state }).then(function(res) { return res.data; });
97
97
  }
98
98
 
99
99
  function getBranchState(branch) {
100
- return axios.get(basePath() + '/branch_state', { params: { branch: branch } }).then(function(res) { return res.data; });
100
+ return axios.get(window.mbeditorBasePath() + '/branch_state', { params: { branch: branch } }).then(function(res) { return res.data; });
101
101
  }
102
102
 
103
103
  function saveBranchState(branch, state) {
104
- return axios.post(basePath() + '/branch_state', { branch: branch, state: state }).then(function(res) { return res.data; });
104
+ return axios.post(window.mbeditorBasePath() + '/branch_state', { branch: branch, state: state }).then(function(res) { return res.data; });
105
105
  }
106
106
 
107
107
  function pruneBranchStates() {
108
- return axios.post(basePath() + '/prune_branch_states').then(function(res) { return res.data; });
108
+ return axios.post(window.mbeditorBasePath() + '/prune_branch_states').then(function(res) { return res.data; });
109
109
  }
110
110
 
111
111
  function getDefinition(symbol, language, extraOptions) {
112
112
  var config = Object.assign({ params: { symbol: symbol, language: language }, timeout: 5000 }, extraOptions || {});
113
- return axios.get(basePath() + '/definition', config).then(function(res) { return res.data; });
113
+ return axios.get(window.mbeditorBasePath() + '/definition', config).then(function(res) { return res.data; });
114
114
  }
115
115
 
116
116
  return {
@@ -1,8 +1,4 @@
1
1
  var GitService = (function () {
2
- function basePath() {
3
- return (window.MBEDITOR_BASE_PATH || '/mbeditor').replace(/\/$/, '');
4
- }
5
-
6
2
  function applyGitInfo(data) {
7
3
  var files = data.workingTree || data.files || [];
8
4
  EditorStore.setState({
@@ -14,7 +10,7 @@ var GitService = (function () {
14
10
  }
15
11
 
16
12
  function fetchInfo() {
17
- return axios.get(basePath() + '/git_info')
13
+ return axios.get(window.mbeditorBasePath() + '/git_info')
18
14
  .then(function(res) {
19
15
  if (res.data && res.data.ok) {
20
16
  applyGitInfo(res.data);
@@ -33,7 +29,7 @@ var GitService = (function () {
33
29
  return fetchInfo().then(function(data) {
34
30
  if (data && data.ok) return data;
35
31
 
36
- return axios.get(basePath() + '/git_status')
32
+ return axios.get(window.mbeditorBasePath() + '/git_status')
37
33
  .then(function(res) {
38
34
  if (res.data.ok) {
39
35
  EditorStore.setState({
@@ -63,31 +59,31 @@ var GitService = (function () {
63
59
  if (baseSha) query += '&base=' + encodeURIComponent(baseSha);
64
60
  if (headSha) query += '&head=' + encodeURIComponent(headSha);
65
61
 
66
- return axios.get(basePath() + '/git/diff' + query).then(function(res) {
62
+ return axios.get(window.mbeditorBasePath() + '/git/diff' + query).then(function(res) {
67
63
  return res.data;
68
64
  });
69
65
  }
70
66
 
71
67
  function fetchBlame(path) {
72
- return axios.get(basePath() + '/git/blame?file=' + encodeURIComponent(path)).then(function(res) {
68
+ return axios.get(window.mbeditorBasePath() + '/git/blame?file=' + encodeURIComponent(path)).then(function(res) {
73
69
  return res.data;
74
70
  });
75
71
  }
76
72
 
77
73
  function fetchFileHistory(path) {
78
- return axios.get(basePath() + '/git/file_history?file=' + encodeURIComponent(path)).then(function(res) {
74
+ return axios.get(window.mbeditorBasePath() + '/git/file_history?file=' + encodeURIComponent(path)).then(function(res) {
79
75
  return res.data;
80
76
  });
81
77
  }
82
78
 
83
79
  function fetchCommitGraph() {
84
- return axios.get(basePath() + '/git/commit_graph').then(function(res) {
80
+ return axios.get(window.mbeditorBasePath() + '/git/commit_graph').then(function(res) {
85
81
  return res.data;
86
82
  });
87
83
  }
88
84
 
89
85
  function fetchCommitDetail(sha) {
90
- return axios.get(basePath() + '/git/commit_detail?sha=' + encodeURIComponent(sha)).then(function(res) {
86
+ return axios.get(window.mbeditorBasePath() + '/git/commit_detail?sha=' + encodeURIComponent(sha)).then(function(res) {
91
87
  return res.data;
92
88
  });
93
89
  }
@@ -1,15 +1,14 @@
1
1
  var SearchService = (function () {
2
2
  var SEARCH_PAGE_SIZE = 50;
3
3
 
4
- function basePath() {
5
- return (window.MBEDITOR_BASE_PATH || '/mbeditor').replace(/\/$/, '');
6
- }
7
-
8
4
  var _miniSearch = new MiniSearch({
9
5
  fields: ['path', 'name'], // indexed fields
10
6
  storeFields: ['path', 'name', 'type'] // returned fields (type: 'file'|'dir')
11
7
  });
12
8
 
9
+ // Flat doc list kept in sync with _miniSearch so we can do substring lookups.
10
+ var _allDocs = [];
11
+
13
12
  function buildIndex(treeData) {
14
13
  // Capture the tree data immediately so a subsequent refresh doesn't
15
14
  // clobber us before the idle callback fires.
@@ -32,31 +31,46 @@ var SearchService = (function () {
32
31
  }
33
32
 
34
33
  traverse(snapshot);
34
+ _allDocs = docs.slice();
35
35
  _miniSearch.removeAll();
36
36
  _miniSearch.addAll(docs);
37
37
  });
38
38
  }
39
39
 
40
40
  // Search files (and optionally folders) in the local MiniSearch index.
41
- // Returns raw MiniSearch results; caller can filter by .type.
41
+ // Also performs a case-insensitive substring scan so that partial-word
42
+ // queries like "project" reliably find "projects_controller.rb".
43
+ // Returns merged results; MiniSearch scored entries come first.
42
44
  function searchFiles(query) {
43
45
  if (!query) return [];
44
- return _miniSearch.search(query, { prefix: true, fuzzy: 0.2 });
46
+ var msResults = _miniSearch.search(query, { prefix: true, fuzzy: 0.2 });
47
+ // Substring fallback — catch anything MiniSearch missed
48
+ var q = query.toLowerCase();
49
+ var msIds = new Set(msResults.map(function(r) { return r.id; }));
50
+ var subResults = _allDocs.filter(function(doc) {
51
+ if (msIds.has(doc.id)) return false;
52
+ return doc.path.toLowerCase().indexOf(q) >= 0 || doc.name.toLowerCase().indexOf(q) >= 0;
53
+ }).map(function(doc) {
54
+ return { id: doc.id, path: doc.path, name: doc.name, type: doc.type, score: 0 };
55
+ });
56
+ return msResults.concat(subResults);
45
57
  }
46
58
 
47
59
  // Fetch one page of project-wide full-text search results.
48
60
  // offset=0 replaces the EditorStore results list; offset>0 appends.
61
+ // The server includes total_count only on offset=0 (fast rg --count pass).
49
62
  function projectSearch(query, offset, limit) {
50
- if (!query) return Promise.resolve({ results: [], hasMore: false });
63
+ if (!query) return Promise.resolve({ results: [], hasMore: false, totalCount: 0 });
51
64
  var off = (typeof offset === 'number') ? offset : 0;
52
65
  var lim = (typeof limit === 'number') ? limit : SEARCH_PAGE_SIZE;
53
66
 
54
- return axios.get(basePath() + '/search', {
67
+ return axios.get(window.mbeditorBasePath() + '/search', {
55
68
  params: { q: query, offset: off, limit: lim }
56
69
  }).then(function(res) {
57
70
  var data = res.data;
58
- var results = Array.isArray(data) ? data : (data && data.results || []);
59
- var hasMore = !Array.isArray(data) && !!(data && data.has_more);
71
+ var results = Array.isArray(data) ? data : (data && data.results || []);
72
+ var hasMore = !Array.isArray(data) && !!(data && data.has_more);
73
+ var totalCount = (data && data.total_count != null) ? data.total_count : null;
60
74
 
61
75
  if (off === 0) {
62
76
  EditorStore.setState({ searchResults: results, searchHasMore: hasMore });
@@ -64,18 +78,37 @@ var SearchService = (function () {
64
78
  var prev = EditorStore.getState().searchResults || [];
65
79
  EditorStore.setState({ searchResults: prev.concat(results), searchHasMore: hasMore });
66
80
  }
67
- return { results: results, hasMore: hasMore };
81
+ return { results: results, hasMore: hasMore, totalCount: totalCount };
68
82
  })
69
83
  .catch(function(err) {
70
84
  EditorStore.setStatus("Search failed: " + err.message, "error");
71
- return { results: [], hasMore: false };
85
+ return { results: [], hasMore: false, totalCount: null };
72
86
  });
73
87
  }
74
88
 
89
+ // Fetch a specific page by index without touching EditorStore.
90
+ // Used by the random-access virtual scroll loader.
91
+ function fetchPage(query, pageIndex) {
92
+ if (!query) return Promise.resolve({ results: [], hasMore: false });
93
+ var offset = pageIndex * SEARCH_PAGE_SIZE;
94
+ return axios.get(basePath() + '/search', {
95
+ params: { q: query, offset: offset, limit: SEARCH_PAGE_SIZE }
96
+ }).then(function(res) {
97
+ var data = res.data;
98
+ var results = Array.isArray(data) ? data : (data && data.results || []);
99
+ var hasMore = !Array.isArray(data) && !!(data && data.has_more);
100
+ return { results: results, hasMore: hasMore };
101
+ }).catch(function(err) {
102
+ EditorStore.setStatus("Search failed: " + err.message, "error");
103
+ return { results: [], hasMore: false };
104
+ });
105
+ }
106
+
75
107
  return {
76
108
  buildIndex: buildIndex,
77
109
  searchFiles: searchFiles,
78
110
  projectSearch: projectSearch,
111
+ fetchPage: fetchPage,
79
112
  PAGE_SIZE: SEARCH_PAGE_SIZE
80
113
  };
81
114
  })();
@@ -257,11 +257,11 @@ var TabManager = (function () {
257
257
  });
258
258
  EditorStore.setState({ panes: newPanes, focusedPaneId: paneId, activeTabId: tabId });
259
259
 
260
- var basePath = (window.MBEDITOR_BASE_PATH || '/mbeditor').replace(/\/$/, '');
261
- axios.get(basePath + '/git/combined_diff', { params: { scope: scope || 'local' } })
260
+ axios.get(window.mbeditorBasePath() + '/git/combined_diff', { params: { scope: scope || 'local' } })
262
261
  .then(function(res) {
262
+ var data = res.data;
263
263
  _updateTab(paneId, tabId, {
264
- combinedDiffText: typeof res.data === 'string' ? res.data : '',
264
+ combinedDiffText: typeof data === 'string' ? data : (data && data.diff) || '',
265
265
  combinedDiffLoaded: true
266
266
  });
267
267
  })
@@ -327,6 +327,18 @@ html, body, #mbeditor-root {
327
327
  .tab-bar::-webkit-scrollbar { height: 3px; }
328
328
  .tab-bar::-webkit-scrollbar-thumb { background: var(--ide-hover-bg); }
329
329
 
330
+ /* Wrap (multi-row) tab layout */
331
+ .tab-bar.tab-bar-wrap {
332
+ flex-wrap: wrap;
333
+ overflow-x: hidden;
334
+ overflow-y: visible;
335
+ height: auto;
336
+ }
337
+ .tab-bar.tab-bar-wrap .tab-item {
338
+ flex-shrink: 1;
339
+ flex-grow: 0;
340
+ }
341
+
330
342
  .tab-item {
331
343
  display: flex;
332
344
  align-items: center;
@@ -475,16 +487,23 @@ html, body, #mbeditor-root {
475
487
  .welcome-tips li { font-size: 12px; color: var(--ide-text-muted); }
476
488
  .welcome-tips li i { color: var(--ide-text-muted); width: 14px; text-align: center; }
477
489
 
478
- /* ── Search results meta ─────────────────────────────────── */
479
- .search-results-meta {
480
- padding: 4px 8px 6px;
490
+ /* ── Search results header (fixed count above scroll area) ── */
491
+ .search-results-header {
492
+ flex-shrink: 0;
493
+ padding: 4px 10px 5px;
481
494
  font-size: 11px;
482
495
  color: var(--ide-text-muted);
483
496
  border-bottom: 1px solid var(--ide-panel-bg-alt);
484
- margin-bottom: 4px;
497
+ user-select: none;
485
498
  }
486
- .search-results-capped { color: #f0a040; }
487
499
  .search-results-empty { padding: 12px 8px; font-size: 12px; color: var(--ide-text-muted); }
500
+ .search-loading-more {
501
+ padding: 10px;
502
+ font-size: 11px;
503
+ color: var(--ide-text-muted);
504
+ text-align: center;
505
+ user-select: none;
506
+ }
488
507
 
489
508
  /* ── Shortcut Help Drawer ────────────────────────────────── */
490
509
  .shelp-backdrop {
@@ -785,7 +804,7 @@ html, body, #mbeditor-root {
785
804
 
786
805
  .statusbar-msg {
787
806
  margin-left: auto;
788
- color: color-mix(in srgb, var(--ide-accent-text) 80%, transparent);
807
+ color: var(--ide-accent-text);
789
808
  font-size: 11px;
790
809
  overflow: hidden;
791
810
  text-overflow: ellipsis;
@@ -1676,6 +1695,70 @@ html, body, #mbeditor-root {
1676
1695
  to { opacity: 1; transform: translateY(0); }
1677
1696
  }
1678
1697
 
1698
+ /* ── Draft restore dialog ───────────────────────────────────── */
1699
+ .ide-draft-restore-overlay {
1700
+ position: fixed;
1701
+ inset: 0;
1702
+ background: rgba(0,0,0,0.55);
1703
+ z-index: 10001;
1704
+ display: flex;
1705
+ align-items: center;
1706
+ justify-content: center;
1707
+ }
1708
+ .ide-draft-restore-dialog {
1709
+ background: var(--ide-panel-bg-alt);
1710
+ border: 1px solid var(--ide-border-input);
1711
+ border-radius: 8px;
1712
+ padding: 20px 24px;
1713
+ min-width: 320px;
1714
+ max-width: 480px;
1715
+ box-shadow: 0 8px 32px rgba(0,0,0,0.6);
1716
+ color: var(--ide-text);
1717
+ font-size: 13px;
1718
+ }
1719
+ .ide-draft-restore-title {
1720
+ font-size: 14px;
1721
+ font-weight: 600;
1722
+ margin-bottom: 10px;
1723
+ color: var(--ide-warning, #e5c07b);
1724
+ }
1725
+ .ide-draft-restore-body { margin-bottom: 8px; color: var(--ide-text-muted); }
1726
+ .ide-draft-restore-list {
1727
+ list-style: none;
1728
+ padding: 0;
1729
+ margin: 0 0 16px 0;
1730
+ max-height: 160px;
1731
+ overflow-y: auto;
1732
+ }
1733
+ .ide-draft-restore-list li {
1734
+ padding: 3px 0;
1735
+ font-family: monospace;
1736
+ font-size: 12px;
1737
+ color: var(--ide-accent-fg, #9cdcfe);
1738
+ }
1739
+ .ide-draft-restore-actions {
1740
+ display: flex;
1741
+ gap: 8px;
1742
+ justify-content: flex-end;
1743
+ }
1744
+ .ide-draft-restore-btn {
1745
+ padding: 5px 14px;
1746
+ border-radius: 4px;
1747
+ border: 1px solid var(--ide-border-input);
1748
+ background: var(--ide-input-bg);
1749
+ color: var(--ide-text);
1750
+ font-size: 12px;
1751
+ cursor: pointer;
1752
+ transition: background 0.1s;
1753
+ }
1754
+ .ide-draft-restore-btn:hover { background: var(--ide-hover-bg); }
1755
+ .ide-draft-restore-btn-primary {
1756
+ background: var(--ide-accent);
1757
+ color: var(--ide-accent-text);
1758
+ border-color: var(--ide-accent);
1759
+ }
1760
+ .ide-draft-restore-btn-primary:hover { opacity: 0.9; }
1761
+
1679
1762
  /* ── Settings panel ─────────────────────────────────────────── */
1680
1763
  .ide-settings-panel { display: flex; flex-direction: column; height: 100%; overflow-y: auto; }
1681
1764
  .ide-settings-body { padding: 8px 12px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px 10px; align-items: start; }