x4js 2.3.15 → 2.3.16

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/aicontext.md CHANGED
@@ -39,7 +39,7 @@ CoreElement<E> named timers + event surface (no DOM)
39
39
  │ ├── Progress, TickLine, Canvas, Image, Spreadsheet, MonacoEditor
40
40
  │ ├── SvgComponent → Chart
41
41
  │ ├── CSizer → HSizer / VSizer
42
- │ ├── FileDialog, ScrollView, Viewport, Flex, Space
42
+ │ ├── FileDialog, ScrollView, Viewport, DockingView, Flex, Space
43
43
  │ ├── HBox-based: ColorInput, Rating, Switch, Keyboard, TextEdit → AutoComplete, Breadcrumbs
44
44
  │ └── VBox-based: Calendar, ColorPicker, FileDrop, Panel, PropertyGrid, TextArea
45
45
  └── Application<E> singleton, no DOM of its own
@@ -50,7 +50,7 @@ EventSource<E> standalone event registry
50
50
  └── StateManager<T>
51
51
 
52
52
  CoreElement-based (non-DOM): DataProxy, DataView, Store
53
- Standalone: Rect, Timer, Color, DataModel, Stylesheet, ComputedStyle,
53
+ Standalone: Rect, Timer, Color, DataModel, Shortcuts, Stylesheet, ComputedStyle,
54
54
  SvgBuilder, X4PDFBuilder, x4_react, CMover, UnsafeHtml (extends String)
55
55
  ```
56
56
 
@@ -240,6 +240,7 @@ setCapture(pointerId: number): void; releaseCapture(pointerId: number): void
240
240
  animate(keyframes: Keyframe[], duration: number): void
241
241
  onGlobalEvent(cb: (ev: EvMessage) => void): void // auto-removed on DOM removal
242
242
  queryInterface<T>(name: string): T // "form-element" → IFormElement; "tab-handler" → ITabHandler
243
+ addShortcut(keys: string | string[], callback: (ev: KeyboardEvent) => void, options?: ShortcutOptions): this // see Shortcuts
243
244
  ```
244
245
 
245
246
  ### Exported helpers
@@ -285,6 +286,7 @@ setEnv(name: string, value: any): void
285
286
  getEnv(name: string, def_value?: any): any
286
287
  static fireGlobal(msg: string, params?: any): void
287
288
  focusNext(next: boolean): boolean
289
+ addShortcut(keys: string | string[], callback: (ev: KeyboardEvent) => void, options?: ShortcutOptions): void // see Shortcuts
288
290
  setupSocketMessaging(path?: string, looseCallback?: () => void): void
289
291
  getStorage(name: string): string
290
292
  setStorage(name: string, value: string|number): void
@@ -387,6 +389,79 @@ class MasonryBox extends Box {
387
389
 
388
390
  ---
389
391
 
392
+ ## `DockingView` — panels the user arranges by dragging their title
393
+
394
+ Not a `Box`. Two kinds of panels, which never mix: **tools**, docked in a band on the left or on the right
395
+ (one above the other, side by side, or as tabs) and which can float; **contents**, what is worked on
396
+ (as the editors of an IDE), which share the room between the bands, split or as tabs.
397
+
398
+ ```ts
399
+ interface DockPanel {
400
+ id: string
401
+ title?: string // shown in its tab; the id when missing
402
+ content: Component
403
+ kind?: 'tool' | 'content' // default 'tool'
404
+ closable?: boolean // for this panel, instead of the view's
405
+ floating?: boolean // idem; a content never floats
406
+ }
407
+ interface DockingViewProps extends ComponentProps {
408
+ panels: DockPanel[]
409
+ layout: DockLayout // where the panels are at first
410
+ floating?: boolean // tools can float above the rest
411
+ closable?: boolean // tools have a button that hides them
412
+ persist?: string // a name: the layout is remembered under it (application storage)
413
+ layoutChange?: EventCallback<ComponentEvent>
414
+ }
415
+ class DockingView extends Component {
416
+ getState(): DockLayout // plain data, to save
417
+ setState(layout: unknown): void // a saved layout; what it lacks comes from props.layout
418
+ resetLayout(): void // back to props.layout
419
+ showPanel(id: string, show?: boolean): void
420
+ isPanelVisible(id: string): boolean
421
+ }
422
+
423
+ interface DockLayout {
424
+ left: DockBand; right: DockBand // the tools
425
+ content: DockNode // the contents; null when there is none
426
+ floats: DockFloat[] // floating tools: { panel, x, y, w, h }
427
+ hidden: string[] // ids of the closed panels
428
+ }
429
+ interface DockBand { size: number; root: DockNode } // size: width in px; root: null when empty
430
+ type DockNode = DockStack | DockSplit
431
+ interface DockStack { type: 'stack'; size: number; panels: string[]; active: string } // one tab per panel
432
+ interface DockSplit { type: 'split'; dir: 'row' | 'column'; size: number; children: DockNode[] }
433
+ // size of a node: its share of its split (0 = an equal share)
434
+ ```
435
+
436
+ ```ts
437
+ const view = new DockingView({
438
+ flex: true,
439
+ floating: true,
440
+ closable: true,
441
+ persist: 'myapp.layout',
442
+ panels: [
443
+ { id: 'tools', title: 'Tools', content: tools },
444
+ { id: 'props', title: 'Properties', content: props },
445
+ { id: 'page', kind: 'content', content: editor },
446
+ ],
447
+ layout: {
448
+ left: { size: 180, root: { type: 'stack', size: 0, panels: ['tools'], active: 'tools' } },
449
+ right: { size: 260, root: { type: 'stack', size: 0, panels: ['props'], active: 'props' } },
450
+ content: { type: 'stack', size: 0, panels: ['page'], active: 'page' },
451
+ floats: [],
452
+ hidden: [],
453
+ },
454
+ })
455
+ ```
456
+
457
+ - A closed panel is hidden, not removed: `showPanel(id)` brings it back where it was. Offer it in a menu.
458
+ - Tools only go in `left`, `right` or `floats`; contents only in `content`. A panel missing from the layout is added where its kind goes.
459
+ - A content alone has no title bar; with several contents, each stack has tabs.
460
+ - What a panel shows is built once (`content`) and moved as the layout changes: never rebuild it.
461
+ - CSS variables: `--dock-line`, `--dock-accent`, `--dock-title-background`, `--dock-title-background-hover`, `--dock-title-color`, `--dock-title-color-active`, `--dock-float-background`, `--dock-float-shadow`.
462
+
463
+ ---
464
+
390
465
  ## Display components
391
466
 
392
467
  ### `Label`
@@ -1100,6 +1175,46 @@ parseRoute(str: string|RegExp, loose?: boolean): Segment
1100
1175
 
1101
1176
  ---
1102
1177
 
1178
+ ## Shortcuts (`core_shortcuts`)
1179
+
1180
+ Keyboard shortcuts. A sequence is written as it is read: `"Mod+C"`, `"Shift+Mod+Z"`, `"Delete"`, `"Alt+ArrowLeft"`.
1181
+ Modifiers: `Shift`, `Ctrl`, `Cmd`, `Alt`, and `Mod` = the key of the commands (Ctrl on Windows and Linux, Cmd on a Mac). Prefer `Mod`.
1182
+
1183
+ ```ts
1184
+ interface ShortcutOptions { editable?: boolean } // true: also while the user types in a field
1185
+
1186
+ component.addShortcut(keys, callback, options?) // a view: while the focus is in the component
1187
+ Application.instance().addShortcut(keys, callback, options?) // the whole application, wherever the focus is
1188
+
1189
+ shortcutText(keys: string): string // for a tooltip or a menu: "Ctrl+Z", "⌘Z" on a Mac
1190
+
1191
+ class Shortcuts { // the list itself; rarely used directly
1192
+ add(keys: string | string[], callback: (ev: KeyboardEvent) => void, options?: ShortcutOptions): void
1193
+ clear(): void
1194
+ handle(ev: KeyboardEvent): boolean // true when the key was a shortcut
1195
+ }
1196
+ ```
1197
+
1198
+ ```ts
1199
+ class MyView extends VBox {
1200
+ constructor(props: BoxProps) {
1201
+ super(props)
1202
+ this.addShortcut('Mod+C', () => this.copy())
1203
+ this.addShortcut(['Mod+Y', 'Shift+Mod+Z'], () => this.redo())
1204
+ this.setContent(new Button({ label: 'Undo', tooltip: shortcutText('Mod+Z'), click: () => this.undo() }))
1205
+ }
1206
+ }
1207
+ ```
1208
+
1209
+ - A component shortcut also works while the focus is nowhere, if the component is the last one with shortcuts that had the focus: of two views shown together, the one the user works in answers.
1210
+ - A key typed in an `input`, a `textarea` or a `select` is not a shortcut, unless `editable` is set.
1211
+ - A key that what has the focus already used (it called `preventDefault` or `stopPropagation`) is not a shortcut.
1212
+ - A shortcut always takes its key (`preventDefault`). For a key that must sometimes be left alone, use a `keydown` handler instead.
1213
+ - An application shortcut also works while a dialog is open.
1214
+ - Do not write a `keydown` handler with a `switch` on `ev.ctrlKey` for commands: use `addShortcut`.
1215
+
1216
+ ---
1217
+
1103
1218
  ## i18n (`core_i18n`)
1104
1219
 
1105
1220
  Two built-in languages (`fr` default, and `en`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x4js",
3
- "version": "2.3.15",
3
+ "version": "2.3.16",
4
4
  "type": "module",
5
5
  "main": "src/x4.ts",
6
6
  "module": "src/x4.ts",
@@ -12,6 +12,7 @@ export * from "./colorinput/colorinput"
12
12
  export * from "./colorpicker/colorpicker"
13
13
  export * from "./combobox/combobox"
14
14
  export * from "./dialog/dialog"
15
+ export * from "./dockingview/dockingview"
15
16
  export * from "./filedrop/filedrop"
16
17
  export * from "./form/form"
17
18
  export * from "./gauge/gauge"
@@ -0,0 +1,279 @@
1
+ /**
2
+ * ___ ___ __
3
+ * \ \/ / / _
4
+ * \ / /_| |_
5
+ * / \____ _|
6
+ * /__/\__\ |_|
7
+ *
8
+ * @file dockingview.module.scss
9
+ * @author Etienne Cochard
10
+ *
11
+ * @copyright (c) 2026 R-libre ingenierie
12
+ *
13
+ * Use of this source code is governed by an MIT-style license
14
+ * that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
15
+ **/
16
+
17
+ .x4dockingview {
18
+ --dock-line: var( --border, #cfdae0 ); // between two panels, under a title
19
+ --dock-title-background: var( --background-secondary, #f3f7f9 );
20
+ --dock-title-background-hover: color-mix( in srgb, var( --dock-accent ) 25%, transparent );
21
+ --dock-title-color: var( --text-ternary, #607784 );
22
+ --dock-title-color-active: var( --text-primary, #002a40 );
23
+ --dock-accent: var( --accent-background, #126e9e ); // the tab in front, the sizer under the pointer, the preview
24
+ --dock-float-background: var( --background-primary, white );
25
+ --dock-float-shadow: 0 6px 24px rgba( 0, 0, 0, 0.35 );
26
+
27
+ position: relative;
28
+ display: flex;
29
+ min-width: 0;
30
+ min-height: 0;
31
+ overflow: hidden;
32
+
33
+ // it has no size of its own: it takes what its container gives it,
34
+ // even in an HBox, which centers its children
35
+ align-self: stretch;
36
+
37
+ // The boxes of the layout: the band of the left, the room of the
38
+ // contents, the band of the right. The floating tools are placed from
39
+ // its top-left corner, which is the one of the box.
40
+ > .dock-frame {
41
+ position: relative;
42
+ display: flex;
43
+ flex: 1 1 0;
44
+ min-width: 0;
45
+ min-height: 0;
46
+ }
47
+
48
+ // The tools of one side. Its width is given by the layout; it only
49
+ // gets narrower when the box has no room left.
50
+ .dock-band {
51
+ display: flex;
52
+ flex: 0 1 auto;
53
+ min-width: 48px;
54
+ min-height: 0;
55
+ overflow: hidden;
56
+
57
+ > .dock-split,
58
+ > .dock-stack {
59
+ flex: 1 1 0;
60
+ }
61
+ }
62
+
63
+ // the room of the contents: what the bands leave
64
+ .dock-area {
65
+ display: flex;
66
+ flex: 1 1 0;
67
+ min-width: 0;
68
+ min-height: 0;
69
+ overflow: hidden;
70
+
71
+ > .dock-split,
72
+ > .dock-stack {
73
+ flex: 1 1 0;
74
+ }
75
+ }
76
+
77
+ .dock-split {
78
+ display: flex;
79
+ min-width: 0;
80
+ min-height: 0;
81
+ overflow: hidden;
82
+
83
+ &.column {
84
+ flex-direction: column;
85
+ }
86
+
87
+ // the smallest size of a child (MIN_SIZE in dockingview.ts)
88
+ &.row > .dock-split,
89
+ &.row > .dock-stack {
90
+ min-width: 48px;
91
+ }
92
+
93
+ &.column > .dock-split,
94
+ &.column > .dock-stack {
95
+ min-height: 48px;
96
+ }
97
+ }
98
+
99
+ // One line between two children. It is caught from a little further
100
+ // than what is drawn.
101
+ .dock-sizer {
102
+ position: relative;
103
+ z-index: 1;
104
+ flex: none;
105
+ align-self: stretch;
106
+ min-width: 1px;
107
+ min-height: 1px;
108
+ background: var( --dock-line );
109
+ touch-action: none;
110
+
111
+ &::after {
112
+ content: "";
113
+ position: absolute;
114
+ }
115
+
116
+ &:hover,
117
+ &:active {
118
+ background: var( --dock-accent );
119
+ }
120
+ }
121
+
122
+ // between two boxes that are side by side
123
+ .dock-sizer.row {
124
+ cursor: ew-resize;
125
+
126
+ &::after {
127
+ inset: 0 -3px;
128
+ }
129
+ }
130
+
131
+ // between two boxes that are one above the other
132
+ .dock-sizer.column {
133
+ cursor: ns-resize;
134
+
135
+ &::after {
136
+ inset: -3px 0;
137
+ }
138
+ }
139
+
140
+ .dock-stack {
141
+ display: flex;
142
+ flex-direction: column;
143
+ min-width: 0;
144
+ min-height: 0;
145
+ overflow: hidden;
146
+
147
+ // what a panel shows keeps its z-index for itself: it stays under
148
+ // the floating panels
149
+ isolation: isolate;
150
+ }
151
+
152
+ // the titles of a stack, which are also what it is dragged by
153
+ .dock-titles {
154
+ display: flex;
155
+ flex: none;
156
+ overflow: hidden;
157
+ background: var( --dock-title-background );
158
+ border-bottom: 1px solid var( --dock-line );
159
+ cursor: grab;
160
+ user-select: none;
161
+ touch-action: none;
162
+ }
163
+
164
+ .dock-title {
165
+ display: flex;
166
+ flex: none;
167
+ align-items: center;
168
+ gap: 6px;
169
+ padding: 6px 8px;
170
+ font-weight: 600;
171
+ white-space: nowrap;
172
+ color: var( --dock-title-color );
173
+
174
+ // under the pointer: it can be taken
175
+ &:hover {
176
+ color: var( --dock-title-color-active );
177
+ background: var( --dock-title-background-hover );
178
+ }
179
+
180
+ // The only title of a stack takes the whole bar: its button is at
181
+ // the end of it. Among several tabs, it follows the text of its tab.
182
+ &:only-child {
183
+ flex: 1 1 0;
184
+ min-width: 0;
185
+
186
+ > span {
187
+ flex: 1 1 0;
188
+ min-width: 0;
189
+ overflow: hidden;
190
+ text-overflow: ellipsis;
191
+ }
192
+ }
193
+
194
+ // several tabs: the one in front stands out
195
+ &.active:not( :only-child ) {
196
+ color: var( --dock-title-color-active );
197
+ box-shadow: inset 0 -2px 0 var( --dock-accent );
198
+ }
199
+ }
200
+
201
+ .dock-close {
202
+ width: 16px;
203
+ height: 16px;
204
+ line-height: 16px;
205
+ border-radius: 3px;
206
+ text-align: center;
207
+ font-weight: 400;
208
+ cursor: pointer;
209
+ opacity: 0.6;
210
+
211
+ &:hover {
212
+ opacity: 1;
213
+ background: var( --dock-line );
214
+ }
215
+ }
216
+
217
+ .dock-body {
218
+ display: flex;
219
+ flex: 1 1 0;
220
+ min-width: 0;
221
+ min-height: 0;
222
+ overflow: hidden;
223
+ }
224
+
225
+ // what a panel shows fills its page
226
+ .dock-page {
227
+ display: flex;
228
+ flex-direction: column;
229
+ flex: 1 1 0;
230
+ min-width: 0;
231
+ min-height: 0;
232
+ overflow: hidden;
233
+
234
+ > * {
235
+ flex: 1 1 0;
236
+ min-width: 0;
237
+ min-height: 0;
238
+ }
239
+ }
240
+
241
+ .dock-float {
242
+ position: absolute;
243
+ z-index: 20;
244
+ display: flex;
245
+ flex-direction: column;
246
+ min-width: 120px;
247
+ min-height: 80px;
248
+ overflow: hidden;
249
+ isolation: isolate;
250
+
251
+ background: var( --dock-float-background );
252
+ border: 1px solid var( --dock-line );
253
+ border-radius: 4px;
254
+ box-shadow: var( --dock-float-shadow );
255
+ }
256
+
257
+ // where the panel that is dragged will go
258
+ .dock-preview {
259
+ position: absolute;
260
+ z-index: 30;
261
+ box-sizing: border-box;
262
+ pointer-events: none;
263
+
264
+ background: color-mix( in srgb, var( --dock-accent ) 30%, transparent );
265
+ border: 1px solid var( --dock-accent );
266
+ border-radius: 3px;
267
+ }
268
+
269
+ // It holds the pointer during a drag: its cursor is the one that shows.
270
+ // It must be in the page to do so, but takes no room.
271
+ .dock-grip {
272
+ position: absolute;
273
+ left: 0;
274
+ top: 0;
275
+ width: 0;
276
+ height: 0;
277
+ cursor: grabbing;
278
+ }
279
+ }