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,58 @@
|
|
|
1
|
+
# State guard
|
|
2
|
+
|
|
3
|
+
> **Package:** `@flexdesk/core` · [All docs](../README.md)
|
|
4
|
+
|
|
5
|
+
## The state guard: who wrote that attribute (0.17.0)
|
|
6
|
+
|
|
7
|
+
`StateGuardService` blocks (`strict`) or reports (`warn`) `data-*` writes that
|
|
8
|
+
are on no allow-list. A module override lets one module write a key nobody
|
|
9
|
+
else may:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { StateGuardService, DEFAULT_STATE_GUARD_CONFIG } from '@flexdesk/core';
|
|
13
|
+
|
|
14
|
+
const guard = new StateGuardService({ eventBus, config: { ...DEFAULT_STATE_GUARD_CONFIG, mode: 'strict' } });
|
|
15
|
+
guard.allow('src/app/my_widget.js', ['data-foo']); // a literal substring of the module, or a RegExp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
A write is attributed to the frame that made it: the URL or path of the file
|
|
19
|
+
that called `setAttribute`, `removeAttribute` or `dataset`, without line and
|
|
20
|
+
column, whatever your directory layout. A FlexDesk widget's write is the
|
|
21
|
+
widget's, not that of the app module that called the widget. To attribute
|
|
22
|
+
writes further up the stack instead, pass `moduleFramePattern`: the first
|
|
23
|
+
frame from the writer up whose location matches names the module, by the
|
|
24
|
+
pattern's first capture group if it has one.
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
config: { ...DEFAULT_STATE_GUARD_CONFIG, moduleFramePattern: /(src\/app\/[^/]+\.js)/ }
|
|
28
|
+
// a write made inside @flexdesk/widgets on behalf of src/app/my_widget.js
|
|
29
|
+
// is now attributed to 'src/app/my_widget.js'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The defaults are `mode: 'warn'`, no module overrides and no frame pattern.
|
|
33
|
+
Before 0.17.0 the module was only found under a `js_new/` path, the defaults
|
|
34
|
+
carried one application's override, and the mode read one application's debug
|
|
35
|
+
global; choose strict mode in your own code.
|
|
36
|
+
|
|
37
|
+
### The defaults cover FlexDesk's own keys (0.17.0)
|
|
38
|
+
|
|
39
|
+
`DEFAULT_STATE_GUARD_CONFIG` includes every data key FlexDesk's own widgets
|
|
40
|
+
write, so the shell, dialogs, forms, tooltips, tiles and flow editors run under
|
|
41
|
+
`mode: 'strict'` with no violation. The list also carries some keys that
|
|
42
|
+
FlexDesk itself does not write. Add your application's keys on top:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
const guard = new StateGuardService({ eventBus, logger, config: {
|
|
46
|
+
...DEFAULT_STATE_GUARD_CONFIG,
|
|
47
|
+
allowedDatasetKeys: [...DEFAULT_STATE_GUARD_CONFIG.allowedDatasetKeys, 'order-id'],
|
|
48
|
+
} });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
An allow-list key may be spelled either way: `orderId` and `order-id` are the
|
|
52
|
+
same key, in the allow-list and in module overrides
|
|
53
|
+
(`allow(pattern, attributes, { datasetKeys })` and `config.moduleOverrides`)
|
|
54
|
+
alike. camelCase maps to kebab-case as the DOM does for ordinary keys: each
|
|
55
|
+
capital letter becomes `-` plus its lower-case form. So `rowID` is `row-i-d`
|
|
56
|
+
(what `dataset.rowID` writes), not `rowid`; to allow `data-id`, list `id`.
|
|
57
|
+
Before 0.17.0 a camelCase entry never matched, and some entries named keys no
|
|
58
|
+
element carries.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Theming and CSS
|
|
2
|
+
|
|
3
|
+
> **Stylesheets:** `flexdesk/css/tokens`, `flexdesk/css/reset`, `flexdesk/css` · [All docs](../README.md)
|
|
4
|
+
|
|
5
|
+
## Styling
|
|
6
|
+
|
|
7
|
+
Three stylesheets, and that is the whole CSS contract:
|
|
8
|
+
|
|
9
|
+
| | Specifier | Resolves to | |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| `dist/tokens.css` | `flexdesk/css/tokens` | `css/tokens.css` | Every rule reads `var(--…)` from here. Swap this one file and the shell restyles. |
|
|
12
|
+
| `dist/reset.css` | `flexdesk/css/reset` | `dist/reset.css` | The reset. |
|
|
13
|
+
| `dist/flexdesk.css` | `flexdesk/css` | `dist/flexdesk.css` | The library. Every class is `twm-` prefixed (the original working name; the prefix is stable and not worth a breaking rename). |
|
|
14
|
+
|
|
15
|
+
A bundler, an import-map generator or `require.resolve` reaches each one by its
|
|
16
|
+
specifier, through the package's `exports` map, which closes every other path
|
|
17
|
+
into the package. The map sends `flexdesk/css/tokens` to the source
|
|
18
|
+
`css/tokens.css`, not `dist/tokens.css`. The two are identical in a release,
|
|
19
|
+
since `dist/` is rebuilt in the release commit, but can differ on a working
|
|
20
|
+
checkout. `flexdesk/css/reset` is missing from 0.16.1 and earlier,
|
|
21
|
+
where it fails with `ERR_PACKAGE_PATH_NOT_EXPORTED`.
|
|
22
|
+
|
|
23
|
+
`createDetailHeader` follows the prefix too: a badge is a `.twm-detail-header__badge`
|
|
24
|
+
pill (a badge's `className` is added beside it, to recolour it), and an action is a
|
|
25
|
+
`.twm-btn` — `variant: 'primary'` adds `.twm-btn--primary`, `'danger'` adds
|
|
26
|
+
`.twm-btn--danger` — that also carries `.twm-detail-header__btn` and
|
|
27
|
+
`.twm-detail-header__btn--<variant>`. The unprefixed `detail-header__badge` and
|
|
28
|
+
`detail-header__btn--<variant>` it emitted before, which no FlexDesk rule styled, are
|
|
29
|
+
gone.
|
|
30
|
+
|
|
31
|
+
`@flexdesk/editor`'s components keep the class names they were extracted with, and
|
|
32
|
+
`flexdesk.css` styles them: `SplitPaneContainer` (`.split-pane-container`,
|
|
33
|
+
`.split-pane`, a 4px `.split-pane-divider` with a 10px hit target),
|
|
34
|
+
`EditorPane` (`.editor-pane`, `.editor-pane__body`, `.notebook-editor-container`)
|
|
35
|
+
and `NotebookSearchBar` (`.nb-search-bar*`). `setPanes()` adds `.split-pane` to the
|
|
36
|
+
elements you hand it and leaves their own classes alone (it used to replace them,
|
|
37
|
+
which stripped `.editor-pane` off an `EditorPane`).
|
|
38
|
+
|
|
39
|
+
One exception to the prefix: `HelpModal` (`@flexdesk/widgets`) keeps its older
|
|
40
|
+
`help-` classes (`help-modal-overlay`, `help-modal`, `help-index`, …), and
|
|
41
|
+
`flexdesk.css` styles them — a centred dialog on a dimmed overlay, drawn over
|
|
42
|
+
managed windows. `.help-article` holds your provider's rendered HTML and styles
|
|
43
|
+
its plain elements (headings, code, tables, lists).
|
|
44
|
+
|
|
45
|
+
Icons are rendered by applying the `material-symbols-outlined` class. FlexDesk does
|
|
46
|
+
not ship the font — supply it from Google Fonts or self-host. Without it, icons fall
|
|
47
|
+
back to their text names and nothing breaks.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Quickstart
|
|
2
|
+
|
|
3
|
+
From an empty folder to a shell with a table in it. You need a text editor and a static file server. There is no bundler, no framework and no build step.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install flexdesk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The package ships a prebuilt `dist/`. Copy it into your site (the examples below call the copy `vendor/flexdesk/`) and load it through an import map:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
mkdir -p vendor && cp -r node_modules/flexdesk/dist vendor/flexdesk
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Under a bundler you need none of that. Import the entries as `flexdesk/core`, `flexdesk/host`, `flexdesk/wm`, `flexdesk/widgets` (and `flexdesk/charts`, `flexdesk/tiles`, `flexdesk/editor`, `flexdesk/canvas`, `flexdesk/flow`), and the stylesheets as `flexdesk/css/tokens`, `flexdesk/css/reset` and `flexdesk/css`. The rest of these docs write the entries as `@flexdesk/wm` and so on, which is the name the import map below gives them.
|
|
18
|
+
|
|
19
|
+
## The page
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<!doctype html>
|
|
23
|
+
<html lang="en">
|
|
24
|
+
<head>
|
|
25
|
+
<meta charset="utf-8" />
|
|
26
|
+
<title>My first shell</title>
|
|
27
|
+
<script type="importmap">
|
|
28
|
+
{ "imports": {
|
|
29
|
+
"@flexdesk/core": "./vendor/flexdesk/core.js",
|
|
30
|
+
"@flexdesk/host": "./vendor/flexdesk/host.js",
|
|
31
|
+
"@flexdesk/wm": "./vendor/flexdesk/wm.js",
|
|
32
|
+
"@flexdesk/widgets": "./vendor/flexdesk/widgets.js"
|
|
33
|
+
} }
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<!-- Optional: the icon font. Without it, icons show their text names. -->
|
|
37
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" />
|
|
38
|
+
|
|
39
|
+
<!-- The whole CSS contract, in this order. -->
|
|
40
|
+
<link href="./vendor/flexdesk/tokens.css" rel="stylesheet" />
|
|
41
|
+
<link href="./vendor/flexdesk/reset.css" rel="stylesheet" />
|
|
42
|
+
<link href="./vendor/flexdesk/flexdesk.css" rel="stylesheet" />
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
html, body { height: 100%; margin: 0; }
|
|
46
|
+
body {
|
|
47
|
+
background: var(--surface-0);
|
|
48
|
+
color: var(--text-primary);
|
|
49
|
+
font-family: var(--font-family-sans);
|
|
50
|
+
display: flex; flex-direction: column;
|
|
51
|
+
}
|
|
52
|
+
#nav { flex: 0 0 auto; }
|
|
53
|
+
#app { flex: 1 1 0; min-height: 0; position: relative; } /* the shell fills this */
|
|
54
|
+
</style>
|
|
55
|
+
</head>
|
|
56
|
+
<body class="twm-body">
|
|
57
|
+
<div id="nav"></div>
|
|
58
|
+
<div id="app"></div>
|
|
59
|
+
<script type="module" src="./main.js"></script>
|
|
60
|
+
</body>
|
|
61
|
+
</html>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The three stylesheets are the whole CSS contract. `tokens.css` defines every colour, space and font the rules read, `reset.css` is the reset, and `flexdesk.css` is the library. It reads the tokens but does not define them, so without `tokens.css` its colours, spacing and fonts are undefined. See [Theming and CSS](concepts/styling.md#styling).
|
|
65
|
+
|
|
66
|
+
FlexDesk does not ship the icon font (Material Symbols Outlined). Load it from Google Fonts or self-host it. Nothing else breaks without it.
|
|
67
|
+
|
|
68
|
+
## Hello, shell
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
import { createShell, createTaxonomy, createEntityCatalog } from '@flexdesk/wm';
|
|
72
|
+
import { createHost } from '@flexdesk/host';
|
|
73
|
+
import { DataTable } from '@flexdesk/widgets';
|
|
74
|
+
|
|
75
|
+
const store = new Map();
|
|
76
|
+
|
|
77
|
+
const shell = await createShell({
|
|
78
|
+
root: document.getElementById('app'),
|
|
79
|
+
|
|
80
|
+
// YOUR ontology. FlexDesk has none.
|
|
81
|
+
taxonomy: createTaxonomy({
|
|
82
|
+
root: 'cosmos',
|
|
83
|
+
kinds: {
|
|
84
|
+
cosmos: { label: 'Cosmos', icon: 'public', isTopNav: true, sources: ['planet'] },
|
|
85
|
+
planet: { label: 'Planet', icon: 'circle' },
|
|
86
|
+
moon: {
|
|
87
|
+
label: 'Moon',
|
|
88
|
+
// "jupiter/europa" — your naming convention, not the library's.
|
|
89
|
+
ancestors: ({ id }) => {
|
|
90
|
+
const i = String(id).indexOf('/');
|
|
91
|
+
return i > 0 ? [{ kind: 'planet', id: String(id).slice(0, i) }] : [];
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
|
|
97
|
+
entities: createEntityCatalog({ sources: [/* what the palette searches */] }),
|
|
98
|
+
content: { planet: (hostEl, props) => { /* render a tile */ } },
|
|
99
|
+
|
|
100
|
+
// Where persistence goes. An in-memory Map is a perfectly good host.
|
|
101
|
+
host: createHost({
|
|
102
|
+
state: {
|
|
103
|
+
read: async (k) => store.get(k) ?? null,
|
|
104
|
+
write: async (k, v) => (store.set(k, v), true),
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
|
|
108
|
+
// Chrome is ELEMENTS, not selectors. The library never guesses at your markup.
|
|
109
|
+
chrome: { topNav: document.getElementById('nav') },
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A working version of exactly this — planets and moons, a mock host, no backend —
|
|
114
|
+
lives in `demo/`.
|
|
115
|
+
|
|
116
|
+
Save that as `main.js`. What each part is:
|
|
117
|
+
|
|
118
|
+
- **`taxonomy`** is your ontology: the kinds of thing your application has and how they nest. FlexDesk has none of its own.
|
|
119
|
+
- **`entities`** is what the command palette (Ctrl+K) searches.
|
|
120
|
+
- **`content`** is one factory per kind. It renders that kind into a tile.
|
|
121
|
+
- **`host`** is where persistence goes. An in-memory `Map` is a perfectly good host; [the host port](concepts/host-port.md#the-host-port) covers the real ones.
|
|
122
|
+
- **`chrome`** is the elements the shell paints into, not selectors. The library never guesses at your markup.
|
|
123
|
+
|
|
124
|
+
## Put a table in a tile
|
|
125
|
+
|
|
126
|
+
Give the root kind a content factory that mounts a [DataTable](components/data-table.md). `DataTable` is in `@flexdesk/widgets`, which the first snippet already imports.
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
const PLANETS = [
|
|
130
|
+
['Mercury', '0', '0.39'],
|
|
131
|
+
['Venus', '0', '0.72'],
|
|
132
|
+
['Earth', '1', '1.00'],
|
|
133
|
+
['Mars', '2', '1.52'],
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const planetsTable = (hostEl) => {
|
|
137
|
+
// A tile body is a flex column. Give the table a box that fills it, in normal flow:
|
|
138
|
+
// absolute positioning would let the table escape the tile.
|
|
139
|
+
const box = document.createElement('div');
|
|
140
|
+
box.style.cssText = 'height:100%; display:flex; flex-direction:column; min-height:0;';
|
|
141
|
+
hostEl.appendChild(box);
|
|
142
|
+
DataTable.mount(box, {
|
|
143
|
+
headers: ['Planet', 'Moons', 'Distance (AU)'],
|
|
144
|
+
rows: PLANETS,
|
|
145
|
+
sortable: true,
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// …and in createShell({ … }), in place of the planet factory:
|
|
150
|
+
content: {
|
|
151
|
+
cosmos: planetsTable,
|
|
152
|
+
// The three panels ask for content by these names. Without a factory they show a placeholder.
|
|
153
|
+
'panel:left': (hostEl) => { hostEl.textContent = 'Left panel'; },
|
|
154
|
+
'panel:right': (hostEl) => { hostEl.textContent = 'Right panel'; },
|
|
155
|
+
'panel:bottom': (hostEl) => { hostEl.textContent = 'Bottom panel'; },
|
|
156
|
+
},
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`cosmos` is the root kind and a top-nav kind, so the shell opens it first. Click a header to sort. The placeholder a panel shows when nothing is registered for it reads "no factory registered yet": the registry working, though it looks like a bug.
|
|
160
|
+
|
|
161
|
+
## See the full demo
|
|
162
|
+
|
|
163
|
+
`demo/` in the repository is a complete version of this page: planets and moons, an in-memory host, panels, desktops and the command palette, with no backend. It loads `dist/`, and ES modules do not load from `file://`, so serve the repository root with any static file server and open `/demo/`:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
git clone https://github.com/nor-os/FlexDesk.git
|
|
167
|
+
cd FlexDesk
|
|
168
|
+
python3 -m http.server 8080 # or any static server
|
|
169
|
+
# then open http://localhost:8080/demo/
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The other pages in `demo/` are one-component pages: `datatable_070.html`, `records.html`, `flow_canvas.html`, `flow_outline.html`, `flow_lanes.html`, `flow_kit.html` and `flow_modeler.html`.
|
|
173
|
+
|
|
174
|
+
## Where next
|
|
175
|
+
|
|
176
|
+
- **A component.** Each has a page: [Shell and navigation](components/shell.md), [DataTable](components/data-table.md), [Forms and dialogs](components/forms-and-dialogs.md), and the rest in the [component index](README.md#components).
|
|
177
|
+
- **How the pieces fit.** [Packages and entry points](concepts/packages-and-entry-points.md), the [host port](concepts/host-port.md), [domain neutrality](concepts/domain-neutrality.md) and [theming](concepts/styling.md).
|
|
178
|
+
- **Doing it well.** [Best practices](best-practices.md).
|
|
179
|
+
- **What changed.** The [changelog](../CHANGELOG.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flexdesk",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.5",
|
|
4
4
|
"description": "A tiling window manager and widget set for desktop-class web applications. Zero runtime dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"canvas.js",
|
|
29
29
|
"flow.js",
|
|
30
30
|
"css",
|
|
31
|
+
"docs",
|
|
31
32
|
"README.md",
|
|
32
33
|
"CHANGELOG.md",
|
|
33
34
|
"LICENSE",
|
|
@@ -107,7 +107,8 @@ class DataSeriesPlotWindow {
|
|
|
107
107
|
this._config = { ...saved };
|
|
108
108
|
this._config.yColumns = (saved.yColumns || []).filter(c => this._numericColumns.has(c.header));
|
|
109
109
|
} else {
|
|
110
|
-
this._config = DataSeriesPlotWindow.createDefaultConfig(
|
|
110
|
+
this._config = DataSeriesPlotWindow.createDefaultConfig(
|
|
111
|
+
this.headers, this._numericColumns, this.classifyHeader);
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
this.#createWindow();
|
|
@@ -694,13 +695,26 @@ class DataSeriesPlotWindow {
|
|
|
694
695
|
|
|
695
696
|
// ── Default config factory ───────────────────────────────────────────
|
|
696
697
|
|
|
697
|
-
|
|
698
|
+
/**
|
|
699
|
+
* The config a series with no saved one opens with: the first numeric
|
|
700
|
+
* column `classifyHeader` calls `'time'` is X (else the first numeric
|
|
701
|
+
* column), and up to MAX_AUTO_COLUMNS other numeric columns are Y.
|
|
702
|
+
*
|
|
703
|
+
* Static, so it has no window: the header classifier is a parameter. A
|
|
704
|
+
* window passes its own (`options.classifyHeader`); a caller that passes
|
|
705
|
+
* none gets the default, which knows `t`/`time`/`step`/`period`.
|
|
706
|
+
*
|
|
707
|
+
* @param {string[]} headers
|
|
708
|
+
* @param {Set<string>} [numericColumns] - absent => every column is numeric
|
|
709
|
+
* @param {(header: string) => string} [classifyHeader=defaultClassifyHeader]
|
|
710
|
+
*/
|
|
711
|
+
static createDefaultConfig(headers, numericColumns, classifyHeader = defaultClassifyHeader) {
|
|
698
712
|
const isNumeric = numericColumns instanceof Set ? (h) => numericColumns.has(h) : () => true;
|
|
699
713
|
|
|
700
714
|
// Find best X column (time-like, must be numeric)
|
|
701
715
|
let xColumn = '';
|
|
702
716
|
for (const h of headers) {
|
|
703
|
-
if (isNumeric(h) &&
|
|
717
|
+
if (isNumeric(h) && classifyHeader(h) === 'time') {
|
|
704
718
|
xColumn = h;
|
|
705
719
|
break;
|
|
706
720
|
}
|
|
@@ -58,7 +58,7 @@ export class SlideOutPanel extends ComponentBase {
|
|
|
58
58
|
header.className = 'twm-slide-out-panel__header';
|
|
59
59
|
|
|
60
60
|
this.#headerIcon = document.createElement('span');
|
|
61
|
-
this.#headerIcon.className = 'material-symbols-outlined';
|
|
61
|
+
this.#headerIcon.className = 'twm-slide-out-panel__icon material-symbols-outlined';
|
|
62
62
|
|
|
63
63
|
this.#headerTitle = document.createElement('span');
|
|
64
64
|
this.#headerTitle.className = 'twm-slide-out-panel__title';
|
|
@@ -206,13 +206,24 @@ export class SlideOutPanel extends ComponentBase {
|
|
|
206
206
|
* engine that predates it), and the width subtracted is the panel's
|
|
207
207
|
* laid-out one, which `min-width` may hold above the requested
|
|
208
208
|
* width. Rounding happens in screen pixels, so at 100% the output is
|
|
209
|
-
* exactly what it always was.
|
|
209
|
+
* exactly what it always was.
|
|
210
|
+
*
|
|
211
|
+
* NEVER WIDER THAN THE HOST (BUG-0035). The right edge is pinned, so a
|
|
212
|
+
* panel wider than its host (700px by default, `min-width: 400px`)
|
|
213
|
+
* reached past the host's left edge across the neighbouring pane. The
|
|
214
|
+
* host's width, in the panel's CSS pixels, goes on the panel as
|
|
215
|
+
* `--twm-slide-out-host-w`, which the stylesheet uses as `max-width`
|
|
216
|
+
* and as the ceiling of the 400px `min-width`; it is set BEFORE the
|
|
217
|
+
* width is read so the read sees the capped layout, and the width is
|
|
218
|
+
* capped here too for an engine that lays nothing out. */
|
|
210
219
|
#trackGeometry = () => {
|
|
211
220
|
if (!this.#open || !this.#panel || !this.#container) return;
|
|
212
221
|
const r = this.#container.getBoundingClientRect();
|
|
213
222
|
const zoom = this.#panel.currentCSSZoom || 1;
|
|
214
|
-
const
|
|
223
|
+
const hostWidth = r.width / zoom;
|
|
215
224
|
const s = this.#panel.style;
|
|
225
|
+
s.setProperty('--twm-slide-out-host-w', `${hostWidth}px`);
|
|
226
|
+
const width = Math.min(this.#panel.offsetWidth || this.#width, hostWidth);
|
|
216
227
|
s.position = 'fixed';
|
|
217
228
|
s.top = `${Math.round(r.top) / zoom}px`;
|
|
218
229
|
s.left = `${Math.round(r.right - width * zoom) / zoom}px`;
|
|
@@ -58,12 +58,15 @@ export function thumbGeometry(scrollSize, clientSize, scrollPos, trackLen) {
|
|
|
58
58
|
*
|
|
59
59
|
* @param {Element} el
|
|
60
60
|
* @param {Element[][]} stacks what `elementsFromPoint` gave at each point,
|
|
61
|
-
* topmost first. The overlay layer's own bars are skipped
|
|
61
|
+
* topmost first. The overlay layer's own bars are skipped, and so are the
|
|
62
|
+
* window's `.twm-resize-handle` edge strips (BUG-0036): they lie over every
|
|
63
|
+
* pane flush against a window edge and are chrome, not cover.
|
|
62
64
|
* @returns {boolean}
|
|
63
65
|
*/
|
|
64
66
|
export function barCovered(el, stacks) {
|
|
67
|
+
const passThrough = (h) => h.closest?.('.twm-autoscroll-layer') || h.closest?.('.twm-resize-handle');
|
|
65
68
|
return stacks.some((stack) => {
|
|
66
|
-
const top = (stack || []).find((h) => !h
|
|
69
|
+
const top = (stack || []).find((h) => !passThrough(h));
|
|
67
70
|
return Boolean(top) && top !== el && !el.contains(top);
|
|
68
71
|
});
|
|
69
72
|
}
|
|
@@ -277,6 +280,16 @@ export function installAutoScrollbars(root = document.body, { exclude = null } =
|
|
|
277
280
|
cancelAnimationFrame(raf);
|
|
278
281
|
raf = requestAnimationFrame(() => { for (const el of managed.keys()) place(el); });
|
|
279
282
|
};
|
|
283
|
+
// An element taken out of the page (a managed window closed) takes its
|
|
284
|
+
// bars with it at once. They live in the layer, not in the element, so
|
|
285
|
+
// otherwise they would stay on screen through the hide delay and fade.
|
|
286
|
+
const sweep = new MutationObserver((mutations) => {
|
|
287
|
+
if (!managed.size || !mutations.some((m) => m.removedNodes.length)) return;
|
|
288
|
+
for (const el of [...managed.keys()]) {
|
|
289
|
+
if (!el.isConnected) { drop(el); hoverChain.delete(el); }
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
sweep.observe(root, { childList: true, subtree: true });
|
|
280
293
|
|
|
281
294
|
doc.addEventListener('mouseover', onOver, true);
|
|
282
295
|
doc.addEventListener('mouseout', onLeaveWindow, true);
|
|
@@ -293,6 +306,7 @@ export function installAutoScrollbars(root = document.body, { exclude = null } =
|
|
|
293
306
|
doc.removeEventListener('scroll', onScroll, true);
|
|
294
307
|
win.removeEventListener('resize', onResize);
|
|
295
308
|
doc.removeEventListener('focusin', onResize, true);
|
|
309
|
+
sweep.disconnect();
|
|
296
310
|
for (const el of [...managed.keys()]) drop(el);
|
|
297
311
|
root.classList.remove(ROOT_CLASS);
|
|
298
312
|
},
|