mbeditor 0.12.9 → 0.13.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 +4 -4
- data/CHANGELOG.md +150 -0
- data/app/assets/javascripts/mbeditor/application.js +7 -1
- data/app/assets/javascripts/mbeditor/collaboration_service.js +31 -0
- data/app/assets/javascripts/mbeditor/color_provider.js +5 -0
- data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +3 -1
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +102 -25
- data/app/assets/javascripts/mbeditor/components/FileTree.js +6 -1
- data/app/assets/javascripts/mbeditor/components/GitPanel.js +10 -1
- data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +15 -8
- data/app/assets/javascripts/mbeditor/components/ImportDialog.js +216 -0
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +598 -130
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +25 -6
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +162 -6
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +39 -33
- data/app/assets/javascripts/mbeditor/components/TabBar.js +11 -2
- data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +312 -0
- data/app/assets/javascripts/mbeditor/editor_plugins.js +427 -111
- data/app/assets/javascripts/mbeditor/file_import.js +78 -0
- data/app/assets/javascripts/mbeditor/file_service.js +122 -18
- data/app/assets/javascripts/mbeditor/git_service.js +130 -8
- data/app/assets/javascripts/mbeditor/history_service.js +33 -38
- data/app/assets/javascripts/mbeditor/log_service.js +4 -0
- data/app/assets/javascripts/mbeditor/search_service.js +30 -2
- data/app/assets/javascripts/mbeditor/tab_manager.js +59 -6
- data/app/assets/javascripts/mbeditor/websocket_service.js +88 -4
- data/app/assets/stylesheets/mbeditor/editor.css +268 -53
- data/app/channels/mbeditor/channel_authentication.rb +7 -0
- data/app/channels/mbeditor/editor_channel.rb +6 -3
- data/app/controllers/mbeditor/application_controller.rb +5 -0
- data/app/controllers/mbeditor/editors_controller.rb +173 -29
- data/app/controllers/mbeditor/git_controller.rb +3 -3
- data/app/controllers/mbeditor/logs_controller.rb +3 -1
- data/app/services/mbeditor/collaboration_doc_store.rb +7 -0
- data/app/services/mbeditor/editor_state_service.rb +36 -26
- data/app/services/mbeditor/exclusion_matcher.rb +12 -10
- data/app/services/mbeditor/file_operation_service.rb +38 -3
- data/app/services/mbeditor/file_tree_service.rb +30 -2
- data/app/services/mbeditor/git_combined_diff_service.rb +13 -3
- data/app/services/mbeditor/git_commit_detail_service.rb +20 -16
- data/app/services/mbeditor/git_diff_service.rb +5 -1
- data/app/services/mbeditor/git_info_service.rb +20 -8
- data/app/services/mbeditor/git_line_diff_service.rb +6 -2
- data/app/services/mbeditor/git_service.rb +65 -15
- data/app/services/mbeditor/js_definition_service.rb +3 -1
- data/app/services/mbeditor/js_globals_service.rb +7 -4
- data/app/services/mbeditor/js_members_service.rb +3 -2
- data/app/services/mbeditor/js_program_service.rb +15 -5
- data/app/services/mbeditor/js_syntax_check_service.rb +15 -4
- data/app/services/mbeditor/process_runner.rb +42 -12
- data/app/services/mbeditor/ri_definition_service.rb +8 -1
- data/app/services/mbeditor/route_service.rb +45 -8
- data/app/services/mbeditor/rubocop_run_service.rb +86 -0
- data/app/services/mbeditor/ruby_definition_service.rb +23 -4
- data/app/services/mbeditor/schema_service.rb +65 -64
- data/app/services/mbeditor/search_replace_service.rb +31 -5
- data/app/services/mbeditor/test_runner_service.rb +98 -10
- data/lib/mbeditor/cable_log_filter.rb +8 -2
- data/lib/mbeditor/configuration.rb +12 -1
- data/lib/mbeditor/editor_bootstrap.rb +18 -12
- data/lib/mbeditor/engine.rb +22 -3
- data/lib/mbeditor/exception_log.rb +2 -2
- data/lib/mbeditor/pending_migrations.rb +33 -0
- data/lib/mbeditor/rack/handle_pending_migrations.rb +25 -15
- data/lib/mbeditor/rack/pending_migration_bypass.rb +104 -0
- data/lib/mbeditor/rack/silence_ping_request.rb +10 -2
- data/lib/mbeditor/route_map.rb +2 -0
- data/lib/mbeditor/ruby_lsp_client.rb +71 -12
- data/lib/mbeditor/version.rb +1 -1
- metadata +7 -2
|
@@ -418,6 +418,8 @@ var TabManager = (function () {
|
|
|
418
418
|
}
|
|
419
419
|
|
|
420
420
|
function closeTab(paneId, path) {
|
|
421
|
+
// A throttled content write must not outlive the tab it belongs to.
|
|
422
|
+
flushContent();
|
|
421
423
|
if (typeof HistoryService !== 'undefined') {
|
|
422
424
|
HistoryService.flushForPath(path);
|
|
423
425
|
}
|
|
@@ -539,23 +541,73 @@ var TabManager = (function () {
|
|
|
539
541
|
EditorStore.setState({ focusedPaneId: paneId, activeTabId: newActive });
|
|
540
542
|
}
|
|
541
543
|
|
|
544
|
+
// Typing calls markDirty/markClean per keystroke, and every store write
|
|
545
|
+
// re-renders the whole app (MbeditorApp subscribes to the entire store). Two
|
|
546
|
+
// rules keep that off the keystroke path:
|
|
547
|
+
//
|
|
548
|
+
// * the dirty flag is only written on a clean<->dirty transition;
|
|
549
|
+
// * `content` is written on a 250 ms trailing edge, so the store sees at
|
|
550
|
+
// most four content updates a second.
|
|
551
|
+
//
|
|
552
|
+
// FLUSH CONTRACT: anything that reads `tab.content` immediately after an edit
|
|
553
|
+
// — save, save-all, format, format-all — must call TabManager.flushContent()
|
|
554
|
+
// first and then re-read the tab from the store, or it works on text up to
|
|
555
|
+
// 250 ms old. Everything else (lint, EOL indicator, markdown preview, the
|
|
556
|
+
// external-change poll) tolerates the lag by design.
|
|
557
|
+
var CONTENT_WRITE_MS = 250;
|
|
558
|
+
var _pendingContent = {}; // "<paneId> <tabId>" -> { paneId, path, content, markdown }
|
|
559
|
+
var _contentTimer = null;
|
|
560
|
+
|
|
561
|
+
function _queueContent(paneId, path, content, updates, markdown) {
|
|
562
|
+
var key = paneId + '' + path;
|
|
563
|
+
if (updates) {
|
|
564
|
+
// A state transition has to land now, and it takes the content with it.
|
|
565
|
+
delete _pendingContent[key];
|
|
566
|
+
updates.content = content;
|
|
567
|
+
_updateTab(paneId, path, updates);
|
|
568
|
+
if (markdown) _syncMarkdownPreviewContent(path, content);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
_pendingContent[key] = { paneId: paneId, path: path, content: content, markdown: markdown };
|
|
572
|
+
if (_contentTimer === null) {
|
|
573
|
+
_contentTimer = setTimeout(flushContent, CONTENT_WRITE_MS);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function flushContent() {
|
|
578
|
+
if (_contentTimer !== null) {
|
|
579
|
+
clearTimeout(_contentTimer);
|
|
580
|
+
_contentTimer = null;
|
|
581
|
+
}
|
|
582
|
+
var pending = _pendingContent;
|
|
583
|
+
_pendingContent = {};
|
|
584
|
+
Object.keys(pending).forEach(function (k) {
|
|
585
|
+
var p = pending[k];
|
|
586
|
+
_updateTab(p.paneId, p.path, { content: p.content });
|
|
587
|
+
if (p.markdown) _syncMarkdownPreviewContent(p.path, p.content);
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
542
591
|
function markDirty(paneId, path, content) {
|
|
543
592
|
// Auto-harden any soft-open tab on first edit.
|
|
544
593
|
var st = EditorStore.getState();
|
|
545
594
|
var paneForTab = st.panes.find(function(p) { return p.id === paneId; });
|
|
546
595
|
var existingTab = paneForTab && paneForTab.tabs.find(function(t) { return t.path === path; });
|
|
547
|
-
var updates =
|
|
596
|
+
var updates = null;
|
|
597
|
+
if (!existingTab || !existingTab.dirty) updates = { dirty: true };
|
|
548
598
|
if (existingTab && existingTab.isSoftOpen) {
|
|
599
|
+
updates = updates || {};
|
|
549
600
|
updates.isSoftOpen = false;
|
|
550
601
|
}
|
|
551
|
-
|
|
552
|
-
if (_isMarkdownPath(path)) {
|
|
553
|
-
_syncMarkdownPreviewContent(path, content);
|
|
554
|
-
}
|
|
602
|
+
_queueContent(paneId, path, content, updates, _isMarkdownPath(path));
|
|
555
603
|
}
|
|
556
604
|
|
|
557
605
|
function markClean(paneId, path, content) {
|
|
558
|
-
|
|
606
|
+
var st = EditorStore.getState();
|
|
607
|
+
var paneForTab = st.panes.find(function(p) { return p.id === paneId; });
|
|
608
|
+
var existingTab = paneForTab && paneForTab.tabs.find(function(t) { return t.path === path; });
|
|
609
|
+
var updates = (existingTab && !existingTab.dirty) ? null : { dirty: false };
|
|
610
|
+
_queueContent(paneId, path, content, updates, _isMarkdownPath(path));
|
|
559
611
|
}
|
|
560
612
|
|
|
561
613
|
function hardenTab(paneId, path) {
|
|
@@ -694,6 +746,7 @@ var TabManager = (function () {
|
|
|
694
746
|
focusPane: focusPane,
|
|
695
747
|
markDirty: markDirty,
|
|
696
748
|
markClean: markClean,
|
|
749
|
+
flushContent: flushContent,
|
|
697
750
|
hardenTab: hardenTab,
|
|
698
751
|
saveTabViewState: saveTabViewState,
|
|
699
752
|
reorderTabInPane: reorderTabInPane,
|
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
// otherwise prevent reconnection.
|
|
9
9
|
var WebSocketService = (function () {
|
|
10
10
|
var _consumer = null;
|
|
11
|
+
// True only when _consumer is one we created ourselves; a consumer borrowed
|
|
12
|
+
// from the host app (window.App.cable) is not ours to disconnect.
|
|
13
|
+
var _ownsConsumer = false;
|
|
11
14
|
var _subscription = null;
|
|
12
15
|
var _connected = false;
|
|
16
|
+
// Distinguishes the first successful handshake from a reconnect, which is the
|
|
17
|
+
// point at which collaboration rooms have to be re-established.
|
|
18
|
+
var _everConnected = false;
|
|
13
19
|
// Why collaboration is or is not working, so the editor can say so instead of
|
|
14
20
|
// failing silently. A rejected subscription used to just schedule a reconnect
|
|
15
21
|
// and tell nobody, which made a blocked cable indistinguishable from an empty
|
|
@@ -62,8 +68,10 @@ var WebSocketService = (function () {
|
|
|
62
68
|
// Reuse an existing consumer the host app may have already created (App.cable
|
|
63
69
|
// is the Rails default). Fall back to creating our own.
|
|
64
70
|
if (typeof window.App !== 'undefined' && window.App.cable) {
|
|
71
|
+
_ownsConsumer = false;
|
|
65
72
|
return window.App.cable;
|
|
66
73
|
}
|
|
74
|
+
_ownsConsumer = true;
|
|
67
75
|
return window.ActionCable.createConsumer(_cableUrl());
|
|
68
76
|
}
|
|
69
77
|
|
|
@@ -72,10 +80,14 @@ var WebSocketService = (function () {
|
|
|
72
80
|
try { _subscription.unsubscribe(); } catch (e) { /* ignore */ }
|
|
73
81
|
_subscription = null;
|
|
74
82
|
}
|
|
75
|
-
|
|
83
|
+
// Only ever disconnect a consumer we created. When the host app already had
|
|
84
|
+
// one (window.App.cable) it is shared: disconnecting it on any mbeditor
|
|
85
|
+
// cable blip took down the host's own channels with it.
|
|
86
|
+
if (_ownsConsumer && _consumer && typeof _consumer.disconnect === 'function') {
|
|
76
87
|
try { _consumer.disconnect(); } catch (e) { /* ignore */ }
|
|
77
88
|
}
|
|
78
89
|
_consumer = null;
|
|
90
|
+
_ownsConsumer = false;
|
|
79
91
|
_connected = false;
|
|
80
92
|
}
|
|
81
93
|
|
|
@@ -139,11 +151,21 @@ var WebSocketService = (function () {
|
|
|
139
151
|
{ channel: 'Mbeditor::EditorChannel' },
|
|
140
152
|
{
|
|
141
153
|
connected: function () {
|
|
154
|
+
var reconnected = _everConnected;
|
|
155
|
+
_everConnected = true;
|
|
142
156
|
_connected = true;
|
|
143
157
|
_status = 'connected';
|
|
144
158
|
// Back to fast retries for the next blip.
|
|
145
159
|
_reconnectAttempts = 0;
|
|
146
160
|
if (_reconnectTimer) { clearTimeout(_reconnectTimer); _reconnectTimer = null; }
|
|
161
|
+
// The drop took the old consumer down and every CollaborationChannel
|
|
162
|
+
// subscription made on it with it. Nothing else notices — cable
|
|
163
|
+
// availability is about support, not liveness — so the rooms would
|
|
164
|
+
// stay silently dead until each file was closed and reopened.
|
|
165
|
+
if (reconnected && typeof CollaborationService !== 'undefined' &&
|
|
166
|
+
typeof CollaborationService.rejoinRooms === 'function') {
|
|
167
|
+
try { CollaborationService.rejoinRooms(); } catch (e) { /* ignore */ }
|
|
168
|
+
}
|
|
147
169
|
},
|
|
148
170
|
disconnected: function () {
|
|
149
171
|
_status = _status === 'rejected' ? 'rejected' : 'dropped';
|
|
@@ -178,12 +200,71 @@ var WebSocketService = (function () {
|
|
|
178
200
|
}
|
|
179
201
|
}
|
|
180
202
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
203
|
+
// ── files_changed coalescing ───────────────────────────────────────────────
|
|
204
|
+
//
|
|
205
|
+
// The server broadcasts once per written file, and each subscriber turns a
|
|
206
|
+
// broadcast into HTTP work: the app refreshes the tree and git status, the
|
|
207
|
+
// Monaco plugin refreshes the TypeScript program, every open editor pane
|
|
208
|
+
// re-runs git line-diff. Saving several files in a row — paste, save, next
|
|
209
|
+
// file — therefore multiplied one write into a fan-out per save, and past
|
|
210
|
+
// the point where the dev server could keep up the queue simply grew: saves
|
|
211
|
+
// got slower and slower until requests hit the axios timeout.
|
|
212
|
+
//
|
|
213
|
+
// MbeditorApp already debounced its own handler for exactly this reason, but
|
|
214
|
+
// a debounce there cannot help the other subscribers. Coalescing here fixes
|
|
215
|
+
// every subscriber at once, including any added later.
|
|
216
|
+
var FILES_CHANGED_COALESCE_MS = 150;
|
|
217
|
+
var _filesChangedTimer = null;
|
|
218
|
+
var _filesChangedAcc = null;
|
|
219
|
+
|
|
220
|
+
// Merge a broadcast into the accumulator. Pure so it can be tested directly;
|
|
221
|
+
// the two invariants are easy to get wrong and both have teeth:
|
|
222
|
+
//
|
|
223
|
+
// - A broadcast with no paths means "something changed, re-check
|
|
224
|
+
// everything". It must not be narrowed by paths that happen to land in
|
|
225
|
+
// the same window, or the re-check silently covers only those files.
|
|
226
|
+
// - `structural` is a union: one structural broadcast in the burst makes
|
|
227
|
+
// the whole flush structural, because a tree walk that gets skipped
|
|
228
|
+
// leaves a created or deleted file missing from the explorer.
|
|
229
|
+
function _mergeFilesChanged(acc, data) {
|
|
230
|
+
var next = acc || { paths: [], pathless: false, structural: false };
|
|
231
|
+
if (data && data.paths) next.paths = next.paths.concat(data.paths);
|
|
232
|
+
else next.pathless = true;
|
|
233
|
+
// Absent `structural` means an older server — the conservative read is true.
|
|
234
|
+
if (!data || data.structural !== false) next.structural = true;
|
|
235
|
+
return next;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function _coalescedPayload(acc) {
|
|
239
|
+
var payload = { type: 'files_changed', structural: acc.structural };
|
|
240
|
+
if (!acc.pathless && acc.paths.length) {
|
|
241
|
+
var seen = {};
|
|
242
|
+
payload.paths = acc.paths.filter(function (p) {
|
|
243
|
+
if (seen['$' + p]) return false;
|
|
244
|
+
seen['$' + p] = true;
|
|
245
|
+
return true;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return payload;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function _flushFilesChanged() {
|
|
252
|
+
_filesChangedTimer = null;
|
|
253
|
+
var acc = _filesChangedAcc;
|
|
254
|
+
_filesChangedAcc = null;
|
|
255
|
+
if (!acc) return;
|
|
256
|
+
var payload = _coalescedPayload(acc);
|
|
257
|
+
_filesChangedCallbacks.slice().forEach(function (fn) {
|
|
258
|
+
try { fn(payload); } catch (e) { /* ignore */ }
|
|
184
259
|
});
|
|
185
260
|
}
|
|
186
261
|
|
|
262
|
+
function _emitFilesChanged(data) {
|
|
263
|
+
_filesChangedAcc = _mergeFilesChanged(_filesChangedAcc, data);
|
|
264
|
+
if (_filesChangedTimer) return;
|
|
265
|
+
_filesChangedTimer = setTimeout(_flushFilesChanged, FILES_CHANGED_COALESCE_MS);
|
|
266
|
+
}
|
|
267
|
+
|
|
187
268
|
function _emitFileSaved(data) {
|
|
188
269
|
_fileSavedCallbacks.forEach(function (fn) {
|
|
189
270
|
try { fn(data); } catch (e) { /* ignore */ }
|
|
@@ -342,6 +423,9 @@ var WebSocketService = (function () {
|
|
|
342
423
|
subscribeCollaboration: subscribeCollaboration,
|
|
343
424
|
perform: perform,
|
|
344
425
|
onFilesChanged: onFilesChanged,
|
|
426
|
+
// Exposed for tests — the burst-merge invariants above are worth pinning.
|
|
427
|
+
_mergeFilesChanged: _mergeFilesChanged,
|
|
428
|
+
_coalescedPayload: _coalescedPayload,
|
|
345
429
|
offFilesChanged: offFilesChanged,
|
|
346
430
|
onFileSaved: onFileSaved,
|
|
347
431
|
offFileSaved: offFileSaved,
|
|
@@ -8,10 +8,86 @@
|
|
|
8
8
|
box-shadow: none !important;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
/*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
/* ── Pico containment ──────────────────────────────────────────────────────
|
|
12
|
+
*
|
|
13
|
+
* The host app's stylesheet loads alongside this one, and when it is Pico (the
|
|
14
|
+
* dummy app's is) it styles bare element and ARIA selectors — `button`,
|
|
15
|
+
* `[role=button]`, `[role=group]`, `[disabled]` — every one of which matches
|
|
16
|
+
* this UI's markup. Two shipped bugs came from exactly that: `[role=button]`
|
|
17
|
+
* painted the tab close buttons as filled primary squares, and
|
|
18
|
+
* `[disabled] { pointer-events: none }` stopped disabled toolbar buttons from
|
|
19
|
+
* receiving hover at all, so their tooltips never appeared.
|
|
20
|
+
*
|
|
21
|
+
* The technique is to zero Pico's own custom properties: every Pico button
|
|
22
|
+
* declaration reads from them, so neutralising the variables disarms the whole
|
|
23
|
+
* ruleset at once, which beats fighting each declaration on specificity. Only
|
|
24
|
+
* the few properties Pico hard-codes need explicit overrides.
|
|
25
|
+
*
|
|
26
|
+
* Two things make this safe to widen, and both depend on where the block sits:
|
|
27
|
+
*
|
|
28
|
+
* - The root is wrapped in :where(), so these rules carry exactly Pico's own
|
|
29
|
+
* specificity, no more. They win over Pico only because the whole of
|
|
30
|
+
* editor.css is bundled after the whole of Pico.
|
|
31
|
+
* - The block lives at the TOP of this file, so every component rule below —
|
|
32
|
+
* .statusbar-btn, .project-actions, .tab-close — is later at equal or
|
|
33
|
+
* greater specificity and still wins.
|
|
34
|
+
*
|
|
35
|
+
* That combination is what makes this a floor rather than a new source of
|
|
36
|
+
* truth: it can only remove Pico's opinions, never override this file's own.
|
|
37
|
+
* Moving it further down would silently invert that.
|
|
38
|
+
*
|
|
39
|
+
* Deliberately NOT reset: Pico's `*` box-sizing and background-repeat rules
|
|
40
|
+
* (this UI assumes border-box, which is what Pico sets), and `position` on
|
|
41
|
+
* [role=group] (Pico's `relative` is the safer value for any absolutely
|
|
42
|
+
* positioned descendant, and nothing here needs it to be static).
|
|
43
|
+
*
|
|
44
|
+
* Monaco is listed separately because its widgets portal out of the shell to
|
|
45
|
+
* document.body (.context-view, .action-widget), so a #mbeditor-root scope
|
|
46
|
+
* would miss them. Monaco renders its own actions as <a role="button">.
|
|
47
|
+
*/
|
|
48
|
+
:where(#mbeditor-root) :is(button, [type="button"], [type="submit"], [type="reset"], [role="button"], [role="group"]),
|
|
49
|
+
.monaco-hover [role=button],
|
|
50
|
+
.action-widget [role=button],
|
|
51
|
+
.context-view [role=button],
|
|
52
|
+
.monaco-editor [role=button],
|
|
53
|
+
.monaco-diff-editor [role=button] {
|
|
54
|
+
--pico-background-color: transparent;
|
|
55
|
+
--pico-border-color: transparent;
|
|
56
|
+
--pico-color: inherit;
|
|
57
|
+
--pico-box-shadow: none;
|
|
58
|
+
--pico-form-element-spacing-vertical: 0;
|
|
59
|
+
--pico-form-element-spacing-horizontal: 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Properties Pico hard-codes, which the variable reset above cannot reach. */
|
|
63
|
+
|
|
64
|
+
/* Pico's `[disabled] { pointer-events: none }` means a disabled control never
|
|
65
|
+
* receives hover, so neither a title attribute nor a CSS hover tooltip can
|
|
66
|
+
* fire on it — and a disabled button is exactly when a user most wants to be
|
|
67
|
+
* told what it is. */
|
|
68
|
+
:where(#mbeditor-root) [disabled],
|
|
69
|
+
:where(#mbeditor-root) [aria-disabled="true"] {
|
|
70
|
+
pointer-events: auto;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Pico lays [role=group] out as a full-width bordered input group. It is used
|
|
74
|
+
* here only to label toolbars for screen readers. */
|
|
75
|
+
:where(#mbeditor-root) [role="group"] {
|
|
76
|
+
display: inline-flex;
|
|
77
|
+
width: auto;
|
|
78
|
+
margin-bottom: 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Pico gives every button a bottom margin to space form controls. */
|
|
82
|
+
:where(#mbeditor-root) :is(button, [type="button"], [type="submit"], [type="reset"]) {
|
|
83
|
+
margin: 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Pico's focus/active state sets its primary blue on --pico-background-color
|
|
87
|
+
and --pico-border-color, which bleeds through as a blue tint on any button
|
|
88
|
+
not overriding its background with a literal value. Kept separate from the
|
|
89
|
+
block above: this one is intentionally more specific than a component rule,
|
|
90
|
+
because no component should be able to opt back into a Pico focus colour. */
|
|
15
91
|
#mbeditor-root button:not(.pico-btn):focus,
|
|
16
92
|
#mbeditor-root button:not(.pico-btn):focus-visible,
|
|
17
93
|
#mbeditor-root button:not(.pico-btn):active {
|
|
@@ -129,57 +205,66 @@ html, body, #mbeditor-root {
|
|
|
129
205
|
|
|
130
206
|
/* ── Activity Bar (VSCode-style, always visible) ───────── */
|
|
131
207
|
.ide-activity-bar {
|
|
132
|
-
width:
|
|
133
|
-
min-width:
|
|
208
|
+
width: 60px;
|
|
209
|
+
min-width: 60px;
|
|
134
210
|
flex-shrink: 0;
|
|
135
211
|
background: var(--ide-panel-bg);
|
|
136
212
|
border-right: 1px solid var(--ide-border);
|
|
137
213
|
display: flex;
|
|
138
214
|
flex-direction: column;
|
|
139
|
-
|
|
140
|
-
|
|
215
|
+
/* stretch, not center: the buttons fill the bar edge to edge so the active
|
|
216
|
+
indicator can sit flush against the left of the screen. Centering them
|
|
217
|
+
left a gutter the indicator was inset by. */
|
|
218
|
+
align-items: stretch;
|
|
219
|
+
padding: 0;
|
|
141
220
|
z-index: 10;
|
|
142
221
|
}
|
|
143
222
|
|
|
144
223
|
.ide-activity-bar-top {
|
|
145
224
|
display: flex;
|
|
146
225
|
flex-direction: column;
|
|
147
|
-
align-items:
|
|
148
|
-
gap:
|
|
226
|
+
align-items: stretch;
|
|
227
|
+
gap: 0;
|
|
149
228
|
flex: 1;
|
|
150
229
|
}
|
|
151
230
|
|
|
152
231
|
.ide-activity-bar-bottom {
|
|
153
232
|
display: flex;
|
|
154
233
|
flex-direction: column;
|
|
155
|
-
align-items:
|
|
156
|
-
gap:
|
|
157
|
-
padding-bottom: 4px;
|
|
234
|
+
align-items: stretch;
|
|
235
|
+
gap: 0;
|
|
158
236
|
}
|
|
159
237
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
238
|
+
/* Scoped through the bar so the glyph size outranks the global
|
|
239
|
+
`button:not(.pico-btn) { font-size: 12px }` reset, which is more specific
|
|
240
|
+
than a bare class and was silently winning. */
|
|
241
|
+
.ide-activity-bar .ide-activity-btn {
|
|
242
|
+
width: 100%;
|
|
243
|
+
height: 60px;
|
|
163
244
|
background: transparent;
|
|
164
245
|
border: none;
|
|
246
|
+
/* Always reserve the indicator's width so the glyph does not shift sideways
|
|
247
|
+
by a pixel the moment a panel is opened. */
|
|
248
|
+
border-left: 2px solid transparent;
|
|
165
249
|
border-radius: 0;
|
|
166
250
|
color: var(--ide-text-muted);
|
|
167
251
|
display: flex;
|
|
168
252
|
align-items: center;
|
|
169
253
|
justify-content: center;
|
|
170
254
|
cursor: pointer;
|
|
171
|
-
font-size:
|
|
172
|
-
transition:
|
|
255
|
+
font-size: 25px;
|
|
256
|
+
transition: color 0.12s ease;
|
|
173
257
|
}
|
|
174
258
|
|
|
175
|
-
.
|
|
176
|
-
|
|
259
|
+
/* The glyph carries the state, not a filled box behind it. */
|
|
260
|
+
.ide-activity-bar .ide-activity-btn:hover {
|
|
261
|
+
background: transparent;
|
|
177
262
|
color: var(--ide-text);
|
|
178
263
|
}
|
|
179
264
|
|
|
180
|
-
.ide-activity-btn.active {
|
|
265
|
+
.ide-activity-bar .ide-activity-btn.active {
|
|
181
266
|
color: var(--ide-accent-fg);
|
|
182
|
-
border-left:
|
|
267
|
+
border-left-color: var(--ide-accent-fg);
|
|
183
268
|
}
|
|
184
269
|
|
|
185
270
|
.sidebar-panel-title {
|
|
@@ -300,25 +385,25 @@ html, body, #mbeditor-root {
|
|
|
300
385
|
border: 1px solid var(--ide-accent);
|
|
301
386
|
border-radius: 3px;
|
|
302
387
|
color: var(--ide-text);
|
|
303
|
-
font-size:
|
|
388
|
+
font-size: 14px;
|
|
304
389
|
font-family: inherit;
|
|
305
390
|
padding: 1px 4px;
|
|
306
391
|
outline: none;
|
|
307
|
-
height:
|
|
308
|
-
line-height:
|
|
392
|
+
height: 21px;
|
|
393
|
+
line-height: 21px;
|
|
309
394
|
}
|
|
310
395
|
|
|
311
396
|
.tree-item-icon {
|
|
312
|
-
width:
|
|
313
|
-
margin-right:
|
|
314
|
-
font-size:
|
|
397
|
+
width: 20px;
|
|
398
|
+
margin-right: 6px;
|
|
399
|
+
font-size: 14px;
|
|
315
400
|
flex-shrink: 0;
|
|
316
401
|
text-align: center;
|
|
317
402
|
}
|
|
318
403
|
|
|
319
404
|
.tree-folder-icon { color: #dcb67a; }
|
|
320
405
|
.tree-file-icon { color: #78b4e0; }
|
|
321
|
-
.tree-item-name { flex: 1; overflow: hidden; text-overflow: ellipsis; font-size:
|
|
406
|
+
.tree-item-name { flex: 1; overflow: hidden; text-overflow: ellipsis; font-size: 14px; }
|
|
322
407
|
|
|
323
408
|
/* ── Custom File Icons ───────────────────────────────────── */
|
|
324
409
|
.ruby-icon { color: #f26262 !important; }
|
|
@@ -1412,7 +1497,90 @@ html, body, #mbeditor-root {
|
|
|
1412
1497
|
cursor: pointer;
|
|
1413
1498
|
}
|
|
1414
1499
|
.import-conflict-btn:hover { background: var(--ide-hover-bg, #4a4a4a); }
|
|
1500
|
+
.import-conflict-btn:disabled { opacity: 0.45; cursor: default; }
|
|
1415
1501
|
.import-conflict-btn-danger { border-color: var(--ide-error, #f48771); color: var(--ide-error, #f48771); }
|
|
1502
|
+
.import-conflict-btn-primary {
|
|
1503
|
+
background: var(--ide-accent, #0e639c);
|
|
1504
|
+
border-color: var(--ide-accent, #0e639c);
|
|
1505
|
+
color: #fff;
|
|
1506
|
+
}
|
|
1507
|
+
.import-conflict-btn-primary:hover:not(:disabled) { background: var(--ide-accent-hover, #1177bb); }
|
|
1508
|
+
|
|
1509
|
+
/* Upload dialog — the pointed-at half of drag-and-drop import */
|
|
1510
|
+
.import-dialog { min-width: 460px; max-width: 620px; }
|
|
1511
|
+
.import-dialog .schema-modal-body { padding: 12px 16px; }
|
|
1512
|
+
.import-pick-row { display: flex; gap: 8px; }
|
|
1513
|
+
.import-pick-btn {
|
|
1514
|
+
background: var(--ide-input-bg, #3c3c3c);
|
|
1515
|
+
color: var(--ide-text, #d4d4d4);
|
|
1516
|
+
border: 1px solid var(--ide-border, #3c3c3c);
|
|
1517
|
+
border-radius: 3px;
|
|
1518
|
+
padding: 6px 12px;
|
|
1519
|
+
font-size: 12px;
|
|
1520
|
+
cursor: pointer;
|
|
1521
|
+
}
|
|
1522
|
+
.import-pick-btn:hover { background: var(--ide-hover-bg, #4a4a4a); }
|
|
1523
|
+
.import-pick-btn i { margin-right: 6px; opacity: 0.75; }
|
|
1524
|
+
.import-dialog-empty,
|
|
1525
|
+
.import-dialog-warning {
|
|
1526
|
+
margin-top: 12px;
|
|
1527
|
+
font-size: 12px;
|
|
1528
|
+
color: var(--ide-text-muted, #888);
|
|
1529
|
+
}
|
|
1530
|
+
.import-dialog-warning { color: var(--ide-warning, #cca700); }
|
|
1531
|
+
.import-dialog-section-title {
|
|
1532
|
+
margin: 16px 0 6px;
|
|
1533
|
+
font-size: 10px;
|
|
1534
|
+
font-weight: 600;
|
|
1535
|
+
text-transform: uppercase;
|
|
1536
|
+
letter-spacing: 0.06em;
|
|
1537
|
+
color: var(--ide-text-muted, #888);
|
|
1538
|
+
}
|
|
1539
|
+
.import-dest-options { display: flex; flex-direction: column; gap: 2px; }
|
|
1540
|
+
.import-dest-option {
|
|
1541
|
+
display: flex;
|
|
1542
|
+
align-items: center;
|
|
1543
|
+
gap: 8px;
|
|
1544
|
+
width: 100%;
|
|
1545
|
+
text-align: left;
|
|
1546
|
+
background: none;
|
|
1547
|
+
border: none;
|
|
1548
|
+
border-radius: 3px;
|
|
1549
|
+
padding: 4px 6px;
|
|
1550
|
+
color: var(--ide-text, #d4d4d4);
|
|
1551
|
+
font-size: 12px;
|
|
1552
|
+
cursor: pointer;
|
|
1553
|
+
}
|
|
1554
|
+
.import-dest-option:hover { background: var(--ide-hover, rgba(255,255,255,0.06)); }
|
|
1555
|
+
.import-dest-option.active { background: var(--ide-selection-bg, rgba(14,99,156,0.35)); }
|
|
1556
|
+
.import-dest-radio { opacity: 0.7; font-size: 11px; }
|
|
1557
|
+
.import-dest-label {
|
|
1558
|
+
font-family: var(--ide-mono-font, monospace);
|
|
1559
|
+
overflow: hidden;
|
|
1560
|
+
text-overflow: ellipsis;
|
|
1561
|
+
white-space: nowrap;
|
|
1562
|
+
}
|
|
1563
|
+
.import-dest-hint { color: var(--ide-text-muted, #888); font-size: 11px; margin-left: auto; flex-shrink: 0; }
|
|
1564
|
+
.import-dest-input {
|
|
1565
|
+
width: 100%;
|
|
1566
|
+
margin-top: 8px;
|
|
1567
|
+
box-sizing: border-box;
|
|
1568
|
+
background: var(--ide-input-bg, #3c3c3c);
|
|
1569
|
+
color: var(--ide-text, #d4d4d4);
|
|
1570
|
+
border: 1px solid var(--ide-border, #3c3c3c);
|
|
1571
|
+
border-radius: 3px;
|
|
1572
|
+
padding: 5px 8px;
|
|
1573
|
+
font-family: var(--ide-mono-font, monospace);
|
|
1574
|
+
font-size: 12px;
|
|
1575
|
+
}
|
|
1576
|
+
.import-dest-input:focus { outline: 1px solid var(--ide-accent, #0e639c); }
|
|
1577
|
+
.import-target-replaces {
|
|
1578
|
+
margin-left: 8px;
|
|
1579
|
+
font-size: 10px;
|
|
1580
|
+
text-transform: uppercase;
|
|
1581
|
+
letter-spacing: 0.06em;
|
|
1582
|
+
color: var(--ide-warning, #cca700);
|
|
1583
|
+
}
|
|
1416
1584
|
.schema-table {
|
|
1417
1585
|
width: 100%;
|
|
1418
1586
|
border-collapse: collapse;
|
|
@@ -1969,12 +2137,20 @@ html, body, #mbeditor-root {
|
|
|
1969
2137
|
flex-shrink: 0;
|
|
1970
2138
|
}
|
|
1971
2139
|
|
|
2140
|
+
/* The title yields the header to the action buttons rather than wrapping.
|
|
2141
|
+
*
|
|
2142
|
+
* It used to break-word onto three lines ("SAMPLE_ / WORKSPA / CE") because a
|
|
2143
|
+
* long workspace name plus six buttons genuinely does not fit a 300px sidebar.
|
|
2144
|
+
* flex-shrink against a `flex-shrink: 0` action group means the buttons always
|
|
2145
|
+
* win: full title, then ellipsis, then gone. No JS measuring, and it adapts on
|
|
2146
|
+
* its own to sections with fewer buttons — the Open Editors header keeps its
|
|
2147
|
+
* title at the same width where Explorer has already dropped it. */
|
|
1972
2148
|
.collapsible-title {
|
|
1973
|
-
flex: 1;
|
|
2149
|
+
flex: 1 1 auto;
|
|
1974
2150
|
min-width: 0;
|
|
1975
|
-
|
|
1976
|
-
overflow
|
|
1977
|
-
|
|
2151
|
+
white-space: nowrap;
|
|
2152
|
+
overflow: hidden;
|
|
2153
|
+
text-overflow: ellipsis;
|
|
1978
2154
|
}
|
|
1979
2155
|
|
|
1980
2156
|
.collapsible-actions {
|
|
@@ -1991,6 +2167,45 @@ html, body, #mbeditor-root {
|
|
|
1991
2167
|
gap: 0;
|
|
1992
2168
|
}
|
|
1993
2169
|
|
|
2170
|
+
/* Tooltip for the icon-only sidebar action buttons. See SidebarActionButton
|
|
2171
|
+
* for why this is not a title attribute. Anchored to the button's right edge
|
|
2172
|
+
* because .ide-sidebar-scrollable is overflow-x: hidden — a left-anchored or
|
|
2173
|
+
* centred tooltip on the rightmost button would be clipped by the sidebar. */
|
|
2174
|
+
.sb-tip {
|
|
2175
|
+
position: relative;
|
|
2176
|
+
display: inline-flex;
|
|
2177
|
+
}
|
|
2178
|
+
.sb-tip::after {
|
|
2179
|
+
content: attr(data-tip);
|
|
2180
|
+
position: absolute;
|
|
2181
|
+
top: calc(100% + 4px);
|
|
2182
|
+
right: 0;
|
|
2183
|
+
z-index: 4000;
|
|
2184
|
+
padding: 4px 7px;
|
|
2185
|
+
background: var(--ide-panel-bg, #252526);
|
|
2186
|
+
border: 1px solid var(--ide-border, #3c3c3c);
|
|
2187
|
+
border-radius: 4px;
|
|
2188
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
|
2189
|
+
color: var(--ide-text, #d4d4d4);
|
|
2190
|
+
/* The section header sets uppercase, 600 weight and letter-spacing; a
|
|
2191
|
+
tooltip inheriting those reads as another heading. */
|
|
2192
|
+
font-size: 11px;
|
|
2193
|
+
font-weight: 400;
|
|
2194
|
+
letter-spacing: 0;
|
|
2195
|
+
text-transform: none;
|
|
2196
|
+
white-space: nowrap;
|
|
2197
|
+
opacity: 0;
|
|
2198
|
+
pointer-events: none;
|
|
2199
|
+
transition: opacity 0.08s ease 0.3s;
|
|
2200
|
+
}
|
|
2201
|
+
.sb-tip:hover::after,
|
|
2202
|
+
.sb-tip:focus-within::after {
|
|
2203
|
+
opacity: 1;
|
|
2204
|
+
}
|
|
2205
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2206
|
+
.sb-tip::after { transition: none; }
|
|
2207
|
+
}
|
|
2208
|
+
|
|
1994
2209
|
/* Open-editors panel: height is controlled by the --open-editors-height CSS variable
|
|
1995
2210
|
set inline from React state (default 140px ≈ 5-6 items), drag-resizable via the
|
|
1996
2211
|
.open-editors-resize-handle strip below. */
|
|
@@ -2652,26 +2867,8 @@ button:not(.pico-btn) {
|
|
|
2652
2867
|
font-size: 12px;
|
|
2653
2868
|
}
|
|
2654
2869
|
|
|
2655
|
-
/*
|
|
2656
|
-
*
|
|
2657
|
-
* because Monaco renders them as <a role="button">. Without this reset, Pico
|
|
2658
|
-
* applies its primary-colour background, white text, and large form-element
|
|
2659
|
-
* padding, making those buttons huge and unreadable on certain themes.
|
|
2660
|
-
* Resetting the Pico CSS variables here is lower-impact than fighting
|
|
2661
|
-
* per-property specificity battles with Monaco's --vscode-* rules.
|
|
2662
|
-
*/
|
|
2663
|
-
.monaco-hover [role=button],
|
|
2664
|
-
.action-widget [role=button],
|
|
2665
|
-
.context-view [role=button],
|
|
2666
|
-
.monaco-editor [role=button],
|
|
2667
|
-
.monaco-diff-editor [role=button] {
|
|
2668
|
-
--pico-background-color: transparent;
|
|
2669
|
-
--pico-border-color: transparent;
|
|
2670
|
-
--pico-color: inherit;
|
|
2671
|
-
--pico-box-shadow: none;
|
|
2672
|
-
--pico-form-element-spacing-vertical: 0;
|
|
2673
|
-
--pico-form-element-spacing-horizontal: 0;
|
|
2674
|
-
}
|
|
2870
|
+
/* Pico containment lives at the top of this file — see the block there. It has
|
|
2871
|
+
* to sit above every component rule to stay a floor rather than an override. */
|
|
2675
2872
|
|
|
2676
2873
|
/* Inputs and selects — keep compact but styled */
|
|
2677
2874
|
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
|
|
@@ -3049,6 +3246,24 @@ button:not(.pico-btn) { margin-bottom: 0; }
|
|
|
3049
3246
|
padding: 2px 6px;
|
|
3050
3247
|
}
|
|
3051
3248
|
.ide-problems-btn:hover { color: var(--ide-fg, #fff); }
|
|
3249
|
+
.ide-problems-btn:disabled { opacity: 0.4; cursor: default; }
|
|
3250
|
+
.ide-problems-btn:disabled:hover { color: var(--ide-fg-muted, #ccc); }
|
|
3251
|
+
.ide-problems-actions { display: flex; gap: 2px; }
|
|
3252
|
+
.ide-problems-actions .ide-problems-btn { white-space: nowrap; }
|
|
3253
|
+
.ide-problems-btn.is-on { color: var(--ide-accent, #0a84ff); }
|
|
3254
|
+
.ide-testrun-stat { font-size: 11px; color: var(--ide-text-muted, #858585); }
|
|
3255
|
+
.ide-testrun-pass { color: #4ec9b0; }
|
|
3256
|
+
.ide-testrun-fail { color: #f14c4c; }
|
|
3257
|
+
.ide-testrun-raw {
|
|
3258
|
+
margin: 0;
|
|
3259
|
+
padding: 8px 14px;
|
|
3260
|
+
font-family: var(--ide-mono, monospace);
|
|
3261
|
+
font-size: 11px;
|
|
3262
|
+
line-height: 1.5;
|
|
3263
|
+
white-space: pre;
|
|
3264
|
+
overflow-x: auto;
|
|
3265
|
+
color: var(--ide-fg, #eee);
|
|
3266
|
+
}
|
|
3052
3267
|
.ide-problems-body { flex: 1; overflow: auto; padding: 4px 0; font-size: 12px; }
|
|
3053
3268
|
.ide-problems-empty {
|
|
3054
3269
|
padding: 10px 14px;
|
|
@@ -19,6 +19,13 @@ module Mbeditor
|
|
|
19
19
|
# True when the connection is allowed (or no hook is configured); otherwise
|
|
20
20
|
# rejects the subscription and returns false.
|
|
21
21
|
def mbeditor_authenticated?
|
|
22
|
+
# The same gate the controllers apply. Without it the cable channels were
|
|
23
|
+
# reachable in any environment, hook or no hook.
|
|
24
|
+
unless Array(Mbeditor.configuration.allowed_environments).map(&:to_sym).include?(Rails.env.to_sym)
|
|
25
|
+
mbeditor_reject_subscription
|
|
26
|
+
return false
|
|
27
|
+
end
|
|
28
|
+
|
|
22
29
|
hook = mbeditor_auth_hook
|
|
23
30
|
return true unless hook
|
|
24
31
|
|