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,127 @@
|
|
|
1
|
+
# Panels and desktops
|
|
2
|
+
|
|
3
|
+
> **Package:** `@flexdesk/wm` · **See also:** [Shell and navigation](shell.md) · [All docs](../README.md)
|
|
4
|
+
|
|
5
|
+
The shell's left, right and bottom panels, and its virtual desktops. A kind says which panels a page offers; each desktop remembers how large the user dragged them; `mountNavPanel` builds a navigation panel you can drive from code; and an app that keeps desktops of its own can mark them so the desktop bar treats them as the app's.
|
|
6
|
+
|
|
7
|
+
**On this page**
|
|
8
|
+
|
|
9
|
+
- [Panels a page does not offer (0.17.0)](#panels-a-page-does-not-offer-0170)
|
|
10
|
+
- [Panels keep the size you dragged them to (0.17.0)](#panels-keep-the-size-you-dragged-them-to-0170)
|
|
11
|
+
- [A navigation panel you drive yourself: `mountNavPanel` (0.17.0)](#a-navigation-panel-you-drive-yourself-mountnavpanel-0170)
|
|
12
|
+
- [Your desktops beside the app's (0.10.0)](#your-desktops-beside-the-apps-0100)
|
|
13
|
+
|
|
14
|
+
## Panels a page does not offer (0.17.0)
|
|
15
|
+
|
|
16
|
+
A kind says which panels it offers while it is the primary tile. Each side is
|
|
17
|
+
`true`, `false` or `{ reason }`; a side it does not name is offered, and a kind
|
|
18
|
+
that does not name a side takes it from its top-nav kind, so one declaration on
|
|
19
|
+
a page covers every record opened under it:
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
kinds: {
|
|
23
|
+
paper: { label: 'Paper', isTopNav: true, panels: { bottom: { reason: 'No console here' } } },
|
|
24
|
+
sheet: { label: 'Sheet', topNav: 'paper' }, // withholds it too
|
|
25
|
+
slide: { label: 'Slide', topNav: 'paper', panels: { bottom: true } }, // offers it
|
|
26
|
+
},
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
While a panel is withheld it is not in the layout, `wm.togglePanel(side)`
|
|
30
|
+
answers `false` and changes nothing (nor what is saved), `wm.isPanelOpen(side)`
|
|
31
|
+
is `false`, and the shell's toggle button and the palette chip carry
|
|
32
|
+
`aria-disabled="true"` with the reason as their tooltip (`data-tooltip` on a
|
|
33
|
+
`twm-has-tooltip` button, `title` otherwise; the button's own comes back after).
|
|
34
|
+
The desktop still remembers the panel was open, and it comes back open when the
|
|
35
|
+
user returns to a page that offers it. With no declarations every panel is
|
|
36
|
+
offered, exactly as before.
|
|
37
|
+
|
|
38
|
+
Content overrides its kind at runtime, until it clears the override with `null`:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
ctx.wm.setPanelAvailability('bottom', { reason: 'Run the model first' });
|
|
42
|
+
ctx.wm.setPanelAvailability('bottom', null); // e.g. in destroy()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`wm.isPanelAvailable(side)` and `wm.getPanelAvailability(side)` (`{ available,
|
|
46
|
+
reason }`) read it, and a change is announced once — whatever caused it and
|
|
47
|
+
however many sides it moved — to `wm.onPanelAvailabilityChange(fn)` (which
|
|
48
|
+
returns the unsubscribe) and on the bus as `wm:panels:availability`, both with
|
|
49
|
+
`{ panels, changed }`. `togglePanel` now answers `true` when it flipped a panel.
|
|
50
|
+
|
|
51
|
+
`PanelStateMachine`, `PANEL_STATES` and `PANEL_TRANSITIONS` are gone from
|
|
52
|
+
`@flexdesk/widgets`: nothing in the library used them, and which panels a page
|
|
53
|
+
offers is now said once, in the taxonomy.
|
|
54
|
+
|
|
55
|
+
`tests/panel_availability.test.mjs` drives all of it against a real shell.
|
|
56
|
+
|
|
57
|
+
## Panels keep the size you dragged them to (0.17.0)
|
|
58
|
+
|
|
59
|
+
Each desktop remembers every panel's share of the window-manager area along
|
|
60
|
+
its axis (width for left and right, height for bottom), taken when a splitter
|
|
61
|
+
drag ends, and saves it with the layout as `panelShares`. A reload, a switch to
|
|
62
|
+
another desktop and back, and toggling one panel all rebuild the panels at
|
|
63
|
+
those shares, and a closed panel comes back at the size it had. Only a panel
|
|
64
|
+
that has never been sized on that desktop opens at the default 1:4. No split is
|
|
65
|
+
built tighter than a drag allows (`TileRenderer.MIN_SLOT_FRACTION` of it per
|
|
66
|
+
slot). A `TileRenderer` built by hand takes `onSplitResized(splitId)`, called
|
|
67
|
+
once when a drag that changed a split's sizes ends.
|
|
68
|
+
|
|
69
|
+
`tests/panel_sizes_kept.test.mjs` covers each of these.
|
|
70
|
+
|
|
71
|
+
## A navigation panel you drive yourself: `mountNavPanel` (0.17.0)
|
|
72
|
+
|
|
73
|
+
`mountNavPanel(hostEl, { modes })` (from `@flexdesk/wm`) builds a mode strip, a
|
|
74
|
+
shared filter box and one lazily-mounted pane per mode. A strip click is no
|
|
75
|
+
longer the only way to switch: the handle's `select(modeId)` takes the same
|
|
76
|
+
path — it mounts the mode on first use, runs its `activate()`, restores that
|
|
77
|
+
mode's filter text — and its promise resolves once `activate()` has run. An
|
|
78
|
+
unknown id is ignored. `active()` names the mode shown.
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
const nav = mountNavPanel(hostEl, {
|
|
82
|
+
modes: [
|
|
83
|
+
{ id: 'files', icon: 'folder', label: 'Files', mount: mountFiles },
|
|
84
|
+
{ id: 'runs', icon: 'history', label: 'Runs', mount: mountRuns,
|
|
85
|
+
filter: false }, // a pane with controls of its own
|
|
86
|
+
],
|
|
87
|
+
showModeStrip: false, // no strip: the app switches with select()
|
|
88
|
+
initialMode: 'runs', // the mode shown at mount (default: the first)
|
|
89
|
+
});
|
|
90
|
+
await nav.select('files'); // mounted and activated
|
|
91
|
+
nav.active(); // 'files'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
A mode with `filter: false` hides the filter box while it is active, and its
|
|
95
|
+
pane is never handed a query; the next mode with a filter shows its own saved
|
|
96
|
+
text again. `showFilter: false` renders no filter box at all. All three options
|
|
97
|
+
default to the old behaviour, so a panel built without them renders exactly as
|
|
98
|
+
before.
|
|
99
|
+
|
|
100
|
+
## Your desktops beside the app's (0.10.0)
|
|
101
|
+
|
|
102
|
+
Pass `chrome.desktops` and the shell paints a desktop bar: a chip per desktop
|
|
103
|
+
and a `+`. An app that keeps desktops of its own (one per page, say) marks them
|
|
104
|
+
on the desktop objects themselves:
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
const shell = await createShell({ /* … */ eventBus,
|
|
108
|
+
chrome: { desktops: barEl }, desktopBar: { switchOnAdd: true } });
|
|
109
|
+
shell.wm.desktops.ensureCount(pages.length);
|
|
110
|
+
shell.wm.desktops.desktops.slice(0, pages.length)
|
|
111
|
+
.forEach((d) => { d.group = 'Workbench'; d.fixed = true; });
|
|
112
|
+
eventBus.emit('wm:changed', {}); // repaint the bar
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Desktops that share a **`group`** are one chip, labelled by the group, and one
|
|
116
|
+
stop for Ctrl+Left/Right; the chip goes back to the member that was on screen
|
|
117
|
+
last (`wm.desktops.chips()` says what the bar draws). A **`fixed`** desktop is
|
|
118
|
+
the app's: the bar offers no rename and no delete, no menu offers it as a
|
|
119
|
+
*Move to desktop* target, and `removeDesktop`, `moveFocusedToDesktop` and
|
|
120
|
+
`moveWindowToDesktop` refuse it. Both are persisted. A desktop the user adds is
|
|
121
|
+
numbered among the ungrouped ones, and `desktopBar.switchOnAdd` puts it on
|
|
122
|
+
screen. With neither set anywhere, the bar is what it always was.
|
|
123
|
+
|
|
124
|
+
*Move to desktop* now carries every tab of the pane and adds them to the target's
|
|
125
|
+
main pane; it used to replace that pane with the active tab alone. A target still
|
|
126
|
+
holding only its seed is replaced, as before. `dispose()` removes the bar, so a
|
|
127
|
+
rebuilt shell paints a fresh one.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Record lists and fact sheets
|
|
2
|
+
|
|
3
|
+
> **Package:** `@flexdesk/widgets` · **For a table of rows:** [DataTable](data-table.md) · [All docs](../README.md)
|
|
4
|
+
|
|
5
|
+
Two read-only components for the parts of an application page that are not a table of rows. `createRecordList` draws a list of a few records, each a name with a quiet line of facts, pills and verbs beside them, and a state. `createFactTable` draws a sheet of facts, a name and its value row after row, and can show a before and an after. Neither needs a height: each is as tall as its contents, in any parent.
|
|
6
|
+
|
|
7
|
+
## Record lists and fact sheets (0.7.0)
|
|
8
|
+
|
|
9
|
+
Two read-only components for the parts of an application page that are not a
|
|
10
|
+
table of rows. Both are new: nothing that exists changes, and a page that does
|
|
11
|
+
not call them looks as it did. Neither needs a height: each is exactly as tall
|
|
12
|
+
as its contents, in any parent.
|
|
13
|
+
|
|
14
|
+
**`createRecordList(host, config)`** draws a list of records, each as a name
|
|
15
|
+
with a quiet line of facts under it, pills and verbs beside them, and a state.
|
|
16
|
+
It is for the administration list of a few records with their own actions
|
|
17
|
+
(keys, connections, deliveries, members), where a table would be the wrong
|
|
18
|
+
shape.
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import { createRecordList } from '@flexdesk/widgets';
|
|
22
|
+
|
|
23
|
+
const list = createRecordList(host, {
|
|
24
|
+
records: keys,
|
|
25
|
+
describe: (k) => ({
|
|
26
|
+
name: k.name,
|
|
27
|
+
facts: [k.key_id, k.role, k.expires ? `expires ${k.expires}` : 'never expires'],
|
|
28
|
+
pills: [k.paused ? { text: 'paused', tone: 'warn' } : { text: 'live', tone: 'success' }],
|
|
29
|
+
state: k.revoked ? 'off' : k.paused ? 'paused' : 'active',
|
|
30
|
+
verbs: [
|
|
31
|
+
{ label: k.paused ? 'Resume' : 'Pause', onClick: (key) => toggle(key),
|
|
32
|
+
refusal: k.revoked ? 'This key is revoked.' : null },
|
|
33
|
+
{ label: 'Revoke', danger: true, onClick: (key) => revoke(key) },
|
|
34
|
+
],
|
|
35
|
+
}),
|
|
36
|
+
getKey: (k) => k.key_id,
|
|
37
|
+
rowAttrs: (k) => ({ 'data-key-id': k.key_id }),
|
|
38
|
+
onActivate: (k) => open(k), // one click, or Enter
|
|
39
|
+
activeKey: openKeyId,
|
|
40
|
+
emptyState: 'No keys yet.',
|
|
41
|
+
});
|
|
42
|
+
list.setRecords(next); // focus stays on the same record's same verb
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
What `describe(record, idx)` returns (without `describe`, each record is
|
|
46
|
+
this object itself):
|
|
47
|
+
|
|
48
|
+
| Field | What it is |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `name` | The first line: text or a node. `mono: true` draws it in the monospace face. |
|
|
51
|
+
| `facts` | The quiet line: text, a node, or an array whose `null`, `false` and `''` parts are left out and the rest joined with `factSeparator` (`' — '`). A part may be a node. |
|
|
52
|
+
| `pills` | Strings or `{ text, tone, title }`. The tones drawn are `muted`, `info`, `success`, `warn` and `danger`. |
|
|
53
|
+
| `verbs` | `{ label, onClick(record, ev), refusal, danger, primary, icon, title, name }`. A verb with a `refusal` is a disabled button with the reason on its `title` and `data-refusal`, **and the reason is a line of text under the facts** (`twm-record__refusal`, named by `data-refusal-for`, which the button is `aria-describedby`). A reason shared by every verb is said once. `null` and `false` verbs are left out. |
|
|
54
|
+
| `state` | A word: `data-state` and `twm-record--state-<word>` on the row. `paused`, `off` and `dead` dim the words and pills but never the verbs; `dead` and `error` mark the left edge in the danger colour. |
|
|
55
|
+
| `error` | A line in the error colour under the facts. |
|
|
56
|
+
| `key` | The record's key, over `getKey`. |
|
|
57
|
+
|
|
58
|
+
| Option | What it does |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `getKey(record, idx)` | The key (`data-record-key`). Defaults to `spec.key`, then `record.key`, then `record.id`, then the index. Keys compare as strings. |
|
|
61
|
+
| `onActivate(record, idx, ev)` | Opens a record. It follows `DataTable`'s `onRowActivate` rule (`row_activation.js`): not for a press on a verb or any control, not for the click that ends a drag selecting text in the list, not for the second click of a double-click. Rows get a pointer and a hover. |
|
|
62
|
+
| `activateOn` | `'click'`, `'dblclick'`, `'enter'`, or an array. Default `['click', 'enter']`. |
|
|
63
|
+
| `keyboard` | Defaults to on exactly when `onActivate` is set. The records are then one tab stop (the last focused, else the open one, else the first), each enabled verb keeps its own, ArrowUp/ArrowDown move between records (from a record or one of its verbs), Home/End go to the ends, and Enter on a record opens it. |
|
|
64
|
+
| `activeKey`, `setActiveKey(key)`, `getActiveKey()` | The record that is open: `twm-record--active` and `aria-current="true"`, drawn as an accent on the left edge. It survives `setRecords`. |
|
|
65
|
+
| `rowClass(record, idx)`, `rowAttrs(record, idx)` | Classes and attributes for the record's `<li>`, as `DataTable`'s (`null`/`false` leaves an attribute off, `true` sets it empty). |
|
|
66
|
+
| `emptyState` | Text, a node, or a function returning either, shown when there are no records. |
|
|
67
|
+
| `facts: 'ellipsis'` | Keeps every fact line to one line and puts the whole line on its `title`. By default a long fact line wraps. |
|
|
68
|
+
| `verbClass` | A class string, or `(verb) => string`, for a consumer with its own buttons. By default verbs are FlexDesk's `twm-btn`, with `--primary` and `--danger`. |
|
|
69
|
+
| `label`, `className` | The list's `aria-label`; a class on the root. |
|
|
70
|
+
|
|
71
|
+
Methods: `setRecords(records)` (keeps the focus on the same record's same verb,
|
|
72
|
+
by key), `getRecords()`, `refresh(key)` (asks `describe` again for that record
|
|
73
|
+
only; with no key, all), `getItem(key)`, `focusRecord(key)`, `dispose()`, and
|
|
74
|
+
`element` (the root).
|
|
75
|
+
|
|
76
|
+
**`createFactTable(host, config)`** draws a sheet of facts: a name and its
|
|
77
|
+
value, row after row. A `DataTable` is the wrong tool for this, because it
|
|
78
|
+
draws a header row, clips a long value to one line and needs a box with a
|
|
79
|
+
height. A fact sheet has no header row, never truncates (a value wraps at
|
|
80
|
+
spaces, inside a long word or URL when it must, and keeps its own line breaks),
|
|
81
|
+
and is as tall as its rows. The label column stays on one line.
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
import { createFactTable } from '@flexdesk/widgets';
|
|
85
|
+
|
|
86
|
+
createFactTable(host, {
|
|
87
|
+
rows: [
|
|
88
|
+
{ label: 'Server', value: 'db-main-01.internal' },
|
|
89
|
+
{ label: 'Connection string', value: dsn, mono: true },
|
|
90
|
+
{ label: 'Options', value: { pool: { min: 2, max: 16 } } }, // drawn as indented JSON
|
|
91
|
+
{ label: 'Replica', value: null },
|
|
92
|
+
],
|
|
93
|
+
nullDisplay: '—',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Before and after:
|
|
97
|
+
createFactTable(host, {
|
|
98
|
+
compare: true,
|
|
99
|
+
headers: ['Field', 'Now', 'After the restore'],
|
|
100
|
+
rows: [{ label: 'Amount', before: 1200, after: 950 }, { label: 'Reviewer', after: 'Bea Beta' }],
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
| Option | What it does |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `rows` | Objects `{ label, value }` (or `{ label, before, after }` with `compare`), with optional `mono`, `numeric`, `labelTitle` (a tooltip on the label), `key` (`data-key`) and `status`; or arrays of cells, `[label, value, …]`. A value is text, a number, a node, or an object (drawn as indented JSON). |
|
|
107
|
+
| `compare: true` | Each row draws its label, its before, an arrow and its after. A row is `twm-facts__row--changed`, `--same`, `--added` (no `before`) or `--removed` (no `after`), worked out from the two sides (objects by their JSON, a node always changed) unless the row's `status` says otherwise; `data-status` says the same. An absent side reads as `missing` (`'—'`). A row with a plain `value` spans both sides. |
|
|
108
|
+
| `headers` | Adds a header row (there is none by default). With `compare`, three names stand over the label, before and after columns. |
|
|
109
|
+
| `numeric` | Column indices drawn in tabular numerals (`twm-facts__num`), header included. |
|
|
110
|
+
| `mono` | Every value in the monospace face. A row's own `mono` does it for that row. |
|
|
111
|
+
| `nullDisplay` | What `null` and `undefined` read as (`twm-facts__null`). By default the cell is empty. |
|
|
112
|
+
| `labelColumn: false` | The first column is ordinary data, not a label. |
|
|
113
|
+
| `wrapLabels: true` | Lets a long label wrap. |
|
|
114
|
+
| `wrap` | How a value wraps. `'anywhere'` (the default) wraps at spaces, and inside a long word or URL when it must. `'words'` wraps at spaces only, so a long word stays whole and the sheet grows wider; use it inside a sideways scroller. `'none'` keeps only the value's own line breaks. |
|
|
115
|
+
| `rowClass(row, idx)`, `rowAttrs(row, idx)` | Classes and attributes for each `<tr>`. |
|
|
116
|
+
| `emptyState`, `label`, `className` | Shown (spanning the sheet) when there are no rows; the `aria-label`; a class on the `<table>`. |
|
|
117
|
+
|
|
118
|
+
Methods: `setRows(rows)`, `getRows()`, `getRow(idx)` (the `<tr>`), `dispose()`,
|
|
119
|
+
and `element` (the `<table>`).
|
|
120
|
+
|
|
121
|
+
Both were measured in headless Edge with real input (`demo/records.html`,
|
|
122
|
+
driven by `demo/records_probe.mjs`): no list, record, sheet or row has zero
|
|
123
|
+
height and none overflows its card; a long fact line, a long URL, a connection
|
|
124
|
+
string and a JSON value wrap inside their column while labels stay on one line;
|
|
125
|
+
every verb, refused ones included, is what `elementFromPoint` finds at its
|
|
126
|
+
centre, and a real press runs it without opening the record; real Tab,
|
|
127
|
+
ArrowDown and Enter open a record; at 300px the verbs go under the record's
|
|
128
|
+
words, inside the card.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Settings page
|
|
2
|
+
|
|
3
|
+
> **Package:** `@flexdesk/widgets` (the store is in `@flexdesk/core`) · [All docs](../README.md)
|
|
4
|
+
|
|
5
|
+
`SettingsView` draws a settings store's schema as a page: a category list, a filter, and the settings themselves. `SettingsCategories` draws the sidebar parts for a view somewhere else, such as an app's left navigation. Where the settings are *saved* is a separate choice, covered under [storage keys](../concepts/domain-neutrality.md#storage-keys-and-names-are-yours-0170).
|
|
6
|
+
|
|
7
|
+
## A settings page: `SettingsView` (0.17.0)
|
|
8
|
+
|
|
9
|
+
`new SettingsView({ store, eventBus })` (in `@flexdesk/widgets`) draws a
|
|
10
|
+
settings store's schema as a page. `store` is a `createSettingsStore()` handle
|
|
11
|
+
or the app-global binding's own, `getSettingsStore()` from `@flexdesk/core`
|
|
12
|
+
(its `set` is the one `setSetting` calls); `eventBus` is the bus that store
|
|
13
|
+
emits on (`registerSettingsEventBus(bus)` for the binding).
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { getSettingsStore, registerSettingsEventBus } from '@flexdesk/core';
|
|
17
|
+
import { SettingsView, settingsViewContent } from '@flexdesk/widgets';
|
|
18
|
+
|
|
19
|
+
registerSettingsEventBus(bus);
|
|
20
|
+
const view = new SettingsView({ store: getSettingsStore(), eventBus: bus });
|
|
21
|
+
view.mount(host);
|
|
22
|
+
view.selectCategory('window');
|
|
23
|
+
view.setQuery('shadow'); // what the filter box does
|
|
24
|
+
await view.resetAll(); // asks first; true when it reset
|
|
25
|
+
view.dispose();
|
|
26
|
+
|
|
27
|
+
// or as tile content (props.category picks the category shown first):
|
|
28
|
+
createShell({ content: { settings: settingsViewContent({ store: getSettingsStore(), eventBus: bus }) } });
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The sidebar holds a filter box, every category in `CategoryDef.order` and
|
|
32
|
+
*Reset all*. Beside it the chosen category's settings are listed under its
|
|
33
|
+
title, description and *Reset category*, grouped by `SettingDef.group` in
|
|
34
|
+
schema order. A row shows the label, a *Modified* marker while the value
|
|
35
|
+
differs from its default, the description, the path, its control and a reset
|
|
36
|
+
button. Text in the filter searches every category's labels, descriptions and
|
|
37
|
+
paths; the matches are listed under "Category › Group", and choosing a
|
|
38
|
+
category clears it.
|
|
39
|
+
|
|
40
|
+
| `type` | Control |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `boolean` | a `ToggleField` |
|
|
43
|
+
| `select` | a select over `options`; a value the options lack (stored, or set elsewhere while the view is open) is kept and shown as *value (not found)* |
|
|
44
|
+
| `number` | a number box with `min`, `max` and `step`; a written value is always in the range and on the step grid (`min + k·step`, or `k·step` with no `min`): snapped to the nearest grid point, and one outside the range moved inward to the nearest grid point inside it (with `max` below `min` the range holds none, and the value is `min`); with no `step` it is only clamped; an empty or unreadable box goes back to the value |
|
|
45
|
+
| `text` | a text box, written `textDebounceMs` (default `SETTINGS_TEXT_DEBOUNCE_MS`, 300) after the last keystroke, or at once on Enter or blur; emptied, it writes `null` when the default is `null` |
|
|
46
|
+
| `colorList` | a `ColorListField`, written on each committed edit while every row holds a colour |
|
|
47
|
+
|
|
48
|
+
A type it does not know shows the value as text, read only.
|
|
49
|
+
|
|
50
|
+
The view is live both ways: a control writes with `store.set`, and the view
|
|
51
|
+
listens for `settings:<path>:changed` on every schema path, `settings:changed`
|
|
52
|
+
with a null path (the resets) and `settings:registered`, which the store now
|
|
53
|
+
emits from `extend()` / `registerSettings()` with `{ paths, categories }`. So a
|
|
54
|
+
`setSetting` made anywhere moves the open view, and a slice registered after
|
|
55
|
+
mount adds its categories and rows without a remount. A row's reset button
|
|
56
|
+
resets that one setting; *Reset category* and *Reset all* first ask
|
|
57
|
+
`confirm({ title, message, confirmLabel, danger })` (default `openConfirm`) and
|
|
58
|
+
change nothing unless it resolves true. Every label, description, category name
|
|
59
|
+
and query is set as text, so markup in any of them is shown as typed.
|
|
60
|
+
|
|
61
|
+
Options: `store`, `eventBus` (both required), `confirm`, `category`,
|
|
62
|
+
`textDebounceMs`, `title` (the sidebar heading, and the content factory's tab
|
|
63
|
+
title; default "Settings"). The markup is `.twm-settings` >
|
|
64
|
+
`.twm-settings__sidebar` + `.twm-settings__content`, with rows
|
|
65
|
+
`.twm-settings__row` (`--modified`); `flexdesk.css` styles it from tokens.
|
|
66
|
+
`tests/settings_view.test.mjs` covers it in jsdom.
|
|
67
|
+
|
|
68
|
+
## The settings categories outside the page: `SettingsCategories` (0.17.2)
|
|
69
|
+
|
|
70
|
+
A `SettingsView` made with `sidebar: false` draws no sidebar: no heading, no
|
|
71
|
+
filter box, no category list and no *Reset all*, only the chosen category's
|
|
72
|
+
settings (or the filter's matches). `new SettingsCategories({ view })` (in
|
|
73
|
+
`@flexdesk/widgets`) draws those parts for that view wherever it is mounted,
|
|
74
|
+
for example in an app's left navigator:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
import { SettingsView, SettingsCategories } from '@flexdesk/widgets';
|
|
78
|
+
|
|
79
|
+
const view = new SettingsView({ store, eventBus: bus, sidebar: false });
|
|
80
|
+
view.mount(pageHost);
|
|
81
|
+
const cats = new SettingsCategories({ view });
|
|
82
|
+
cats.mount(navigatorHost);
|
|
83
|
+
|
|
84
|
+
const off = view.subscribe(({ category, query }) => { /* e.g. a tab title */ });
|
|
85
|
+
// …
|
|
86
|
+
off();
|
|
87
|
+
cats.dispose();
|
|
88
|
+
view.dispose();
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
It keeps no state of its own. Typing in its filter box is `view.setQuery()`,
|
|
92
|
+
clicking a category is `view.selectCategory()` and its *Reset all* is
|
|
93
|
+
`view.resetAll()`, which asks the view's `confirm` first and changes nothing
|
|
94
|
+
unless it resolves true. It redraws from the view whenever the view's category,
|
|
95
|
+
filter or registered categories change, so the two agree whichever side moved
|
|
96
|
+
them: `view.selectCategory('window')` marks that category in the list, and
|
|
97
|
+
`view.setQuery('shadow')` puts the text in its filter box. As in the view's own
|
|
98
|
+
sidebar, choosing a category clears the filter, and no category is marked while
|
|
99
|
+
the filter is in effect. Its clicks and keystrokes act once the view is mounted.
|
|
100
|
+
|
|
101
|
+
Two methods on `SettingsView` serve it and anything else that follows the view:
|
|
102
|
+
`subscribe(fn)` calls `fn({ category, query })` after each such change and
|
|
103
|
+
answers the unsubscribe; `isFiltering()` is true while the filter's trimmed
|
|
104
|
+
text is not empty. `settingsViewContent({ …, sidebar: false })` passes the
|
|
105
|
+
option through. A view made without `sidebar` (or with `sidebar: true`) draws
|
|
106
|
+
exactly as in 0.17.1.
|
|
107
|
+
|
|
108
|
+
The markup is `.twm-settings-categories` > `.twm-settings__search`,
|
|
109
|
+
`.twm-settings__nav` (`button.twm-settings__cat`, `--active`) and
|
|
110
|
+
`.twm-settings__reset-all`, the same parts and classes as the sidebar's;
|
|
111
|
+
`flexdesk.css` styles it, leaving its width and padding to the host.
|
|
112
|
+
`tests/settings_view.test.mjs` §11–§14 cover it.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Shell and navigation
|
|
2
|
+
|
|
3
|
+
> **Package:** `@flexdesk/wm` · **Next:** [Panels and desktops](panels-and-desktops.md), [Command palette](command-palette.md), [Keys, Back and tab layout](keymap.md) · [All docs](../README.md)
|
|
4
|
+
|
|
5
|
+
`createShell` boots the tiling window manager into an element of your page: tiles that split and tab, the top navigation, dockable panels, virtual desktops and the command palette. It knows nothing about your domain. You give it a taxonomy of *kinds*, a content factory for each kind and a [host](../concepts/host-port.md), and it draws and persists the rest.
|
|
6
|
+
|
|
7
|
+
A complete first boot is in the [quickstart](../quickstart.md#hello-shell). This page covers what you can add to it: the opt-in shell features, kinds that exist only once, what happens when a link names a kind nobody registered, and what a pane can tell its own chrome.
|
|
8
|
+
|
|
9
|
+
**On this page**
|
|
10
|
+
|
|
11
|
+
- [Opt-in shell features](#opt-in-shell-features)
|
|
12
|
+
- [A kind that exists once: `single` (0.17.0)](#a-kind-that-exists-once-single-0170)
|
|
13
|
+
- [An open single instance takes new props: `navigate(props)` (0.17.2)](#an-open-single-instance-takes-new-props-navigateprops-0172)
|
|
14
|
+
- [A link to a kind nobody registered (0.17.2)](#a-link-to-a-kind-nobody-registered-0172)
|
|
15
|
+
- [A pane that names what it shows: `ctx.setTitle` (0.12.1)](#a-pane-that-names-what-it-shows-ctxsettitle-0121)
|
|
16
|
+
- [Buttons that change with the pane: `ctx.setChromeActions` (0.17.0)](#buttons-that-change-with-the-pane-ctxsetchromeactions-0170)
|
|
17
|
+
|
|
18
|
+
## Opt-in shell features
|
|
19
|
+
|
|
20
|
+
These change what the user sees, so all of them are off until you ask:
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
await createShell({
|
|
24
|
+
// …everything above…
|
|
25
|
+
snapPromotion: true, // drag a floating window onto a tile: it snaps, and docks back in
|
|
26
|
+
promoteInPlace: true, // a window floated out of a tile stays inside that tile's pane
|
|
27
|
+
backToOpenList: true, // Back in a record opened beside its list closes onto that list
|
|
28
|
+
floatActiveTab: true, // floating a tile takes the tab on screen, not the whole pane
|
|
29
|
+
tabLayout: 'header', // tabs in the tile header, in place of its title, with a + (0.8.0)
|
|
30
|
+
newTabButton: false, // …and without the + (0.8.1)
|
|
31
|
+
chrome: {
|
|
32
|
+
topNav: document.getElementById('nav'),
|
|
33
|
+
zoom: document.getElementById('status-bar'), // content zoom, 50–200%
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## A kind that exists once: `single` (0.17.0)
|
|
39
|
+
|
|
40
|
+
Some content holds heavy app-wide state and must not be mounted twice. Mark its
|
|
41
|
+
kind `single` in the taxonomy:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
createTaxonomy({
|
|
45
|
+
root: 'cosmos',
|
|
46
|
+
kinds: {
|
|
47
|
+
cosmos: { label: 'Cosmos', isTopNav: true },
|
|
48
|
+
observatory: { label: 'Observatory', isTopNav: true, single: true }, // one, ever
|
|
49
|
+
planet: { label: 'Planet', single: 'id' }, // one per props.id
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- **Every open path reveals the instance that exists** — `navigate`, the top
|
|
55
|
+
nav, `openInPrimary`, `openInTab*`, `splitLeafWith`, Back. It switches to the
|
|
56
|
+
desktop it is on, activates its tab (bringing back a top-nav page the tile
|
|
57
|
+
had switched away from) and focuses its tile, or raises its window. Nothing
|
|
58
|
+
is mounted twice, and a reveal is one `onChange` / `wm:changed`
|
|
59
|
+
(`'single-reveal'`), however many steps it took.
|
|
60
|
+
- **A page restore never brings back a copy.** A tile's archived top-nav page
|
|
61
|
+
can still list an instance that has since moved to another tile or a window;
|
|
62
|
+
switching back to that page (top nav, `openInPrimary`, Back) leaves it out.
|
|
63
|
+
- **The copy verbs refuse.** Alt+T, Alt+N, Alt+Shift+H/V do nothing; the tile
|
|
64
|
+
menu's *Open in new tab* and *Open a copy in a window* are greyed with the
|
|
65
|
+
reason as their tooltip (`taxonomy.singleReason(kind)`); the palette draws its
|
|
66
|
+
split and window hints greyed, and Ctrl/Shift+Enter on such a row does nothing.
|
|
67
|
+
- **The move verbs still work**: float, dock, drag a tab, move to a desktop.
|
|
68
|
+
- **A restored layout keeps the first** of any duplicates (desktop order, then
|
|
69
|
+
tile order): the other leaf shows its next tab, or closes if it held nothing
|
|
70
|
+
else.
|
|
71
|
+
|
|
72
|
+
The root kind cannot be `single` — every new pane, tab and desktop is seeded
|
|
73
|
+
with it. The taxonomy answers `isSingle(kind)`, `instanceKey(kind, props)` and
|
|
74
|
+
`singleReason(kind)`; the window manager answers `isSingleKind(kind)`,
|
|
75
|
+
`findInstance(kind, props)` and `revealInstance(kind, props)`.
|
|
76
|
+
|
|
77
|
+
## An open single instance takes new props: `navigate(props)` (0.17.2)
|
|
78
|
+
|
|
79
|
+
Revealing a `single` instance used to drop the props the open asked for: the
|
|
80
|
+
observatory kept showing the planet it was opened with. A content factory that
|
|
81
|
+
returns a `navigate(props)` hook from its mount is now handed them:
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
content: {
|
|
85
|
+
observatory: (el, props) => {
|
|
86
|
+
const view = mountObservatory(el, props);
|
|
87
|
+
return {
|
|
88
|
+
destroy: () => view.destroy(),
|
|
89
|
+
navigate: (next) => view.focusPlanet(next.planet), // already open: go there
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
wm.navigate('observatory', { planet: 'mars' }); // open: revealed, then navigate({ planet: 'mars' })
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- **Every open path that reveals the instance calls it once**, after the
|
|
98
|
+
reveal, with a copy of the new props — `navigate`, the top nav,
|
|
99
|
+
`openInPrimary`, `openInTab*`, `splitLeafWith`, Back. The same props twice
|
|
100
|
+
are two calls; nothing is deduplicated, and no second instance is mounted.
|
|
101
|
+
- **The props are recorded on the instance's tab** (or its window's), so a
|
|
102
|
+
top-nav page restore or a reload mounts it with the latest. The tile is not
|
|
103
|
+
rebuilt by the write.
|
|
104
|
+
- **An open that names no props only reveals** — the top-nav button,
|
|
105
|
+
`openInPrimary(kind)`, `navigate(kind)`: it asks to see the instance, not to
|
|
106
|
+
empty it, so the hook is not called and the tab keeps its props.
|
|
107
|
+
- **A content without the hook is revealed as before**: its tab keeps its props
|
|
108
|
+
and nothing is called. A hook that throws is a console warning.
|
|
109
|
+
|
|
110
|
+
## A link to a kind nobody registered (0.17.2)
|
|
111
|
+
|
|
112
|
+
`wm.navigate(kind, props, opts)` returns `true` once it has routed the
|
|
113
|
+
navigation. A `kind` the shell's `content` map has no factory for is refused
|
|
114
|
+
instead, on every destination (`main`, `origin`, a new tab, a window, a split,
|
|
115
|
+
the legacy `target`):
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
if (!wm.navigate(link.kind, link.props)) {
|
|
119
|
+
// nothing opened: tell the user the link is dead
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- **One `console.warn` naming the kind**, per call, and `false` back.
|
|
124
|
+
- **Nothing changes**: no tile, tab, window or desktop, no per-tab history, no
|
|
125
|
+
`onChange` — and no "no factory registered yet" placeholder tile, which is
|
|
126
|
+
what such a link used to leave behind.
|
|
127
|
+
- **No exception reaches the caller**, whatever `kind` is (`undefined`, `null`
|
|
128
|
+
and `''` included).
|
|
129
|
+
|
|
130
|
+
A shell that only navigates to kinds it registered sees no change; the return
|
|
131
|
+
value is new, so code that ignored it is unaffected. A split destination used
|
|
132
|
+
to hand back the new leaf's id; it now returns `true` like the others — call
|
|
133
|
+
`wm.splitLeafWith(...)` for the id. A restored layout that names an
|
|
134
|
+
unregistered kind still draws its placeholder: only `navigate` refuses.
|
|
135
|
+
|
|
136
|
+
## A pane that names what it shows: `ctx.setTitle` (0.12.1)
|
|
137
|
+
|
|
138
|
+
A content factory receives `(hostEl, props, ctx)`, and `ctx` carries, per pane,
|
|
139
|
+
`setTitle(text)`. It renames the pane's chrome title, at mount or any time
|
|
140
|
+
after, and the title survives repaints — including a panel's, whose tree title
|
|
141
|
+
the shell resets to Navigator / Inspector / Console on every layout pass:
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
content: {
|
|
145
|
+
'panel:bottom': (hostEl, props, ctx) => {
|
|
146
|
+
// …a panel with tabs of its own…
|
|
147
|
+
const show = (tab) => { /* swap the body */ ctx.setTitle(tab.label); };
|
|
148
|
+
show(tabs[0]);
|
|
149
|
+
return { destroy() {} };
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`setTitle(null)` or `''` gives the title back to the tree. It changes what the
|
|
155
|
+
chrome shows only — the tree, the tab labels and the saved layout are left
|
|
156
|
+
alone — and a call from content whose pane has since been rebuilt does nothing.
|
|
157
|
+
|
|
158
|
+
## Buttons that change with the pane: `ctx.setChromeActions` (0.17.0)
|
|
159
|
+
|
|
160
|
+
A factory may return `chromeActions` — `[{icon, title, onClick}]`, drawn in the
|
|
161
|
+
pane's chrome left of the split/float/close buttons — but those are read once.
|
|
162
|
+
`ctx.setChromeActions(specs)` replaces them at any time, the way `setTitle`
|
|
163
|
+
replaces the title: called during the mount it wins over the returned list, the
|
|
164
|
+
later call wins, the list survives repaints, `null` or `[]` clears it, and a
|
|
165
|
+
call from content whose pane has since been rebuilt does nothing. The content is
|
|
166
|
+
never re-mounted.
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
content: {
|
|
170
|
+
page: (hostEl, props, ctx) => {
|
|
171
|
+
let wide = false;
|
|
172
|
+
const paint = () => ctx.setChromeActions([
|
|
173
|
+
{ icon: wide ? 'width_normal' : 'width_wide', title: 'Page width',
|
|
174
|
+
pressed: wide, onClick: () => { wide = !wide; paint(); } },
|
|
175
|
+
{ separator: true },
|
|
176
|
+
{ icon: 'add', label: 'New', title: 'Add a page', onClick: addPage },
|
|
177
|
+
{ icon: 'publish', title: 'Nothing to publish yet', disabled: true },
|
|
178
|
+
]);
|
|
179
|
+
paint();
|
|
180
|
+
return { destroy() {} };
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
A spec may add **`label`** (text beside the glyph, or instead of it),
|
|
186
|
+
**`pressed`** (`aria-pressed`, plus a pressed style; leave it out for a button
|
|
187
|
+
that is not a toggle), **`disabled`** (greyed and inert, with its tooltip kept —
|
|
188
|
+
it is `aria-disabled`, not the `disabled` attribute) and **`{separator: true}`**
|
|
189
|
+
(a divider). A spec with none of them renders exactly as before.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Slide-out panel
|
|
2
|
+
|
|
3
|
+
> **Package:** `@flexdesk/widgets` · **Used by:** [the tile settings form](tiles.md#a-tiles-settings-as-a-form-mounttileconfigform-0170) · [All docs](../README.md)
|
|
4
|
+
|
|
5
|
+
`SlideOutPanel` is an inline panel that slides in over its host container from the right edge. A page uses it to show the settings of the element the user picked (a plot, a widget, a node) without taking over the shell's right panel.
|
|
6
|
+
|
|
7
|
+
## A slide-out panel: `SlideOutPanel`
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { SlideOutPanel } from '@flexdesk/widgets';
|
|
11
|
+
|
|
12
|
+
const panel = new SlideOutPanel({ width: 480, onClose: () => commit() });
|
|
13
|
+
panel.mount(hostEl); // hostEl: position: relative; overflow: hidden
|
|
14
|
+
|
|
15
|
+
const body = panel.open('Plot settings', 'tune'); // a title and a Material Symbols icon name
|
|
16
|
+
body.append(settingsForm); // you draw the content
|
|
17
|
+
|
|
18
|
+
panel.close(); // or Esc, the × button, or a click outside the panel
|
|
19
|
+
panel.dispose(); // closes it if open, then removes it from the host
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- **The host.** It must be `position: relative; overflow: hidden`. `mount(host)` adds a transparent, click-catching backdrop and the panel to it. A second `mount` does nothing.
|
|
23
|
+
- **Options.** `width` is in px and defaults to 700. The stylesheet gives the panel `min-width: 400px`, so a smaller width is held at 400, and no width goes past the host's own (see [A slide-out stays inside its host](#a-slide-out-stays-inside-its-host-0174)). `onClose` is called after the panel has closed and its content has been cleared.
|
|
24
|
+
- **`open(title, icon)`** clears the content, sets the header and returns the content element, which is also `panel.contentEl` and scrolls vertically. With no `icon` the header draws none. Opening a panel that is already open replaces its title, icon and content without sliding it in again. Before `mount`, `open` returns `null`. `isOpen` says whether it is open.
|
|
25
|
+
- **Closing.** The close button, a click on the backdrop and Esc all call `close()`. Esc is heard on the document while the panel is open, and the panel calls `stopPropagation()` on it. `close()` on a closed panel does nothing. `dispose()` closes first, so `onClose` runs if the panel was open.
|
|
26
|
+
|
|
27
|
+
## Where it sits
|
|
28
|
+
|
|
29
|
+
While it is open the panel is `position: fixed`, pinned to the right edge of its host's visible frame, at the host's full height. A host may be a scroll container, and an absolutely positioned child of one would scroll away with the content. Fixed positioning keeps it in place, and the panel measures its host again whenever the geometry can have changed:
|
|
30
|
+
|
|
31
|
+
- on window scroll and window resize;
|
|
32
|
+
- when the host, or any of its ancestors, is resized. A tile splitter drag or a dock panel toggling moves the host without any window event, and a toggle beside a fixed-width tile resizes an ancestor while the tile only shifts;
|
|
33
|
+
- under the shell's content zoom the host's rectangle is read in screen pixels, so the panel divides it by its effective zoom (`currentCSSZoom`). It stays flush with the host's right edge, top and height at 50% and at 150%. An engine without `currentCSSZoom` is treated as 100%.
|
|
34
|
+
|
|
35
|
+
`tests/slide_out_host_resize.test.mjs` and `tests/slide_out_panel_zoom.test.mjs` drive the tracking in jsdom.
|
|
36
|
+
|
|
37
|
+
## Styling
|
|
38
|
+
|
|
39
|
+
| Class | What it is |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `.twm-slide-out-panel` | The panel. `--open` is added while it is open; it slides in over 200 ms. While open it carries the host's width as `--twm-slide-out-host-w`, its `max-width` and the ceiling of its 400px `min-width` (0.17.4) |
|
|
42
|
+
| `.twm-slide-out-panel__header` | The header row: the icon, the title and the close button |
|
|
43
|
+
| `.twm-slide-out-panel__icon` | The header icon, a `material-symbols-outlined` glyph at **18px** (0.17.4) |
|
|
44
|
+
| `.twm-slide-out-panel__title` | The title, on one line, cut with an ellipsis |
|
|
45
|
+
| `.twm-slide-out-panel__close` | The close button, also a `.twm-btn-icon`. Its `material-symbols-outlined` icon is **18px** (0.17.4) |
|
|
46
|
+
| `.twm-slide-out-panel__body` | The content area, `overflow-y: auto` |
|
|
47
|
+
| `.twm-slide-out-panel__backdrop` | The transparent click-catcher over the host. `--visible` is added while the panel is open |
|
|
48
|
+
|
|
49
|
+
The header icon and the close button are `material-symbols-outlined` glyphs. Their 18px rules use two classes each, so they win over the icon font's own 24px (the icon-font rule in [Theming and CSS](../concepts/styling.md)).
|
|
50
|
+
|
|
51
|
+
## A slide-out stays inside its host (0.17.4)
|
|
52
|
+
|
|
53
|
+
A `SlideOutPanel` is never wider than the host it is mounted in. Its right edge
|
|
54
|
+
is pinned to the host's right edge, and while it is open it carries the host's
|
|
55
|
+
width as `--twm-slide-out-host-w`, which `.twm-slide-out-panel` uses as its
|
|
56
|
+
`max-width` and as the ceiling of its 400px `min-width`. In a host narrower than
|
|
57
|
+
the panel's `width` (700px by default) the panel is the host's width and starts
|
|
58
|
+
at the host's left edge, at any content zoom, and it is capped again whenever
|
|
59
|
+
the host is resized (a splitter drag, a dock toggling). In a wider host nothing
|
|
60
|
+
changes.
|
|
61
|
+
|
|
62
|
+
The header icon (`.twm-slide-out-panel__icon`) and the close button's icon are
|
|
63
|
+
**18px**, by rules that win over `.material-symbols-outlined`'s 24px.
|
|
64
|
+
|
|
65
|
+
Until 0.17.4 a panel in a narrow tile reached past the tile's left edge across
|
|
66
|
+
the neighbouring pane, and neither icon had a size of its own: both took
|
|
67
|
+
whatever the page's global icon size was. `tests/slide_out_panel_clamp.test.mjs`
|
|
68
|
+
holds it.
|