x4js 2.3.9 → 2.3.11

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
@@ -26,7 +26,7 @@ import { ... } from 'x4js' // src/x4.ts
26
26
  CoreElement<E> named timers + event surface (no DOM)
27
27
  ├── Component<P,E> base DOM component
28
28
  │ ├── Box<P,E>
29
- │ │ ├── HBox / VBox
29
+ │ │ ├── HBox / VBox → HSplitBox / VSplitBox
30
30
  │ │ ├── StackBox → AssistBox
31
31
  │ │ ├── GridBox / MasonryBox
32
32
  │ │ ├── Form
@@ -35,10 +35,11 @@ CoreElement<E> named timers + event surface (no DOM)
35
35
  │ │ ├── BtnGroup, Tabs, Video, Saturation
36
36
  │ ├── Button, Input, Icon, Label, SimpleText, Checkbox, Radio, Link
37
37
  │ ├── Listbox, Gridview, Treeview, Select, Combobox, Slider, Gauge
38
- │ ├── Progress, TickLine, Canvas, Image, SvgComponent, Spreadsheet, MonacoEditor
38
+ │ ├── Progress, TickLine, Canvas, Image, Spreadsheet, MonacoEditor
39
+ │ ├── SvgComponent → Chart
39
40
  │ ├── CSizer → HSizer / VSizer
40
41
  │ ├── FileDialog, ScrollView, Viewport, Flex, Space
41
- │ ├── HBox-based: ColorInput, Rating, Switch, Keyboard, TextEdit, Breadcrumbs
42
+ │ ├── HBox-based: ColorInput, Rating, Switch, Keyboard, TextEdit → AutoComplete, Breadcrumbs
42
43
  │ └── VBox-based: Calendar, ColorPicker, FileDrop, Panel, PropertyGrid, TextArea
43
44
  └── Application<E> singleton, no DOM of its own
44
45
 
@@ -608,6 +609,26 @@ class Combobox extends Component {
608
609
  }
609
610
  ```
610
611
 
612
+ ### `AutoComplete` — text field with on-demand suggestions
613
+
614
+ ```ts
615
+ interface AutoCompleteProps extends TextEditProps { // label, labelWidth, required, placeholder, value...
616
+ search: (ev: EvSearch) => void // required; ev.text, ev.setItems(items) — may be called async
617
+ change? // EvAutoCompleteChange { value, item /* null = free text */ }
618
+ strict?: boolean // only a picked item (or empty) is accepted; default false
619
+ itemValue?: "text" | "id" // what goes into the field when an item is picked; default "text"
620
+ delay?: number // ms before search, default 250
621
+ minChars?: number // default 1
622
+ renderer?: (item: ListItem) => Component
623
+ }
624
+ class AutoComplete extends TextEdit {
625
+ getSelection(): ListboxID // id of the picked item, undefined for a free text
626
+ setSelection(item: ListItem): void; showDropDown(): void
627
+ }
628
+ // Use Combobox for a fixed, known list; AutoComplete when suggestions come from a search (server).
629
+ // In a Form the value is the field content (text or id, per itemValue).
630
+ ```
631
+
611
632
  ### `ColorInput`
612
633
 
613
634
  ```ts
@@ -710,12 +731,29 @@ interface SpreadsheetProps extends ComponentProps {
710
731
  columns: SpreadsheetColumn[] // required
711
732
  footer?: boolean
712
733
  rowClassifier?: RowClassifier
734
+ enterMove?: "down" | "right" | "none" // selection move after Enter in a cell editor (default "down")
713
735
  click?, dblClick?, contextMenu?, selectionChange?
736
+ cellChange? // EvCellChange, fired before an edited value is written to the store
737
+ }
738
+ interface SpreadsheetColumn { // GridColumn +
739
+ editable?: boolean | ((row, col) => boolean) // inline edition (off by default), col = column index
740
+ editor?: (row, col, value) => Component // custom editor, must implement queryInterface("form-element")
714
741
  }
742
+ interface EvCellChange { row; col /* index */; colId /* store key */; value /* writable */; oldValue }
715
743
  class Spreadsheet extends Component {
716
744
  getSelection(): /* cell */; navigate(sens: kbNav): boolean; lock(lock): void
745
+ editCell(row, col, clear = false): boolean // cell must be visible
746
+ stopEdit(commit = true): void; isEditing(): boolean
717
747
  }
718
748
 
749
+ // Inline edition: F2 / Enter / double-click / typing starts, Enter / Tab / click elsewhere validates, Escape cancels,
750
+ // Delete clears, editable checkboxes toggle on click or Space. Default editor is an Input matching the column type.
751
+ // cellChange: ev.preventDefault() refuses the value, ev.value can be replaced.
752
+ // Select / Combobox in a cell (the store holds the id, use `formatter` to display the text):
753
+ // { id: 2, title: "Status", width: 120, editable: true,
754
+ // formatter: ( id ) => states.find( x => x.id===id )?.text,
755
+ // editor: ( row, col, value ) => new Select( { items: states, value } ) }
756
+
719
757
  // Store: in-memory cell store (extends CoreElement)
720
758
  class Store extends CoreElement {
721
759
  setData(row, col, data): void; getData(row, col): any; hasData(row, col?): boolean
@@ -724,6 +762,28 @@ class Store extends CoreElement {
724
762
  }
725
763
  ```
726
764
 
765
+ ### `Chart` — minimal svg chart (line, area, bar, pie, donut)
766
+
767
+ ```ts
768
+ interface ChartProps extends SvgProps {
769
+ type?: "line" | "area" | "bar" | "pie" | "donut" // default "line"
770
+ series: ChartSerie[] // required; pie & donut use the first serie
771
+ labels?: string[] // default: 1, 2, 3...
772
+ xaxis?: ChartAxis; yaxis?: ChartAxis // { name?, ticks?, min?, max?, step? }, all computed by default
773
+ legend?: boolean // default: shown when 2+ named series
774
+ stacked?: boolean
775
+ formatter?: (value: number) => string // default: chartFormatUnit (1.5k, 2M...)
776
+ click? // EvChartClick { serie, index, value }
777
+ }
778
+ interface ChartSerie { name?; values: number[] /* undefined = hole */; color?; fill?; dots? }
779
+ class Chart extends SvgComponent {
780
+ setType(type): void; setSeries(series, labels?): void; setLabels(labels): void
781
+ setTicks(sens: "x" | "y", ticks: ChartTick[]): void
782
+ }
783
+ // Colors: CSS variables --chart-color-1..8 (or serie.color). Size it with width/height/flex like any component.
784
+ // Minimal by design: no dual axis, mixed types, time axis, zoom or animation — use a dedicated library for those.
785
+ ```
786
+
727
787
  ### `PropertyGrid`
728
788
 
729
789
  ```ts
@@ -894,6 +954,13 @@ class Form extends Box {
894
954
  ## Sizers / viewport / misc
895
955
 
896
956
  ```ts
957
+ // PREFERRED for resizable panels: HSplitBox / VSplitBox (sizers are inserted automatically between panels).
958
+ // The panel with `flex` takes the remaining space, its neighbour is the one resized.
959
+ class HSplitBox extends HBox // panels side by side, resizable width: { content: Component[] }
960
+ class VSplitBox extends VBox // panels stacked, resizable height: { content: Component[] }
961
+ // new HSplitBox( { content: [ new Treeview( { width: 250 } ), new Panel( { flex: 1 } ) ] } )
962
+
963
+ // Low level: only place sizers by hand when a SplitBox does not fit (popup edges, header cells...).
897
964
  class CSizer extends Component // base splitter
898
965
  class HSizer extends CSizer // horizontal splitter
899
966
  class VSizer extends CSizer // vertical splitter
package/aiguide.md CHANGED
@@ -132,6 +132,23 @@ super();
132
132
 
133
133
  when the component expects props.
134
134
 
135
+ When writing a custom component, pass the whole props object: do not remove your own values from it before calling `super`. The base class keeps what it receives in `this.props`, so they stay readable later.
136
+
137
+ ```ts
138
+ // avoid: search and strict are lost for this.props
139
+ const { search, strict, ...base } = props;
140
+ super(base);
141
+
142
+ // prefer: override only what must not reach the parent
143
+ super({ ...props, type: "text" });
144
+ ```
145
+
146
+ Connect the event callbacks given in props with `mapPropEvents`, not with hand-written `if (props.x) this.on(...)`:
147
+
148
+ ```ts
149
+ this.mapPropEvents(props, "change", "search");
150
+ ```
151
+
135
152
  For derived application components, prefer building the component's main content explicitly after `super(props)`.
136
153
 
137
154
  Ordinary nested x4 components may naturally use their `content` property where convenient.
@@ -192,6 +209,10 @@ Component objects
192
209
  Real DOM
193
210
  ```
194
211
 
212
+ `addDOMEvent` belongs to custom components (a canvas-based widget, for example).
213
+
214
+ On standard components, use their props and events.
215
+
195
216
  ---
196
217
 
197
218
  ## 6. Objects vs JSX
@@ -327,6 +348,12 @@ export const deviceModel = new DeviceModel();
327
348
 
328
349
  A model may encapsulate server access directly.
329
350
 
351
+ Views should never call `fetch`.
352
+
353
+ All server access goes through model classes deriving from one base class that owns the single `fetch` call (timeout, network failure, HTTP status, expired session, fetch mask).
354
+
355
+ Create one model per functional domain. It normally corresponds to one URL root.
356
+
330
357
  Keep responsibilities simple:
331
358
 
332
359
  ```text id="t39sbp"
@@ -467,6 +494,18 @@ Do not introduce a JavaScript theme abstraction simply to mirror CSS variables.
467
494
 
468
495
  Presentation belongs in CSS when CSS already solves the problem.
469
496
 
497
+ CSS styles a component; it never removes behaviour.
498
+
499
+ Do not hide a control a component creates: configure the component through its props (for example `closable: false`).
500
+
501
+ If the props cannot express it, say so instead of working around it.
502
+
503
+ To retheme an application, redefine the primary colour scale (`--color-primary-a5` … `--color-primary-a90`) once per theme.
504
+
505
+ Do not recolour components one by one, and do not override the variables derived from the scale.
506
+
507
+ A dark theme gets its own scale.
508
+
470
509
  ---
471
510
 
472
511
  ## 19. Browser and TypeScript capabilities
@@ -578,6 +617,27 @@ The goal is to build the application with the smallest set of mechanisms that ke
578
617
 
579
618
  ---
580
619
 
620
+ ## 23. Translations
621
+
622
+ x4 starts in French.
623
+
624
+ Application texts are declared with `addTranslation` for each language.
625
+
626
+ Select the language before any component is built, since a component reads its texts at construction.
627
+
628
+ Call `selectLanguage` after `addTranslation`:
629
+
630
+ ```ts
631
+ addTranslation("fr", { /* ... */ });
632
+ addTranslation("en", { /* ... */ });
633
+
634
+ selectLanguage("en");
635
+
636
+ // only now build the application components
637
+ ```
638
+
639
+ ---
640
+
581
641
  ## Final rule
582
642
 
583
643
  When generating x4 code:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x4js",
3
- "version": "2.3.9",
3
+ "version": "2.3.11",
4
4
  "type": "module",
5
5
  "main": "src/x4.ts",
6
6
  "module": "src/x4.ts",
@@ -0,0 +1,28 @@
1
+ /**
2
+ * ___ ___ __
3
+ * \ \/ / / _
4
+ * \ / /_| |_
5
+ * / \____ _|
6
+ * /__/\__\ |_|
7
+ *
8
+ * @file autocomplete.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
+ // the field itself is a textedit
18
+
19
+ .x4dropdownlist.autocomplete {
20
+ .x4item {
21
+ gap: 8px;
22
+
23
+ .id {
24
+ font-weight: 500;
25
+ min-width: 4em;
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,361 @@
1
+ /**
2
+ * ___ ___ __
3
+ * \ \/ / / _
4
+ * \ / /_| |_
5
+ * / \____ _|
6
+ * /__/\__\ |_|
7
+ *
8
+ * @file autocomplete.ts
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
+ import { Component, ComponentEvent, ComponentEvents, EvChange } from '../../core/component';
18
+ import { EventCallback } from '../../core/core_events';
19
+ import { class_ns, kbNav } from '../../core/core_tools';
20
+
21
+ import { HBox } from '../boxes/boxes';
22
+ import { DropdownList } from '../combobox/combobox';
23
+ import { TextInputProps } from '../input/input';
24
+ import { SimpleText } from '../label/label';
25
+ import { ListboxID, ListItem } from '../listbox/listbox';
26
+ import { TextEdit, TextEditBase } from '../textedit/textedit';
27
+
28
+ import "./autocomplete.module.scss"
29
+
30
+ /**
31
+ * fired when suggestions are needed for a text
32
+ * setItems can be called later (async), late answers are ignored
33
+ */
34
+
35
+ export interface EvSearch extends ComponentEvent {
36
+ text: string;
37
+ setItems: ( items: ListItem[] ) => void;
38
+ }
39
+
40
+ /**
41
+ * fired when the value is validated
42
+ */
43
+
44
+ export interface EvAutoCompleteChange extends EvChange {
45
+ item: ListItem; // picked item, null for a free text
46
+ }
47
+
48
+ interface AutoCompleteEvents extends ComponentEvents {
49
+ search: EvSearch;
50
+ change: EvAutoCompleteChange;
51
+ }
52
+
53
+ export interface AutoCompleteProps extends Omit<TextInputProps,"type"|"change">, TextEditBase {
54
+ search: EventCallback<EvSearch>;
55
+ change?: EventCallback<EvAutoCompleteChange>;
56
+
57
+ /** only a picked item (or nothing) is accepted, default false: free text is kept */
58
+ strict?: boolean;
59
+
60
+ /** what is written in the field when an item is picked (default "text") */
61
+ itemValue?: "text" | "id";
62
+
63
+ /** delay between the last key and the search, in ms (default 250) */
64
+ delay?: number;
65
+
66
+ /** number of chars needed to start a search (default 1) */
67
+ minChars?: number;
68
+
69
+ /** custom renderer for the suggestions */
70
+ renderer?: ( item: ListItem ) => Component;
71
+ }
72
+
73
+ // suggestion as: id + text
74
+ const render_id_item = ( item: ListItem ) => {
75
+ return new HBox( {
76
+ cls: item.cls,
77
+ content: [
78
+ new SimpleText( { cls: "id", text: item.id+"" } ),
79
+ new SimpleText( { cls: "text", text: item.text } ),
80
+ ]
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Text field with suggestions asked on demand (search event).
86
+ *
87
+ * @example
88
+ * new AutoComplete( {
89
+ * label: "Customer",
90
+ * strict: true,
91
+ * search: async ( ev ) => {
92
+ * const rows = await api.searchCustomers( ev.text );
93
+ * ev.setItems( rows.map( r => ( { id: r.id, text: r.name } ) ) );
94
+ * },
95
+ * change: ( ev ) => console.log( ev.value, ev.item ),
96
+ * })
97
+ */
98
+
99
+ @class_ns( "x4" )
100
+ export class AutoComplete extends TextEdit<AutoCompleteEvents> {
101
+
102
+ private _popup: DropdownList;
103
+
104
+ private _strict: boolean;
105
+ private _by_id: boolean;
106
+ private _delay: number;
107
+ private _min_chars: number;
108
+
109
+ private _text: string; // last validated text
110
+ private _item: ListItem; // last picked item (null: free text)
111
+ private _seq: number; // search sequence, to ignore late answers
112
+ private _count: number; // number of suggestions
113
+
114
+ constructor( props: AutoCompleteProps ) {
115
+ // the change of the input (each key) starts the search, our change event is fired on validation
116
+ super( { ...props, type: "text", change: ( ) => this._on_input( ) } );
117
+
118
+ this._strict = !!props.strict;
119
+ this._by_id = props.itemValue=="id";
120
+ this._delay = props.delay ?? 250;
121
+ this._min_chars = props.minChars ?? 1;
122
+
123
+ this._text = this.getInput( ).getValue( );
124
+ this._item = null;
125
+ this._seq = 0;
126
+ this._count = 0;
127
+
128
+ this.mapPropEvents( props, 'change', 'search' );
129
+
130
+ this._popup = new DropdownList( {
131
+ cls: "autocomplete",
132
+ items: [],
133
+ renderer: props.renderer ?? ( this._by_id ? render_id_item : undefined )
134
+ } );
135
+
136
+ this._popup.on( "click", ( ev ) => {
137
+ const item = this._popup.getList( ).getItem( ev.context as ListboxID );
138
+ if( item ) {
139
+ this._pick( item );
140
+ }
141
+ });
142
+
143
+ this.getInput( ).addDOMEvent( "keydown", ( ev ) => this._on_key( ev ) );
144
+
145
+ this.addDOMEvent( "focusout", ( ev ) => this._on_focusout( ev ) );
146
+ }
147
+
148
+ /**
149
+ *
150
+ */
151
+
152
+ private _on_input( ) {
153
+ const text = this.getInput( ).getValue( );
154
+
155
+ // pending answers are obsolete
156
+ this._seq++;
157
+
158
+ if( text.length<this._min_chars ) {
159
+ this.clearTimeout( "search" );
160
+ this._popup.show( false );
161
+ return;
162
+ }
163
+
164
+ this.setTimeout( "search", this._delay, ( ) => this._search( text ) );
165
+ }
166
+
167
+ /**
168
+ *
169
+ */
170
+
171
+ private _search( text: string ) {
172
+ const seq = ++this._seq;
173
+
174
+ const setItems = ( items: ListItem[] ) => {
175
+ // a newer search was started (or the field was left)
176
+ if( seq!=this._seq ) {
177
+ return;
178
+ }
179
+
180
+ this._setItems( items ?? [] );
181
+ }
182
+
183
+ this.fire( "search", { text, setItems } );
184
+ }
185
+
186
+ private _setItems( items: ListItem[] ) {
187
+ const list = this._popup.getList( );
188
+ list.setItems( items );
189
+ this._count = items.length;
190
+
191
+ if( !items.length ) {
192
+ this._popup.show( false );
193
+ return;
194
+ }
195
+
196
+ this.showDropDown( );
197
+
198
+ // strict: Enter picks the first suggestion
199
+ if( this._strict ) {
200
+ list.navigate( kbNav.first );
201
+ }
202
+ }
203
+
204
+ /**
205
+ *
206
+ */
207
+
208
+ private _on_key( ev: KeyboardEvent ) {
209
+ const list = this._popup.getList( );
210
+ const open = this._popup.isOpen( );
211
+
212
+ switch( ev.key ) {
213
+ case "ArrowUp":
214
+ case "ArrowDown": {
215
+ if( open ) {
216
+ list.navigate( ev.key=="ArrowDown" ? kbNav.next : kbNav.prev );
217
+ }
218
+ else if( this._count ) {
219
+ this.showDropDown( );
220
+ }
221
+ else {
222
+ this.clearTimeout( "search" );
223
+ this._search( this.getInput( ).getValue( ) );
224
+ }
225
+
226
+ break;
227
+ }
228
+
229
+ case "Enter": {
230
+ const [sel] = open ? list.getSelection( ) : [];
231
+ const item = sel!==undefined ? list.getItem( sel ) : null;
232
+
233
+ if( !item ) {
234
+ // nothing to pick: the key is not for us
235
+ this._popup.show( false );
236
+ return;
237
+ }
238
+
239
+ this._pick( item );
240
+ break;
241
+ }
242
+
243
+ case "Escape": {
244
+ if( !open ) {
245
+ return;
246
+ }
247
+
248
+ this._popup.show( false );
249
+ break;
250
+ }
251
+
252
+ default: {
253
+ return;
254
+ }
255
+ }
256
+
257
+ ev.preventDefault( );
258
+ ev.stopPropagation( );
259
+ }
260
+
261
+ /**
262
+ *
263
+ */
264
+
265
+ private _on_focusout( ev: FocusEvent ) {
266
+ // focus is moving inside the control
267
+ const to = ev.relatedTarget as Node;
268
+ if( to && this.dom.contains( to ) ) {
269
+ return;
270
+ }
271
+
272
+ this.clearTimeout( "search" );
273
+ this._seq++;
274
+ this._popup.show( false );
275
+
276
+ const input = this.getInput( );
277
+ const text = input.getValue( );
278
+ if( text===this._text ) {
279
+ return;
280
+ }
281
+
282
+ // strict: back to the last validated value (but the field can be emptied)
283
+ if( this._strict && text!=="" ) {
284
+ input.setValue( this._text );
285
+ return;
286
+ }
287
+
288
+ this._commit( text, null );
289
+ }
290
+
291
+ /**
292
+ * an item was choosen in the suggestions
293
+ */
294
+
295
+ private _pick( item: ListItem ) {
296
+ const text = ( this._by_id ? item.id : item.text ) + "";
297
+
298
+ this.clearTimeout( "search" );
299
+ this._seq++;
300
+
301
+ this.getInput( ).setValue( text );
302
+ this._popup.show( false );
303
+
304
+ this._commit( text, item );
305
+ }
306
+
307
+ private _commit( text: string, item: ListItem ) {
308
+ const changed = text!==this._text || item?.id!==this._item?.id;
309
+
310
+ this._text = text;
311
+ this._item = item;
312
+
313
+ if( changed ) {
314
+ this.fire( "change", { value: text, item } );
315
+ }
316
+ }
317
+
318
+ /**
319
+ *
320
+ */
321
+
322
+ showDropDown( ) {
323
+ if( this.isDisabled() || this._popup.isOpen( ) ) {
324
+ return;
325
+ }
326
+
327
+ const rc = ( this.query( ":scope > #edit" ) ?? this ).getBoundingRect( );
328
+ this._popup.setStyleValue( "minWidth", rc.width+"px" );
329
+ this._popup.displayNear( rc, "top left", "bottom left", {x:0,y:6} );
330
+ }
331
+
332
+ /**
333
+ * change the text of the field (no item is selected, no event)
334
+ */
335
+
336
+ override setValue( value: string ) {
337
+ super.setValue( value );
338
+
339
+ this._text = this.getInput( ).getValue( );
340
+ this._item = null;
341
+ }
342
+
343
+ /**
344
+ * select an item as if it was picked in the suggestions (no event)
345
+ */
346
+
347
+ setSelection( item: ListItem ) {
348
+ super.setValue( ( this._by_id ? item.id : item.text ) + "" );
349
+
350
+ this._text = this.getInput( ).getValue( );
351
+ this._item = item;
352
+ }
353
+
354
+ /**
355
+ * @returns the id of the picked item, undefined if the text does not come from the suggestions
356
+ */
357
+
358
+ getSelection( ): ListboxID {
359
+ return this.getInput( ).getValue( )===this._text ? this._item?.id : undefined;
360
+ }
361
+ }
@@ -17,6 +17,7 @@
17
17
  import { asap, class_ns, isArray, isNumber } from '../../core/core_tools';
18
18
  import { Component, ComponentEvents, ComponentProps, EvSelectionChange } from "../../core/component"
19
19
  import { EventCallback } from '../../core/core_events';
20
+ import { HSizer, VSizer } from '../sizers/sizer';
20
21
 
21
22
  import "./boxes.module.scss";
22
23
 
@@ -91,6 +92,71 @@ export class VBox<P extends BoxProps=BoxProps,E extends ComponentEvents=Componen
91
92
  }
92
93
 
93
94
 
95
+ // :: SPLITBOX ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
96
+
97
+ export interface SplitBoxProps extends Omit<BoxProps,"content"> {
98
+ /** Panels to lay out, a sizer is inserted between each of them. */
99
+ content: Component[];
100
+ }
101
+
102
+ /**
103
+ * insert a sizer between each panel.
104
+ * the sizer resizes the panel that is not flex (the flex one takes the remaining space),
105
+ * when both are of the same kind, the previous panel is resized.
106
+ */
107
+
108
+ function split_content( panels: Component[], horz: boolean ) {
109
+ const els = panels?.filter( x => !!x ) ?? [];
110
+ const res: Component[] = [];
111
+
112
+ els.forEach( ( p, i ) => {
113
+ if( i>0 ) {
114
+ const prev = els[i-1];
115
+ const next = prev.hasClass( "x4flex" ) && !p.hasClass( "x4flex" );
116
+ res.push( horz ? new HSizer( next ) : new VSizer( next ) );
117
+ }
118
+
119
+ res.push( p );
120
+ });
121
+
122
+ return res;
123
+ }
124
+
125
+ /**
126
+ * A horizontal box where panels are separated by sizers: panels are side by side
127
+ * (like in a {@link HBox}) and the user can change their width.
128
+ * The CSS class for this component is automatically generated as `x4hsplitbox`.
129
+ *
130
+ * @example
131
+ * new HSplitBox( {
132
+ * content: [
133
+ * new Treeview( { width: 250, ... } ), // resized by the sizer
134
+ * new Panel( { flex: 1, ... } ), // takes the remaining space
135
+ * ]
136
+ * })
137
+ */
138
+
139
+ @class_ns( "x4" )
140
+ export class HSplitBox<P extends SplitBoxProps=SplitBoxProps,E extends ComponentEvents=ComponentEvents> extends HBox<P,E> {
141
+ constructor( props: P ) {
142
+ super( { ...props, content: split_content( props.content, true ) } );
143
+ }
144
+ }
145
+
146
+ /**
147
+ * A vertical box where panels are separated by sizers: panels are stacked
148
+ * (like in a {@link VBox}) and the user can change their height.
149
+ * The CSS class for this component is automatically generated as `x4vsplitbox`.
150
+ */
151
+
152
+ @class_ns( "x4" )
153
+ export class VSplitBox<P extends SplitBoxProps=SplitBoxProps,E extends ComponentEvents=ComponentEvents> extends VBox<P,E> {
154
+ constructor( props: P ) {
155
+ super( { ...props, content: split_content( props.content, false ) } );
156
+ }
157
+ }
158
+
159
+
94
160
  type ContentBuilder = ( ) => Component;
95
161
 
96
162
 
@@ -14,12 +14,12 @@
14
14
  * that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
15
15
  **/
16
16
 
17
- import { Menu, MenuElement, MenuProps } from 'x4js';
18
17
  import { Component, ComponentEvents, ComponentProps, EvClick } from "../../core/component"
19
18
  import { EventCallback } from '../../core/core_events';
20
19
  import { class_ns, isFunction, UnsafeHtml } from '../../core/core_tools';
21
20
 
22
21
  import { Icon } from "../icon/icon"
22
+ import { Menu, MenuElement } from '../menu/menu';
23
23
 
24
24
  import "./button.module.scss";
25
25