flexdesk 0.4.6 → 0.4.7

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/README.md CHANGED
@@ -1,276 +1,301 @@
1
- # FlexDesk
2
-
3
- A tiling window manager and widget set for desktop-class web applications.
4
-
5
- Tiles that split and tab, a command palette, dockable panels, virtual desktops, a
6
- DataTable with sticky headers and CSV export, managed windows, modals, toasts,
7
- trees and context menus.
8
-
9
- **Zero runtime dependencies.** No React, no framework, nothing. If you install
10
- FlexDesk, you install FlexDesk.
11
-
12
- FlexDesk was extracted from [EcoAgent](https://github.com/nor-os/EcoAgent), a
13
- macroeconomic simulation platform, and it knows nothing about economics: no
14
- application vocabulary, no import of anything application-specific, in any
15
- shipped file.
16
-
17
- ```bash
18
- npm install flexdesk
19
- ```
20
-
21
- Or vendor `dist/` and load it through an import map — no bundler, no build step.
22
- That is exactly what EcoAgent does:
23
-
24
- ```html
25
- <script type="importmap">
26
- { "imports": {
27
- "@flexdesk/core": "./vendor/flexdesk/core.js",
28
- "@flexdesk/host": "./vendor/flexdesk/host.js",
29
- "@flexdesk/wm": "./vendor/flexdesk/wm.js",
30
- "@flexdesk/widgets": "./vendor/flexdesk/widgets.js"
31
- } }
32
- </script>
33
- <link href="./vendor/flexdesk/flexdesk.css" rel="stylesheet" />
34
- ```
35
-
36
- ## Seven entry points
37
-
38
- Because someone who wants a DataTable should not have to download a window
39
- manager, and neither of them should drag in a plotting engine or a code editor.
40
-
41
- | Entry | Size | What is in it |
42
- |---|---|---|
43
- | `@flexdesk/core` | 30 kB | Event bus, settings store, logging, state machine, state guard |
44
- | `@flexdesk/host` | 5 kB | The host port (below) |
45
- | `@flexdesk/wm` | 166 kB | Tiling window manager: tabs, splits, desktops, command palette, taxonomy, `createShell()` |
46
- | `@flexdesk/widgets` | 192 kB | DataTable, modals, managed windows, toasts, trees, panels, context menus |
47
- | `@flexdesk/charts` | 26 kB | Plotly wrapper, chart types, subplot layouts, downsampling, plot windows |
48
- | `@flexdesk/tiles` | 67 kB | Draggable/resizable widget grid, tile base class, widget registry, layout persistence |
49
- | `@flexdesk/editor` | 62 kB | Monaco loader + factory, multi-file tab bar, search, split panes, undo manager |
50
-
51
- They share code through chunks, so importing two of them does **not** give you two
52
- event buses — and therefore not two copies of every module-level singleton. A test
53
- asserts each kernel symbol is defined exactly once across the whole `dist/`.
54
-
55
- Two different things here are called "tiles", which is unfortunate but honest to
56
- the code: `@flexdesk/wm` tiles the *window*; `@flexdesk/tiles` is a dashboard
57
- *widget grid*.
58
-
59
- ## Hello, shell
60
-
61
- ```js
62
- import { createShell, createTaxonomy, createEntityCatalog } from '@flexdesk/wm';
63
- import { createHost } from '@flexdesk/host';
64
- import { DataTable } from '@flexdesk/widgets';
65
-
66
- const store = new Map();
67
-
68
- const shell = await createShell({
69
- root: document.getElementById('app'),
70
-
71
- // YOUR ontology. FlexDesk has none.
72
- taxonomy: createTaxonomy({
73
- root: 'cosmos',
74
- kinds: {
75
- cosmos: { label: 'Cosmos', isTopNav: true, sources: ['planet'] },
76
- planet: { label: 'Planet', icon: 'circle' },
77
- moon: {
78
- label: 'Moon',
79
- // "jupiter/europa" — your naming convention, not the library's.
80
- ancestors: ({ id }) => {
81
- const i = String(id).indexOf('/');
82
- return i > 0 ? [{ kind: 'planet', id: String(id).slice(0, i) }] : [];
83
- },
84
- },
85
- },
86
- }),
87
-
88
- entities: createEntityCatalog({ sources: [/* what the palette searches */] }),
89
- content: { planet: (hostEl, props) => { /* render a tile */ } },
90
-
91
- // Where persistence goes. An in-memory Map is a perfectly good host.
92
- host: createHost({
93
- state: {
94
- read: async (k) => store.get(k) ?? null,
95
- write: async (k, v) => (store.set(k, v), true),
96
- },
97
- }),
98
-
99
- // Chrome is ELEMENTS, not selectors. The library never guesses at your markup.
100
- chrome: { topNav: document.getElementById('nav') },
101
- });
102
- ```
103
-
104
- A working version of exactly this — planets and moons, a mock host, no backend —
105
- lives in `demo/`.
106
-
107
- ### Opt-in shell features
108
-
109
- These change what the user sees, so all of them are off until you ask:
110
-
111
- ```js
112
- await createShell({
113
- // …everything above…
114
- snapPromotion: true, // drag a floating window onto a tile: it snaps, and docks back in
115
- promoteInPlace: true, // a window floated out of a tile stays inside that tile's pane
116
- backToOpenList: true, // Back in a record opened beside its list closes onto that list
117
- floatActiveTab: true, // floating a tile takes the tab on screen, not the whole pane
118
- chrome: {
119
- topNav: document.getElementById('nav'),
120
- zoom: document.getElementById('status-bar'), // content zoom, 50–200%
121
- },
122
- });
123
- ```
124
-
125
- **`chrome.zoom`** paints a − / track / + / readout control into the element you
126
- pass, restores the saved zoom through the host's `state` capability, and scales
127
- the content of tiles and floating windows. It deliberately scales *content only*
128
- — never the root, a tile's frame or a window's frame — because CSS `zoom`
129
- establishes a scaled coordinate space and every window drag, resize and snap
130
- measures in real pixels. `shell.chrome.zoom` exposes `get()` and `set(percent)`
131
- for a settings pane. An embedder that builds its own `WindowManager` instead of
132
- calling `createShell` mounts the same control with `mountZoomControl(el, { root,
133
- host })`.
134
-
135
- **`backToOpenList`** is for apps that open a record from a list in a new tab.
136
- Without it, Back in that record has no history to pop, so it walks the taxonomy
137
- up and rewrites the record into its parent, and the tile now shows the list
138
- twice. With it, Back closes the record tab and activates the list it came from:
139
- the nearest tab to the left, else to the right, that is not a record (no
140
- `props.id`) and belongs to the same top-nav section. Per-tab history still comes
141
- first, and a record with no such list beside it still walks up in place.
142
-
143
- **`floatActiveTab`** is for apps whose tabs are separate records rather than
144
- views of one pane. The float button, Alt+F and the tile menu take only the tab
145
- on screen and leave its siblings in the tile, so the window holds one tab and
146
- draws no strip. Without it the whole pane floats, strip and all.
147
-
148
- **Back and the breadcrumb are one rule.** `wm.navigateBack()` (Backspace) and
149
- `wm.navigateUp(kind, props, { ctx })` (every breadcrumb crumb) share it. With
150
- `backToOpenList`, a record whose section has a list open closes onto that list;
151
- otherwise Back walks the taxonomy up and a crumb navigates to its level, in
152
- place. A crumb naming an entity, such as an epic above a story, opens that
153
- entity. `navigate` stays the verb for going to exactly a target; use
154
- `navigateUp` for anything that means "up to this level".
155
-
156
- A filter or other props passed to `openInPrimary` for a list that is already
157
- showing are applied to it. A bare open, such as a top-nav click, keeps the tab
158
- as you left it.
159
-
160
- **Back in a floating window** always acts on that window, whichever option is
161
- set. The last click decides: inside a window, Backspace walks that window up;
162
- anywhere else, it walks the focused tile as before. With `backToOpenList`, a
163
- record in a window closes onto an open list of its section, first among the
164
- window's own tabs and then in any tile of its desktop.
165
-
166
- ### Column sizing
167
-
168
- `fitColumns(columns, avail)` (from `@flexdesk/widgets`) turns per-column
169
- measurements into widths. Pass each column's widest cell (`natural`), the width
170
- most cells fit (`typical`), the header label (`header`) and any user-dragged
171
- width (`pinned`). Short columns such as ids, dates and statuses keep their full
172
- values, and free-text columns (`text`, or anything 240px and wider) take the
173
- rest. When space runs short, short columns lose their rare outliers first,
174
- then text shrinks to a floor. The widths are fractional and add up to `avail`
175
- exactly whenever the floors fit. `DataTable` does not use it yet; an embedder
176
- that measures its own columns can.
177
-
178
- ### Scrollbars
179
-
180
- `installAutoScrollbars(root)` (from `@flexdesk/widgets`) replaces the browser's
181
- scrollbars under `root` with thin overlay bars that show while an element is
182
- hovered or scrolling and fade out afterwards. Native bars are hidden, so no
183
- element reserves layout width for one. The overlay bars live in one fixed
184
- layer, so a container that redraws its contents cannot lose its bar, and
185
- textareas get one too. Containers that already use `installOverlayScrollbar`
186
- keep their own.
187
-
188
- ## The host port
189
-
190
- FlexDesk never touches `pywebview`, `fetch`, `localStorage` or the filesystem. It
191
- asks a **Host**, and you supply one. `createHost({...})` validates the capabilities
192
- you pass and freezes the result; anything you leave out is simply absent, and the
193
- library degrades rather than throwing.
194
-
195
- ```js
196
- createHost({
197
- state: { read, write }, // layout persistence
198
- dialogs: { saveFile }, // optional — CSV export falls back to a Blob
199
- window: { minimize, maximize, ... }, // optional — no native chrome buttons without it
200
- })
201
- ```
202
-
203
- `NULL_HOST` is a valid host with nothing behind it.
204
-
205
- ## Peer dependencies
206
-
207
- `@flexdesk/charts` needs Plotly (4.4 MB). `@flexdesk/editor` needs Monaco (14 MB).
208
- Neither is bundled, and **neither is guessed at**:
209
-
210
- ```js
211
- import { setPlotlySource } from '@flexdesk/charts';
212
- import { setMonacoBasePath, setDefaultLanguage } from '@flexdesk/editor';
213
-
214
- setPlotlySource('/vendor/plotly/plotly.min.js');
215
- setMonacoBasePath('/vendor/monaco');
216
- setDefaultLanguage('my-dsl'); // default: 'plaintext'
217
- ```
218
-
219
- There is deliberately **no default path**. A hardcoded `'vendor/monaco'` is one
220
- application's directory layout, and for everyone else it is a 404 that surfaces as
221
- an editor which silently never appears. `initMonaco()` throws instead.
222
-
223
- If you already load Plotly yourself (a `<script>` tag that sets `window.Plotly`),
224
- the chart loader short-circuits and you can skip `setPlotlySource`.
225
-
226
- ## Layouts are keyed by whatever you key them by
227
-
228
- `@flexdesk/tiles` stores layouts under keys **you** choose and resolves a starting
229
- layout through a callback **you** supply:
230
-
231
- ```js
232
- import { configureLayoutPersistence } from '@flexdesk/tiles';
233
-
234
- configureLayoutPersistence({
235
- layoutsKey: 'myapp.layouts', // default: 'twm.tiles.layouts'
236
- resolveTemplate: (ctx) => pickTemplate(ctx) ?? null, // null => empty grid
237
- });
238
- ```
239
-
240
- **Configure it before anything reads it.** The store is a singleton; built before
241
- you supply your key, it is built with the default, and every layout your users have
242
- saved becomes silently invisible. That is a data-loss bug that raises no error, so
243
- the configuration step is explicit rather than lazy.
244
-
245
- A layout is keyed by *something* — a document, a user, a run — and the library has
246
- no opinion about which. It calls that a `contextId`.
247
-
248
- ## Styling
249
-
250
- Three stylesheets, and that is the whole CSS contract:
251
-
252
- | | |
253
- |---|---|
254
- | `dist/tokens.css` | Every rule reads `var(--…)` from here. Swap this one file and the shell restyles. |
255
- | `dist/reset.css` | The reset. |
256
- | `dist/flexdesk.css` | The library. Every class is `twm-` prefixed (the original working name; the prefix is stable and not worth a breaking rename). |
257
-
258
- Icons are rendered by applying the `material-symbols-outlined` class. FlexDesk does
259
- not ship the font — supply it from Google Fonts or self-host. Without it, icons fall
260
- back to their text names and nothing breaks.
261
-
262
- ## Build
263
-
264
- ```bash
265
- npm install
266
- npm run build # -> dist/
267
- ```
268
-
269
- `dist/` is committed. The packaging decision is *prebuilt dist*: this repo owns the
270
- build so that a consumer with no bundler can load the library through a plain import
271
- map. See `.gitignore` — it says so there too, because it is the kind of thing someone
272
- "tidies up" later.
273
-
274
- ## Licence
275
-
276
- MIT. See [`LICENSE`](LICENSE) and [`THIRD_PARTY_LICENSES.md`](THIRD_PARTY_LICENSES.md).
1
+ # FlexDesk
2
+
3
+ A tiling window manager and widget set for desktop-class web applications.
4
+
5
+ Tiles that split and tab, a command palette, dockable panels, virtual desktops, a
6
+ DataTable with sticky headers and CSV export, managed windows, modals, toasts,
7
+ trees and context menus.
8
+
9
+ **Zero runtime dependencies.** No React, no framework, nothing. If you install
10
+ FlexDesk, you install FlexDesk.
11
+
12
+ FlexDesk was extracted from [EcoAgent](https://github.com/nor-os/EcoAgent), a
13
+ macroeconomic simulation platform, and it knows nothing about economics: no
14
+ application vocabulary, no import of anything application-specific, in any
15
+ shipped file.
16
+
17
+ ```bash
18
+ npm install flexdesk
19
+ ```
20
+
21
+ Or vendor `dist/` and load it through an import map — no bundler, no build step.
22
+ That is exactly what EcoAgent does:
23
+
24
+ ```html
25
+ <script type="importmap">
26
+ { "imports": {
27
+ "@flexdesk/core": "./vendor/flexdesk/core.js",
28
+ "@flexdesk/host": "./vendor/flexdesk/host.js",
29
+ "@flexdesk/wm": "./vendor/flexdesk/wm.js",
30
+ "@flexdesk/widgets": "./vendor/flexdesk/widgets.js"
31
+ } }
32
+ </script>
33
+ <link href="./vendor/flexdesk/flexdesk.css" rel="stylesheet" />
34
+ ```
35
+
36
+ ## Seven entry points
37
+
38
+ Because someone who wants a DataTable should not have to download a window
39
+ manager, and neither of them should drag in a plotting engine or a code editor.
40
+
41
+ | Entry | Size | What is in it |
42
+ |---|---|---|
43
+ | `@flexdesk/core` | 30 kB | Event bus, settings store, logging, state machine, state guard |
44
+ | `@flexdesk/host` | 5 kB | The host port (below) |
45
+ | `@flexdesk/wm` | 166 kB | Tiling window manager: tabs, splits, desktops, command palette, taxonomy, `createShell()` |
46
+ | `@flexdesk/widgets` | 192 kB | DataTable, modals, managed windows, toasts, trees, panels, context menus |
47
+ | `@flexdesk/charts` | 26 kB | Plotly wrapper, chart types, subplot layouts, downsampling, plot windows |
48
+ | `@flexdesk/tiles` | 67 kB | Draggable/resizable widget grid, tile base class, widget registry, layout persistence |
49
+ | `@flexdesk/editor` | 62 kB | Monaco loader + factory, multi-file tab bar, search, split panes, undo manager |
50
+
51
+ They share code through chunks, so importing two of them does **not** give you two
52
+ event buses — and therefore not two copies of every module-level singleton. A test
53
+ asserts each kernel symbol is defined exactly once across the whole `dist/`.
54
+
55
+ Two different things here are called "tiles", which is unfortunate but honest to
56
+ the code: `@flexdesk/wm` tiles the *window*; `@flexdesk/tiles` is a dashboard
57
+ *widget grid*.
58
+
59
+ ## Hello, shell
60
+
61
+ ```js
62
+ import { createShell, createTaxonomy, createEntityCatalog } from '@flexdesk/wm';
63
+ import { createHost } from '@flexdesk/host';
64
+ import { DataTable } from '@flexdesk/widgets';
65
+
66
+ const store = new Map();
67
+
68
+ const shell = await createShell({
69
+ root: document.getElementById('app'),
70
+
71
+ // YOUR ontology. FlexDesk has none.
72
+ taxonomy: createTaxonomy({
73
+ root: 'cosmos',
74
+ kinds: {
75
+ cosmos: { label: 'Cosmos', isTopNav: true, sources: ['planet'] },
76
+ planet: { label: 'Planet', icon: 'circle' },
77
+ moon: {
78
+ label: 'Moon',
79
+ // "jupiter/europa" — your naming convention, not the library's.
80
+ ancestors: ({ id }) => {
81
+ const i = String(id).indexOf('/');
82
+ return i > 0 ? [{ kind: 'planet', id: String(id).slice(0, i) }] : [];
83
+ },
84
+ },
85
+ },
86
+ }),
87
+
88
+ entities: createEntityCatalog({ sources: [/* what the palette searches */] }),
89
+ content: { planet: (hostEl, props) => { /* render a tile */ } },
90
+
91
+ // Where persistence goes. An in-memory Map is a perfectly good host.
92
+ host: createHost({
93
+ state: {
94
+ read: async (k) => store.get(k) ?? null,
95
+ write: async (k, v) => (store.set(k, v), true),
96
+ },
97
+ }),
98
+
99
+ // Chrome is ELEMENTS, not selectors. The library never guesses at your markup.
100
+ chrome: { topNav: document.getElementById('nav') },
101
+ });
102
+ ```
103
+
104
+ A working version of exactly this — planets and moons, a mock host, no backend —
105
+ lives in `demo/`.
106
+
107
+ ### Opt-in shell features
108
+
109
+ These change what the user sees, so all of them are off until you ask:
110
+
111
+ ```js
112
+ await createShell({
113
+ // …everything above…
114
+ snapPromotion: true, // drag a floating window onto a tile: it snaps, and docks back in
115
+ promoteInPlace: true, // a window floated out of a tile stays inside that tile's pane
116
+ backToOpenList: true, // Back in a record opened beside its list closes onto that list
117
+ floatActiveTab: true, // floating a tile takes the tab on screen, not the whole pane
118
+ chrome: {
119
+ topNav: document.getElementById('nav'),
120
+ zoom: document.getElementById('status-bar'), // content zoom, 50–200%
121
+ },
122
+ });
123
+ ```
124
+
125
+ **`chrome.zoom`** paints a − / track / + / readout control into the element you
126
+ pass, restores the saved zoom through the host's `state` capability, and scales
127
+ the content of tiles and floating windows. It deliberately scales *content only*
128
+ — never the root, a tile's frame or a window's frame — because CSS `zoom`
129
+ establishes a scaled coordinate space and every window drag, resize and snap
130
+ measures in real pixels. `shell.chrome.zoom` exposes `get()` and `set(percent)`
131
+ for a settings pane. An embedder that builds its own `WindowManager` instead of
132
+ calling `createShell` mounts the same control with `mountZoomControl(el, { root,
133
+ host })`.
134
+
135
+ **`backToOpenList`** is for apps that open a record from a list in a new tab.
136
+ Without it, Back in that record has no history to pop, so it walks the taxonomy
137
+ up and rewrites the record into its parent, and the tile now shows the list
138
+ twice. With it, Back closes the record tab and activates the list it came from:
139
+ the nearest tab to the left, else to the right, that is not a record (no
140
+ `props.id`) and belongs to the same top-nav section. Per-tab history still comes
141
+ first, and a record with no such list beside it still walks up in place.
142
+
143
+ **`floatActiveTab`** is for apps whose tabs are separate records rather than
144
+ views of one pane. The float button, Alt+F and the tile menu take only the tab
145
+ on screen and leave its siblings in the tile, so the window holds one tab and
146
+ draws no strip. Without it the whole pane floats, strip and all.
147
+
148
+ **Back and the breadcrumb are one rule.** `wm.navigateBack()` (Backspace) and
149
+ `wm.navigateUp(kind, props, { ctx })` (every breadcrumb crumb) share it. With
150
+ `backToOpenList`, a record whose section has a list open closes onto that list;
151
+ otherwise Back walks the taxonomy up and a crumb navigates to its level, in
152
+ place. A crumb naming an entity, such as an epic above a story, opens that
153
+ entity. `navigate` stays the verb for going to exactly a target; use
154
+ `navigateUp` for anything that means "up to this level".
155
+
156
+ A filter or other props passed to `openInPrimary` for a list that is already
157
+ showing are applied to it. A bare open, such as a top-nav click, keeps the tab
158
+ as you left it.
159
+
160
+ **Back in a floating window** always acts on that window, whichever option is
161
+ set. The last click decides: inside a window, Backspace walks that window up;
162
+ anywhere else, it walks the focused tile as before. With `backToOpenList`, a
163
+ record in a window closes onto an open list of its section, first among the
164
+ window's own tabs and then in any tile of its desktop.
165
+
166
+ **A page's own levels come first** (0.4.7). A page can have levels that are not
167
+ tabs, such as a section whose list opens one member in place of itself. Return
168
+ `navigateBack()` from the content factory's mount, and Backspace asks it before
169
+ anything above: it climbs one of the page's own levels and returns `true`. Only
170
+ a strict `true` counts. `false`, any other value, a missing function or a throw
171
+ (logged as a warning) all let Back walk the tab history, the open list and the
172
+ taxonomy exactly as before. It is asked in the same place as the rest of Back:
173
+ the window you last clicked in, else the focused tile. Return
174
+ `canNavigateBack()` as well, and `wm.canNavigateBack()` reports the page's own
175
+ levels, for that same window or tile. `TileRenderer.contentOf(leafId)` reads
176
+ what a tile's factory returned. To draw the page's levels, call
177
+ `renderBreadcrumb(container, segments)` from `@flexdesk/wm`, with
178
+ `segments = [{ label, icon?, onClick? }]`. It uses the tile breadcrumb's
179
+ markup, so it looks the same, and each `onClick` is yours: the last segment,
180
+ and any without `onClick`, is drawn as plain text.
181
+
182
+ ### Column sizing
183
+
184
+ `fitColumns(columns, avail)` (from `@flexdesk/widgets`) turns per-column
185
+ measurements into widths. Pass each column's widest cell (`natural`), the width
186
+ most cells fit (`typical`), the header label (`header`) and any user-dragged
187
+ width (`pinned`). Short columns such as ids, dates and statuses keep their full
188
+ values, and free-text columns (`text`, or anything 240px and wider) take the
189
+ rest. When space runs short, short columns lose their rare outliers first,
190
+ then text shrinks to a floor. The widths are fractional and add up to `avail`
191
+ exactly whenever the floors fit. `DataTable` does not use it yet; an embedder
192
+ that measures its own columns can.
193
+
194
+ ### Sorting a paged DataTable
195
+
196
+ A client-paged `DataTable` (no `onPageChange`) keeps its page when it is
197
+ sorted, so a sort on page 3 shows rows 201–300 of the new order. Pass
198
+ `resetPageOnSort: true` (0.4.7) to start every new order at the first page, a
199
+ header click included, the way a new filter already does. It is off by default
200
+ because keeping the page is what every earlier release did. Server-side paging
201
+ is not affected: its page is your `offset`, which a sort never changes.
202
+
203
+ ### Scrollbars
204
+
205
+ `installAutoScrollbars(root)` (from `@flexdesk/widgets`) replaces the browser's
206
+ scrollbars under `root` with thin overlay bars that show while an element is
207
+ hovered or scrolling and fade out afterwards. Native bars are hidden, so no
208
+ element reserves layout width for one. The overlay bars live in one fixed
209
+ layer, so a container that redraws its contents cannot lose its bar, and
210
+ textareas get one too. Containers that already use `installOverlayScrollbar`
211
+ keep their own.
212
+
213
+ ## The host port
214
+
215
+ FlexDesk never touches `pywebview`, `fetch`, `localStorage` or the filesystem. It
216
+ asks a **Host**, and you supply one. `createHost({...})` validates the capabilities
217
+ you pass and freezes the result; anything you leave out is simply absent, and the
218
+ library degrades rather than throwing.
219
+
220
+ ```js
221
+ createHost({
222
+ state: { read, write }, // layout persistence
223
+ dialogs: { saveFile }, // optional — CSV export falls back to a Blob
224
+ window: { minimize, maximize, ... }, // optional — no native chrome buttons without it
225
+ })
226
+ ```
227
+
228
+ `NULL_HOST` is a valid host with nothing behind it.
229
+
230
+ ## Peer dependencies
231
+
232
+ `@flexdesk/charts` needs Plotly (4.4 MB). `@flexdesk/editor` needs Monaco (14 MB).
233
+ Neither is bundled, and **neither is guessed at**:
234
+
235
+ ```js
236
+ import { setPlotlySource } from '@flexdesk/charts';
237
+ import { setMonacoBasePath, setDefaultLanguage } from '@flexdesk/editor';
238
+
239
+ setPlotlySource('/vendor/plotly/plotly.min.js');
240
+ setMonacoBasePath('/vendor/monaco');
241
+ setDefaultLanguage('my-dsl'); // default: 'plaintext'
242
+ ```
243
+
244
+ There is deliberately **no default path**. A hardcoded `'vendor/monaco'` is one
245
+ application's directory layout, and for everyone else it is a 404 that surfaces as
246
+ an editor which silently never appears. `initMonaco()` throws instead.
247
+
248
+ If you already load Plotly yourself (a `<script>` tag that sets `window.Plotly`),
249
+ the chart loader short-circuits and you can skip `setPlotlySource`.
250
+
251
+ ## Layouts are keyed by whatever you key them by
252
+
253
+ `@flexdesk/tiles` stores layouts under keys **you** choose and resolves a starting
254
+ layout through a callback **you** supply:
255
+
256
+ ```js
257
+ import { configureLayoutPersistence } from '@flexdesk/tiles';
258
+
259
+ configureLayoutPersistence({
260
+ layoutsKey: 'myapp.layouts', // default: 'twm.tiles.layouts'
261
+ resolveTemplate: (ctx) => pickTemplate(ctx) ?? null, // null => empty grid
262
+ });
263
+ ```
264
+
265
+ **Configure it before anything reads it.** The store is a singleton; built before
266
+ you supply your key, it is built with the default, and every layout your users have
267
+ saved becomes silently invisible. That is a data-loss bug that raises no error, so
268
+ the configuration step is explicit rather than lazy.
269
+
270
+ A layout is keyed by *something* — a document, a user, a run — and the library has
271
+ no opinion about which. It calls that a `contextId`.
272
+
273
+ ## Styling
274
+
275
+ Three stylesheets, and that is the whole CSS contract:
276
+
277
+ | | |
278
+ |---|---|
279
+ | `dist/tokens.css` | Every rule reads `var(--…)` from here. Swap this one file and the shell restyles. |
280
+ | `dist/reset.css` | The reset. |
281
+ | `dist/flexdesk.css` | The library. Every class is `twm-` prefixed (the original working name; the prefix is stable and not worth a breaking rename). |
282
+
283
+ Icons are rendered by applying the `material-symbols-outlined` class. FlexDesk does
284
+ not ship the font — supply it from Google Fonts or self-host. Without it, icons fall
285
+ back to their text names and nothing breaks.
286
+
287
+ ## Build
288
+
289
+ ```bash
290
+ npm install
291
+ npm run build # -> dist/
292
+ ```
293
+
294
+ `dist/` is committed. The packaging decision is *prebuilt dist*: this repo owns the
295
+ build so that a consumer with no bundler can load the library through a plain import
296
+ map. See `.gitignore` — it says so there too, because it is the kind of thing someone
297
+ "tidies up" later.
298
+
299
+ ## Licence
300
+
301
+ MIT. See [`LICENSE`](LICENSE) and [`THIRD_PARTY_LICENSES.md`](THIRD_PARTY_LICENSES.md).