flexdesk 0.17.3 → 0.17.5
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/CHANGELOG.md +104 -65
- package/README.md +27 -4544
- package/css/base.css +18 -1
- package/css/flexdesk.css +18 -1
- package/dist/charts.js +20 -3
- package/dist/charts.js.map +2 -2
- package/dist/{chunk-4A7SJC3T.js → chunk-RKNFBX5K.js} +15 -4
- package/dist/{chunk-4A7SJC3T.js.map → chunk-RKNFBX5K.js.map} +2 -2
- package/dist/flexdesk.css +18 -1
- package/dist/tiles.js +1 -1
- package/dist/widgets.js +14 -2
- package/dist/widgets.js.map +2 -2
- package/docs/README.md +97 -0
- package/docs/best-practices.md +45 -0
- package/docs/components/charts.md +235 -0
- package/docs/components/command-palette.md +102 -0
- package/docs/components/conversation.md +72 -0
- package/docs/components/data-table-grid.md +328 -0
- package/docs/components/data-table.md +596 -0
- package/docs/components/editor.md +22 -0
- package/docs/components/flow-canvas-editor.md +482 -0
- package/docs/components/flow-editors.md +176 -0
- package/docs/components/flow-lane-editor.md +165 -0
- package/docs/components/flow-outline-editor.md +176 -0
- package/docs/components/forms-and-dialogs.md +294 -0
- package/docs/components/input-fields.md +72 -0
- package/docs/components/keymap.md +143 -0
- package/docs/components/layout-widgets.md +163 -0
- package/docs/components/markdown-and-outline.md +106 -0
- package/docs/components/menus.md +86 -0
- package/docs/components/notifications.md +194 -0
- package/docs/components/panels-and-desktops.md +127 -0
- package/docs/components/record-list-and-fact-table.md +128 -0
- package/docs/components/settings-view.md +112 -0
- package/docs/components/shell.md +189 -0
- package/docs/components/slide-out-panel.md +68 -0
- package/docs/components/tiles.md +210 -0
- package/docs/components/wizard.md +54 -0
- package/docs/concepts/building-and-packaging.md +33 -0
- package/docs/concepts/domain-neutrality.md +110 -0
- package/docs/concepts/host-port.md +137 -0
- package/docs/concepts/packages-and-entry-points.md +32 -0
- package/docs/concepts/peer-dependencies.md +42 -0
- package/docs/concepts/state-guard.md +58 -0
- package/docs/concepts/styling.md +47 -0
- package/docs/quickstart.md +179 -0
- package/package.json +2 -1
- package/src/charts/data_series_plot_window.js +17 -3
- package/src/ui/components/slide_out_panel.js +14 -3
- package/src/ui/utils/auto_scrollbars.js +16 -2
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Dashboard tiles
|
|
2
|
+
|
|
3
|
+
> **Package:** `@flexdesk/tiles` · [All docs](../README.md)
|
|
4
|
+
|
|
5
|
+
A draggable, resizable widget grid: the tile base class, the grid, a widget registry, layout persistence, chart and KPI tiles, list and people tiles, the figure tile every Plotly tile draws through, and a form for a tile's settings.
|
|
6
|
+
|
|
7
|
+
Two different things in FlexDesk are called "tiles". `@flexdesk/wm` tiles the *window* (see [Shell and navigation](shell.md)); `@flexdesk/tiles` is this dashboard *widget grid*.
|
|
8
|
+
|
|
9
|
+
**On this page**
|
|
10
|
+
|
|
11
|
+
- [Layouts are keyed by whatever you key them by](#layouts-are-keyed-by-whatever-you-key-them-by)
|
|
12
|
+
- [Chart and KPI tiles (0.13.0)](#chart-and-kpi-tiles-0130)
|
|
13
|
+
- [Figure tiles: one override, the draw is the base's (0.17.0)](#figure-tiles-one-override-the-draw-is-the-bases-0170)
|
|
14
|
+
- [List and people tiles, and one scope for the board (0.16.1)](#list-and-people-tiles-and-one-scope-for-the-board-0161)
|
|
15
|
+
- [A tile's settings as a form: `mountTileConfigForm` (0.17.0)](#a-tiles-settings-as-a-form-mounttileconfigform-0170)
|
|
16
|
+
|
|
17
|
+
## Layouts are keyed by whatever you key them by
|
|
18
|
+
|
|
19
|
+
`@flexdesk/tiles` stores layouts under keys **you** choose and resolves a starting
|
|
20
|
+
layout through a callback **you** supply:
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
import { configureLayoutPersistence } from '@flexdesk/tiles';
|
|
24
|
+
|
|
25
|
+
configureLayoutPersistence({
|
|
26
|
+
layoutsKey: 'myapp.layouts', // default: 'twm.tiles.layouts'
|
|
27
|
+
resolveTemplate: (ctx) => pickTemplate(ctx) ?? null, // null => empty grid
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Configure it before anything reads it.** The store is a singleton; built before
|
|
32
|
+
you supply your key, it is built with the default, and every layout your users have
|
|
33
|
+
saved becomes silently invisible. That is a data-loss bug that raises no error, so
|
|
34
|
+
the configuration step is explicit rather than lazy.
|
|
35
|
+
|
|
36
|
+
A layout is keyed by *something* — a document, a user, a run — and the library has
|
|
37
|
+
no opinion about which. It calls that a `contextId`.
|
|
38
|
+
|
|
39
|
+
## Chart and KPI tiles (0.13.0)
|
|
40
|
+
|
|
41
|
+
`@flexdesk/tiles` ships two widgets: `flexdesk.chart` (line, bar, area, scatter,
|
|
42
|
+
pie, donut, funnel, histogram — Plotly, so `setPlotlySource` first) and
|
|
43
|
+
`flexdesk.kpi` (one number, a label and an optional delta; plain DOM, no Plotly).
|
|
44
|
+
Nothing registers at import — call `registerChartTiles()` (idempotent). You hand
|
|
45
|
+
a tile **already-computed** series; turning records into series is yours:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import { registerChartTiles } from '@flexdesk/tiles';
|
|
49
|
+
registerChartTiles();
|
|
50
|
+
const chart = grid.addTile('flexdesk.chart', { x: 0, y: 0, w: 6, h: 4 },
|
|
51
|
+
{ chart_type: 'line', title: 'Burndown', unit: 'pts' });
|
|
52
|
+
chart.onPointClick = ({ customdata }) => openItem(customdata);
|
|
53
|
+
chart.setData({
|
|
54
|
+
series: [
|
|
55
|
+
{ x: days, y: remaining, name: 'Remaining', interpolation: 'step', customdata: ids },
|
|
56
|
+
{ x: days, y: ideal, name: 'Ideal', lineStyle: 'dashed' },
|
|
57
|
+
],
|
|
58
|
+
markers: [{ x: today, label: 'Today' }], // vertical lines
|
|
59
|
+
note: 'From 41 stories', // a line under the figure
|
|
60
|
+
});
|
|
61
|
+
grid.addTile('flexdesk.kpi', { x: 6, y: 0, w: 3, h: 2 }, { title: 'Open', unit: 'bugs' })
|
|
62
|
+
.setData({ value: 42, reference: 37 }); // ▲ +5 bugs
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`setData(null)` or an empty series is the empty state, and it releases any figure
|
|
66
|
+
still drawing. A pie takes `{labels, values}`, a histogram `{values}`. The config
|
|
67
|
+
(`chart_type`, `title`, `unit`, …) round-trips through `getLayout()`/`setLayout()`.
|
|
68
|
+
`buildOverlayShapes` gained `verticalLines: [{x, label?, color?, lineStyle?}]` for
|
|
69
|
+
the same marker outside a tile.
|
|
70
|
+
|
|
71
|
+
A chart tile's **Download PNG** (0.14.1) goes through the grid's `host`
|
|
72
|
+
(`new TileGrid({ host })`): with `dialogs`, the native Save dialog — a save
|
|
73
|
+
toasts where the file went, a cancel says nothing — and with no host, or a
|
|
74
|
+
`saveFile` that answers null, the browser download it always was.
|
|
75
|
+
|
|
76
|
+
## Figure tiles: one override, the draw is the base's (0.17.0)
|
|
77
|
+
|
|
78
|
+
`FigureTile` is the base every Plotly tile draws through — `ChartTile` is one.
|
|
79
|
+
A subclass overrides **one** method, `figure(data)`, answering
|
|
80
|
+
`{traces, layout?, config?, frames?}` or `null`, synchronously:
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
import { FigureTile, registerWidget } from '@flexdesk/tiles';
|
|
84
|
+
import { buildHistogramTrace } from '@flexdesk/charts';
|
|
85
|
+
|
|
86
|
+
class SpreadTile extends FigureTile {
|
|
87
|
+
static TYPE = 'myapp.spread';
|
|
88
|
+
static TITLE = 'Spread';
|
|
89
|
+
figure(data) {
|
|
90
|
+
if (!data?.values?.length) return null; // the empty state
|
|
91
|
+
return { traces: [buildHistogramTrace({ x: data.values })], layout: { margin: { t: 8 } } };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
registerWidget(SpreadTile);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The base awaits `ensurePlotly()` (so a page using `setPlotlySource` draws on
|
|
98
|
+
first view, and data that changes while Plotly loads draws once), draws through
|
|
99
|
+
`createChart` with `scrollZoom` and the mode bar off (`config` goes over that),
|
|
100
|
+
adds `frames`, and discards a draw that lands after a newer one. `null` is the
|
|
101
|
+
empty state with `static EMPTY_MESSAGE` — call `this.showEmpty(why)` yourself
|
|
102
|
+
first to say why. A `figure()` that throws, or a draw that fails, is the error
|
|
103
|
+
state plus `tile:render-failed {tileId, message}`. Loading, empty, error and
|
|
104
|
+
`dispose()` purge the figure; a resize that settles relayouts it once. The ⋯
|
|
105
|
+
menu is `TileBase`'s entries plus **Download PNG** (through `host.dialogs`, as
|
|
106
|
+
above); extend it with `[...super.menuItems(), …]` and route your actions
|
|
107
|
+
through `_onMenuAction`, calling `super` for the rest. **Expand** opens
|
|
108
|
+
`openRawTracesWindow` with the last figure drawn (`getExpandData()`), and
|
|
109
|
+
`onPointClick({customdata, point, tile})` — an option or a property — hears a
|
|
110
|
+
click on a mark. `setData(data)` feeds one tile; `null` clears it.
|
|
111
|
+
|
|
112
|
+
`ChartTile` is now a `FigureTile`: its menu stays Download PNG alone, and its
|
|
113
|
+
header gains the Expand button.
|
|
114
|
+
|
|
115
|
+
## List and people tiles, and one scope for the board (0.16.1)
|
|
116
|
+
|
|
117
|
+
Two more widgets, registered by `registerListTiles()` (idempotent, nothing at
|
|
118
|
+
import). Like the chart tile they take **rows you have already worked out**;
|
|
119
|
+
what they own is what every list on a board needs: the count ("7 of 12"), the
|
|
120
|
+
line that says what empty means, a button in the bar, a click, the keyboard and
|
|
121
|
+
a right-click menu per row.
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
import { registerListTiles, createScopePicker } from '@flexdesk/tiles';
|
|
125
|
+
registerListTiles();
|
|
126
|
+
const late = grid.addTile('flexdesk.list', { x: 0, y: 0, w: 12, h: 4 },
|
|
127
|
+
{ title: 'Overdue', empty_text: 'Nothing is late.', max_rows: 0 });
|
|
128
|
+
late.onRowActivate = ({ row, event }) => open(row.key, { modified: event.ctrlKey });
|
|
129
|
+
late.rowMenu = ({ row }) => [{ label: 'Open', icon: 'open_in_new', action: 'open' }];
|
|
130
|
+
late.onRowMenuAction = ({ action, row }) => { /* … */ };
|
|
131
|
+
late.onAction = () => openAsList();
|
|
132
|
+
late.setData({
|
|
133
|
+
rows: [{ key: '7', icon: 'task', lead: 'TASK-0007', title: 'Sign-off',
|
|
134
|
+
cells: [{ text: 'priya' }, { text: '9 days late', tone: 'danger' }] }],
|
|
135
|
+
action: { label: 'Open as a list', icon: 'list' },
|
|
136
|
+
});
|
|
137
|
+
grid.addTile('flexdesk.people', { x: 0, y: 4, w: 6, h: 4 }, { title: 'Who has what' })
|
|
138
|
+
.setData({ people: [{ key: '', name: 'Nobody', muted: true, count: 2,
|
|
139
|
+
pills: [{ text: '2 undated', tone: 'muted' }] }] });
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- A row is `role="button"`: click, Enter and Space call `onRowActivate` with the
|
|
143
|
+
event; Up/Down/Home/End move. Right-click, the context-menu key and Shift+F10
|
|
144
|
+
open the row's menu (`showContextMenu`'s items; none means no menu). A row
|
|
145
|
+
carries `data-row-key`; `rowsDraggable` makes rows draggable.
|
|
146
|
+
- `{rows: []}` keeps the bar and says `empty` (or `config.empty_text`) where the
|
|
147
|
+
rows would be; `setData(null)` is the empty state. Text is always text.
|
|
148
|
+
- `tone` (`danger`, `warning`, `ok`, `muted`) picks a colour only; `className`
|
|
149
|
+
on a row or a cell is yours, added as it is.
|
|
150
|
+
- `groupByPerson(items, personOf)` groups for a people tile, most first.
|
|
151
|
+
|
|
152
|
+
**One scope for the whole board.** `createScopePicker({options, value, onChange})`
|
|
153
|
+
is a row of buttons, one pressed. `grid.setScope(value)` (or
|
|
154
|
+
`new TileGrid({ scope, onScopeChange })`) hands the value to every tile as
|
|
155
|
+
`tile.scope` and `tile.onScopeChange(scope)`, a tile added later starts with
|
|
156
|
+
it, a grid with a data source reloads its tiles, and the grid's `onScopeChange`
|
|
157
|
+
is told. What a scope means is yours; it is not part of the layout.
|
|
158
|
+
|
|
159
|
+
## A tile's settings as a form: `mountTileConfigForm` (0.17.0)
|
|
160
|
+
|
|
161
|
+
`getConfigSchema()` answers `{fields}`; `mountTileConfigForm` draws them, so a
|
|
162
|
+
config panel is no longer a control written by hand per field:
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
import { mountTileConfigForm, openTileConfig } from '@flexdesk/tiles';
|
|
166
|
+
const form = mountTileConfigForm(host, {
|
|
167
|
+
schema: tile.getConfigSchema(),
|
|
168
|
+
config: tile.config, // defaults filled by mergeWithDefaults
|
|
169
|
+
data, // what getFieldOptions reads for dynamic options
|
|
170
|
+
onChange: (key, value, config) => tile.update(undefined, config),
|
|
171
|
+
renderers: { variable: ({ field, value, data, readonly, commit }) => myPicker(...) },
|
|
172
|
+
});
|
|
173
|
+
form.getValues(); form.setValues(next); form.isValid(); form.focusFirstInvalid();
|
|
174
|
+
form.flush(); form.dispose();
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- A field is `{key, type, label?, description?, default?, required?, readonly?,
|
|
178
|
+
min?, max?, step?, options?, visibleWhen?}`. Without a `label` the field is
|
|
179
|
+
called by its `key`, on its label and in its error messages. Every `FieldTypes` entry has a
|
|
180
|
+
control (text, number, checkbox, select, multiselect, color, range); any other
|
|
181
|
+
`type` needs a renderer, which answers an element or
|
|
182
|
+
`{element, setValue?, dispose?}` and calls `commit(value)` on an edit.
|
|
183
|
+
- An edit is checked by `validateField`. An invalid one shows its error under the
|
|
184
|
+
field and is **not applied**; a valid one calls `onChange` with the field's key.
|
|
185
|
+
`isValid()` is false while a shown row has an error; `focusFirstInvalid()`
|
|
186
|
+
focuses the first such row's control (a renderer's element, or the first
|
|
187
|
+
focusable element inside it) and answers whether there was one.
|
|
188
|
+
- `commitOn: 'input'` (the default) checks text, number, range and colour
|
|
189
|
+
controls on every keystroke; `commitOn: 'change'` checks them once the edit is
|
|
190
|
+
finished (blur, Enter, a released slider), for an `onChange` that is expensive.
|
|
191
|
+
`flush()` commits an edit typed but not yet finished.
|
|
192
|
+
- `visibleWhen: {field, in: [...]}` (or `{field, equals}`) hides the row, never
|
|
193
|
+
rebuilds the form, so the control being typed into keeps its focus.
|
|
194
|
+
- A field with no `key` throws. **The built-in tiles' schemas now use `key` and
|
|
195
|
+
`description`** (they said `name` and `help`, which `validateConfig` and
|
|
196
|
+
`mergeWithDefaults` never read: BUG-0021). A consumer that read `field.name`
|
|
197
|
+
or `field.help` from them reads `field.key` and `field.description`.
|
|
198
|
+
|
|
199
|
+
The grid's cogwheel emits `grid:config-modal-show`, which now also carries the
|
|
200
|
+
grid's `data`. Nothing listens unless you ask:
|
|
201
|
+
`openTileConfig(eventBus, {mode: 'slide-out', host})` shows the form in a
|
|
202
|
+
`SlideOutPanel` over `host` and saves every finished, valid edit (it runs the
|
|
203
|
+
form with `commitOn: 'change'`, so typing a title is one save, not one per
|
|
204
|
+
keystroke; closing the panel saves an edit still being typed);
|
|
205
|
+
`{mode: 'modal'}` (the default) saves on Save, and refuses Save while a field
|
|
206
|
+
shows an error (the modal stays open and the first invalid field is focused). A request that arrives while a
|
|
207
|
+
modal is open is ignored. It answers a function that unsubscribes and closes the
|
|
208
|
+
slide-out; an open modal is left to the user, keeps its form and still saves on
|
|
209
|
+
Save. A renderer's `commit` on a `readonly` field applies nothing and answers
|
|
210
|
+
`{valid: false, error}`.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Wizard
|
|
2
|
+
|
|
3
|
+
> **Package:** `@flexdesk/widgets` · [All docs](../README.md)
|
|
4
|
+
|
|
5
|
+
A multi-step flow: a step indicator, a body slot and a footer with Cancel, Back and Next (Finish on the last step). Mount it inline in your page, or open it in a modal `ManagedWindow`.
|
|
6
|
+
|
|
7
|
+
## A step wizard: `Wizard` (0.17.0)
|
|
8
|
+
|
|
9
|
+
`new Wizard({ steps, data, onFinish, onCancel, labels })` (in `@flexdesk/widgets`)
|
|
10
|
+
draws a multi-step flow: a step indicator, a body slot and a footer with Cancel,
|
|
11
|
+
Back and Next (Finish on the last step). Mount it inline with `mount(el)`, or in a
|
|
12
|
+
modal `ManagedWindow` with `openInWindow({ id, title, ... })`.
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
const wizard = new Wizard({
|
|
16
|
+
data: { file: null },
|
|
17
|
+
steps: [
|
|
18
|
+
{ id: 'source', label: 'Source', icon: 'upload_file',
|
|
19
|
+
render(bodyEl, ctx) { // once per visit; may return a cleanup function
|
|
20
|
+
const input = picker(bodyEl);
|
|
21
|
+
input.onchange = () => { ctx.data.file = input.files[0]; ctx.revalidate(); };
|
|
22
|
+
},
|
|
23
|
+
canAdvance: (ctx) => ctx.data.file
|
|
24
|
+
? { ok: true } : { ok: false, hint: 'Choose a file first' } },
|
|
25
|
+
{ id: 'review', label: 'Review', render: (bodyEl, ctx) => review(bodyEl, ctx.data) },
|
|
26
|
+
],
|
|
27
|
+
onFinish: async (ctx) => api.import(ctx.data), // a rejection marks the step as error
|
|
28
|
+
onCancel: () => {},
|
|
29
|
+
});
|
|
30
|
+
wizard.openInWindow({ id: 'import', title: 'Import' });
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- **Gating.** While the current step's `canAdvance(ctx)` answers `{ok: false, hint}`,
|
|
34
|
+
Next is disabled and the hint is shown in the footer. `revalidate()` (also
|
|
35
|
+
`ctx.revalidate()`) asks again and redraws the indicator and the footer; it never
|
|
36
|
+
re-renders the body, so an input keeps its focus and its value.
|
|
37
|
+
- **Steps** are drawn done / current / upcoming / error, joined by a connector. A
|
|
38
|
+
done step is a button that jumps back to it; an upcoming step is inert, so the
|
|
39
|
+
only way forward is Next.
|
|
40
|
+
- **Moves.** `next()`, `back()` and `goTo(idOrIndex)` (back to any earlier step, or
|
|
41
|
+
forward one step through the gate) resolve `true` when they moved.
|
|
42
|
+
`setStepState(id, 'error' | null, message)` marks or clears a step.
|
|
43
|
+
- **Finish.** On the last step Next reads Finish and calls `onFinish(ctx)`. Until
|
|
44
|
+
it settles every button is disabled and moves, cancels and closes are refused,
|
|
45
|
+
not queued. Resolved, the wizard closes; rejected, the step is marked as error
|
|
46
|
+
and the rejection's message is shown, and Finish can be pressed again.
|
|
47
|
+
- **Cancel** (the button, `cancel()`, or the window's close button and Escape)
|
|
48
|
+
closes the wizard and calls `onCancel(ctx)` exactly once. `dispose()` tears it
|
|
49
|
+
down without calling either callback.
|
|
50
|
+
|
|
51
|
+
Every transition goes through a core `StateMachine` (`step`, `finishing`,
|
|
52
|
+
`finished`, `cancelled`; pass `eventBus` to see its `fsm:*` events). `ctx` is
|
|
53
|
+
`{data, step, index, revalidate, next, back, goTo, setStepState}`, with `data`
|
|
54
|
+
the object you passed.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Build and packaging
|
|
2
|
+
|
|
3
|
+
> **Files:** `build.mjs`, `dist/`, the `exports` map in `package.json` · [All docs](../README.md)
|
|
4
|
+
|
|
5
|
+
## Build
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run build # -> dist/
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`dist/` is committed. The packaging decision is *prebuilt dist*: this repo owns the
|
|
13
|
+
build so that a consumer with no bundler can load the library through a plain import
|
|
14
|
+
map. See `.gitignore` — it says so there too, because it is the kind of thing someone
|
|
15
|
+
"tidies up" later.
|
|
16
|
+
|
|
17
|
+
### The `development` condition resolves in the installed package (0.17.1)
|
|
18
|
+
|
|
19
|
+
Every JavaScript entry in the `exports` map has two conditions: `import`, the
|
|
20
|
+
prebuilt bundle under `dist/`, and `development`, the source barrel at the
|
|
21
|
+
package root (`core.js`, `host.js`, `wm.js`, `widgets.js`, `charts.js`,
|
|
22
|
+
`tiles.js`, `editor.js`, `canvas.js`, `flow.js`), which re-exports `src/`.
|
|
23
|
+
Up to 0.17.0 those nine barrels were missing from `files`, so the published
|
|
24
|
+
package did not contain them and a resolver that took the `development` branch
|
|
25
|
+
(`require.resolve` under `--conditions=development`, for one) got a path to
|
|
26
|
+
nothing. From 0.17.1 they ship, beside the `src/` they import.
|
|
27
|
+
|
|
28
|
+
Nothing about which file an `import` resolves to has changed: `import` is
|
|
29
|
+
listed first, so an ES import still gets `dist/`, with or without the
|
|
30
|
+
`development` condition.
|
|
31
|
+
|
|
32
|
+
A suite (`tests/exports_packed.test.mjs`) runs `npm pack --dry-run` and fails
|
|
33
|
+
if any target of any condition in the `exports` map is not in the tarball.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Domain neutrality: your names, keys and globals
|
|
2
|
+
|
|
3
|
+
> **Packages:** `@flexdesk/core`, `@flexdesk/widgets`, `@flexdesk/editor`, `@flexdesk/charts`, `@flexdesk/tiles` · [All docs](../README.md)
|
|
4
|
+
|
|
5
|
+
FlexDesk was extracted from [EcoAgent](https://github.com/nor-os/EcoAgent), a macroeconomic simulation platform, and it knows nothing about economics: no application vocabulary, and no import of anything application-specific, in any shipped file. For you that means everything with a name is yours to choose. Storage keys, theme names, drag MIME types and export file names are options with generic defaults, and nothing reads an application's globals. The two sections here list what you can set and where.
|
|
6
|
+
|
|
7
|
+
## Storage keys and names are yours (0.17.0)
|
|
8
|
+
|
|
9
|
+
Every localStorage key FlexDesk writes is an option, and so is each name in the
|
|
10
|
+
table below that it registers where other code can see it, except the last row:
|
|
11
|
+
the tile tab MIME is fixed. The defaults are FlexDesk's own; pass yours at boot,
|
|
12
|
+
before the first read:
|
|
13
|
+
|
|
14
|
+
| What | Default | Set it with |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| Settings (the `getSetting`/`setSetting` binding) | `'flexdesk.settings.v1'` | `configureSettings({ storageKey })` from `@flexdesk/core` |
|
|
17
|
+
| ManagedWindow geometry | `'flexdesk.managedWindows.v1'` | `ManagedWindow.configure({ storageKey })` from `@flexdesk/widgets` |
|
|
18
|
+
| Saved tile layouts | `'twm.tiles.layouts'` | `configureLayoutPersistence({ layoutsKey })` from `@flexdesk/tiles` |
|
|
19
|
+
| Saved tile configurations | `'twm.tiles.configs'` | `configureLayoutPersistence({ configsKey })` from `@flexdesk/tiles` |
|
|
20
|
+
| Built-in Monaco themes | `'flexdesk-dark'`, `'flexdesk-light'` | `setEditorThemeNames({ dark, light })` from `@flexdesk/editor` |
|
|
21
|
+
| MIME type of a dragged editor tab | `'application/x-flexdesk-tab'` | `setTabDragMime(mime)` from `@flexdesk/editor` |
|
|
22
|
+
| Chart PNG export file name | `'chart'` (saved as `chart.png`) | `setChartExportFilename(name)` from `@flexdesk/charts` |
|
|
23
|
+
| `LoggingService` service name on each entry | `'flexdesk'` | `new LoggingService({ name })` from `@flexdesk/core` |
|
|
24
|
+
| MIME type of a dragged tile tab | `'application/x-twm-tile-tab'` | fixed: not an option; read it as `TILE_TAB_MIME` (or `TileRenderer.TAB_MIME`) from `@flexdesk/wm` |
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { configureSettings } from '@flexdesk/core';
|
|
28
|
+
import { ManagedWindow } from '@flexdesk/widgets';
|
|
29
|
+
import { setEditorThemeNames, setTabDragMime } from '@flexdesk/editor';
|
|
30
|
+
import { setChartExportFilename } from '@flexdesk/charts';
|
|
31
|
+
|
|
32
|
+
configureSettings({ storageKey: 'myapp.settings.v1' });
|
|
33
|
+
ManagedWindow.configure({ storageKey: 'myapp.windows.v1' });
|
|
34
|
+
setEditorThemeNames({ dark: 'myapp-dark', light: 'myapp-light' });
|
|
35
|
+
setTabDragMime('application/x-myapp-tab');
|
|
36
|
+
setChartExportFilename('myapp_chart');
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Each default is exported beside its setter (`DEFAULT_SETTINGS_STORAGE_KEY`,
|
|
40
|
+
`DEFAULT_WINDOW_STORAGE_KEY`, `DEFAULT_EDITOR_THEME_NAMES`,
|
|
41
|
+
`DEFAULT_TAB_DRAG_MIME`, `DEFAULT_CHART_EXPORT_FILENAME`), and each setter has a
|
|
42
|
+
getter (`getSettingsStorageKey()`, `ManagedWindow.storageKey`,
|
|
43
|
+
`getEditorThemeNames()`, `getTabDragMime()`, `getChartExportFilename()`). An
|
|
44
|
+
embedder that passes nothing gets the defaults silently: nothing is logged.
|
|
45
|
+
|
|
46
|
+
- **Settings** are read lazily: importing `@flexdesk/core` touches no storage,
|
|
47
|
+
and the binding reads its key on the first `getSetting`/`setSetting`/reset.
|
|
48
|
+
`registerSettings()` and `registerSettingsEventBus()` may come before or after
|
|
49
|
+
`configureSettings()`. Configured after the binding was in use, it moves: values
|
|
50
|
+
return to their defaults, are reloaded from the new key, and one
|
|
51
|
+
`settings:changed` (`path: null`) is emitted. The old key is not touched. A
|
|
52
|
+
private store from `createSettingsStore()` moves the same way with
|
|
53
|
+
`store.setStorageKey(key)` (and answers `store.getStorageKey()`).
|
|
54
|
+
- **ManagedWindow** reads geometry when a window is first shown; configure it
|
|
55
|
+
before the first `show()`.
|
|
56
|
+
- **Editor themes**: a factory starts on the dark name current when it is
|
|
57
|
+
created, and `init()` defines the built-in themes under the names current
|
|
58
|
+
then. An app with a theme of its own defines it with `monaco.editor.defineTheme`
|
|
59
|
+
and passes its name to `factory.setTheme(name)`.
|
|
60
|
+
- **The tab MIME** is read at drag time by every `NotebookTabBar` and
|
|
61
|
+
`EditorPane`, so the two always agree.
|
|
62
|
+
|
|
63
|
+
**Upgrading from 0.16.** The old defaults were one application's names:
|
|
64
|
+
`'ecosim.settings.v1'`, `'ecosim.managedWindows.v1'`, `'ecosim-dark'` /
|
|
65
|
+
`'ecosim-light'`, `'application/x-ecosim-tab'`, `'ecosim_chart'` and the log
|
|
66
|
+
service name `'ecosim'`. If your users have settings or window geometry saved
|
|
67
|
+
under the old keys, pass those keys explicitly and nothing is lost:
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
configureSettings({ storageKey: 'ecosim.settings.v1' });
|
|
71
|
+
ManagedWindow.configure({ storageKey: 'ecosim.managedWindows.v1' });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Code that called `factory.setTheme('ecosim-light')` now calls
|
|
75
|
+
`factory.setTheme(getEditorThemeNames().light)` (or names the themes back with
|
|
76
|
+
`setEditorThemeNames`), and code that read `'application/x-ecosim-tab'` off a drag
|
|
77
|
+
reads `getTabDragMime()`.
|
|
78
|
+
|
|
79
|
+
## No application globals: the gallery picker and plot-window events (0.17.0)
|
|
80
|
+
|
|
81
|
+
Nothing under `src/` outside `src/host` reads `window.pywebview` or an
|
|
82
|
+
application's global; `tests/no_app_globals.test.mjs` pins it, with the toast
|
|
83
|
+
helpers ([Notifications](../components/notifications.md#notifications-notificationcenter)) and these two:
|
|
84
|
+
|
|
85
|
+
**`mountGalleryPicker(el, { entries | host, kind, ... })`** takes its entries from
|
|
86
|
+
`entries`, or else from the host's optional `gallery` capability:
|
|
87
|
+
`host.gallery.list(kind)` answers `[{id, kind, label, description?, fields?}]`
|
|
88
|
+
(or a promise of them). Neither shipped adapter advertises it; add your own:
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
const host = createHost({ ...createPywebviewHost({ bridgeRef }),
|
|
92
|
+
gallery: { list: (kind) => myBackend.starters(kind) } });
|
|
93
|
+
await mountGalleryPicker(el, { host, kind: 'report', onChange });
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
With neither `entries` nor the capability, the picker shows its `emptyLabel`
|
|
97
|
+
and logs one warning; a `list` that throws does the same. Until 0.17.0 it
|
|
98
|
+
called one application's bridge method.
|
|
99
|
+
|
|
100
|
+
**`PlotPopoutWindow({ streamEvents })`** names the bus events a live data
|
|
101
|
+
source emits, next to `refreshEvent`: `{ delta, complete }`. The window
|
|
102
|
+
redraws on each while live update is on. There are no default names: without
|
|
103
|
+
`streamEvents`, or for a name left out or given as `null`, nothing is listened
|
|
104
|
+
for, and the window updates through `refreshEvent` and explicit calls only.
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
openPlotPopoutWindow({ id, title, plotConfig, getResults, services,
|
|
108
|
+
refreshEvent: 'run:finished',
|
|
109
|
+
streamEvents: { delta: 'sim:delta', complete: 'sim:done' } });
|
|
110
|
+
```
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Host port
|
|
2
|
+
|
|
3
|
+
> **Package:** `@flexdesk/host` · [All docs](../README.md)
|
|
4
|
+
|
|
5
|
+
## The host port
|
|
6
|
+
|
|
7
|
+
FlexDesk never touches `pywebview`, `fetch`, `localStorage` or the filesystem. It
|
|
8
|
+
asks a **Host**, and you supply one. `createHost({...})` validates the capabilities
|
|
9
|
+
you pass and freezes the result; anything you leave out is simply absent, and the
|
|
10
|
+
library degrades rather than throwing.
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
createHost({
|
|
14
|
+
state: { read, write }, // layout persistence
|
|
15
|
+
dialogs: { saveFile, openFile? }, // optional — CSV export falls back to a Blob;
|
|
16
|
+
// openFile (itself optional) to pickFile()
|
|
17
|
+
window: { minimize, maximize, ... }, // optional — no native chrome buttons without it
|
|
18
|
+
gallery: { list }, // optional (0.17.0) — mountGalleryPicker's entries
|
|
19
|
+
})
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`NULL_HOST` is a valid host with nothing behind it.
|
|
23
|
+
|
|
24
|
+
Two adapters ship ready-made. `createPywebviewHost()` speaks to a pywebview
|
|
25
|
+
window, and `createPhotinoHost()` (0.14.0) to a Photino.NET window over
|
|
26
|
+
`window.external.sendMessage` / `receiveMessage`:
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import { createPhotinoHost } from '@flexdesk/host';
|
|
30
|
+
|
|
31
|
+
// In a browser tab this is NULL_HOST. Keep a capability of your own by passing
|
|
32
|
+
// it in, or leave one out with null.
|
|
33
|
+
const host = createPhotinoHost({ state: myServerState, onEvent: (name, value) => {} });
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`dialogs.saveFile` falls back to a Blob download when the window refuses, and
|
|
37
|
+
the messages the C# side has to answer are in
|
|
38
|
+
[`src/host/photino_host.md`](../../src/host/photino_host.md).
|
|
39
|
+
|
|
40
|
+
**The dialogs a pywebview bridge answers.** `createPywebviewHost()` calls these
|
|
41
|
+
methods on `window.pywebview.api`, positionally. Each is optional: a missing
|
|
42
|
+
one makes its capability answer `null`, and FlexDesk degrades.
|
|
43
|
+
|
|
44
|
+
| Bridge method | Arguments | Reply | Behind |
|
|
45
|
+
|---|---|---|---|
|
|
46
|
+
| `save_file_dialog` | `(data, filename, kind)`: the file's text (`csv`) or base64 bytes (`png`), the suggested name, the type | `{ok: true, path}` \| `{ok: false, cancelled: true}` \| `{ok: false, error}` | `dialogs.saveFile` |
|
|
47
|
+
| `read_file_dialog` (0.17.0) | `(accept, kind, encoding)`: a list of extensions or MIME types to filter by (`['.json']`, may be empty), a label for the filter or `null`, and `'text'` or `'base64'` | `{ok: true, name, path, text}` (`'text'`, read as UTF-8) or `{ok: true, name, path, data}` (`'base64'`, the bytes) \| `{ok: false, cancelled: true}` \| `{ok: false, error}` | `dialogs.openFile` |
|
|
48
|
+
|
|
49
|
+
### Opening a file: `pickFile` (0.17.0)
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
import { pickFile } from '@flexdesk/host';
|
|
53
|
+
|
|
54
|
+
const r = await pickFile(host, { accept: ['.json'], kind: 'json' }); // encoding: 'text' (default) or 'base64'
|
|
55
|
+
if (r.ok) load(JSON.parse(r.text));
|
|
56
|
+
else if (r.error) toast(r.error); // r.cancelled: say nothing
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`pickFile(host, {accept, kind, encoding})` takes `accept` as a list or as a
|
|
60
|
+
comma-separated string (`'.json,.csv'`); the host always receives the list
|
|
61
|
+
(`['.json', '.csv']`). It asks the host's
|
|
62
|
+
`dialogs.openFile` first, a new optional member of `dialogs`: a host whose
|
|
63
|
+
`dialogs` has only `saveFile` is still complete. Without it, or when it
|
|
64
|
+
answers `null` (no dialog behind it), a transient hidden
|
|
65
|
+
`<input type="file">` opens the browser's chooser, so the same call works in a
|
|
66
|
+
browser tab, under `NULL_HOST`. Either way it resolves one of
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
{ok: true, name, path?, text} encoding 'text'
|
|
70
|
+
{ok: true, name, path?, data} encoding 'base64': the file's bytes
|
|
71
|
+
{ok: false, cancelled: true} the user closed the chooser
|
|
72
|
+
{ok: false, error} show it
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`path` comes only from a native host. The pywebview adapter maps `openFile` to
|
|
76
|
+
`read_file_dialog(accept, kind, encoding)` (the table above); the Photino
|
|
77
|
+
adapter sends `dialogs.openFile`, documented in
|
|
78
|
+
[`photino_host.md`](../../src/host/photino_host.md). The browser chooser reports a
|
|
79
|
+
cancel through the input's `cancel` event, which every current engine fires.
|
|
80
|
+
|
|
81
|
+
### A window with its own chrome (0.15.0)
|
|
82
|
+
|
|
83
|
+
The same app runs three ways, and the page needs no switch for it:
|
|
84
|
+
|
|
85
|
+
| | title bar and borders | FlexDesk draws |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| a browser tab | the browser's | nothing |
|
|
88
|
+
| a Photino window, native frame | the OS's | nothing |
|
|
89
|
+
| a Photino window, custom frame | none: your top bar is the title bar | minimise / maximise / close, resize handles, drag |
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
import { WindowChromeController } from '@flexdesk/widgets';
|
|
93
|
+
|
|
94
|
+
new WindowChromeController({
|
|
95
|
+
host, // createPhotinoHost(), or any Host
|
|
96
|
+
topBar: '.my-top-bar', // drags the window (default .twm-global-top-bar)
|
|
97
|
+
controls: '.my-top-bar .right', // where the three buttons go (default: the bar's .twm-bar-right)
|
|
98
|
+
noDrag: '.my-search', // more that never starts a drag
|
|
99
|
+
}).initialize(); // resolves true only with custom chrome
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`host.window.chrome()` decides. The Photino host asks its native side once
|
|
103
|
+
(`host.describe`, its `frame` field) and answers `'custom'` only when the
|
|
104
|
+
window was made frameless, so a browser tab and a native-frame window get no
|
|
105
|
+
handles, no buttons and no CSS. With custom chrome, a drag on the top bar and
|
|
106
|
+
a press on a resize handle go to the host's `startNativeDrag()` /
|
|
107
|
+
`startNativeResize(edge)`. On Windows the native side hands them to the OS,
|
|
108
|
+
so the window snaps, takes Win+arrows and resizes like any other.
|
|
109
|
+
[`photino_host.md`](../../src/host/photino_host.md) has the native half: keep the
|
|
110
|
+
frame styles, return the whole window from `WM_NCCALCSIZE`, start gestures with
|
|
111
|
+
`WM_NCLBUTTONDOWN`. Don't use Photino's `SetChromeless`, which loses all of that.
|
|
112
|
+
|
|
113
|
+
### Links out, and the browser's right-click menu (0.16.0)
|
|
114
|
+
|
|
115
|
+
In a native window a `target="_blank"` link or a `window.open()` opens nothing,
|
|
116
|
+
or a bare popup with no way back. Hand them to the system browser instead:
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
import { routeExternalLinks } from '@flexdesk/host';
|
|
120
|
+
import { suppressNativeContextMenu } from '@flexdesk/widgets';
|
|
121
|
+
|
|
122
|
+
routeExternalLinks({ host }); // a no-op without host.shell, so call it anywhere
|
|
123
|
+
suppressNativeContextMenu(); // only your own menus, plus the native one in text
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`routeExternalLinks` sends http(s) and mailto links that leave the app (a
|
|
127
|
+
`_blank` target, another origin, a middle click) and external `window.open`
|
|
128
|
+
calls to `host.shell.openExternal(url)`. That is a new optional host
|
|
129
|
+
capability, which `createPhotinoHost()` offers as `shell.openExternal`. If the
|
|
130
|
+
native side refuses, the original `window.open` runs. In a browser tab the
|
|
131
|
+
host has no `shell`, and nothing changes.
|
|
132
|
+
|
|
133
|
+
`suppressNativeContextMenu` prevents the browser's or webview's own menu (Back,
|
|
134
|
+
Reload, Print, Inspect) on any right-click that no handler claimed with
|
|
135
|
+
`preventDefault()`. It keeps the native menu in editable text and on selected
|
|
136
|
+
text, where Paste and Copy are the point, and wherever `allow(event)` says so.
|
|
137
|
+
Both calls return `{ dispose }`.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Packages and entry points
|
|
2
|
+
|
|
3
|
+
> **Install:** see the [quickstart](../quickstart.md) · **Component pages:** [docs index](../README.md#components) · [All docs](../README.md)
|
|
4
|
+
|
|
5
|
+
## Nine entry points
|
|
6
|
+
|
|
7
|
+
Because someone who wants a DataTable should not have to download a window
|
|
8
|
+
manager, and neither of them should drag in a plotting engine or a code editor.
|
|
9
|
+
|
|
10
|
+
| Entry | Size | What is in it |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `@flexdesk/core` | 30 kB | Event bus, settings store, logging, state machine, state guard |
|
|
13
|
+
| `@flexdesk/host` | 5 kB | The [host port](host-port.md) |
|
|
14
|
+
| `@flexdesk/wm` | 166 kB | Tiling window manager: tabs, splits, desktops, command palette, taxonomy, `createShell()` |
|
|
15
|
+
| `@flexdesk/widgets` | 192 kB | DataTable, modals, managed windows, toasts, trees, panels, context menus, menu bars, a document outline, progress, a conversation view |
|
|
16
|
+
| `@flexdesk/charts` | 26 kB | Plotly wrapper, chart types, subplot layouts, downsampling, a plot grid, a plot-config editor, plot windows, injected series transforms |
|
|
17
|
+
| `@flexdesk/tiles` | 67 kB | Draggable/resizable widget grid, tile base class, widget registry, layout persistence, figure, chart and KPI tiles |
|
|
18
|
+
| `@flexdesk/editor` | 62 kB | Monaco loader + factory, multi-file tab bar, search, split panes, undo manager |
|
|
19
|
+
| `@flexdesk/canvas` | 14 kB | The canvas kernel: a pannable, zoomable surface, the edge router, a generic graph layer ([Flow editors](../components/flow-editors.md#flow-editors)) |
|
|
20
|
+
| `@flexdesk/flow` | 381 kB | The flow-editor kit and the canvas, outline and lane editors ([Flow editors](../components/flow-editors.md#flow-editors)) |
|
|
21
|
+
|
|
22
|
+
The sizes of `@flexdesk/canvas` and `@flexdesk/flow` are the entry files as
|
|
23
|
+
`node build.mjs` reported them for the flow editors' integration build; like
|
|
24
|
+
every entry they also import shared chunks. The other rows are older figures.
|
|
25
|
+
|
|
26
|
+
They share code through chunks, so importing two of them does **not** give you two
|
|
27
|
+
event buses — and therefore not two copies of every module-level singleton. A test
|
|
28
|
+
asserts each kernel symbol is defined exactly once across the whole `dist/`.
|
|
29
|
+
|
|
30
|
+
Two different things here are called "tiles", which is unfortunate but honest to
|
|
31
|
+
the code: `@flexdesk/wm` tiles the *window*; `@flexdesk/tiles` is a dashboard
|
|
32
|
+
*widget grid*.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Peer dependencies: Plotly and Monaco
|
|
2
|
+
|
|
3
|
+
> **Used by:** [Charts](../components/charts.md), [Editor](../components/editor.md), the chart and figure [tiles](../components/tiles.md) · [All docs](../README.md)
|
|
4
|
+
|
|
5
|
+
## Peer dependencies
|
|
6
|
+
|
|
7
|
+
`@flexdesk/charts` needs Plotly (4.4 MB). `@flexdesk/editor` needs Monaco (14 MB).
|
|
8
|
+
Neither is bundled, and **neither is guessed at**:
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
import { setPlotlySource } from '@flexdesk/charts';
|
|
12
|
+
import { setMonacoBasePath, setDefaultLanguage } from '@flexdesk/editor';
|
|
13
|
+
|
|
14
|
+
setPlotlySource('/vendor/plotly/plotly.min.js');
|
|
15
|
+
setMonacoBasePath('/vendor/monaco');
|
|
16
|
+
setDefaultLanguage('my-dsl'); // default: 'plaintext'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
There is deliberately **no default path**. A hardcoded `'vendor/monaco'` is one
|
|
20
|
+
application's directory layout, and for everyone else it is a 404 that surfaces as
|
|
21
|
+
an editor which silently never appears. `initMonaco()` throws instead.
|
|
22
|
+
|
|
23
|
+
`getEditorFactory()` and `initMonaco()` **reject** when no path was set, when
|
|
24
|
+
`vs/loader.js` fails to load, or when Monaco's AMD loader reports an error, so a
|
|
25
|
+
consumer can show a message instead of waiting forever. What happens next
|
|
26
|
+
depends on which of those it was:
|
|
27
|
+
|
|
28
|
+
- **`vs/loader.js` failed to load** (a 404, a wrong path): not sticky. Fix the
|
|
29
|
+
path (`setMonacoBasePath`, or pass it to `initMonaco`) and the next call
|
|
30
|
+
tries again.
|
|
31
|
+
- **Monaco's AMD loader reported an error** (`vs/editor/editor.main` or a
|
|
32
|
+
dependency failed): **final for the page**. Monaco's loader caches the failed
|
|
33
|
+
module and has no way to forget it, so every later call rejects with that
|
|
34
|
+
same first error, whatever path it is given. Fix the path and reload.
|
|
35
|
+
|
|
36
|
+
Loading is shared: if you call `initMonaco(path)` yourself, `getEditorFactory()`
|
|
37
|
+
uses that load and needs no `setMonacoBasePath()`. `monacoReady` only ever
|
|
38
|
+
resolves — it is the "Monaco has arrived" signal, not the place to learn that
|
|
39
|
+
an attempt failed.
|
|
40
|
+
|
|
41
|
+
If you already load Plotly yourself (a `<script>` tag that sets `window.Plotly`),
|
|
42
|
+
the chart loader short-circuits and you can skip `setPlotlySource`.
|