flexdesk 0.4.7 → 0.5.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/README.md +416 -301
- package/css/base.css +123 -2
- package/css/flexdesk.css +127 -4
- package/css/overrides.css +4 -2
- package/css/tokens.css +14 -0
- package/dist/charts.js +782 -782
- package/dist/{chunk-ELXVW542.js → chunk-24MS7W5Y.js} +2 -2
- package/dist/{chunk-LH5TSOZW.js → chunk-6JSOVNID.js} +42 -2
- package/dist/{chunk-LH5TSOZW.js.map → chunk-6JSOVNID.js.map} +3 -3
- package/dist/{chunk-F5FYDCOA.js → chunk-CNXP3SIN.js} +2373 -2373
- package/dist/{chunk-F5FYDCOA.js.map → chunk-CNXP3SIN.js.map} +7 -7
- package/dist/{chunk-WMGOJ3I6.js → chunk-KH2GJTKR.js} +3702 -2628
- package/dist/chunk-KH2GJTKR.js.map +7 -0
- package/dist/flexdesk.css +127 -4
- package/dist/tiles.js +2195 -2195
- package/dist/tokens.css +14 -0
- package/dist/widgets.js +5822 -5796
- package/dist/widgets.js.map +1 -1
- package/dist/wm.js +8075 -8074
- package/dist/wm.js.map +7 -7
- package/package.json +81 -81
- package/src/tiling/landing_table.js +13 -5
- package/src/tiling/wm.js +3916 -3916
- package/src/ui/components/data_table.js +4205 -3294
- package/src/ui/components/row_activation.js +70 -0
- package/src/ui/components/table_values.js +423 -0
- package/dist/chunk-WMGOJ3I6.js.map +0 -7
- /package/dist/{chunk-ELXVW542.js.map → chunk-24MS7W5Y.js.map} +0 -0
package/README.md
CHANGED
|
@@ -1,301 +1,416 @@
|
|
|
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
|
-
###
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
the
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
`
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
+
**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
|
+
### DataTable options in 0.5.0: lists that need no help
|
|
204
|
+
|
|
205
|
+
0.5.0 adds options for the ordinary list in an application: one that sits in a
|
|
206
|
+
page, opens a row, marks the open one, and sorts dates as dates. Every option
|
|
207
|
+
is off until you set it. A table that sets none of them looks and behaves as it
|
|
208
|
+
did in 0.4, apart from the few changes listed at the end of this section. This
|
|
209
|
+
was measured in headless Edge: 64 cells in 7 default tables, an empty and a
|
|
210
|
+
loading one among them, computed the same under 0.4.7 and 0.5.0.
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
import { DataTable } from '@flexdesk/widgets';
|
|
214
|
+
|
|
215
|
+
// House rules once, then one call per list: build AND draw.
|
|
216
|
+
const ListTable = DataTable.withDefaults({
|
|
217
|
+
mode: 'compact', sortable: true, pagination: false,
|
|
218
|
+
fitContent: true, firstColumn: 'plain', autoDispose: true,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const table = ListTable.mount(host, {
|
|
222
|
+
headers: ['Name', 'Role', 'Last seen', 'Took'],
|
|
223
|
+
rows, // values, not formatted text
|
|
224
|
+
columnTypes: [null, null, 'datetime', 'duration'],
|
|
225
|
+
getRowKey: (row) => row[0],
|
|
226
|
+
activeRow: openName,
|
|
227
|
+
onRowActivate: (i, row) => open(row), // one click opens
|
|
228
|
+
rowClass: (row) => (row[1] === 'Suspended' ? 'is-off' : null),
|
|
229
|
+
emptyState: 'Nobody yet.',
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Layout**
|
|
234
|
+
|
|
235
|
+
| Option | What it does |
|
|
236
|
+
|---|---|
|
|
237
|
+
| `fitContent: true` | Makes the table as tall as its rows. By default the wrapper is `height: 100%`. In a parent without a height (an ordinary block in a page that scrolls) that resolves to nothing, so the header draws and the rows do not. |
|
|
238
|
+
| `maxHeight: 240` | Implies `fitContent` and caps the table at this height. A number is px; a string is used as CSS. Past the cap, the body scrolls under its header. |
|
|
239
|
+
| `firstColumn: 'plain'` | Styles the first column like the others. By default it is styled as a time column: 120–150px wide, grey, weight 500. |
|
|
240
|
+
| `--twm-dt-cell-pad-block`, `--twm-dt-cell-pad-inline`, `--twm-dt-cell-line-height` | Three CSS tokens. Set them on `:root` or on any ancestor to give both densities one row height. They are unset by default, and then each density keeps its own 0.4 values. In Edge, with the tokens set to `2px`, `6px` and `1.35`, normal and compact rows both measured 21.19px. Without them, normal rows measured 27px. The normal density reads the line-height token on the table's `thead`/`tbody` and the cells inherit it, so your own line-height for the cells, the rows or the table still wins, from a cascade layer too. |
|
|
241
|
+
|
|
242
|
+
**Opening a row, and which one is open**
|
|
243
|
+
|
|
244
|
+
| Option | What it does |
|
|
245
|
+
|---|---|
|
|
246
|
+
| `onRowActivate(rowIdx, row, ev)` | Opens a row. It fires after the selection has been updated (`onRowClick` still fires before it, as in 0.4). It does not fire for a press on a control in the row (a button, link, field, label, `<summary>`, editable text, or `[data-twm-action]`), for a click that ends a drag selecting text in the table, for the second click of a double-click, or for the Ctrl click that takes a `selectable: 'single'` table's row off the selection. `ev.type` tells you which gesture fired it. If your `onSelectionChange` or `onRowClick` redraws the table (`setData`, `render`), the row that was pressed still opens; `row` is that row as it was drawn. |
|
|
247
|
+
| `activateOn` | Which gestures open a row: `'click'` (the default), `'dblclick'`, `'enter'`, or an array of them. A pick list, where one click selects, uses `['dblclick', 'enter']`. Enter opens the selected row (the one row selected, or the selection's anchor), which is the row a double-click on it would open. If nothing is selected, Enter opens the active row. |
|
|
248
|
+
| `clickable` | Adds `twm-dt--clickable` to the table, which gives rows a pointer cursor and a hover, readonly tables included. It defaults to on exactly when `onRowActivate` is set. |
|
|
249
|
+
| `selectable: 'single'` | Lets a click select exactly one row. Shift and Ctrl do not extend the selection, Ctrl+A selects nothing, and `setSelection` keeps the last index it is given. |
|
|
250
|
+
| `getRowKey(row, idx)`, `activeRow`, `setActiveRow(key)`, `getActiveRow()` | Mark the open row: `twm-dt-row--active` and `aria-current="true"`, drawn as an accent on the left edge. The mark is not the selection. It survives `setData`, sorting, filtering and paging, and a right-click does not move it. |
|
|
251
|
+
|
|
252
|
+
The landing helper `attachLandingTableBehavior` now uses the same rule as
|
|
253
|
+
`onRowActivate` to decide whether a click opens a row. That changes three
|
|
254
|
+
things, with no option (see the list at the end of this section).
|
|
255
|
+
|
|
256
|
+
**Row state**
|
|
257
|
+
|
|
258
|
+
| Option | What it does |
|
|
259
|
+
|---|---|
|
|
260
|
+
| `rowClass(row, idx)`, `rowAttrs(row, idx)` | Add classes and attributes to the row's `<tr>`. They follow the row through sorts and filters, and `updateRow` asks again, so a class the row no longer earns is removed. |
|
|
261
|
+
| `cellClass(value, colIdx, row, idx)` | Adds classes to a cell, after its type class. |
|
|
262
|
+
| `rowIcon(row, idx)` | Draws an icon (`'name'` or `{ icon, title, tone }`) at the start of the first cell. It is drawn from an attribute, so the icon's name is not part of the cell's text, its tooltip or a copy. |
|
|
263
|
+
| `emptyState` | Text, a node, or a function, shown in place of `emptyMessage`. It is hidden while `setLoading(true)` is on. |
|
|
264
|
+
| `setError(message \| node \| Error \| null)` | Shows a failure inside the table. If there are rows on screen, they stay, under a `role="alert"` banner. If there are none, the failure takes the place of the empty row. `setData({ rows })` and `setError(null)` clear it. |
|
|
265
|
+
| `showRowNumbers: 'position'` | Numbers rows 1, 2, 3 in the order shown, after a sort, a filter, or on a later page. `true` still numbers rows by their original index. |
|
|
266
|
+
| `nullDisplay: 'NULL'` | Draws `null` and `undefined` as this text, adds `twm-dt-cell--null` to the cell, and copies the text as well. |
|
|
267
|
+
|
|
268
|
+
**Values, not text**
|
|
269
|
+
|
|
270
|
+
| Option | What it does |
|
|
271
|
+
|---|---|
|
|
272
|
+
| `columnTypes: [..., 'date' \| 'datetime' \| 'duration']` | Sorts and filters the column by its value: an ISO 8601 string, a `Date` or epoch ms for dates, and milliseconds or text such as `'3.2 s'` for durations. A date-only value such as `2026-10-05` is a calendar date, so the time zone never moves it. Any other text, and a date that does not exist (`2026-02-31`), is shown as it came and sorts last. Non-ISO text is not passed to `Date.parse`, because browsers read it differently (V8 reads `'12'` as a date in 2001). Only `columnTypes` makes a column a typed column. A `getColumnType` that returns `'date'` still sets only the cell's class name, as in 0.4. |
|
|
273
|
+
| `dateFormat`, `dateTimeFormat` | **Default ISO: `YYYY-MM-DD` and `YYYY-MM-DD HH:mm`.** Pass a pattern (`DD.MM.YYYY`, `D MMM YYYY, h:mm a`, with `[literal]` text) or a function `(date) => string`, for every column or per column index. |
|
|
274
|
+
| `durationFormat` | `'auto'` (`850 ms`, `3.2 s`, `2m 5s`, `1h 2m`), `'clock'` (`1:02:05`), or a function. |
|
|
275
|
+
| `dateTimeZone: 'UTC'` | Reads and draws typed dates in UTC instead of the local zone. |
|
|
276
|
+
| `sortValue(value, colIdx, row)` | Sets what a column sorts by, for every column or (as an object or array) per column. Return `undefined` to fall back to the column's own rule. |
|
|
277
|
+
|
|
278
|
+
The column filters work on values. On a date column, `2026-10` matches October,
|
|
279
|
+
`>2026-10` matches after October, `<=2026-10-05` includes the 5th, and
|
|
280
|
+
`2026-01..2026-03` matches January through the end of March. A date operand is
|
|
281
|
+
ISO with two-digit parts, so an operator over a half-typed one (`>20`,
|
|
282
|
+
`>2026-1`) filters nothing until it is complete. Text without an operator that
|
|
283
|
+
is not such a date (`5 Oct`, `Oct`, `2026-1`) is matched against the cell as
|
|
284
|
+
drawn, so a filter narrows as you type it. On a duration column, `>1s`,
|
|
285
|
+
`<500ms` and `1s..1m` work. `formatDate`, `formatDuration`, `parseDateValue`
|
|
286
|
+
and `parseDuration` are exported, so text drawn elsewhere can use the same
|
|
287
|
+
words as the table.
|
|
288
|
+
|
|
289
|
+
**Housekeeping**
|
|
290
|
+
|
|
291
|
+
| Option | What it does |
|
|
292
|
+
|---|---|
|
|
293
|
+
| `DataTable.mount(host, config)` | Builds the table and draws it. The constructor still draws nothing until `render()`. |
|
|
294
|
+
| `DataTable.withDefaults(defaults)` | Returns a subclass whose `defaults` sit under every config it is given. `instanceof DataTable` holds, and it can be narrowed again. |
|
|
295
|
+
| `autoDispose: true` | Disposes the table once it has been taken out of the document and is still out a task later. It lets go of everything `dispose()` does, but it removes only the table's own elements and never empties the host. By then you may have mounted the next list into the host or written your own message there, and that stays. Do not set it on a table you detach and attach again later, such as a cached tab, because a disposed table comes back empty. |
|
|
296
|
+
| `resetColumnWidths()` | Forgets every dragged, fitted or restored width, the persisted ones included, and measures the columns again. Use it after a zoom or a font change. |
|
|
297
|
+
|
|
298
|
+
**Changes that need no option.** A table that sets none of the options above
|
|
299
|
+
behaves as it did in 0.4, except for these:
|
|
300
|
+
|
|
301
|
+
- The copy menu's document listeners (click, scroll, Escape, resize) now exist
|
|
302
|
+
only while the menu is open. Before 0.5.0, a table that had ever been
|
|
303
|
+
right-clicked kept them until `dispose()`.
|
|
304
|
+
- `attachLandingTableBehavior` judges a click by the `onRowActivate` rule:
|
|
305
|
+
- A double-click opens the row once, not twice.
|
|
306
|
+
- A click while text in the host is selected does not open the row. That is
|
|
307
|
+
the click that ends a drag across a cell.
|
|
308
|
+
- A click on a `<label>`, a `<summary>` or editable (`contenteditable`) text
|
|
309
|
+
in a row does not open the row either. 0.4.7 skipped only buttons, links
|
|
310
|
+
and form fields. A `[data-twm-action]` control still runs its action.
|
|
311
|
+
- The CSS reads the new tokens with the 0.4 values as fallbacks, so the
|
|
312
|
+
computed styles are the same while the tokens are unset. One exception: the
|
|
313
|
+
normal density's line-height token is declared on the `thead` and `tbody`.
|
|
314
|
+
A line-height rule of yours aimed at those two elements themselves now loses
|
|
315
|
+
to it if it is in a cascade layer, or has zero specificity and loads before
|
|
316
|
+
`flexdesk.css`. A rule for the cells, the rows or the table does not.
|
|
317
|
+
|
|
318
|
+
### Scrollbars
|
|
319
|
+
|
|
320
|
+
`installAutoScrollbars(root)` (from `@flexdesk/widgets`) replaces the browser's
|
|
321
|
+
scrollbars under `root` with thin overlay bars that show while an element is
|
|
322
|
+
hovered or scrolling and fade out afterwards. Native bars are hidden, so no
|
|
323
|
+
element reserves layout width for one. The overlay bars live in one fixed
|
|
324
|
+
layer, so a container that redraws its contents cannot lose its bar, and
|
|
325
|
+
textareas get one too. Containers that already use `installOverlayScrollbar`
|
|
326
|
+
keep their own.
|
|
327
|
+
|
|
328
|
+
## The host port
|
|
329
|
+
|
|
330
|
+
FlexDesk never touches `pywebview`, `fetch`, `localStorage` or the filesystem. It
|
|
331
|
+
asks a **Host**, and you supply one. `createHost({...})` validates the capabilities
|
|
332
|
+
you pass and freezes the result; anything you leave out is simply absent, and the
|
|
333
|
+
library degrades rather than throwing.
|
|
334
|
+
|
|
335
|
+
```js
|
|
336
|
+
createHost({
|
|
337
|
+
state: { read, write }, // layout persistence
|
|
338
|
+
dialogs: { saveFile }, // optional — CSV export falls back to a Blob
|
|
339
|
+
window: { minimize, maximize, ... }, // optional — no native chrome buttons without it
|
|
340
|
+
})
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`NULL_HOST` is a valid host with nothing behind it.
|
|
344
|
+
|
|
345
|
+
## Peer dependencies
|
|
346
|
+
|
|
347
|
+
`@flexdesk/charts` needs Plotly (4.4 MB). `@flexdesk/editor` needs Monaco (14 MB).
|
|
348
|
+
Neither is bundled, and **neither is guessed at**:
|
|
349
|
+
|
|
350
|
+
```js
|
|
351
|
+
import { setPlotlySource } from '@flexdesk/charts';
|
|
352
|
+
import { setMonacoBasePath, setDefaultLanguage } from '@flexdesk/editor';
|
|
353
|
+
|
|
354
|
+
setPlotlySource('/vendor/plotly/plotly.min.js');
|
|
355
|
+
setMonacoBasePath('/vendor/monaco');
|
|
356
|
+
setDefaultLanguage('my-dsl'); // default: 'plaintext'
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
There is deliberately **no default path**. A hardcoded `'vendor/monaco'` is one
|
|
360
|
+
application's directory layout, and for everyone else it is a 404 that surfaces as
|
|
361
|
+
an editor which silently never appears. `initMonaco()` throws instead.
|
|
362
|
+
|
|
363
|
+
If you already load Plotly yourself (a `<script>` tag that sets `window.Plotly`),
|
|
364
|
+
the chart loader short-circuits and you can skip `setPlotlySource`.
|
|
365
|
+
|
|
366
|
+
## Layouts are keyed by whatever you key them by
|
|
367
|
+
|
|
368
|
+
`@flexdesk/tiles` stores layouts under keys **you** choose and resolves a starting
|
|
369
|
+
layout through a callback **you** supply:
|
|
370
|
+
|
|
371
|
+
```js
|
|
372
|
+
import { configureLayoutPersistence } from '@flexdesk/tiles';
|
|
373
|
+
|
|
374
|
+
configureLayoutPersistence({
|
|
375
|
+
layoutsKey: 'myapp.layouts', // default: 'twm.tiles.layouts'
|
|
376
|
+
resolveTemplate: (ctx) => pickTemplate(ctx) ?? null, // null => empty grid
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
**Configure it before anything reads it.** The store is a singleton; built before
|
|
381
|
+
you supply your key, it is built with the default, and every layout your users have
|
|
382
|
+
saved becomes silently invisible. That is a data-loss bug that raises no error, so
|
|
383
|
+
the configuration step is explicit rather than lazy.
|
|
384
|
+
|
|
385
|
+
A layout is keyed by *something* — a document, a user, a run — and the library has
|
|
386
|
+
no opinion about which. It calls that a `contextId`.
|
|
387
|
+
|
|
388
|
+
## Styling
|
|
389
|
+
|
|
390
|
+
Three stylesheets, and that is the whole CSS contract:
|
|
391
|
+
|
|
392
|
+
| | |
|
|
393
|
+
|---|---|
|
|
394
|
+
| `dist/tokens.css` | Every rule reads `var(--…)` from here. Swap this one file and the shell restyles. |
|
|
395
|
+
| `dist/reset.css` | The reset. |
|
|
396
|
+
| `dist/flexdesk.css` | The library. Every class is `twm-` prefixed (the original working name; the prefix is stable and not worth a breaking rename). |
|
|
397
|
+
|
|
398
|
+
Icons are rendered by applying the `material-symbols-outlined` class. FlexDesk does
|
|
399
|
+
not ship the font — supply it from Google Fonts or self-host. Without it, icons fall
|
|
400
|
+
back to their text names and nothing breaks.
|
|
401
|
+
|
|
402
|
+
## Build
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
npm install
|
|
406
|
+
npm run build # -> dist/
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
`dist/` is committed. The packaging decision is *prebuilt dist*: this repo owns the
|
|
410
|
+
build so that a consumer with no bundler can load the library through a plain import
|
|
411
|
+
map. See `.gitignore` — it says so there too, because it is the kind of thing someone
|
|
412
|
+
"tidies up" later.
|
|
413
|
+
|
|
414
|
+
## Licence
|
|
415
|
+
|
|
416
|
+
MIT. See [`LICENSE`](LICENSE) and [`THIRD_PARTY_LICENSES.md`](THIRD_PARTY_LICENSES.md).
|