flexdesk 0.2.0 → 0.3.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.
- package/css/base.css +1484 -181
- package/css/flexdesk.css +1311 -18
- package/css/overrides.css +44 -0
- package/css/tokens.css +45 -0
- package/dist/charts.js +5 -3
- package/dist/charts.js.map +1 -1
- package/dist/{chunk-DVU44T77.js → chunk-ELXVW542.js} +196 -75
- package/dist/chunk-ELXVW542.js.map +7 -0
- package/dist/chunk-LH5TSOZW.js +1237 -0
- package/dist/chunk-LH5TSOZW.js.map +7 -0
- package/dist/{chunk-TLZUUFOE.js → chunk-O5OHMWBB.js} +10 -2
- package/dist/chunk-O5OHMWBB.js.map +7 -0
- package/dist/{chunk-CT4YXXLP.js → chunk-QIU5S2RU.js} +371 -73
- package/dist/chunk-QIU5S2RU.js.map +7 -0
- package/dist/chunk-QNQHQ24V.js +408 -0
- package/dist/chunk-QNQHQ24V.js.map +7 -0
- package/dist/{chunk-DRYCDMEG.js → chunk-XKDTIT4Q.js} +168 -12
- package/dist/chunk-XKDTIT4Q.js.map +7 -0
- package/dist/editor.js +3 -380
- package/dist/editor.js.map +3 -3
- package/dist/flexdesk.css +1311 -18
- package/dist/tiles.js +168 -41
- package/dist/tiles.js.map +2 -2
- package/dist/tokens.css +45 -0
- package/dist/widgets.js +44 -14
- package/dist/widgets.js.map +2 -2
- package/dist/wm.js +2983 -142
- package/dist/wm.js.map +4 -4
- package/package.json +3 -2
- package/src/charts/chart_types.js +167 -0
- package/src/charts/plotly_wrapper.js +178 -10
- package/src/editor/notebook_tab_bar.js +39 -3
- package/src/tiles/tile_base.js +143 -35
- package/src/tiles/tile_grid.js +52 -1
- package/src/tiling/command_palette.js +71 -18
- package/src/tiling/desktops.js +36 -12
- package/src/tiling/keymap.js +24 -4
- package/src/tiling/shell.js +135 -24
- package/src/tiling/tab_strip.js +184 -0
- package/src/tiling/tile_breadcrumb.js +34 -2
- package/src/tiling/tile_renderer.js +1386 -21
- package/src/tiling/tile_tab_menu.js +101 -0
- package/src/tiling/tile_tree.js +82 -0
- package/src/tiling/wm.js +2352 -74
- package/src/ui/components/action_dropdown.js +34 -3
- package/src/ui/components/autocomplete_field.js +65 -13
- package/src/ui/components/context_menu.js +79 -8
- package/src/ui/components/data_table.js +508 -84
- package/src/ui/components/managed_window.js +928 -36
- package/src/ui/components/modal.js +214 -8
- package/dist/chunk-CT4YXXLP.js.map +0 -7
- package/dist/chunk-DRYCDMEG.js.map +0 -7
- package/dist/chunk-DVU44T77.js.map +0 -7
- package/dist/chunk-TLZUUFOE.js.map +0 -7
- package/dist/chunk-UCJ2WD4D.js +0 -625
- package/dist/chunk-UCJ2WD4D.js.map +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flexdesk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A tiling window manager and widget set for desktop-class web applications. Zero runtime dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"build": "node build.mjs",
|
|
60
|
-
"check": "node build.mjs --check"
|
|
60
|
+
"check": "node build.mjs --check",
|
|
61
|
+
"test": "node tests/run.mjs"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
63
64
|
"esbuild": "^0.28.1"
|
|
@@ -17,6 +17,15 @@ export const CHART_TYPES_2D = Object.freeze([
|
|
|
17
17
|
{ value: 'area', label: 'Area' },
|
|
18
18
|
{ value: 'area-stacked', label: 'Area (Stacked %)' },
|
|
19
19
|
{ value: 'phase', label: 'Phase' },
|
|
20
|
+
// Categorical part-of-whole. They belong in this list because the list is
|
|
21
|
+
// what a consumer renders as its chart-type dropdown — but they are NOT
|
|
22
|
+
// (x[], y[]) charts, and `buildTrace` leaves for them before the switch:
|
|
23
|
+
// read the early return there before adding a fourth. `indicator` is
|
|
24
|
+
// deliberately NOT here, because it takes a SCALAR — a dropdown offering it
|
|
25
|
+
// beside these would offer a rendering of data the chart is not bound to.
|
|
26
|
+
{ value: 'pie', label: 'Pie' },
|
|
27
|
+
{ value: 'donut', label: 'Donut' },
|
|
28
|
+
{ value: 'funnel', label: 'Funnel' },
|
|
20
29
|
]);
|
|
21
30
|
|
|
22
31
|
/**
|
|
@@ -76,6 +85,7 @@ const INTERPOLATION_MAP = {
|
|
|
76
85
|
* @param {string} [options.yAxisId] - Y-axis identifier (y, y2, y3, etc.)
|
|
77
86
|
* @param {boolean} [options.showMarkers] - Show data point markers
|
|
78
87
|
* @param {number} [options.seriesIndex] - Index for default color selection
|
|
88
|
+
* @param {Array<string>} [options.colors] - Per-CATEGORY colors, pie/donut/funnel only
|
|
79
89
|
* @returns {Object} Plotly trace object
|
|
80
90
|
*/
|
|
81
91
|
export function buildTrace(options) {
|
|
@@ -93,10 +103,62 @@ export function buildTrace(options) {
|
|
|
93
103
|
showMarkers = false,
|
|
94
104
|
seriesIndex = 0,
|
|
95
105
|
stackGroup = null,
|
|
106
|
+
colors = null,
|
|
96
107
|
} = options;
|
|
97
108
|
|
|
98
109
|
const seriesColor = color || getSeriesColor(seriesIndex);
|
|
99
110
|
|
|
111
|
+
// ── categorical part-of-whole: pie, donut, funnel ────────────────────
|
|
112
|
+
// These leave here, ABOVE the switch, and that placement is the whole
|
|
113
|
+
// point rather than a tidiness. Everything below is written against the
|
|
114
|
+
// base trace built at the end of the switch — `{type, name, x, y}` — and a
|
|
115
|
+
// pie has neither an x nor a y: Plotly reads `labels` and `values`, and a
|
|
116
|
+
// stray `x` rides along ignored right up until someone puts the trace on a
|
|
117
|
+
// shared axis and it is not ignored any more.
|
|
118
|
+
//
|
|
119
|
+
// The marker block further down is the sharper edge. It fires on
|
|
120
|
+
// `showMarkers` for ANY chart type and assigns `marker = {color, size}` —
|
|
121
|
+
// which would overwrite `marker.colors`, the per-slice palette, with one
|
|
122
|
+
// scalar colour and turn a seven-slice pie into a single blue disc. A
|
|
123
|
+
// `case` in the switch cannot avoid either hazard; leaving before both can.
|
|
124
|
+
if (chartType === 'pie' || chartType === 'donut' || chartType === 'funnel') {
|
|
125
|
+
// `seriesIndex` is not consulted: a pie IS the chart, so its slices
|
|
126
|
+
// start at the top of the palette rather than continuing someone
|
|
127
|
+
// else's series numbering.
|
|
128
|
+
const sliceColors = Array.isArray(colors) && colors.length
|
|
129
|
+
? colors
|
|
130
|
+
: y.map((_, i) => getSeriesColor(i));
|
|
131
|
+
|
|
132
|
+
if (chartType === 'funnel') {
|
|
133
|
+
// A funnel is bar-shaped, so it is `marker.color` (singular, and
|
|
134
|
+
// an array is legal) — NOT the `marker.colors` a pie wants. The
|
|
135
|
+
// two spellings are not interchangeable and neither errors.
|
|
136
|
+
return {
|
|
137
|
+
type: 'funnel',
|
|
138
|
+
name,
|
|
139
|
+
y: x, // stages read down the categorical axis
|
|
140
|
+
x: y, // ...and the measure runs across
|
|
141
|
+
marker: { color: sliceColors },
|
|
142
|
+
textinfo: 'value+percent initial',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
type: 'pie',
|
|
148
|
+
name,
|
|
149
|
+
labels: x,
|
|
150
|
+
values: y,
|
|
151
|
+
marker: { colors: sliceColors },
|
|
152
|
+
// Plotly sorts slices descending by default. The caller has
|
|
153
|
+
// already ordered its rows — a dashboard binding carries its own
|
|
154
|
+
// `sort` — and a second, invisible reordering here makes the chart
|
|
155
|
+
// disagree with the table beside it and with the config that
|
|
156
|
+
// produced both.
|
|
157
|
+
sort: false,
|
|
158
|
+
...(chartType === 'donut' ? { hole: 0.55 } : {}),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
100
162
|
// Determine Plotly trace type and mode
|
|
101
163
|
let plotlyType, mode;
|
|
102
164
|
switch (chartType) {
|
|
@@ -697,3 +759,108 @@ export function buildHeatmapTrace(options) {
|
|
|
697
759
|
|
|
698
760
|
return trace;
|
|
699
761
|
}
|
|
762
|
+
|
|
763
|
+
// Delta sense, in the palette's own idiom (Solarized, per COLOR_PALETTE). A
|
|
764
|
+
// caller whose metric is better when it falls — cost, latency, churn — passes
|
|
765
|
+
// its own two colours rather than expecting a flag here: "up is good" is a
|
|
766
|
+
// property of the measure, and this file has never been told what the measure
|
|
767
|
+
// is.
|
|
768
|
+
const DELTA_UP = '#859900'; // Solarized green
|
|
769
|
+
const DELTA_DOWN = '#dc322f'; // Solarized red
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Create a Plotly `indicator` trace — a big number, optionally with a gauge
|
|
773
|
+
* arc under it and a delta against a reference.
|
|
774
|
+
*
|
|
775
|
+
* This is NOT reachable through `buildTrace`, and `indicator` is not in
|
|
776
|
+
* `CHART_TYPES_2D`: `buildTrace`'s contract is two parallel arrays and an
|
|
777
|
+
* indicator takes a single scalar, so offering it in the same dropdown would
|
|
778
|
+
* offer a rendering that cannot consume what the dropdown's chart is bound to.
|
|
779
|
+
*
|
|
780
|
+
* The returned object is a plain literal and yours to extend — `domain` for
|
|
781
|
+
* two indicators in one div, `number.prefix` / `number.valueformat` for
|
|
782
|
+
* currency. Number formatting is deliberately not a parameter: a consumer that
|
|
783
|
+
* already owns a value formatter and hands Plotly a second one shows the
|
|
784
|
+
* currency symbol twice, and neither side knows the other did it.
|
|
785
|
+
*
|
|
786
|
+
* @param {Object} options
|
|
787
|
+
* @param {number} options.value - The number to show
|
|
788
|
+
* @param {number} [options.min] - Gauge floor (default 0)
|
|
789
|
+
* @param {number} [options.max] - Gauge ceiling; null draws NO gauge
|
|
790
|
+
* @param {string} [options.title] - Label above the number
|
|
791
|
+
* @param {Array<{range: number[], color: string}>} [options.steps] - Banded background
|
|
792
|
+
* @param {number|{value: number, color: string, width: number, thickness: number}} [options.threshold]
|
|
793
|
+
* @param {string} [options.color] - The gauge bar colour
|
|
794
|
+
* @param {number} [options.seriesIndex] - Palette index when `color` is absent
|
|
795
|
+
* @param {number|{reference: number, relative: boolean, increasing: string, decreasing: string}} [options.delta]
|
|
796
|
+
* @returns {Object} Plotly indicator trace
|
|
797
|
+
*/
|
|
798
|
+
export function buildIndicatorTrace(options) {
|
|
799
|
+
const {
|
|
800
|
+
value,
|
|
801
|
+
min = 0,
|
|
802
|
+
max = null,
|
|
803
|
+
title = '',
|
|
804
|
+
steps = null,
|
|
805
|
+
threshold = null,
|
|
806
|
+
color = null,
|
|
807
|
+
seriesIndex = 0,
|
|
808
|
+
delta = null,
|
|
809
|
+
} = options;
|
|
810
|
+
|
|
811
|
+
// A gauge with no ceiling has no arc to draw. Inventing one — twice the
|
|
812
|
+
// value, the next round number — draws a chart that is quietly wrong about
|
|
813
|
+
// the only thing a gauge communicates, so an absent `max` degrades to the
|
|
814
|
+
// bare number instead.
|
|
815
|
+
const hasGauge = typeof max === 'number' && Number.isFinite(max);
|
|
816
|
+
|
|
817
|
+
// `0` is an ordinary reference — last month's total was zero — so the test
|
|
818
|
+
// is `typeof`, never truthiness. Under a truthiness test `delta: 0` drops
|
|
819
|
+
// the comparison the tile exists to draw, silently and only sometimes.
|
|
820
|
+
const deltaSpec = typeof delta === 'number' ? { reference: delta }
|
|
821
|
+
: (delta && typeof delta === 'object') ? delta
|
|
822
|
+
: null;
|
|
823
|
+
|
|
824
|
+
const parts = ['number'];
|
|
825
|
+
if (hasGauge) parts.unshift('gauge');
|
|
826
|
+
if (deltaSpec) parts.push('delta');
|
|
827
|
+
|
|
828
|
+
const trace = {
|
|
829
|
+
type: 'indicator',
|
|
830
|
+
mode: parts.join('+'),
|
|
831
|
+
value,
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
if (title) trace.title = { text: title };
|
|
835
|
+
|
|
836
|
+
if (deltaSpec) {
|
|
837
|
+
trace.delta = {
|
|
838
|
+
reference: deltaSpec.reference,
|
|
839
|
+
relative: deltaSpec.relative === true,
|
|
840
|
+
increasing: { color: deltaSpec.increasing || DELTA_UP },
|
|
841
|
+
decreasing: { color: deltaSpec.decreasing || DELTA_DOWN },
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
if (hasGauge) {
|
|
846
|
+
trace.gauge = {
|
|
847
|
+
axis: { range: [min, max] },
|
|
848
|
+
bar: { color: color || getSeriesColor(seriesIndex) },
|
|
849
|
+
};
|
|
850
|
+
// Passed through rather than derived. A band means something to the
|
|
851
|
+
// consumer — a target, an SLA, a red zone — and this file cannot guess
|
|
852
|
+
// where it sits.
|
|
853
|
+
if (Array.isArray(steps) && steps.length) trace.gauge.steps = steps;
|
|
854
|
+
|
|
855
|
+
if (threshold !== null && threshold !== undefined) {
|
|
856
|
+
const t = typeof threshold === 'number' ? { value: threshold } : threshold;
|
|
857
|
+
trace.gauge.threshold = {
|
|
858
|
+
line: { color: t.color || DELTA_DOWN, width: t.width ?? 3 },
|
|
859
|
+
thickness: t.thickness ?? 0.9,
|
|
860
|
+
value: t.value,
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return trace;
|
|
866
|
+
}
|
|
@@ -184,36 +184,204 @@ export async function ensurePlotly() {
|
|
|
184
184
|
+ 'or call setPlotlySource("/path/to/plotly.min.js") before rendering a chart.');
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
// Start loading —
|
|
188
|
-
//
|
|
189
|
-
|
|
187
|
+
// Start loading — make Plotly's UMD header take its browser-global branch
|
|
188
|
+
// WITHOUT asking any other script on the page to give up AMD, even for a
|
|
189
|
+
// moment.
|
|
190
|
+
//
|
|
191
|
+
// ══ WHY THE SCRIPT TAG IS NOT USED FIRST ═══════════════════════════════
|
|
192
|
+
//
|
|
193
|
+
// Plotly's header asks one question — `typeof define === 'function' &&
|
|
194
|
+
// define.amd` — and takes the AMD branch if the answer is yes, leaving
|
|
195
|
+
// `window.Plotly` unset because nothing calls the anonymous module back. A
|
|
196
|
+
// `<script>` tag therefore has to change what that question answers, and a
|
|
197
|
+
// global is the only place to change it. That is what this function used to
|
|
198
|
+
// do: it hid `define.amd` for the length of the fetch (see
|
|
199
|
+
// `loadThroughMaskedDefine` below for the two ways a cruder version of that
|
|
200
|
+
// breaks).
|
|
201
|
+
//
|
|
202
|
+
// MASKING IS STILL WRONG, AND MONACO IS THE PROOF. The window lasts as long
|
|
203
|
+
// as a 4.4 MB download, and any OTHER script that executes inside it asks
|
|
204
|
+
// the same question and gets the answer meant for Plotly. Monaco's
|
|
205
|
+
// `editor.main.js` — 3.7 MB, fetched on demand, so it lands wherever it
|
|
206
|
+
// lands — asks it twice:
|
|
207
|
+
//
|
|
208
|
+
// typeof define=="function"&&define.amd ? define(ne[447], …) // marked
|
|
209
|
+
// … || typeof define=="function"&&define.amd) && (globalThis.monaco = m)
|
|
210
|
+
//
|
|
211
|
+
// The first is the copy of `marked` bundled INSIDE `editor.main.js`. Told
|
|
212
|
+
// there is no AMD, it registers itself as `globalThis.marked` instead of as
|
|
213
|
+
// the module `vs/base/common/marked/marked` — so Monaco's loader goes
|
|
214
|
+
// looking for that module as a FILE, which the `min` distribution does not
|
|
215
|
+
// ship (it is bundled, precisely here). The 404 fails the whole
|
|
216
|
+
// `vs/editor/editor.main` require:
|
|
217
|
+
//
|
|
218
|
+
// Loading "vs/base/common/marked/marked" failed
|
|
219
|
+
// Here are the modules that depend on it: vs/base/browser/markdownRenderer
|
|
220
|
+
//
|
|
221
|
+
// The second means `window.monaco` is never set even if the first is
|
|
222
|
+
// survived. Monaco's `monacoReady` has no reject path, so neither shows up
|
|
223
|
+
// as a failure: the editor simply never arrives, for the life of the page,
|
|
224
|
+
// and every consumer sits in its textarea fallback. Measured on the running
|
|
225
|
+
// deployment on 2026-09-04: 5 boots in 8 lost Monaco this way, 0 in 8 with
|
|
226
|
+
// the Plotly warm-up removed.
|
|
227
|
+
//
|
|
228
|
+
// ══ SO THE GLOBALS ARE NOT TOUCHED AT ALL ══════════════════════════════
|
|
229
|
+
//
|
|
230
|
+
// Fetch the source, then evaluate it in a function whose PARAMETERS are
|
|
231
|
+
// named `define`, `module` and `exports`. Inside that scope `typeof define`
|
|
232
|
+
// is "undefined" — the answer Plotly needs — while `window.define` is never
|
|
233
|
+
// read, written or deleted, so no other script can observe anything. The
|
|
234
|
+
// window is not a window at all: it is one synchronous evaluation, and
|
|
235
|
+
// nothing else can run inside it by construction.
|
|
236
|
+
//
|
|
237
|
+
// The masked-tag path stays as the FALLBACK for a source this page cannot
|
|
238
|
+
// fetch — a CDN without CORS is the case that matters, and it is exactly the
|
|
239
|
+
// case where a `<script>` tag still works.
|
|
240
|
+
_plotlyLoadPromise = (async () => {
|
|
241
|
+
let code;
|
|
242
|
+
try {
|
|
243
|
+
const response = await fetch(_plotlySrc, { credentials: 'same-origin' });
|
|
244
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
245
|
+
code = await response.text();
|
|
246
|
+
} catch (err) {
|
|
247
|
+
// The FETCH failed, which a tag may still survive. An evaluation
|
|
248
|
+
// failure is not caught here: it would fail identically through a
|
|
249
|
+
// tag, and swallowing it would replace a real message with a
|
|
250
|
+
// misleading one.
|
|
251
|
+
return loadThroughMaskedDefine();
|
|
252
|
+
}
|
|
253
|
+
return evaluateWithoutAmd(code, _plotlySrc);
|
|
254
|
+
})();
|
|
255
|
+
|
|
256
|
+
return _plotlyLoadPromise;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Run Plotly's bundle with the three UMD globals shadowed as local names.
|
|
261
|
+
*
|
|
262
|
+
* `new Function` is not strict, so `this` is the global object inside the body —
|
|
263
|
+
* which is what Plotly's header is invoked with (`}(self, …)` in 2.x, `this` in
|
|
264
|
+
* older builds), and is why the browser-global branch still lands `Plotly` on
|
|
265
|
+
* the window rather than on a scope that disappears.
|
|
266
|
+
*
|
|
267
|
+
* @param {string} code the bundle's source
|
|
268
|
+
* @param {string} src where it came from, for the debugger's file list
|
|
269
|
+
* @returns {object} the `Plotly` global
|
|
270
|
+
*/
|
|
271
|
+
function evaluateWithoutAmd(code, src) {
|
|
272
|
+
// eslint-disable-next-line no-new-func
|
|
273
|
+
const run = new Function('define', 'module', 'exports',
|
|
274
|
+
`${code}\n//# sourceURL=${src}`);
|
|
275
|
+
run(undefined, undefined, undefined);
|
|
276
|
+
if (typeof Plotly === 'undefined') {
|
|
277
|
+
throw new Error(`Plotly.js was evaluated from ${src} but did not define a `
|
|
278
|
+
+ 'Plotly global — the file may not be a Plotly UMD bundle.');
|
|
279
|
+
}
|
|
280
|
+
return Plotly;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The `<script>` tag path, for a source this page cannot fetch.
|
|
285
|
+
*
|
|
286
|
+
* ══ WHY `define` IS MASKED AND NOT REMOVED ═════════════════════════════════
|
|
287
|
+
*
|
|
288
|
+
* Hiding `window.define` outright breaks the page two ways, both observed on a
|
|
289
|
+
* running deployment on 2026-08-31:
|
|
290
|
+
*
|
|
291
|
+
* 1. `window.define = undefined`, restored on load. Monaco's `loader.js`
|
|
292
|
+
* installs an AMD loader DURING the window, Plotly's header then sees
|
|
293
|
+
* `typeof define === 'function' && define.amd`, registers as an anonymous
|
|
294
|
+
* module nothing calls back, and the load "succeeds" with `window.Plotly`
|
|
295
|
+
* still undefined:
|
|
296
|
+
* This chart could not be drawn: Plotly.js loaded but Plotly global
|
|
297
|
+
* not found
|
|
298
|
+
* — true, unhelpful, and it sends the reader hunting for a missing file
|
|
299
|
+
* that is being served correctly.
|
|
300
|
+
*
|
|
301
|
+
* 2. The mirror. Monaco loads in TWO stages: `loader.js` sets `define`, and
|
|
302
|
+
* `editor.main.js` arrives later and calls it. If stage two lands while
|
|
303
|
+
* `define` is hidden:
|
|
304
|
+
* editor.main.js:5 Uncaught TypeError: globalDefine is not a function
|
|
305
|
+
* One race costs the chart; the other costs the editor.
|
|
306
|
+
*
|
|
307
|
+
* Masking the PROPERTY rather than the function fixes both — `define` stays
|
|
308
|
+
* callable, `amd` is withheld — and it is still not safe for anybody else who
|
|
309
|
+
* asks the `define.amd` question inside the window. `ensurePlotly` says what
|
|
310
|
+
* that costs; this path is taken only when there is no alternative.
|
|
311
|
+
*
|
|
312
|
+
* @returns {Promise<object>} the `Plotly` global
|
|
313
|
+
*/
|
|
314
|
+
function loadThroughMaskedDefine() {
|
|
315
|
+
return new Promise((resolve, reject) => {
|
|
316
|
+
const hadDefine = 'define' in window;
|
|
190
317
|
const savedDefine = window.define;
|
|
191
|
-
|
|
318
|
+
let arrivedDefine; // what somebody installed mid-window
|
|
319
|
+
let arrived = false;
|
|
320
|
+
let guarded = false;
|
|
321
|
+
|
|
322
|
+
/** The same function with `amd` withheld. Plotly reads `define.amd` and
|
|
323
|
+
* stops; everyone else calls it and it behaves. */
|
|
324
|
+
const maskAmd = (value) => {
|
|
325
|
+
if (typeof value !== 'function') return undefined;
|
|
326
|
+
const masked = function define(...args) { return value.apply(this, args); };
|
|
327
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
328
|
+
if (key === 'amd' || key === 'length' || key === 'name') continue;
|
|
329
|
+
try { masked[key] = value[key]; } catch { /* getters that throw */ }
|
|
330
|
+
}
|
|
331
|
+
masked.amd = undefined;
|
|
332
|
+
return masked;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
Object.defineProperty(window, 'define', {
|
|
337
|
+
configurable: true,
|
|
338
|
+
enumerable: true,
|
|
339
|
+
get: () => maskAmd(arrived ? arrivedDefine : savedDefine),
|
|
340
|
+
set: (value) => { arrivedDefine = value; arrived = true; },
|
|
341
|
+
});
|
|
342
|
+
guarded = true;
|
|
343
|
+
} catch {
|
|
344
|
+
// A host that refuses the redefinition still gets the old
|
|
345
|
+
// behaviour, which is right far more often than it is wrong.
|
|
346
|
+
window.define = undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Put the REAL `define` back, exactly once, preferring what arrived. */
|
|
350
|
+
const release = () => {
|
|
351
|
+
const value = arrived ? arrivedDefine : savedDefine;
|
|
352
|
+
if (guarded) {
|
|
353
|
+
delete window.define;
|
|
354
|
+
guarded = false;
|
|
355
|
+
}
|
|
356
|
+
// `hadDefine` matters: assigning `undefined` leaves an own property
|
|
357
|
+
// whose value is undefined, and `'define' in window` then answers
|
|
358
|
+
// true for a global that was never there. Some loaders test that.
|
|
359
|
+
if (arrived || hadDefine) window.define = value;
|
|
360
|
+
};
|
|
192
361
|
|
|
193
362
|
const script = document.createElement('script');
|
|
194
363
|
script.src = _plotlySrc;
|
|
195
364
|
script.async = true;
|
|
196
365
|
|
|
197
366
|
script.onload = () => {
|
|
198
|
-
|
|
367
|
+
release();
|
|
199
368
|
if (typeof Plotly !== 'undefined') {
|
|
200
|
-
console.log('[PlotlyWrapper] Plotly.js loaded successfully');
|
|
201
369
|
resolve(Plotly);
|
|
202
370
|
} else {
|
|
203
|
-
reject(new Error('Plotly.js loaded but Plotly global not found'
|
|
371
|
+
reject(new Error('Plotly.js loaded but Plotly global not found. '
|
|
372
|
+
+ 'Something defined an AMD loader while it was loading, so '
|
|
373
|
+
+ 'Plotly registered as a module instead of a global.'));
|
|
204
374
|
}
|
|
205
375
|
};
|
|
206
376
|
|
|
207
377
|
script.onerror = () => {
|
|
208
|
-
|
|
378
|
+
release();
|
|
209
379
|
reject(new Error(`Failed to load Plotly.js from ${_plotlySrc} — `
|
|
210
380
|
+ 'call setPlotlySource() with the path to your copy.'));
|
|
211
381
|
};
|
|
212
382
|
|
|
213
383
|
document.head.appendChild(script);
|
|
214
384
|
});
|
|
215
|
-
|
|
216
|
-
return _plotlyLoadPromise;
|
|
217
385
|
}
|
|
218
386
|
|
|
219
387
|
/**
|
|
@@ -71,9 +71,25 @@ export class NotebookTabBar {
|
|
|
71
71
|
const title = e.target.closest('.tab')?.querySelector('.twm-tab-title');
|
|
72
72
|
return title?.getAttribute('contenteditable') !== 'true';
|
|
73
73
|
},
|
|
74
|
-
onDragStart: (e, key) => {
|
|
74
|
+
onDragStart: (e, key, item) => {
|
|
75
75
|
// Carry the cross-pane MIME so other panes accept this tab.
|
|
76
76
|
try { e.dataTransfer.setData(TAB_MIME, key); } catch (_) {}
|
|
77
|
+
// C33. …AND LET THE HOST ADD ITS OWN, which is the whole of
|
|
78
|
+
// what a second consumer of this strip needs from it.
|
|
79
|
+
//
|
|
80
|
+
// `DragReorder` publishes this hook for exactly this purpose
|
|
81
|
+
// ("host hook to augment dataTransfer (e.g. add a cross-pane
|
|
82
|
+
// MIME)", `../ui/components/drag_reorder.js`), and this class
|
|
83
|
+
// consumed it and stopped there. The tile renderer mounts one
|
|
84
|
+
// of these per tile and needs to mark the drag as one of ITS
|
|
85
|
+
// tabs — a different question from "is this an editor tab",
|
|
86
|
+
// asked of the same gesture.
|
|
87
|
+
//
|
|
88
|
+
// ADDITIVE BY CONSTRUCTION: `TAB_MIME` is set first and
|
|
89
|
+
// unchanged, so `EditorPane`'s existing cross-pane drop cannot
|
|
90
|
+
// notice that anyone else is listening.
|
|
91
|
+
try { this.#callbacks.onDragStart?.(e, key, item); }
|
|
92
|
+
catch (err) { console.error('[tab-bar] onDragStart threw', err); }
|
|
77
93
|
},
|
|
78
94
|
onReorder: (order) => this.#callbacks.onReorder?.(order),
|
|
79
95
|
});
|
|
@@ -226,10 +242,30 @@ export class NotebookTabBar {
|
|
|
226
242
|
this.#container.removeEventListener('dragleave', this.#onStripDragLeave);
|
|
227
243
|
}
|
|
228
244
|
|
|
229
|
-
/** Check whether a drag event carries a cross-pane tab (not from this bar).
|
|
245
|
+
/** Check whether a drag event carries a cross-pane tab (not from this bar).
|
|
246
|
+
*
|
|
247
|
+
* C33. A HOST MAY NAME A SECOND MIME, at mount, as
|
|
248
|
+
* `callbacks.externalTabMimes` — the tile renderer's tabs are not editor
|
|
249
|
+
* tabs and do not carry `application/x-ecosim-tab`, so without this the
|
|
250
|
+
* strip in tile B would refuse a tab dragged out of tile A's strip. Empty
|
|
251
|
+
* by default, so every existing consumer keeps exactly today's admission
|
|
252
|
+
* test.
|
|
253
|
+
*
|
|
254
|
+
* THE `isActive()` EARLY RETURN STAYS FIRST AND STAYS UNCHANGED. It is the
|
|
255
|
+
* one line that keeps this bar's own reorder from being read as a foreign
|
|
256
|
+
* drop, and no widening of the MIME list may be allowed to reach past it.
|
|
257
|
+
*
|
|
258
|
+
* Only `types` is consulted, never `getData` — under the HTML5 protected
|
|
259
|
+
* mode `getData` returns the empty string for every event except `drop`,
|
|
260
|
+
* so a payload read here would find nothing and arm nothing. */
|
|
230
261
|
#isExternalTabDrag(e) {
|
|
231
262
|
if (this.#dragReorder?.isActive()) return false; // same-pane drag is active
|
|
232
|
-
|
|
263
|
+
const extra = Array.isArray(this.#callbacks.externalTabMimes)
|
|
264
|
+
? this.#callbacks.externalTabMimes : [];
|
|
265
|
+
try {
|
|
266
|
+
const types = e.dataTransfer.types;
|
|
267
|
+
return types.includes(TAB_MIME) || extra.some((m) => types.includes(m));
|
|
268
|
+
} catch { return false; }
|
|
233
269
|
}
|
|
234
270
|
|
|
235
271
|
// Same-pane reorder is handled by the shared DragReorder module (set
|