x4js 2.3.15 → 2.3.17

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
@@ -264,7 +265,7 @@ interface EvDblClick extends ComponentEvent {}
264
265
  interface EvBtnClick extends CoreEvent { button: string }
265
266
 
266
267
  // Spacers
267
- class Flex extends Component // CSS "x4flex"
268
+ class Flex extends Component // new Flex(), no argument; CSS "x4flex"
268
269
  class Space extends Component // new Space(width?: number|string, cls?: string)
269
270
  ```
270
271
 
@@ -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,84 @@ 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
+ sides?: DockSide[] // a tool: the bands it may be docked in, instead of the view's
407
+ }
408
+ interface DockingViewProps extends ComponentProps {
409
+ panels: DockPanel[]
410
+ layout: DockLayout // where the panels are at first
411
+ floating?: boolean // tools can float above the rest
412
+ closable?: boolean // tools have a button that hides them
413
+ sides?: DockSide[] // the bands tools may be docked in; left and right when missing
414
+ persist?: string // a name: the layout is remembered under it (application storage)
415
+ layoutChange?: EventCallback<ComponentEvent>
416
+ }
417
+ class DockingView extends Component {
418
+ getState(): DockLayout // plain data, to save
419
+ setState(layout: unknown): void // a saved layout; what it lacks comes from props.layout
420
+ resetLayout(): void // back to props.layout
421
+ showPanel(id: string, show?: boolean): void
422
+ isPanelVisible(id: string): boolean
423
+ }
424
+
425
+ interface DockLayout {
426
+ left: DockBand; right: DockBand // the tools, on the sides
427
+ top?: DockBand; bottom?: DockBand // the tools above and under the contents, between left and right
428
+ content: DockNode // the contents; null when there is none
429
+ floats: DockFloat[] // floating tools: { panel, x, y, w, h }
430
+ hidden: string[] // ids of the closed panels
431
+ }
432
+ interface DockBand { size: number; root: DockNode } // size: width in px (height for top/bottom); root: null when empty
433
+ type DockSide = 'left' | 'right' | 'top' | 'bottom'
434
+ type DockNode = DockStack | DockSplit
435
+ interface DockStack { type: 'stack'; size: number; panels: string[]; active: string } // one tab per panel
436
+ interface DockSplit { type: 'split'; dir: 'row' | 'column'; size: number; children: DockNode[] }
437
+ // size of a node: its share of its split (0 = an equal share)
438
+ ```
439
+
440
+ ```ts
441
+ const view = new DockingView({
442
+ flex: true,
443
+ floating: true,
444
+ closable: true,
445
+ persist: 'myapp.layout',
446
+ panels: [
447
+ { id: 'tools', title: 'Tools', content: tools },
448
+ { id: 'props', title: 'Properties', content: props },
449
+ { id: 'page', kind: 'content', content: editor },
450
+ ],
451
+ layout: {
452
+ left: { size: 180, root: { type: 'stack', size: 0, panels: ['tools'], active: 'tools' } },
453
+ right: { size: 260, root: { type: 'stack', size: 0, panels: ['props'], active: 'props' } },
454
+ content: { type: 'stack', size: 0, panels: ['page'], active: 'page' },
455
+ floats: [],
456
+ hidden: [],
457
+ },
458
+ })
459
+ ```
460
+
461
+ - A closed panel is hidden, not removed: `showPanel(id)` brings it back where it was. Offer it in a menu.
462
+ - Tools only go in the bands or in `floats`; contents only in `content`. A panel missing from the layout is added where its kind goes.
463
+ - A tool goes in `left` or `right` only, unless `sides` says otherwise (on the view, or on one panel): `sides: ['bottom']` for what is wide and low (a log, a list of results). It is never offered a place in a band it may not be in.
464
+ - A content alone has no title bar; with several contents, each stack has tabs.
465
+ - What a panel shows is built once (`content`) and moved as the layout changes: never rebuild it.
466
+ - 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`.
467
+
468
+ ---
469
+
390
470
  ## Display components
391
471
 
392
472
  ### `Label`
@@ -460,14 +540,15 @@ class Image extends Component {
460
540
  ### `Progress`
461
541
 
462
542
  ```ts
463
- interface ProgressProps extends ComponentProps { /* min/max/value */ }
543
+ interface ProgressProps extends ComponentProps { value: number; min?: number /* 0 */; max?: number /* 100 */ }
544
+ // the bar is where value is between min (0%) and max (100%)
464
545
  class Progress extends Component { setValue(value: number): void }
465
546
  ```
466
547
 
467
548
  ### `Gauge`
468
549
 
469
550
  ```ts
470
- interface GaugeProps extends ComponentProps { min?: number; max?: number; colors?: string[] }
551
+ interface GaugeProps extends ComponentProps { min?: number /* 0 */; max?: number /* 100 */; colors?: string[] }
471
552
  class Gauge extends Component {
472
553
  setRange(min: number, max: number): void
473
554
  setPos(pos: number): void
@@ -541,6 +622,11 @@ class TextEdit extends HBox {
541
622
  ### `TextArea`
542
623
 
543
624
  ```ts
625
+ interface TextAreaProps extends BaseProps { // BaseProps: see Input (name, readonly, placeholder...)
626
+ label?: string; value?: string
627
+ resize?: boolean | "none" | "vertical" | "horizontal" | "both"
628
+ trim?: boolean // getText() trims, unless false
629
+ }
544
630
  class TextArea extends VBox { getText(): string; setText(text: string): void }
545
631
  ```
546
632
 
@@ -561,7 +647,13 @@ class Checkbox extends Component {
561
647
  ### `Radio`
562
648
 
563
649
  ```ts
564
- interface RadioProps extends ComponentProps { /* label, value, name, group */ }
650
+ interface RadioProps extends ComponentProps {
651
+ label: string; icon?: string
652
+ name: string // required: the radios that have the same name are one group
653
+ value: number | string // required
654
+ checked?: boolean
655
+ change?: EventCallback<EvChange>
656
+ }
565
657
  class Radio extends Component {
566
658
  getValue(): string; setCheck(ck: boolean): void
567
659
  setLabel(text: string): void
@@ -572,14 +664,19 @@ class Radio extends Component {
572
664
  ### `Switch`
573
665
 
574
666
  ```ts
575
- interface SwitchProps extends ComponentProps { /* label, checked, name */ }
667
+ interface SwitchProps extends ComponentProps { label: string; checked?: boolean; name?: string }
668
+ // with a name, a Form reads and writes it as a boolean
576
669
  class Switch extends HBox {}
577
670
  ```
578
671
 
579
672
  ### `Slider`
580
673
 
581
674
  ```ts
582
- interface SliderProps extends ComponentProps { /* min, max, value, step */ }
675
+ interface SliderProps extends ComponentProps {
676
+ value: number; min: number; max: number // required
677
+ step?: number; vertical?: boolean
678
+ change?: EventCallback<EvChange>
679
+ }
583
680
  class Slider extends Component {
584
681
  setMin(min: number): void; setMax(max: number): void; setValue(v: number): void
585
682
  }
@@ -590,17 +687,29 @@ class Slider extends Component {
590
687
  ```ts
591
688
  interface SelectProps extends ComponentProps {
592
689
  items: ListItem[] // required
593
- value: string // required
594
- multiple?: boolean; name?: string
690
+ value?: ListboxID // the id of the item that is selected
691
+ multiple?: boolean; required?: boolean; name?: string
595
692
  change?: EventCallback<EvChange>; focus?: EventCallback<EvFocus>
596
693
  }
597
- class Select extends Component { getValue(): string; setValue(value: string): void; setItems(items: ListItem[]): void }
694
+ class Select extends Component {
695
+ getValue(): ListboxID; setValue(value: ListboxID): void
696
+ getSelValues(): ListboxID[]; setSelValues(values: ListboxID[]): void // multiple
697
+ setItems(items: ListItem[]): void
698
+ }
598
699
  ```
599
700
 
600
701
  ### `Combobox` — input + dropdown list
601
702
 
602
703
  ```ts
603
- interface ComboboxProps extends ComponentProps { /* items, value, renderer... */ }
704
+ interface ComboboxProps extends ComponentProps {
705
+ items: ListItem[] // required
706
+ value?: ListboxID // the id of the item that is selected
707
+ label?: string; labelWidth?: number | string
708
+ readonly?: boolean // true by default: the user picks, and cannot type. false to let him type
709
+ renderer?: (item: ListItem) => Component // draws an item of the list (the field still shows item.text)
710
+ required?: boolean; name?: string
711
+ selectionChange?: EventCallback<EvSelectionChange>
712
+ }
604
713
  class Combobox extends Component {
605
714
  getValue(): string; setValue(value: string): void
606
715
  getInput(): Input
@@ -950,7 +1059,14 @@ class ProgressionBox extends Dialog {
950
1059
  ### `Notification`
951
1060
 
952
1061
  ```ts
953
- class Notification extends Popup { display(time_in_s?: number): void }
1062
+ interface NotificationProps extends ComponentProps {
1063
+ title: string; text: string | UnsafeHtml
1064
+ mode?: "success" | "danger"
1065
+ loading?: boolean // a turning icon
1066
+ iconId?: string
1067
+ closable?: boolean // a button that closes it
1068
+ }
1069
+ class Notification extends Popup { display(time_in_s?: number): void } // 0: until it is closed
954
1070
  ```
955
1071
 
956
1072
  ---
@@ -1100,6 +1216,46 @@ parseRoute(str: string|RegExp, loose?: boolean): Segment
1100
1216
 
1101
1217
  ---
1102
1218
 
1219
+ ## Shortcuts (`core_shortcuts`)
1220
+
1221
+ Keyboard shortcuts. A sequence is written as it is read: `"Mod+C"`, `"Shift+Mod+Z"`, `"Delete"`, `"Alt+ArrowLeft"`.
1222
+ Modifiers: `Shift`, `Ctrl`, `Cmd`, `Alt`, and `Mod` = the key of the commands (Ctrl on Windows and Linux, Cmd on a Mac). Prefer `Mod`.
1223
+
1224
+ ```ts
1225
+ interface ShortcutOptions { editable?: boolean } // true: also while the user types in a field
1226
+
1227
+ component.addShortcut(keys, callback, options?) // a view: while the focus is in the component
1228
+ Application.instance().addShortcut(keys, callback, options?) // the whole application, wherever the focus is
1229
+
1230
+ shortcutText(keys: string): string // for a tooltip or a menu: "Ctrl+Z", "⌘Z" on a Mac
1231
+
1232
+ class Shortcuts { // the list itself; rarely used directly
1233
+ add(keys: string | string[], callback: (ev: KeyboardEvent) => void, options?: ShortcutOptions): void
1234
+ clear(): void
1235
+ handle(ev: KeyboardEvent): boolean // true when the key was a shortcut
1236
+ }
1237
+ ```
1238
+
1239
+ ```ts
1240
+ class MyView extends VBox {
1241
+ constructor(props: BoxProps) {
1242
+ super(props)
1243
+ this.addShortcut('Mod+C', () => this.copy())
1244
+ this.addShortcut(['Mod+Y', 'Shift+Mod+Z'], () => this.redo())
1245
+ this.setContent(new Button({ label: 'Undo', tooltip: shortcutText('Mod+Z'), click: () => this.undo() }))
1246
+ }
1247
+ }
1248
+ ```
1249
+
1250
+ - 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.
1251
+ - A key typed in an `input`, a `textarea` or a `select` is not a shortcut, unless `editable` is set.
1252
+ - A key that what has the focus already used (it called `preventDefault` or `stopPropagation`) is not a shortcut.
1253
+ - A shortcut always takes its key (`preventDefault`). For a key that must sometimes be left alone, use a `keydown` handler instead.
1254
+ - An application shortcut also works while a dialog is open.
1255
+ - Do not write a `keydown` handler with a `switch` on `ev.ctrlKey` for commands: use `addShortcut`.
1256
+
1257
+ ---
1258
+
1103
1259
  ## i18n (`core_i18n`)
1104
1260
 
1105
1261
  Two built-in languages (`fr` default, and `en`).
@@ -1499,6 +1655,7 @@ Optional keys: `external`, `define`, `esbuild`, `dev` (`host`, `port`, `https`,
1499
1655
  - `Component` stores its instance on its DOM node. Use `componentFromDOM(el)` to retrieve a component from an `HTMLElement`.
1500
1656
  - Numbers passed to `setStyle`/`setStyleValue` are auto-suffixed with `"px"` except for unitless properties (see the `unitless` set: `zIndex`, `opacity`, `flexGrow`...).
1501
1657
  - `show(false)` / `hide()` toggle the `"x4hidden"` CSS class, not `display:none`.
1658
+ - x4 sets `user-select: none` on every element (`*`): text that must be selectable needs `user-select: text` on it and on its descendants.
1502
1659
  - `enable(false)` / `disable()` propagate the `disabled` state to child `<input>` and `<button>` elements.
1503
1660
  - `Application` is a strict singleton — one instance per page (an assertion fails otherwise).
1504
1661
  - `Popup.show()` routes to `displayCenter()`. To position, call `displayAt()` or `displayNear()` instead.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x4js",
3
- "version": "2.3.15",
3
+ "version": "2.3.17",
4
4
  "type": "module",
5
5
  "main": "src/x4.ts",
6
6
  "module": "src/x4.ts",
@@ -50,10 +50,10 @@
50
50
  }
51
51
  }
52
52
 
53
- // props.nullable, no color: crossed out
53
+ // props.nullable, no color: the checkerboard that stands for what is transparent
54
54
  &.none {
55
55
  border: 1px solid var( --border );
56
- background: linear-gradient( to top right, transparent 45%, #d04040 45%, #d04040 55%, transparent 55% );
56
+ background: repeating-conic-gradient( #c8c8c8 0% 25%, #ffffff 0% 50% ) 0 0 / 6px 6px;
57
57
  }
58
58
  }
59
59
 
@@ -147,9 +147,9 @@
147
147
  border-color: var( --border-focus );
148
148
  }
149
149
 
150
- // no color: crossed out
150
+ // no color: the checkerboard that stands for what is transparent
151
151
  &.none {
152
- background: linear-gradient( to top right, transparent 45%, #d04040 45%, #d04040 55%, transparent 55% );
152
+ background: repeating-conic-gradient( #c8c8c8 0% 25%, #ffffff 0% 50% ) 0 0 / 6px 6px;
153
153
  }
154
154
  }
155
155
  }
@@ -87,6 +87,10 @@ interface ComboboxProps extends Omit<ComponentProps,"content"> {
87
87
  readonly?: boolean;
88
88
  required?: boolean;
89
89
  items: ListItem[];
90
+
91
+ /** draws an item of the list, instead of its text. The field still shows the text of the item that is chosen. */
92
+ renderer?: ( item: ListItem ) => Component;
93
+
90
94
  selectionChange?: EventCallback<EvSelectionChange>,
91
95
  }
92
96
 
@@ -139,7 +143,7 @@ export class Combobox extends Component<ComboboxProps,ComboboxEvents> {
139
143
  this.setAttribute( "required", true );
140
144
  }
141
145
 
142
- this._popup = new DropdownList( { items: props.items } );
146
+ this._popup = new DropdownList( { items: props.items, renderer: props.renderer } );
143
147
  const list = this._popup.getList( );
144
148
 
145
149
  const _select = ( sel: ListboxID ) => {
@@ -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,297 @@
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, what is between the
38
+ // two, the band of the right. The floating tools are placed from its
39
+ // 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
+ // Between the bands of the left and of the right, one above the other:
49
+ // the band of the top, the room of the contents, the band of the bottom.
50
+ .dock-middle {
51
+ display: flex;
52
+ flex-direction: column;
53
+ flex: 1 1 0;
54
+ min-width: 0;
55
+ min-height: 0;
56
+ overflow: hidden;
57
+ }
58
+
59
+ // The tools of one side. Its width is given by the layout; it only
60
+ // gets narrower when the box has no room left.
61
+ .dock-band {
62
+ display: flex;
63
+ flex: 0 1 auto;
64
+ min-width: 48px;
65
+ min-height: 0;
66
+ overflow: hidden;
67
+
68
+ > .dock-split,
69
+ > .dock-stack {
70
+ flex: 1 1 0;
71
+ }
72
+
73
+ // the bands that lie flat: it is their height that the layout gives
74
+ &.top,
75
+ &.bottom {
76
+ min-width: 0;
77
+ min-height: 48px;
78
+ }
79
+ }
80
+
81
+ // the room of the contents: what the bands leave
82
+ .dock-area {
83
+ display: flex;
84
+ flex: 1 1 0;
85
+ min-width: 0;
86
+ min-height: 0;
87
+ overflow: hidden;
88
+
89
+ > .dock-split,
90
+ > .dock-stack {
91
+ flex: 1 1 0;
92
+ }
93
+ }
94
+
95
+ .dock-split {
96
+ display: flex;
97
+ min-width: 0;
98
+ min-height: 0;
99
+ overflow: hidden;
100
+
101
+ &.column {
102
+ flex-direction: column;
103
+ }
104
+
105
+ // the smallest size of a child (MIN_SIZE in dockingview.ts)
106
+ &.row > .dock-split,
107
+ &.row > .dock-stack {
108
+ min-width: 48px;
109
+ }
110
+
111
+ &.column > .dock-split,
112
+ &.column > .dock-stack {
113
+ min-height: 48px;
114
+ }
115
+ }
116
+
117
+ // One line between two children. It is caught from a little further
118
+ // than what is drawn.
119
+ .dock-sizer {
120
+ position: relative;
121
+ z-index: 1;
122
+ flex: none;
123
+ align-self: stretch;
124
+ min-width: 1px;
125
+ min-height: 1px;
126
+ background: var( --dock-line );
127
+ touch-action: none;
128
+
129
+ &::after {
130
+ content: "";
131
+ position: absolute;
132
+ }
133
+
134
+ &:hover,
135
+ &:active {
136
+ background: var( --dock-accent );
137
+ }
138
+ }
139
+
140
+ // between two boxes that are side by side
141
+ .dock-sizer.row {
142
+ cursor: ew-resize;
143
+
144
+ &::after {
145
+ inset: 0 -3px;
146
+ }
147
+ }
148
+
149
+ // between two boxes that are one above the other
150
+ .dock-sizer.column {
151
+ cursor: ns-resize;
152
+
153
+ &::after {
154
+ inset: -3px 0;
155
+ }
156
+ }
157
+
158
+ .dock-stack {
159
+ display: flex;
160
+ flex-direction: column;
161
+ min-width: 0;
162
+ min-height: 0;
163
+ overflow: hidden;
164
+
165
+ // what a panel shows keeps its z-index for itself: it stays under
166
+ // the floating panels
167
+ isolation: isolate;
168
+ }
169
+
170
+ // the titles of a stack, which are also what it is dragged by
171
+ .dock-titles {
172
+ display: flex;
173
+ flex: none;
174
+ overflow: hidden;
175
+ background: var( --dock-title-background );
176
+ border-bottom: 1px solid var( --dock-line );
177
+ cursor: grab;
178
+ user-select: none;
179
+ touch-action: none;
180
+ }
181
+
182
+ .dock-title {
183
+ display: flex;
184
+ flex: none;
185
+ align-items: center;
186
+ gap: 6px;
187
+ padding: 6px 8px;
188
+ font-weight: 600;
189
+ white-space: nowrap;
190
+ color: var( --dock-title-color );
191
+
192
+ // under the pointer: it can be taken
193
+ &:hover {
194
+ color: var( --dock-title-color-active );
195
+ background: var( --dock-title-background-hover );
196
+ }
197
+
198
+ // The only title of a stack takes the whole bar: its button is at
199
+ // the end of it. Among several tabs, it follows the text of its tab.
200
+ &:only-child {
201
+ flex: 1 1 0;
202
+ min-width: 0;
203
+
204
+ > span {
205
+ flex: 1 1 0;
206
+ min-width: 0;
207
+ overflow: hidden;
208
+ text-overflow: ellipsis;
209
+ }
210
+ }
211
+
212
+ // several tabs: the one in front stands out
213
+ &.active:not( :only-child ) {
214
+ color: var( --dock-title-color-active );
215
+ box-shadow: inset 0 -2px 0 var( --dock-accent );
216
+ }
217
+ }
218
+
219
+ .dock-close {
220
+ width: 16px;
221
+ height: 16px;
222
+ line-height: 16px;
223
+ border-radius: 3px;
224
+ text-align: center;
225
+ font-weight: 400;
226
+ cursor: pointer;
227
+ opacity: 0.6;
228
+
229
+ &:hover {
230
+ opacity: 1;
231
+ background: var( --dock-line );
232
+ }
233
+ }
234
+
235
+ .dock-body {
236
+ display: flex;
237
+ flex: 1 1 0;
238
+ min-width: 0;
239
+ min-height: 0;
240
+ overflow: hidden;
241
+ }
242
+
243
+ // what a panel shows fills its page
244
+ .dock-page {
245
+ display: flex;
246
+ flex-direction: column;
247
+ flex: 1 1 0;
248
+ min-width: 0;
249
+ min-height: 0;
250
+ overflow: hidden;
251
+
252
+ > * {
253
+ flex: 1 1 0;
254
+ min-width: 0;
255
+ min-height: 0;
256
+ }
257
+ }
258
+
259
+ .dock-float {
260
+ position: absolute;
261
+ z-index: 20;
262
+ display: flex;
263
+ flex-direction: column;
264
+ min-width: 120px;
265
+ min-height: 80px;
266
+ overflow: hidden;
267
+ isolation: isolate;
268
+
269
+ background: var( --dock-float-background );
270
+ border: 1px solid var( --dock-line );
271
+ border-radius: 4px;
272
+ box-shadow: var( --dock-float-shadow );
273
+ }
274
+
275
+ // where the panel that is dragged will go
276
+ .dock-preview {
277
+ position: absolute;
278
+ z-index: 30;
279
+ box-sizing: border-box;
280
+ pointer-events: none;
281
+
282
+ background: color-mix( in srgb, var( --dock-accent ) 30%, transparent );
283
+ border: 1px solid var( --dock-accent );
284
+ border-radius: 3px;
285
+ }
286
+
287
+ // It holds the pointer during a drag: its cursor is the one that shows.
288
+ // It must be in the page to do so, but takes no room.
289
+ .dock-grip {
290
+ position: absolute;
291
+ left: 0;
292
+ top: 0;
293
+ width: 0;
294
+ height: 0;
295
+ cursor: grabbing;
296
+ }
297
+ }