x4js 2.3.16 → 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
@@ -265,7 +265,7 @@ interface EvDblClick extends ComponentEvent {}
265
265
  interface EvBtnClick extends CoreEvent { button: string }
266
266
 
267
267
  // Spacers
268
- class Flex extends Component // CSS "x4flex"
268
+ class Flex extends Component // new Flex(), no argument; CSS "x4flex"
269
269
  class Space extends Component // new Space(width?: number|string, cls?: string)
270
270
  ```
271
271
 
@@ -403,12 +403,14 @@ interface DockPanel {
403
403
  kind?: 'tool' | 'content' // default 'tool'
404
404
  closable?: boolean // for this panel, instead of the view's
405
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
406
407
  }
407
408
  interface DockingViewProps extends ComponentProps {
408
409
  panels: DockPanel[]
409
410
  layout: DockLayout // where the panels are at first
410
411
  floating?: boolean // tools can float above the rest
411
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
412
414
  persist?: string // a name: the layout is remembered under it (application storage)
413
415
  layoutChange?: EventCallback<ComponentEvent>
414
416
  }
@@ -421,12 +423,14 @@ class DockingView extends Component {
421
423
  }
422
424
 
423
425
  interface DockLayout {
424
- left: DockBand; right: DockBand // the tools
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
425
428
  content: DockNode // the contents; null when there is none
426
429
  floats: DockFloat[] // floating tools: { panel, x, y, w, h }
427
430
  hidden: string[] // ids of the closed panels
428
431
  }
429
- interface DockBand { size: number; root: DockNode } // size: width in px; root: null when empty
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'
430
434
  type DockNode = DockStack | DockSplit
431
435
  interface DockStack { type: 'stack'; size: number; panels: string[]; active: string } // one tab per panel
432
436
  interface DockSplit { type: 'split'; dir: 'row' | 'column'; size: number; children: DockNode[] }
@@ -455,7 +459,8 @@ const view = new DockingView({
455
459
  ```
456
460
 
457
461
  - 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.
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.
459
464
  - A content alone has no title bar; with several contents, each stack has tabs.
460
465
  - What a panel shows is built once (`content`) and moved as the layout changes: never rebuild it.
461
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`.
@@ -535,14 +540,15 @@ class Image extends Component {
535
540
  ### `Progress`
536
541
 
537
542
  ```ts
538
- 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%)
539
545
  class Progress extends Component { setValue(value: number): void }
540
546
  ```
541
547
 
542
548
  ### `Gauge`
543
549
 
544
550
  ```ts
545
- interface GaugeProps extends ComponentProps { min?: number; max?: number; colors?: string[] }
551
+ interface GaugeProps extends ComponentProps { min?: number /* 0 */; max?: number /* 100 */; colors?: string[] }
546
552
  class Gauge extends Component {
547
553
  setRange(min: number, max: number): void
548
554
  setPos(pos: number): void
@@ -616,6 +622,11 @@ class TextEdit extends HBox {
616
622
  ### `TextArea`
617
623
 
618
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
+ }
619
630
  class TextArea extends VBox { getText(): string; setText(text: string): void }
620
631
  ```
621
632
 
@@ -636,7 +647,13 @@ class Checkbox extends Component {
636
647
  ### `Radio`
637
648
 
638
649
  ```ts
639
- 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
+ }
640
657
  class Radio extends Component {
641
658
  getValue(): string; setCheck(ck: boolean): void
642
659
  setLabel(text: string): void
@@ -647,14 +664,19 @@ class Radio extends Component {
647
664
  ### `Switch`
648
665
 
649
666
  ```ts
650
- 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
651
669
  class Switch extends HBox {}
652
670
  ```
653
671
 
654
672
  ### `Slider`
655
673
 
656
674
  ```ts
657
- 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
+ }
658
680
  class Slider extends Component {
659
681
  setMin(min: number): void; setMax(max: number): void; setValue(v: number): void
660
682
  }
@@ -665,17 +687,29 @@ class Slider extends Component {
665
687
  ```ts
666
688
  interface SelectProps extends ComponentProps {
667
689
  items: ListItem[] // required
668
- value: string // required
669
- multiple?: boolean; name?: string
690
+ value?: ListboxID // the id of the item that is selected
691
+ multiple?: boolean; required?: boolean; name?: string
670
692
  change?: EventCallback<EvChange>; focus?: EventCallback<EvFocus>
671
693
  }
672
- 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
+ }
673
699
  ```
674
700
 
675
701
  ### `Combobox` — input + dropdown list
676
702
 
677
703
  ```ts
678
- 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
+ }
679
713
  class Combobox extends Component {
680
714
  getValue(): string; setValue(value: string): void
681
715
  getInput(): Input
@@ -1025,7 +1059,14 @@ class ProgressionBox extends Dialog {
1025
1059
  ### `Notification`
1026
1060
 
1027
1061
  ```ts
1028
- 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
1029
1070
  ```
1030
1071
 
1031
1072
  ---
@@ -1614,6 +1655,7 @@ Optional keys: `external`, `define`, `esbuild`, `dev` (`host`, `port`, `https`,
1614
1655
  - `Component` stores its instance on its DOM node. Use `componentFromDOM(el)` to retrieve a component from an `HTMLElement`.
1615
1656
  - Numbers passed to `setStyle`/`setStyleValue` are auto-suffixed with `"px"` except for unitless properties (see the `unitless` set: `zIndex`, `opacity`, `flexGrow`...).
1616
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.
1617
1659
  - `enable(false)` / `disable()` propagate the `disabled` state to child `<input>` and `<button>` elements.
1618
1660
  - `Application` is a strict singleton — one instance per page (an assertion fails otherwise).
1619
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.16",
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 ) => {
@@ -34,9 +34,9 @@
34
34
  // even in an HBox, which centers its children
35
35
  align-self: stretch;
36
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.
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
40
  > .dock-frame {
41
41
  position: relative;
42
42
  display: flex;
@@ -45,6 +45,17 @@
45
45
  min-height: 0;
46
46
  }
47
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
+
48
59
  // The tools of one side. Its width is given by the layout; it only
49
60
  // gets narrower when the box has no room left.
50
61
  .dock-band {
@@ -58,6 +69,13 @@
58
69
  > .dock-stack {
59
70
  flex: 1 1 0;
60
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
+ }
61
79
  }
62
80
 
63
81
  // the room of the contents: what the bands leave
@@ -19,13 +19,13 @@ import { Component, ComponentEvent, ComponentEvents, ComponentProps } from '../.
19
19
  import { EventCallback } from '../../core/core_events';
20
20
  import { CSizer } from '../sizers/sizer';
21
21
 
22
- import { bandOf, dockActivate, dockBand, dockFloat, dockMove, dockRestore, dockShow, dropZone, floatOf, isShown, keepInside, sharesOf, shownPanel, sideRect, stackOf, stacksOf, visiblePanels } from './docklayout';
23
- import { DockFloat, DockKinds, DockLayout, DockNode, DockRect, DockStack } from './docklayout';
22
+ import { bandOf, dockActivate, dockBand, dockFloat, dockMove, dockRestore, dockShow, dockSides, dropZone, floatOf, isFlat, isShown, keepInside, sharesOf, shownPanel, sideRect, stackOf, stacksOf, visiblePanels } from './docklayout';
23
+ import { DockFloat, DockKinds, DockLayout, DockNode, DockRect, DockSide, DockStack } from './docklayout';
24
24
 
25
25
  import "./dockingview.module.scss";
26
26
 
27
27
  // the layout is given and read by the host: its types go with the component
28
- export type { DockLayout, DockBand, DockNode, DockSplit, DockStack, DockFloat } from './docklayout';
28
+ export type { DockLayout, DockBand, DockNode, DockSplit, DockStack, DockFloat, DockSide } from './docklayout';
29
29
 
30
30
  /** under this distance, in pixels, a press on a title is a click, not a drag */
31
31
  const DRAG_SLOP = 4;
@@ -39,8 +39,12 @@ const MIN_SIZE = 48;
39
39
  /** what a sizer always leaves to the contents, between the two bands */
40
40
  const MIN_AREA = 80;
41
41
 
42
- /** width of a band whose layout gives none */
42
+ /** width of a band whose layout gives none; height, for the band of the top or of the bottom */
43
43
  const BAND_SIZE = 200;
44
+ const BAND_HEIGHT = 160;
45
+
46
+ /** the bands, in the order they are looked at */
47
+ const SIDES: DockSide[] = ["left", "right", "top", "bottom"];
44
48
 
45
49
  /** smallest size of a panel that starts to float */
46
50
  const FLOAT_MIN_W = 160;
@@ -68,6 +72,9 @@ export interface DockPanel {
68
72
 
69
73
  /** a tool: instead of what the box says. A content never floats. */
70
74
  floating?: boolean;
75
+
76
+ /** a tool: the bands it may be docked in, instead of what the box says */
77
+ sides?: DockSide[];
71
78
  }
72
79
 
73
80
  export interface DockingViewProps extends ComponentProps {
@@ -82,6 +89,14 @@ export interface DockingViewProps extends ComponentProps {
82
89
  /** tools have a button that hides them (see showPanel) */
83
90
  closable?: boolean;
84
91
 
92
+ /**
93
+ * The bands tools may be docked in: "left", "right", "top", "bottom".
94
+ * The left and the right ones when missing. A tool is never offered a
95
+ * place in a band it may not be in, neither beside one of its tools nor
96
+ * as one of their tabs.
97
+ */
98
+ sides?: DockSide[];
99
+
85
100
  /**
86
101
  * A name: the layout is then remembered under it, in the storage of
87
102
  * the application, and found again the next time.
@@ -142,6 +157,11 @@ interface Drag {
142
157
  * dropped anywhere else floats above the rest. With `closable`, its title
143
158
  * has a button that hides it; showPanel() brings it back.
144
159
  *
160
+ * With `sides`, a tool may also be docked above or under the contents, in
161
+ * a band that lies between those of the left and of the right: for what is
162
+ * wide and low (a log, a list of results, a timeline). `sides` says which
163
+ * bands a tool may be in, for all the tools or for one of them.
164
+ *
145
165
  * A content is what is worked on (a document, as the editors of an IDE).
146
166
  * Contents share the room between the bands: a title dragged to a side of
147
167
  * another content is put beside it, to its middle it becomes one of its
@@ -175,7 +195,7 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
175
195
 
176
196
  // the boxes of the layout as it is shown now
177
197
  private nodes = new Map<DockNode, Component>( );
178
- private bands = new Map<"left" | "right", Component>( );
198
+ private bands = new Map<DockSide, Component>( );
179
199
  private floats = new Map<string, Component>( );
180
200
  private area: Component; // the room of the contents, between the bands
181
201
 
@@ -277,7 +297,23 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
277
297
 
278
298
  private kinds( ): DockKinds {
279
299
  const ids = this.props.panels.map( panel => panel.id );
280
- return { tools: ids.filter( id => this.isTool( id ) ), contents: ids.filter( id => !this.isTool( id ) ) };
300
+ const tools = ids.filter( id => this.isTool( id ) );
301
+
302
+ // what a tool says for itself, otherwise what the box says for all
303
+ const sides: Record<string, DockSide[]> = {};
304
+ for( const id of tools ) {
305
+ sides[id] = this.panel( id ).sides ?? this.props.sides;
306
+ }
307
+
308
+ return { tools, contents: ids.filter( id => !this.isTool( id ) ), sides };
309
+ }
310
+
311
+ /**
312
+ * true when a tool may be docked in a band
313
+ */
314
+
315
+ private mayDock( id: string, side: DockSide ): boolean {
316
+ return dockSides( this.kinds( ), id ).includes( side );
281
317
  }
282
318
 
283
319
  private mayFloat( id: string ): boolean {
@@ -316,8 +352,10 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
316
352
  // :: BUILDING ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
317
353
 
318
354
  /**
319
- * Make the boxes of the layout again: the band of the left, the room
320
- * of the contents, the band of the right, and the floating tools.
355
+ * Make the boxes of the layout again: the band of the left, then,
356
+ * one above the other, the band of the top, the room of the contents
357
+ * and the band of the bottom, then the band of the right; and the
358
+ * floating tools.
321
359
  *
322
360
  * What the panels show is not built again: their pages are moved. They
323
361
  * go straight from a place in the document to another one, the park
@@ -346,9 +384,19 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
346
384
  content: content && isShown( content, hidden ) ? this.makeNode( content, titled ) : null,
347
385
  } );
348
386
 
387
+ // between the bands of the sides, which take the whole height
388
+ const middle = new Component( {
389
+ cls: "dock-middle",
390
+ content: [
391
+ ...this.makeBand( "top" ),
392
+ this.area,
393
+ ...this.makeBand( "bottom" ),
394
+ ],
395
+ } );
396
+
349
397
  this.frame.setContent( [
350
398
  ...this.makeBand( "left" ),
351
- this.area,
399
+ middle,
352
400
  ...this.makeBand( "right" ),
353
401
  ...floats.filter( float => !hidden.includes( float.panel ) ).map( float => this.makeFloat( float ) ),
354
402
  ] );
@@ -362,39 +410,58 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
362
410
  }
363
411
 
364
412
  /**
365
- * A band and the sizer that changes its width, in the order they are
366
- * shown; nothing when none of its tools is shown.
413
+ * A band and the sizer that changes its width (its height, for the
414
+ * band of the top or of the bottom), in the order they are shown;
415
+ * nothing when none of its tools is shown.
367
416
  */
368
417
 
369
- private makeBand( side: "left" | "right" ): Component[] {
418
+ private makeBand( side: DockSide ): Component[] {
370
419
  const band = this.layout[side];
371
420
 
372
- if( !band.root || !isShown( band.root, this.layout.hidden ) ) {
421
+ if( !band?.root || !isShown( band.root, this.layout.hidden ) ) {
373
422
  return [];
374
423
  }
375
424
 
425
+ const flat = isFlat( side );
426
+
376
427
  const box = new Component( {
377
428
  cls: `dock-band ${side}`,
378
- width: band.size || BAND_SIZE,
379
429
  content: this.makeNode( band.root, true ),
380
430
  } );
381
431
 
432
+ if( flat ) {
433
+ box.setHeight( band.size || BAND_HEIGHT );
434
+ }
435
+ else {
436
+ box.setWidth( band.size || BAND_SIZE );
437
+ }
438
+
382
439
  this.bands.set( side, box );
383
440
 
384
- // what the band gets is taken from the contents
385
- const sign = side === "left" ? 1 : -1;
441
+ // what the band gets is taken from the contents: the sizer is on
442
+ // the side of the band that faces them
443
+ const first = side === "left" || side === "top";
444
+ const sign = first ? 1 : -1;
386
445
  let size = 0;
387
446
  let max = 0;
388
447
 
389
- const sizer = this.makeSizer( true, ( ) => {
448
+ const sizer = this.makeSizer( !flat, ( ) => {
449
+ const room = this.area.getBoundingRect( );
450
+
390
451
  size = band.size;
391
- max = size + Math.max( 0, this.area.getBoundingRect( ).width - MIN_AREA );
452
+ max = size + Math.max( 0, ( flat ? room.height : room.width ) - MIN_AREA );
392
453
  }, delta => {
393
454
  band.size = Math.round( Math.max( MIN_SIZE, Math.min( size + delta * sign, max ) ) );
394
- box.setWidth( band.size );
455
+
456
+ if( flat ) {
457
+ box.setHeight( band.size );
458
+ }
459
+ else {
460
+ box.setWidth( band.size );
461
+ }
395
462
  } );
396
463
 
397
- return side === "left" ? [box, sizer] : [sizer, box];
464
+ return first ? [box, sizer] : [sizer, box];
398
465
  }
399
466
 
400
467
  private makeNode( node: DockNode, titled: boolean ): Component {
@@ -588,12 +655,23 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
588
655
  visit( this.layout.content, null );
589
656
  }
590
657
 
591
- for( const side of ["left", "right"] as const ) {
658
+ for( const side of SIDES ) {
592
659
  const band = this.layout[side];
593
- const width = Math.round( this.bands.get( side )?.getBoundingRect( ).width ?? 0 );
660
+ if( !band ) {
661
+ continue;
662
+ }
663
+
664
+ // A band that is not shown has no box: its size stays what it
665
+ // was. Otherwise its width, or its height when it lies flat.
666
+ const box = this.bands.get( side );
667
+
668
+ if( box ) {
669
+ const rc = box.getBoundingRect( );
670
+ const size = Math.round( isFlat( side ) ? rc.height : rc.width );
594
671
 
595
- if( width > 0 ) {
596
- band.size = width;
672
+ if( size > 0 ) {
673
+ band.size = size;
674
+ }
597
675
  }
598
676
 
599
677
  if( band.root ) {
@@ -761,8 +839,11 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
761
839
  *
762
840
  * a side of a tool beside it, above or under it
763
841
  * the middle of another tool one of its tabs
764
- * the left or the right of the contents the band of that side, under its tools
842
+ * a side of the contents the band of that side, after its tools
765
843
  * anywhere else it floats, if it may
844
+ *
845
+ * The first three only for a band the tool may be docked in (`sides`):
846
+ * the left and the right ones, unless the host says otherwise.
766
847
  */
767
848
 
768
849
  private dropTool( x: number, y: number ): Drop {
@@ -777,8 +858,10 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
777
858
 
778
859
  const inside = ( rect: DockRect ) => dropZone( rect, x, y, 1 ) !== null;
779
860
 
780
- // the width the tool brings to a band: the one it has now, a third of the box at most
861
+ // the width the tool brings to a band: the one it has now, a third of the box at most;
862
+ // and its height, for a band that lies flat
781
863
  const width = Math.round( Math.max( MIN_SIZE, Math.min( ( float?.w ?? page.w ) || BAND_SIZE, within.w / 3 ) ) );
864
+ const height = Math.round( Math.max( MIN_SIZE, Math.min( ( float?.h ?? page.h ) || BAND_HEIGHT, within.h / 3 ) ) );
782
865
 
783
866
  const floating = ( ): Drop => {
784
867
  if( !this.mayFloat( panel ) ) {
@@ -800,44 +883,52 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
800
883
  return floating( );
801
884
  }
802
885
 
803
- // a tool of a band
804
- for( const stack of [...stacksOf( layout.left.root ), ...stacksOf( layout.right.root )] ) {
805
- const box = this.nodes.get( stack );
806
- const rect = box ? this.rectOf( box ) : null;
807
- const zone = rect ? dropZone( rect, x, y, SIDE_BAND ) : null;
808
-
809
- if( !zone ) {
886
+ // a tool of a band, when the one that is dragged may be in that band
887
+ for( const side of SIDES ) {
888
+ if( !this.mayDock( panel, side ) ) {
810
889
  continue;
811
890
  }
812
891
 
813
- const panels = visiblePanels( stack, hidden );
892
+ for( const stack of stacksOf( layout[side]?.root ) ) {
893
+ const box = this.nodes.get( stack );
894
+ const rect = box ? this.rectOf( box ) : null;
895
+ const zone = rect ? dropZone( rect, x, y, SIDE_BAND ) : null;
814
896
 
815
- if( zone === "center" ) {
816
- // the middle of its own stack: it leaves it
817
- return stack === own ? floating( ) : { rect, apply: ( ) => dockMove( layout, panel, panels[0], "tab" ) };
818
- }
897
+ if( !zone ) {
898
+ continue;
899
+ }
819
900
 
820
- // beside the stack it is alone in: it is already there
821
- if( stack === own && panels.length === 1 ) {
822
- return null;
823
- }
901
+ const panels = visiblePanels( stack, hidden );
824
902
 
825
- // Half of the stack is shown. On the left or on the right, the
826
- // band gets wider instead: the two tools keep their width.
827
- const half = ( zone === "left" || zone === "right" ? rect.w : rect.h ) / 2;
828
- return { rect: sideRect( rect, zone, half ), apply: ( ) => dockMove( layout, panel, panels[0], zone, width ) };
903
+ if( zone === "center" ) {
904
+ // the middle of its own stack: it leaves it
905
+ return stack === own ? floating( ) : { rect, apply: ( ) => dockMove( layout, panel, panels[0], "tab" ) };
906
+ }
907
+
908
+ // beside the stack it is alone in: it is already there
909
+ if( stack === own && panels.length === 1 ) {
910
+ return null;
911
+ }
912
+
913
+ // Half of the stack is shown. On the left or on the right, the
914
+ // band gets wider instead: the two tools keep their width. In
915
+ // a band that lies flat, it is above and under, and its height.
916
+ const half = ( zone === "left" || zone === "right" ? rect.w : rect.h ) / 2;
917
+ return { rect: sideRect( rect, zone, half ), apply: ( ) => dockMove( layout, panel, panels[0], zone, isFlat( side ) ? height : width ) };
918
+ }
829
919
  }
830
920
 
831
- // the left or the right of the contents: the band of that side
921
+ // a side of the contents: the band of that side
832
922
  const area = this.rectOf( this.area );
833
- const side = x < area.x + SIDE_BAND ? "left" : x >= area.x + area.w - SIDE_BAND ? "right" : null;
923
+ const side = this.edgeOf( panel, area, x, y );
834
924
 
835
925
  if( !side ) {
836
926
  return floating( );
837
927
  }
838
928
 
929
+ const flat = isFlat( side );
839
930
  const band = this.bands.get( side );
840
- const root = layout[side].root;
931
+ const root = layout[side]?.root;
841
932
  const shown = stacksOf( root ).filter( stack => isShown( stack, hidden ) );
842
933
 
843
934
  // the only tool of that band: it is already there
@@ -845,14 +936,60 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
845
936
  return null;
846
937
  }
847
938
 
848
- // A band that shows nothing is made beside the contents, as wide as
849
- // the tool is now. Otherwise the tool goes under what the band
850
- // holds: one more row of it, or its lower half.
851
- const rows = root?.type === "split" && root.dir === "column" ? root.children.filter( child => isShown( child, hidden ) ).length : 1;
939
+ // A band that shows nothing is made beside the contents, as big as
940
+ // the tool is now. Otherwise the tool goes after what the band
941
+ // holds: one more row of it, or its lower half; one more column, or
942
+ // its right half, for a band that lies flat.
943
+ const size = flat ? height : width;
944
+ const count = root?.type === "split" && root.dir === ( flat ? "row" : "column" ) ? root.children.filter( child => isShown( child, hidden ) ).length : 1;
852
945
  const bandRect = band ? this.rectOf( band ) : null;
853
- const rect = bandRect ? sideRect( bandRect, "bottom", bandRect.h / ( rows + 1 ) ) : sideRect( area, side, width );
854
946
 
855
- return { rect, apply: ( ) => dockBand( layout, panel, side, band ? 0 : width ) };
947
+ let rect: DockRect;
948
+
949
+ if( !bandRect ) {
950
+ rect = sideRect( area, side, size );
951
+ }
952
+ else if( flat ) {
953
+ rect = sideRect( bandRect, "right", bandRect.w / ( count + 1 ) );
954
+ }
955
+ else {
956
+ rect = sideRect( bandRect, "bottom", bandRect.h / ( count + 1 ) );
957
+ }
958
+
959
+ return { rect, apply: ( ) => dockBand( layout, panel, side, band ? 0 : size ) };
960
+ }
961
+
962
+ /**
963
+ * The side of the contents a point is near, among those the band of
964
+ * which a tool may be docked in; the nearest one, null for none.
965
+ *
966
+ * A point beyond the side counts too: it is between the contents and
967
+ * the band. But it has to be along the side: over a band of another
968
+ * side, it is not near this one.
969
+ */
970
+
971
+ private edgeOf( panel: string, area: DockRect, x: number, y: number ): DockSide {
972
+ const along = ( side: DockSide ) => isFlat( side ) ? x >= area.x && x < area.x + area.w : y >= area.y && y < area.y + area.h;
973
+
974
+ // how deep the point is from each side: 0 on it, 1 where its zone ends
975
+ const depth = ( side: DockSide ): number => {
976
+ switch( side ) {
977
+ case "left": return ( x - area.x ) / SIDE_BAND;
978
+ case "right": return ( area.x + area.w - x ) / SIDE_BAND;
979
+ case "top": return ( y - area.y ) / SIDE_BAND;
980
+ case "bottom": return ( area.y + area.h - y ) / SIDE_BAND;
981
+ }
982
+ };
983
+
984
+ let nearest: DockSide = null;
985
+
986
+ for( const side of SIDES ) {
987
+ if( this.mayDock( panel, side ) && along( side ) && depth( side ) < 1 && ( nearest === null || depth( side ) < depth( nearest ) ) ) {
988
+ nearest = side;
989
+ }
990
+ }
991
+
992
+ return nearest;
856
993
  }
857
994
 
858
995
  /**
@@ -19,16 +19,21 @@
19
19
  *
20
20
  * There are two kinds of panels, which never mix:
21
21
  *
22
- * tools docked in a band on the left or on the right: one above the
23
- * other, side by side, or as tabs of the same stack; they
24
- * can also float.
22
+ * tools docked in a band along a side of the contents: one above
23
+ * the other, side by side, or as tabs of the same stack;
24
+ * they can also float. Which bands a tool may be in is said
25
+ * by the view (DockKinds.sides): the left and the right
26
+ * ones, unless it says otherwise.
25
27
  * contents what is worked on (a document, as the editors of an IDE).
26
28
  * They share the room between the bands: side by side, one
27
29
  * above the other, or as tabs of the same stack.
28
30
  *
29
31
  * A layout is plain data, so that it can be saved as it is:
30
32
  *
31
- * left, right the two bands: a width, and a tree of tools.
33
+ * left, right the bands of the sides: a width, and a tree of tools.
34
+ * They take the whole height.
35
+ * top, bottom the bands above and under the contents, between those of
36
+ * the sides: a height, and a tree of tools.
32
37
  * content the tree of the contents.
33
38
  *
34
39
  * In a tree, a split puts its children side by side (row)
@@ -73,7 +78,10 @@ export interface DockStack {
73
78
 
74
79
  export type DockNode = DockSplit | DockStack;
75
80
 
76
- /** the tools docked on one side: `size` is its width in pixels, `root` is null when it has none */
81
+ /**
82
+ * The tools docked on one side: `size` is its width in pixels (its height,
83
+ * for the band of the top or of the bottom), `root` is null when it has none.
84
+ */
77
85
  export interface DockBand {
78
86
  size: number;
79
87
  root: DockNode;
@@ -91,9 +99,16 @@ export interface DockFloat extends DockRect {
91
99
  panel: string;
92
100
  }
93
101
 
102
+ /**
103
+ * `top` and `bottom` may be missing: a band that is missing is an empty
104
+ * one. A layout that does not use them does not have them.
105
+ */
106
+
94
107
  export interface DockLayout {
95
108
  left: DockBand;
96
109
  right: DockBand;
110
+ top?: DockBand;
111
+ bottom?: DockBand;
97
112
  content: DockNode;
98
113
  floats: DockFloat[];
99
114
  hidden: string[];
@@ -103,12 +118,47 @@ export interface DockLayout {
103
118
  export interface DockKinds {
104
119
  tools: string[];
105
120
  contents: string[];
121
+
122
+ /** the bands a tool may be docked in, by its id; the left and the right ones for a tool that is not there */
123
+ sides?: Record<string, DockSide[]>;
106
124
  }
107
125
 
126
+ /** the bands, in the order they are looked at */
127
+ const SIDES: DockSide[] = ["left", "right", "top", "bottom"];
128
+
129
+ /** where a tool may be docked when nothing says it */
130
+ const USUAL_SIDES: DockSide[] = ["left", "right"];
131
+
108
132
  function copy( layout: DockLayout ): DockLayout {
109
133
  return structuredClone( layout );
110
134
  }
111
135
 
136
+ /**
137
+ * true for the bands of the top and of the bottom: they lie flat. Their
138
+ * size is a height, and their tools are side by side where those of the
139
+ * left and of the right are one above the other.
140
+ */
141
+
142
+ export function isFlat( side: DockSide ): boolean {
143
+ return side === "top" || side === "bottom";
144
+ }
145
+
146
+ /**
147
+ * The bands a tool may be docked in. A list that is empty counts as none
148
+ * given: a tool has to be somewhere when its layout is lost.
149
+ */
150
+
151
+ export function dockSides( kinds: DockKinds, panel: string ): DockSide[] {
152
+ const given = kinds.sides ? kinds.sides[panel] : null;
153
+
154
+ if( !given ) {
155
+ return USUAL_SIDES;
156
+ }
157
+
158
+ const sides = given.filter( side => SIDES.includes( side ) );
159
+ return sides.length ? sides : USUAL_SIDES;
160
+ }
161
+
112
162
  /**
113
163
  * the stacks of a tree, from left to right and from top to bottom
114
164
  */
@@ -122,7 +172,7 @@ export function stacksOf( node: DockNode ): DockStack[] {
122
172
  }
123
173
 
124
174
  function allStacks( layout: DockLayout ): DockStack[] {
125
- return [...stacksOf( layout.left.root ), ...stacksOf( layout.right.root ), ...stacksOf( layout.content )];
175
+ return [...SIDES.flatMap( side => stacksOf( layout[side]?.root ) ), ...stacksOf( layout.content )];
126
176
  }
127
177
 
128
178
  function parentOf( root: DockNode, node: DockNode ): DockSplit {
@@ -179,8 +229,13 @@ function normalize( node: DockNode ): DockNode {
179
229
 
180
230
  function tidy( layout: DockLayout ): void {
181
231
  layout.content = normalize( layout.content );
182
- layout.left.root = normalize( layout.left.root );
183
- layout.right.root = normalize( layout.right.root );
232
+
233
+ for( const side of SIDES ) {
234
+ const band = layout[side];
235
+ if( band ) {
236
+ band.root = normalize( band.root );
237
+ }
238
+ }
184
239
  }
185
240
 
186
241
  /**
@@ -189,10 +244,13 @@ function tidy( layout: DockLayout ): void {
189
244
 
190
245
  function detach( layout: DockLayout, panel: string ): void {
191
246
  // A tool that was beside others, alone in its stack, takes its width
192
- // with it: its band gets narrower by its share of the row.
193
- for( const band of [layout.left, layout.right] ) {
194
- const root = band.root;
195
- const at = root?.type === "split" && root.dir === "row" ? root.children.findIndex( child => child.type === "stack" && child.panels.length === 1 && child.panels[0] === panel ) : -1;
247
+ // with it: its band gets narrower by its share of the row. In a band
248
+ // that lies flat it is its height, and its share of the column.
249
+ for( const side of SIDES ) {
250
+ const band = layout[side];
251
+ const root = band?.root;
252
+ const across = isFlat( side ) ? "column" : "row";
253
+ const at = root?.type === "split" && root.dir === across ? root.children.findIndex( child => child.type === "stack" && child.panels.length === 1 && child.panels[0] === panel ) : -1;
196
254
 
197
255
  if( root?.type === "split" && at >= 0 ) {
198
256
  const shares = sharesOf( root.children );
@@ -227,14 +285,8 @@ export function stackOf( layout: DockLayout, panel: string ): DockStack {
227
285
  * the band a panel is docked in, null when it is a content, floats or is unknown
228
286
  */
229
287
 
230
- export function bandOf( layout: DockLayout, panel: string ): "left" | "right" {
231
- const holds = ( band: DockBand ) => stacksOf( band.root ).some( stack => stack.panels.includes( panel ) );
232
-
233
- if( holds( layout.left ) ) {
234
- return "left";
235
- }
236
-
237
- return holds( layout.right ) ? "right" : null;
288
+ export function bandOf( layout: DockLayout, panel: string ): DockSide {
289
+ return SIDES.find( side => stacksOf( layout[side]?.root ).some( stack => stack.panels.includes( panel ) ) ) ?? null;
238
290
  }
239
291
 
240
292
  export function floatOf( layout: DockLayout, panel: string ): DockFloat {
@@ -286,6 +338,10 @@ export function sharesOf( nodes: DockNode[] ): number[] {
286
338
  * takes half of its room. But a tool put on the left or on the right of
287
339
  * another one does not make it narrower: with `width`, the band gets
288
340
  * wider by that much, which is what the tool gets.
341
+ *
342
+ * In a band that lies flat (top, bottom) it is the other way round: a
343
+ * tool put above or under another one makes the band higher, by `width`,
344
+ * which is then a height.
289
345
  */
290
346
 
291
347
  export function dockMove( layout: DockLayout, panel: string, target: string, where: DockWhere, width = 0 ): DockLayout {
@@ -319,7 +375,9 @@ export function dockMove( layout: DockLayout, panel: string, target: string, whe
319
375
 
320
376
  const dir = where === "left" || where === "right" ? "row" : "column";
321
377
  const before = where === "left" || where === "top";
322
- const wider = !!band && dir === "row" && width > 0;
378
+
379
+ // across the band: the direction in which it grows
380
+ const wider = !!band && dir === ( isFlat( side ) ? "column" : "row" ) && width > 0;
323
381
 
324
382
  const half = dest.size / 2;
325
383
  const fresh: DockStack = { type: "stack", size: wider ? width : half, panels: [panel], active: panel };
@@ -357,17 +415,22 @@ export function dockMove( layout: DockLayout, panel: string, target: string, whe
357
415
  }
358
416
 
359
417
  /**
360
- * Dock a tool in a band, under the tools that are already there.
361
- * `size` is the width the band gets when it was empty.
418
+ * Dock a tool in a band, after the tools that are already there: under
419
+ * them on the left and on the right, on their right in a band that lies
420
+ * flat (top, bottom). `size` is the width the band gets when it was empty;
421
+ * its height, when it lies flat.
362
422
  */
363
423
 
364
- export function dockBand( layout: DockLayout, panel: string, side: "left" | "right", size = 0 ): DockLayout {
424
+ export function dockBand( layout: DockLayout, panel: string, side: DockSide, size = 0 ): DockLayout {
365
425
  const result = copy( layout );
366
- const band = result[side];
426
+ const band = result[side] ??= { size: 0, root: null };
367
427
 
368
428
  detach( result, panel );
369
429
  tidy( result );
370
430
 
431
+ // along the band: the direction its tools follow each other in
432
+ const along = isFlat( side ) ? "row" : "column";
433
+
371
434
  const fresh: DockStack = { type: "stack", size: 0, panels: [panel], active: panel };
372
435
  const root = band.root;
373
436
 
@@ -378,15 +441,15 @@ export function dockBand( layout: DockLayout, panel: string, side: "left" | "rig
378
441
  band.size = size;
379
442
  }
380
443
  }
381
- else if( root.type === "split" && root.dir === "column" ) {
382
- // as high as the others, on average
444
+ else if( root.type === "split" && root.dir === along ) {
445
+ // as big as the others, on average
383
446
  fresh.size = sharesOf( root.children ).reduce( ( a, b ) => a + b ) / root.children.length;
384
447
  root.children.push( fresh );
385
448
  }
386
449
  else {
387
- // what was there and the tool share the height
450
+ // what was there and the tool share the band
388
451
  root.size = 0;
389
- band.root = { type: "split", dir: "column", size: 0, children: [root, fresh] };
452
+ band.root = { type: "split", dir: along, size: 0, children: [root, fresh] };
390
453
  }
391
454
 
392
455
  return result;
@@ -475,7 +538,7 @@ function sizeOf( from: Record<string, unknown>, name: string ): number {
475
538
  * A layout rebuilt from something that was saved, or null when it is not a
476
539
  * layout or holds no panel. Only what is understood is kept, and only the
477
540
  * panels that are known, each once and where its kind may be: a tool in a
478
- * band or floating, a content among the contents.
541
+ * band it may be docked in, or floating; a content among the contents.
479
542
  *
480
543
  * (a field is read only once it is known to be there: what was saved may be
481
544
  * a state of x4, which reports the reading of a missing property)
@@ -527,17 +590,21 @@ function sanitize( saved: unknown, kinds: DockKinds ): DockLayout {
527
590
  return null;
528
591
  };
529
592
 
530
- const band = ( name: string ): DockBand => {
593
+ // a band only keeps the tools that may be docked in it
594
+ const band = ( name: DockSide ): DockBand => {
531
595
  const from = name in saved ? saved[name] : null;
532
596
  if( !isObject( from ) ) {
533
597
  return { size: 0, root: null };
534
598
  }
535
599
 
536
- return { size: sizeOf( from, "size" ), root: normalize( "root" in from ? node( from.root, kinds.tools ) : null ) };
600
+ const allowed = kinds.tools.filter( id => dockSides( kinds, id ).includes( name ) );
601
+ return { size: sizeOf( from, "size" ), root: normalize( "root" in from ? node( from.root, allowed ) : null ) };
537
602
  };
538
603
 
539
604
  const left = band( "left" );
540
605
  const right = band( "right" );
606
+ const top = band( "top" );
607
+ const bottom = band( "bottom" );
541
608
  const content = normalize( "content" in saved ? node( saved.content, kinds.contents ) : null );
542
609
 
543
610
  const floats: DockFloat[] = [];
@@ -561,7 +628,23 @@ function sanitize( saved: unknown, kinds: DockKinds ): DockLayout {
561
628
  }
562
629
 
563
630
  // nothing in it that is known: not worth more than no layout at all
564
- return seen.size > 0 ? { left, right, content, floats, hidden } : null;
631
+ if( seen.size === 0 ) {
632
+ return null;
633
+ }
634
+
635
+ // the bands of the top and of the bottom are only there when they hold
636
+ // something: a layout that does not use them is as it always was
637
+ const layout: DockLayout = { left, right, content, floats, hidden };
638
+
639
+ if( top.root ) {
640
+ layout.top = top;
641
+ }
642
+
643
+ if( bottom.root ) {
644
+ layout.bottom = bottom;
645
+ }
646
+
647
+ return layout;
565
648
  }
566
649
 
567
650
  /**
@@ -570,7 +653,9 @@ function sanitize( saved: unknown, kinds: DockKinds ): DockLayout {
570
653
  *
571
654
  * `kinds` are the panels that exist today. A saved layout may be older
572
655
  * than the application: the panels it does not know are added where the
573
- * default layout has them, those that are gone are dropped.
656
+ * default layout has them, those that are gone are dropped. So is a tool
657
+ * that is in a band it may not be docked in anymore: it goes where the
658
+ * default layout has it.
574
659
  */
575
660
 
576
661
  export function dockRestore( saved: unknown, kinds: DockKinds, defaults: DockLayout ): DockLayout {
@@ -582,8 +667,10 @@ export function dockRestore( saved: unknown, kinds: DockKinds, defaults: DockLay
582
667
  continue;
583
668
  }
584
669
 
670
+ // where the default layout has it; otherwise on the right if it may, or in the first band it may be in
671
+ const allowed = dockSides( kinds, panel );
585
672
  const float = floatOf( usual, panel );
586
- const side = bandOf( usual, panel ) ?? "right";
673
+ const side = bandOf( usual, panel ) ?? ( allowed.includes( "right" ) ? "right" : allowed[0] );
587
674
 
588
675
  if( kinds.contents.includes( panel ) ) {
589
676
  layout = addContent( layout, panel );
@@ -592,7 +679,9 @@ export function dockRestore( saved: unknown, kinds: DockKinds, defaults: DockLay
592
679
  layout = dockFloat( layout, panel, float );
593
680
  }
594
681
  else {
595
- layout = dockBand( layout, panel, side, usual[side].size );
682
+ // as wide as the default layout has that band, when it has it
683
+ const band = usual[side];
684
+ layout = dockBand( layout, panel, side, band ? band.size : 0 );
596
685
  }
597
686
 
598
687
  if( usual.hidden.includes( panel ) ) {
@@ -59,6 +59,10 @@ export class Gauge extends Component<GaugeProps> {
59
59
  constructor( props: GaugeProps ) {
60
60
  super( props );
61
61
 
62
+ // no range given: 0 to 100
63
+ this.props.min = props.min ?? 0;
64
+ this.props.max = props.max ?? 100;
65
+
62
66
  this.pos = 70;
63
67
  this.stops = [];
64
68
 
@@ -410,7 +410,12 @@ export class Input extends Component<InputProps,InputEvents> {
410
410
  */
411
411
 
412
412
  public setNumValue( value: number, ndec = -1 ) {
413
-
413
+
414
+ // no number (a field that is not filled): the field is emptied
415
+ if( value===undefined || value===null || isNaN( value ) ) {
416
+ return this.setValue( "" );
417
+ }
418
+
414
419
  if( ndec==-2 && this.props.type=='number' ) {
415
420
  const p = this.props as NumberProps;
416
421
 
@@ -18,7 +18,11 @@
18
18
 
19
19
  :root {
20
20
  --notification-border: var( --border );
21
- --notification-background: white;
21
+ --notification-background: var( --background-primary );
22
+
23
+ --notification-title-color: var( --text-primary );
24
+ --notification-text-color: var( --text-ternary );
25
+ --notification-button-color: var( --text-ternary );
22
26
 
23
27
  --notification-icon-default: var( --accent-background );
24
28
  --notification-icon-success: var( --success-background );
@@ -54,11 +58,12 @@
54
58
  .title {
55
59
  font-weight: bold;
56
60
  padding: 6px 6px 3px 6px;
61
+ color: var( --notification-title-color );
57
62
  }
58
63
 
59
64
  .text {
60
65
  padding: 3px 6px 6px 6px;
61
- color: var( --color-gray-7 );
66
+ color: var( --notification-text-color );
62
67
  }
63
68
  }
64
69
 
@@ -81,7 +86,7 @@
81
86
 
82
87
  .x4button {
83
88
  height: 24px;
84
- color: var( --color-gray-7 );
89
+ color: var( --notification-button-color );
85
90
  border: none;
86
91
 
87
92
  //&:hover {
@@ -52,6 +52,9 @@ interface NotificationProps extends ComponentProps {
52
52
  * ```
53
53
  * --notification-border
54
54
  * --notification-background
55
+ * --notification-title-color
56
+ * --notification-text-color
57
+ * --notification-button-color
55
58
  * --notification-icon-default
56
59
  * --notification-icon-success
57
60
  * --notification-icon-danger
@@ -21,8 +21,8 @@ import "./progress.module.scss";
21
21
 
22
22
  interface ProgressProps extends ComponentProps {
23
23
  value: number;
24
- min: number;
25
- max: number;
24
+ min?: number; // 0 when missing
25
+ max?: number; // 100 when missing
26
26
  }
27
27
 
28
28
  /**
@@ -46,7 +46,11 @@ export class Progress extends Component<ProgressProps> {
46
46
  }
47
47
 
48
48
  setValue( value: number ) {
49
- const perc = value / (this.props.max-this.props.min) * 100;
49
+ const min = this.props.min ?? 0;
50
+ const max = this.props.max ?? 100;
51
+
52
+ // where the value is between the two: min is 0%, max is 100%
53
+ const perc = (value-min) / (max-min) * 100;
50
54
  this._bar.setStyleValue( "width", perc+"%" );
51
55
  }
52
56
  }
@@ -28,6 +28,7 @@ interface SwitchProps extends ComponentProps {
28
28
  label: string;
29
29
  checked?: boolean;
30
30
  value?: string;
31
+ name?: string; // with it, a Form reads and writes the switch (a boolean)
31
32
  }
32
33
 
33
34
  /**
@@ -52,7 +53,7 @@ export class Switch extends HBox<SwitchProps> {
52
53
  new Component( {
53
54
  cls: "switch",
54
55
  content: [
55
- new Input( { type: "checkbox", id: inputId, checked: props.checked } ),
56
+ new Input( { type: "checkbox", id: inputId, name: props.name, checked: props.checked } ),
56
57
  new Component( { cls: "track" } ),
57
58
  new Component( { cls: "thumb" } ),
58
59
  ]