mbeditor 0.3.9 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e496861ff75f2bb3c09388afbc609c07230ad8a914de28040b867ccb922a04d
4
- data.tar.gz: f5825193b830cca72f626a86a15483eb46ccb4cd335388a6ccaed088841ff313
3
+ metadata.gz: 1a73400b5400313c994fbd40ebae5000cc776f07eebeb9790380a622a193ba73
4
+ data.tar.gz: bdd3a1d8825e5ff23eef4fab724a33e3f48a0ab8ce3be316652c822f378ceffa
5
5
  SHA512:
6
- metadata.gz: dd1ed23cefc9609ca7d99d77410104b63107dc0e72f80e72279adc4a01cba1a6da3fe2d6e0467ec23b7db1166d3ccbdfcf380d4057c5644008ab4a9fa87e5092
7
- data.tar.gz: 0b6bc8dcec6f206fc3867c19bc7ffe028230b8503109dc8473e10997abe41c56db0fd3970f88c23bbf6fd69873c1d905cc0ce11a2010c6205cccc891ee511982
6
+ metadata.gz: af01460ee4ece9edf319dfd84da74e0a4d6016cdc96c73c8f7bd1a1a421de254b7abcf157ec81c182591e94dff9b44a437b274f48c02129c6099a67f89dc6af2
7
+ data.tar.gz: 84d1af77774b4676e193db1800ba33401df62893491a134de11c84e6e3f17f26d2a7cc8b19c99bbdc05bbb9af018f079017ba1466680b29a00a065173399c572
data/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0] - 2026-04-22
9
+
10
+ ### Added
11
+ - **Draft auto-save and restore** — unsaved edits are written to `localStorage` every 500 ms per file. On reconnect after a server outage, a dialog offers to restore any drafts that diverged from the in-memory tab content.
12
+ - **Tab bar layout setting** — new "Tab bar layout" preference (Scroll / Wrap multi-row) stored in editor prefs; `TabBar` receives a `tabDisplayMode` prop and applies the appropriate CSS class; horizontal wheel scroll is disabled in wrap mode.
13
+
14
+ ### Fixed
15
+ - **Drag-to-split** — pane 2 drop zone is now only shown when the cursor actively hovers over the right-half content area, preventing the tab bar from collapsing during a drag. Dropping onto a tab bar element is no longer intercepted by the cross-pane drop handler, preserving same-pane reordering.
16
+ - **Quick Open sorting** — files consistently rank above directories; within the same priority tier results are sorted by match relevance (exact basename > prefix match > substring > other).
17
+ - **Search total count** — the count thread is joined with a 100 ms timeout; `total_count` is omitted from the response when the thread has not finished, so the first page is never blocked by the counting subprocess.
18
+ - **Status bar message colour** — removed `color-mix` transparency that was dimming the accent-text colour.
19
+
8
20
  ## [0.3.9] - 2026-04-21
9
21
 
10
22
  ### Added
@@ -60,7 +60,8 @@ var DEFAULT_EDITOR_PREFS = {
60
60
  prettierBracketSpacing: true,
61
61
  vimMode: false,
62
62
  fileTreeTypeahead: true,
63
- quickOpenShowFolders: false
63
+ quickOpenShowFolders: false,
64
+ tabDisplayMode: 'scroll'
64
65
  };
65
66
 
66
67
  var SidebarActionButton = function SidebarActionButton(_ref) {
@@ -417,6 +418,36 @@ var MbeditorApp = function MbeditorApp() {
417
418
  var prevGitBranchRef = useRef(null);
418
419
  var isSwitchingBranchRef = useRef(false);
419
420
 
421
+ // ── Draft backup helpers ─────────────────────────────────────────────────
422
+ var draftWriteTimerRef = useRef({});
423
+ var serverOnlineRef = useRef(true);
424
+
425
+ var _draftKey = function _draftKey(path) {
426
+ var base = typeof window.mbeditorBasePath === 'function' ? window.mbeditorBasePath() : '';
427
+ return 'mbeditor_draft\x00' + base + '\x00' + path;
428
+ };
429
+ var _saveDraftNow = function _saveDraftNow(path, content) {
430
+ try { localStorage.setItem(_draftKey(path), JSON.stringify({ content: content, ts: Date.now() })); } catch (e) {}
431
+ };
432
+ var _clearDraft = function _clearDraft(path) {
433
+ try { localStorage.removeItem(_draftKey(path)); } catch (e) {}
434
+ };
435
+ var _loadDraft = function _loadDraft(path) {
436
+ try { return JSON.parse(localStorage.getItem(_draftKey(path))); } catch (e) { return null; }
437
+ };
438
+ var _scheduleDraftWrite = function _scheduleDraftWrite(path, content) {
439
+ if (draftWriteTimerRef.current[path]) clearTimeout(draftWriteTimerRef.current[path]);
440
+ draftWriteTimerRef.current[path] = setTimeout(function () {
441
+ delete draftWriteTimerRef.current[path];
442
+ _saveDraftNow(path, content);
443
+ }, 500);
444
+ };
445
+
446
+ var _useState_dro = useState(null);
447
+ var _useState_dro2 = _slicedToArray(_useState_dro, 2);
448
+ var draftRestoreOffer = _useState_dro2[0];
449
+ var setDraftRestoreOffer = _useState_dro2[1];
450
+
420
451
  var clamp = function clamp(value, min, max) {
421
452
  return Math.min(max, Math.max(min, value));
422
453
  };
@@ -937,6 +968,28 @@ var MbeditorApp = function MbeditorApp() {
937
968
  return function () { clearTimeout(timeoutId); };
938
969
  }, []);
939
970
 
971
+ // On reconnect: scan open dirty tabs for newer localStorage drafts and offer restore.
972
+ useEffect(function () {
973
+ if (!serverOnline) {
974
+ serverOnlineRef.current = false;
975
+ return;
976
+ }
977
+ if (serverOnlineRef.current) return; // was already online — no transition
978
+ serverOnlineRef.current = true;
979
+ var st = EditorStore.getState();
980
+ var offers = [];
981
+ st.panes.forEach(function (pane) {
982
+ pane.tabs.forEach(function (tab) {
983
+ if (!tab.dirty || !tab.path || tab.path.startsWith('mbeditor://')) return;
984
+ var draft = _loadDraft(tab.path);
985
+ if (draft && draft.content !== tab.content) {
986
+ offers.push({ paneId: pane.id, tabId: tab.id, path: tab.path, name: tab.name, draftContent: draft.content });
987
+ }
988
+ });
989
+ });
990
+ if (offers.length > 0) setDraftRestoreOffer(offers);
991
+ }, [serverOnline]);
992
+
940
993
  // Auto-refresh the file tree every 10s to pick up external changes (new files, deletions, etc.)
941
994
  // Uses functional setTreeData to skip the re-render when nothing has changed.
942
995
  useEffect(function () {
@@ -1248,6 +1301,7 @@ var MbeditorApp = function MbeditorApp() {
1248
1301
  });
1249
1302
  EditorStore.setState({ panes: newPanes });
1250
1303
  EditorStore.setStatus("Saved", "success");
1304
+ _clearDraft(tab.path);
1251
1305
 
1252
1306
  // Hot reload for Markdown: sync preview tab after save
1253
1307
  if (/\.(md|markdown)$/i.test(tab.path)) {
@@ -1302,16 +1356,21 @@ var MbeditorApp = function MbeditorApp() {
1302
1356
  var pane2 = EditorStore.getState().panes.find(function (p) {
1303
1357
  return p.id === 2;
1304
1358
  });
1305
- if (!pane2 || pane2.tabs.length === 0) {
1306
- dragSplitWidthRef.current = 50;
1307
- setPane1Width(50);
1308
- } else {
1309
- dragSplitWidthRef.current = pane1Width;
1310
- }
1359
+ var alreadySplit = pane2 && pane2.tabs.length > 0;
1360
+ // Only set the split ref; do NOT pre-split the view width here.
1361
+ // Pane 2 appears as a drop zone only when the cursor actually hovers
1362
+ // over the right-half editor content, keeping the tab bar intact.
1363
+ dragSplitWidthRef.current = alreadySplit ? pane1Width : 50;
1311
1364
  setDraggedTab({ sourcePaneId: sourcePaneId, tabId: tabId });
1312
1365
  };
1313
1366
 
1314
1367
  var clearDragState = function clearDragState() {
1368
+ // If pane 2 is still empty after the drag, restore pane 1 to full width.
1369
+ var pane2 = EditorStore.getState().panes.find(function (p) { return p.id === 2; });
1370
+ if (!pane2 || pane2.tabs.length === 0) {
1371
+ setPane1Width(100);
1372
+ dragSplitWidthRef.current = 50;
1373
+ }
1315
1374
  setDraggedTab(null);
1316
1375
  setDragOverPaneId(null);
1317
1376
  };
@@ -2062,6 +2121,7 @@ var MbeditorApp = function MbeditorApp() {
2062
2121
  tabs: tabs,
2063
2122
  activeId: activeId,
2064
2123
  paneId: paneId,
2124
+ tabDisplayMode: editorPrefs.tabDisplayMode || 'scroll',
2065
2125
  onSelect: function (id) {
2066
2126
  // Sync explorer selection with the newly active tab so there's only one highlight
2067
2127
  var tab = tabs.find(function(t) { return t.id === id; });
@@ -2604,16 +2664,40 @@ var MbeditorApp = function MbeditorApp() {
2604
2664
  if (!draggedTab) return;
2605
2665
  e.preventDefault();
2606
2666
 
2667
+ // If the cursor is over the tab bar, suppress the cross-pane split overlay
2668
+ // so same-pane tab reordering within any tab bar is unaffected.
2669
+ if (e.target && e.target.closest && e.target.closest('.tab-bar')) {
2670
+ if (dragOverPaneId !== null) setDragOverPaneId(null);
2671
+ e.dataTransfer.dropEffect = 'move';
2672
+ return;
2673
+ }
2674
+
2607
2675
  var rect = e.currentTarget.getBoundingClientRect();
2608
2676
  var splitAtX = rect.left + rect.width * (dragSplitWidthRef.current / 100);
2609
2677
  var hoverPaneId = e.clientX >= splitAtX ? 2 : 1;
2610
2678
  var nextDropPane = hoverPaneId === draggedTab.sourcePaneId ? null : hoverPaneId;
2611
2679
 
2612
- e.dataTransfer.dropEffect = nextDropPane ? 'move' : 'none';
2680
+ // When cursor first enters the right-half content area and pane 2 is empty,
2681
+ // apply the 50% split width so the drop zone becomes visible.
2682
+ if (nextDropPane === 2) {
2683
+ var pane2Empty = EditorStore.getState().panes.find(function(p) { return p.id === 2; });
2684
+ if (!pane2Empty || pane2Empty.tabs.length === 0) {
2685
+ setPane1Width(50);
2686
+ }
2687
+ }
2688
+
2689
+ e.dataTransfer.dropEffect = 'move';
2613
2690
  if (dragOverPaneId !== nextDropPane) setDragOverPaneId(nextDropPane);
2614
2691
  },
2615
2692
  onDropCapture: function (e) {
2616
2693
  if (!draggedTab) return;
2694
+
2695
+ // If dropping onto a tab bar element, let the tab item's own onDrop
2696
+ // bubble-phase handler manage the reorder — don't intercept here.
2697
+ if (e.target && e.target.closest && e.target.closest('.tab-bar')) {
2698
+ return;
2699
+ }
2700
+
2617
2701
  e.preventDefault();
2618
2702
 
2619
2703
  var rect = e.currentTarget.getBoundingClientRect();
@@ -2628,10 +2712,12 @@ var MbeditorApp = function MbeditorApp() {
2628
2712
  }
2629
2713
  },
2630
2714
  state.panes.map(function (pane, idx) {
2631
- if (pane.id === 2 && pane.tabs.length === 0 && !draggedTab) return null; // Show pane 2 while dragging to allow drop-to-split
2715
+ // Show empty pane 2 as a drop zone only when the cursor is actively hovering
2716
+ // over its half of the editor content (dragOverPaneId === 2).
2717
+ if (pane.id === 2 && pane.tabs.length === 0 && dragOverPaneId !== 2) return null;
2632
2718
 
2633
2719
  // Dynamic width distribution
2634
- var isSplit = state.panes[1].tabs.length > 0 || !!draggedTab;
2720
+ var isSplit = state.panes[1].tabs.length > 0 || dragOverPaneId === 2;
2635
2721
  var flexBasis = '100%';
2636
2722
  if (isSplit) flexBasis = pane.id === 1 ? pane1Width + "%" : 100 - pane1Width + "%";
2637
2723
 
@@ -3109,6 +3195,18 @@ var MbeditorApp = function MbeditorApp() {
3109
3195
  onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { fileTreeTypeahead: v }); }); }
3110
3196
  })
3111
3197
  ),
3198
+ React.createElement(
3199
+ 'label', { className: 'ide-settings-row ide-settings-row-half' },
3200
+ React.createElement('span', { className: 'ide-settings-label' }, 'Tab bar layout'),
3201
+ React.createElement(
3202
+ 'select', {
3203
+ value: editorPrefs.tabDisplayMode || 'scroll',
3204
+ onChange: function(e) { var v = e.target.value; setEditorPrefs(function(p) { return Object.assign({}, p, { tabDisplayMode: v }); }); }
3205
+ },
3206
+ React.createElement('option', { value: 'scroll' }, 'Scroll'),
3207
+ React.createElement('option', { value: 'wrap' }, 'Wrap (multi-row)')
3208
+ )
3209
+ ),
3112
3210
  React.createElement(
3113
3211
  'label', { className: 'ide-settings-row ide-settings-row-check' },
3114
3212
  React.createElement('span', { className: 'ide-settings-label' }, 'Quick Open: show folders'),
@@ -3206,8 +3304,10 @@ var MbeditorApp = function MbeditorApp() {
3206
3304
  var valNorm = val.replace(/\r\n/g, '\n');
3207
3305
  if (valNorm === cleanNorm) {
3208
3306
  TabManager.markClean(pane.id, pActiveTab.id, val);
3307
+ _clearDraft(pActiveTab.path);
3209
3308
  } else {
3210
3309
  TabManager.markDirty(pane.id, pActiveTab.id, val);
3310
+ _scheduleDraftWrite(pActiveTab.path, val);
3211
3311
  }
3212
3312
  }
3213
3313
  });
@@ -3409,12 +3509,22 @@ var MbeditorApp = function MbeditorApp() {
3409
3509
  state.gitInfo.behind
3410
3510
  )
3411
3511
  ),
3412
- !serverOnline && React.createElement(
3413
- "div",
3414
- { className: "statusbar-offline" },
3415
- React.createElement("i", { className: "fas fa-exclamation-triangle" }),
3416
- " Server offline"
3417
- ),
3512
+ !serverOnline && (function () {
3513
+ var dirtyCount = state.panes.reduce(function (acc, p) {
3514
+ return acc + p.tabs.filter(function (t) { return t.dirty; }).length;
3515
+ }, 0);
3516
+ return React.createElement(
3517
+ "div",
3518
+ {
3519
+ className: "statusbar-offline",
3520
+ title: dirtyCount > 0 ? dirtyCount + " unsaved file" + (dirtyCount !== 1 ? "s" : "") + " — changes are backed up locally" : "Server offline"
3521
+ },
3522
+ React.createElement("i", { className: "fas fa-exclamation-triangle" }),
3523
+ dirtyCount > 0
3524
+ ? " Offline \u2014 " + dirtyCount + " unsaved"
3525
+ : " Server offline"
3526
+ );
3527
+ })(),
3418
3528
  activeFileCommit && React.createElement(
3419
3529
  "div",
3420
3530
  { className: "statusbar-file-commit", title: activeFileCommit.title + " — " + activeFileCommit.author },
@@ -3549,6 +3659,63 @@ var MbeditorApp = function MbeditorApp() {
3549
3659
  onSelectFolder: handleOpenFolderInExplorer,
3550
3660
  onClose: function () { return setQuickOpen(false); }
3551
3661
  }),
3662
+ draftRestoreOffer && React.createElement(
3663
+ "div",
3664
+ {
3665
+ className: "ide-draft-restore-overlay",
3666
+ role: "dialog",
3667
+ "aria-modal": "true",
3668
+ "aria-label": "Restore unsaved drafts"
3669
+ },
3670
+ React.createElement(
3671
+ "div",
3672
+ { className: "ide-draft-restore-dialog" },
3673
+ React.createElement("div", { className: "ide-draft-restore-title" },
3674
+ React.createElement("i", { className: "fas fa-save", style: { marginRight: 8 } }),
3675
+ "Unsaved drafts found"
3676
+ ),
3677
+ React.createElement("div", { className: "ide-draft-restore-body" },
3678
+ draftRestoreOffer.length + " file" + (draftRestoreOffer.length !== 1 ? "s have" : " has") + " locally backed-up drafts from when the server was offline:"
3679
+ ),
3680
+ React.createElement(
3681
+ "ul",
3682
+ { className: "ide-draft-restore-list" },
3683
+ draftRestoreOffer.map(function (o) {
3684
+ return React.createElement("li", { key: o.path }, o.name || o.path);
3685
+ })
3686
+ ),
3687
+ React.createElement(
3688
+ "div",
3689
+ { className: "ide-draft-restore-actions" },
3690
+ React.createElement(
3691
+ "button",
3692
+ {
3693
+ type: "button",
3694
+ className: "ide-draft-restore-btn ide-draft-restore-btn-primary",
3695
+ onClick: function () {
3696
+ draftRestoreOffer.forEach(function (offer) {
3697
+ TabManager.markDirty(offer.paneId, offer.tabId, offer.draftContent);
3698
+ });
3699
+ setDraftRestoreOffer(null);
3700
+ }
3701
+ },
3702
+ "Restore all"
3703
+ ),
3704
+ React.createElement(
3705
+ "button",
3706
+ {
3707
+ type: "button",
3708
+ className: "ide-draft-restore-btn",
3709
+ onClick: function () {
3710
+ draftRestoreOffer.forEach(function (offer) { _clearDraft(offer.path); });
3711
+ setDraftRestoreOffer(null);
3712
+ }
3713
+ },
3714
+ "Discard drafts"
3715
+ )
3716
+ )
3717
+ )
3718
+ ),
3552
3719
  contextMenu && React.createElement(
3553
3720
  React.Fragment,
3554
3721
  null,
@@ -94,6 +94,17 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
94
94
  return 50;
95
95
  }
96
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
+
97
108
  var getQuickOpenIcon = function getQuickOpenIcon(path, name, type) {
98
109
  if (type === 'dir') {
99
110
  return React.createElement('i', { className: 'fas fa-folder quick-open-result-icon quick-open-folder-icon', 'aria-hidden': 'true' });
@@ -117,11 +128,14 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
117
128
  var res = SearchService.searchFiles(query);
118
129
  // Filter by type: always include files; include dirs only when showFolders is on
119
130
  var filtered = showFolders ? res : res.filter(function(r) { return r.type !== 'dir'; });
120
- // Stable priority sort: controller > model > helper > concern > view > job > other > noise.
121
- // JS sort is stable in modern engines, so relative order within the same tier
122
- // (i.e. MiniSearch relevance score) is preserved.
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.
123
134
  filtered.sort(function(a, b) {
124
- return getFilePriority(a.path) - getFilePriority(b.path);
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);
125
139
  });
126
140
  setResults(filtered.slice(0, 200));
127
141
  setSelectedIndex(0);
@@ -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
  } },
@@ -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;
@@ -792,7 +804,7 @@ html, body, #mbeditor-root {
792
804
 
793
805
  .statusbar-msg {
794
806
  margin-left: auto;
795
- color: color-mix(in srgb, var(--ide-accent-text) 80%, transparent);
807
+ color: var(--ide-accent-text);
796
808
  font-size: 11px;
797
809
  overflow: hidden;
798
810
  text-overflow: ellipsis;
@@ -1683,6 +1695,70 @@ html, body, #mbeditor-root {
1683
1695
  to { opacity: 1; transform: translateY(0); }
1684
1696
  }
1685
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
+
1686
1762
  /* ── Settings panel ─────────────────────────────────────────── */
1687
1763
  .ide-settings-panel { display: flex; flex-direction: column; height: 100%; overflow-y: auto; }
1688
1764
  .ide-settings-body { padding: 8px 12px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px 10px; align-items: start; }
@@ -328,7 +328,12 @@ module Mbeditor
328
328
  results = stream_search_results(query, needed)
329
329
  has_more = results.length > offset + limit
330
330
  response = { results: results[offset, limit] || [], has_more: has_more }
331
- response[:total_count] = count_thread.value if count_thread
331
+ if count_thread
332
+ # Give the count thread up to 100 ms; omit total_count when it hasn't finished yet
333
+ # so the first page is never blocked by the counting subprocess.
334
+ count_thread.join(0.1)
335
+ response[:total_count] = count_thread.value unless count_thread.alive?
336
+ end
332
337
 
333
338
  render json: response
334
339
  rescue StandardError => e
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mbeditor
4
- VERSION = "0.3.9"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mbeditor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.9
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oliver Noonan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-21 00:00:00.000000000 Z
11
+ date: 2026-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails