x4js 2.3.27 → 2.3.28

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.
Files changed (31) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/aicontext.md +32 -5
  4. package/package.json +2 -1
  5. package/src/components/autocomplete/autocomplete.ts +5 -1
  6. package/src/components/boxes/boxes.ts +2 -2
  7. package/src/components/button/button.module.scss +4 -3
  8. package/src/components/checkbox/checkbox.module.scss +67 -1
  9. package/src/components/checkbox/checkbox.ts +22 -6
  10. package/src/components/colorinput/colorinput.module.scss +1 -0
  11. package/src/components/combobox/combobox.module.scss +4 -1
  12. package/src/components/gridview/gridview.module.scss +6 -0
  13. package/src/components/input/input.module.scss +1 -1
  14. package/src/components/messages/messages.module.scss +9 -4
  15. package/src/components/propgrid/progrid.module.scss +13 -5
  16. package/src/components/radio/radio.module.scss +54 -31
  17. package/src/components/radio/radio.ts +12 -1
  18. package/src/components/sizers/sizer.ts +17 -5
  19. package/src/components/spreadsheet/spreadsheet.module.scss +6 -0
  20. package/src/components/tabs/tabs.module.scss +2 -0
  21. package/src/components/tabs/tabs.ts +1 -0
  22. package/src/components/textarea/textarea.module.scss +9 -0
  23. package/src/components/textarea/textarea.ts +1 -0
  24. package/src/components/themes.scss +1 -0
  25. package/src/components/treeview/treeview.module.scss +6 -1
  26. package/src/components/treeview/treeview.ts +1 -0
  27. package/src/components/video/video.module.scss +5 -2
  28. package/src/components/video/video.ts +14 -1
  29. package/src/core/core_pdf.ts +17 -0
  30. package/src/core/core_react.ts +12 -18
  31. package/src/core/core_svg.ts +9 -6
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 R-libre ingenierie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,8 @@
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/rlibre/x4/main/logox4-v3-dark.svg">
3
+ <img src="https://raw.githubusercontent.com/rlibre/x4/main/logox4-v3.svg" alt="x4 logo" width="96">
4
+ </picture>
5
+
1
6
  # x4js
2
7
  **A TypeScript framework for people who build applications, not markup.**
3
8
 
package/aicontext.md CHANGED
@@ -740,9 +740,14 @@ new SimpleTextArea({ flex: 1, placeholder: 'Your message', resize: false })
740
740
 
741
741
  ```ts
742
742
  interface CheckboxProps extends ComponentProps {
743
- label: string; checked?: boolean; value?: boolean|number|string; name?: string
743
+ label?: string; icon?: string // no label: the icon alone (give it a tooltip)
744
+ checked?: boolean; value?: boolean|number|string; name?: string
744
745
  change?: EventCallback<EvChange>
745
746
  }
747
+ // cls: "left" (label on the left), "button" ("button outline" rounded), "danger" (checked in red, its box or its
748
+ // button: "button danger"); without the box, the input is still reached by Tab (--checkbox-button-*, -danger-*)
749
+ // A toggle button is a Checkbox with cls "button": getCheck() tells whether it is pressed, change when it changes
750
+ // new Checkbox( { cls: "button", icon: icon_bold, tooltip: "Bold", change: ( ev ) => this.setBold( ev.value ) } )
746
751
  // in a Form: without value (or a boolean one), a boolean; with one, the value when checked, null when not
747
752
  class Checkbox extends Component {
748
753
  getCheck(): boolean; setCheck(ck: boolean): void
@@ -766,6 +771,8 @@ class Radio extends Component {
766
771
  setLabel(text: string): void
767
772
  checkValue(vname: string): void // check the radio whose value === vname, in the group of its form (else of the page)
768
773
  }
774
+ // cls: "button" (a button; "button outline" rounded), "danger" (checked in red, its round or its button: "button
775
+ // danger"). Without the round, the input is still reached by Tab.
769
776
  ```
770
777
 
771
778
  ### `Switch`
@@ -845,6 +852,8 @@ class AutoComplete extends TextEdit {
845
852
  getSelection(): ListboxID // id of the picked item, undefined for a free text
846
853
  setSelection(item: ListItem): void; showDropDown(): void
847
854
  }
855
+ // Keys: ↓ (or ↑), the list closed, opens it: the suggestions found last, or else a search of the text as it is
856
+ // (even empty: minChars is for typing). The list open: ↑ ↓ choose, Enter picks, Escape closes.
848
857
  // Use Combobox for a fixed, known list; AutoComplete when suggestions come from a search (server).
849
858
  // In a Form the value is the field content (text or id, per itemValue).
850
859
  ```
@@ -1121,6 +1130,8 @@ class Button extends Component {
1121
1130
  setText(text: string|UnsafeHtml): void
1122
1131
  setIcon(icon: string): void
1123
1132
  }
1133
+ // A toggle button (pressed / not pressed) is not a Button: it is a Checkbox with cls "button" (see Checkbox),
1134
+ // and one choice among several pressed buttons, Radios with cls "button".
1124
1135
  ```
1125
1136
 
1126
1137
  ### `Tabs`
@@ -1345,20 +1356,27 @@ class VSplitBox extends VBox // panels stacked, resizable height:
1345
1356
 
1346
1357
  // Low level: only place sizers by hand when a SplitBox does not fit (popup edges, header cells...).
1347
1358
  class CSizer extends Component // base splitter; events start, resize { size, width, height }, stop
1348
- class HSizer extends CSizer // horizontal splitter
1349
- class VSizer extends CSizer // vertical splitter
1359
+ class HSizer extends CSizer { // horizontal splitter, between two children of a HBox
1360
+ constructor(side: "before" | "after" | boolean = "after") // the child it resizes; a boolean (older code): true after, false before
1361
+ }
1362
+ class VSizer extends CSizer { // vertical splitter, between two children of a VBox; same constructor
1363
+ constructor(side: "before" | "after" | boolean = "after")
1364
+ }
1365
+ // new HBox( { content: [ list, new HSizer( "before" ), new VBox( { flex: 1 } ) ] } ) // the list is resized
1350
1366
  // A pointercancel (a touch taken by the browser) ends the resize as a pointerup does: stop is fired.
1351
1367
  // The columns of a Gridview or a Spreadsheet are 16px wide at least (the min-width of a cell).
1352
1368
  class ScrollView extends Component { getViewport(): Component }
1353
1369
  class Viewport extends Component
1354
1370
  class Canvas extends Component { getContext(): CanvasRenderingContext2D; paint(): void }
1355
- class Video extends Box { // class x4video
1371
+ class Video extends Box { // class x4video; props: src? (a file, url), stream? (wins over src), autoplay?, muted?, playsInline?
1356
1372
  play(): Promise<boolean> // false: could not start (a click of the user wanted, a source not playable); no source: waits for one
1357
1373
  pause(); mute(set: boolean)
1358
1374
  get player(): HTMLVideoElement; get duration(): number; get paused(): boolean
1359
1375
  get volume(): number; get videoWidth(): number; get videoHeight(): number
1376
+ get src(): string; set src(url: string)
1360
1377
  set stream(stream: MediaStream)
1361
1378
  }
1379
+ // The image fills the box of the Video, fitted (object-fit: contain): the box keeps its size, whatever the size of the stream.
1362
1380
  class Calendar extends VBox { // props: date?, minDate?, maxDate? (days out of them cannot be chosen), change ("change": the day clicked)
1363
1381
  getDate(): Date; setDate(date: Date): void // copies: the calendar keeps its own date
1364
1382
  }
@@ -1781,6 +1799,8 @@ class X4PDFBuilder {
1781
1799
  page(width: number, height: number, callback: (ctx: PageContext) => void): void // points; without size 612 x 792
1782
1800
  build(): Uint8Array
1783
1801
  buildAsync(): Promise<Uint8Array> // streams compressed (FlateDecode)
1802
+ buildBlob(): Blob // the file, application/pdf (build)
1803
+ buildURL(): string // an object url of it: iframe src, link, window.open; URL.revokeObjectURL frees it
1784
1804
  }
1785
1805
  export interface PageContext { // origin bottom left, y goes up
1786
1806
  text(str, x, y, size, opts?: { align?: 'left'|'center'|'right'; width?; color?; bold? }): void
@@ -1839,6 +1859,13 @@ createPainter(c2d: CanvasRenderingContext2D, w: number, h: number): CanvasEx /
1839
1859
  initTooltips(): void // initialize the global tooltip system
1840
1860
  ```
1841
1861
 
1862
+ JSX, in `.tsx` files: in tsconfig.json `"jsx": "react"` and `"jsxFactory": "x4_react.create_element"` (esbuild reads
1863
+ them), and `"files": [ "node_modules/x4js/src/x4tsx.d.ts" ]` for the JSX types (`ref`); each `.tsx` file imports `x4_react`.
1864
+ `<X a={1}>{kids}</X>` is `new X( { a: 1, content: kids } )`; a tag in lower case is an html element (its attributes set,
1865
+ an attribute `on...` an event); arrays in braces are flattened, `null`, `undefined` and `false` give nothing (`setContent`
1866
+ does not flatten nested arrays: build such a content in JSX); `ref={[owner, "name"]}` keeps the element in
1867
+ `owner.refs.name`, the owner a Box.
1868
+
1842
1869
  No tooltip is shown until the application calls `initTooltips()` once (the prop `tooltip` only sets an attribute).
1843
1870
  `initTooltips()` shows the text of the attribute `tooltip` (the prop `tooltip`), or the one `getTip()`
1844
1871
  of a `"tip-handler"` gives, **as html**. A text that comes from a user is escaped by the application
@@ -2079,6 +2106,6 @@ Optional keys: `external`, `define`, `esbuild`, `dev` (`host`, `port`, `https`,
2079
2106
  - `Gridview` is backed by a `DataStore`; `Spreadsheet` is backed by a `Store` (different, cell-based).
2080
2107
  - `DataRecordID` is currently typed `any`; `strictNullChecks` is intentionally off in the recommended `tsconfig.json`.
2081
2108
  - `MonacoEditor` requires `MonacoEditor.start()` (async) before use; register extra typings with `MonacoEditor.addTypelib(name, code)`.
2082
- - `x4_react.create_element` is the JSX pragma;
2109
+ - `x4_react.create_element` is the JSX pragma (the setup: JSX factory & helpers).
2083
2110
  - A DOM handler of an ancestor is called for the events of its descendants, as in the browser. Not for the events that do not bubble: `focus`, `blur`, `mouseenter`, `mouseleave` (use `focusin` / `focusout`, `mouseover` / `mouseout`), nor for those of x4: `created`, `removed`, `resized`.
2084
2111
  - Several handlers of one event on one node are called in the order they were added. One that returns `true` stops those that follow it.
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "x4js",
3
- "version": "2.3.27",
3
+ "version": "2.3.28",
4
+ "license": "MIT",
4
5
  "type": "module",
5
6
  "main": "src/x4.ts",
6
7
  "module": "src/x4.ts",
@@ -63,7 +63,7 @@ export interface AutoCompleteProps extends Omit<TextInputProps,"type"|"change">,
63
63
  /** delay between the last key and the search, in ms (default 250) */
64
64
  delay?: number;
65
65
 
66
- /** number of chars needed to start a search (default 1) */
66
+ /** number of chars typed needed to start a search (default 1); ↓ searches whatever the length */
67
67
  minChars?: number;
68
68
 
69
69
  /** custom renderer for the suggestions */
@@ -84,6 +84,10 @@ const render_id_item = ( item: ListItem ) => {
84
84
  /**
85
85
  * Text field with suggestions asked on demand (search event).
86
86
  *
87
+ * Keys: ↓ (or ↑), the list closed, opens it: the suggestions found last, or else a search of
88
+ * the text as it is, whatever its length (minChars is for typing). The list open: ↑ ↓ choose,
89
+ * Enter picks, Escape closes.
90
+ *
87
91
  * @example
88
92
  * new AutoComplete( {
89
93
  * label: "Customer",
@@ -123,8 +123,8 @@ function split_content( panels: Component[], horz: boolean ) {
123
123
  els.forEach( ( p, i ) => {
124
124
  if( i>0 ) {
125
125
  const prev = els[i-1];
126
- const next = prev.hasClass( "x4flex" ) && !p.hasClass( "x4flex" );
127
- res.push( horz ? new HSizer( next ) : new VSizer( next ) );
126
+ const side = prev.hasClass( "x4flex" ) && !p.hasClass( "x4flex" ) ? "after" : "before";
127
+ res.push( horz ? new HSizer( side ) : new VSizer( side ) );
128
128
  }
129
129
 
130
130
  res.push( p );
@@ -165,11 +165,12 @@
165
165
  color: var( --ol-button-color-active );
166
166
  fill: var( --ol-button-color-active );
167
167
 
168
+ // in the colour of the text: the background stays light, not the accent of a filled button
168
169
  #icon {
169
- color: var( --button-icon-color );
170
- fill: var( --button-icon-color );
170
+ color: var( --ol-button-color-active );
171
+ fill: var( --ol-button-color-active );
171
172
  }
172
- }
173
+ }
173
174
  }
174
175
 
175
176
  .x4button.danger:not([disabled]) {
@@ -25,6 +25,16 @@
25
25
  --checkbox-check-border: var( --border );
26
26
  --checkbox-check-border-hover: var( --border-hover );
27
27
  --checkbox-check-color: var( --accent-color );
28
+ --checkbox-focus: var( --accent-background );
29
+
30
+ --checkbox-button-border: var( --border );
31
+ --checkbox-button-color: var( --text-ternary );
32
+ --checkbox-button-color-checked: var( --accent-background-active );
33
+ --checkbox-button-background-checked: var( --background-secondary );
34
+ --checkbox-button-border-checked: var( --accent-background );
35
+
36
+ --checkbox-danger-color: var( --alert-background );
37
+ --checkbox-danger-background: var( --alert-background-light );
28
38
  }
29
39
 
30
40
  .x4checkbox {
@@ -136,8 +146,64 @@
136
146
  order: 2;
137
147
  }
138
148
 
139
- &.left #label {
149
+ // its label has no id: the element, before the box
150
+ &.left label {
140
151
  order: 0;
141
152
  text-align: left;
142
153
  }
154
+ }
155
+
156
+ // button: a button, its background and its border marked when checked (no bold: its width
157
+ // would change with its state). The box hidden, its input still in the page (not display: none),
158
+ // so that Tab and the space bar reach it; the focus of the keyboard shown around it
159
+ .x4checkbox.button {
160
+ // the colour of its icon and its text
161
+ --checkbox-ink: var( --checkbox-button-color );
162
+
163
+ gap: 0;
164
+ margin: 0;
165
+ padding: 8px;
166
+ border: 1px solid var( --checkbox-button-border );
167
+
168
+ &.outline {
169
+ border-radius: 22px;
170
+ }
171
+
172
+ .inner {
173
+ width: 0;
174
+ height: 0;
175
+ overflow: hidden;
176
+ opacity: 0;
177
+ }
178
+
179
+ &:has(input:focus-visible) {
180
+ outline: 1px dotted var( --checkbox-focus );
181
+ outline-offset: 2px;
182
+ }
183
+
184
+ label {
185
+ --label-color: var( --checkbox-ink );
186
+
187
+ #icon {
188
+ color: var( --checkbox-ink );
189
+ fill: var( --checkbox-ink );
190
+ width: 24px;
191
+ height: 24px;
192
+ }
193
+ }
194
+
195
+ &:has(input:checked) {
196
+ --checkbox-ink: var( --checkbox-button-color-checked );
197
+ background-color: var( --checkbox-button-background-checked );
198
+ border-color: var( --checkbox-button-border-checked );
199
+ }
200
+ }
201
+
202
+ // danger: checked in the red of the alerts, its box or its button
203
+ .x4checkbox.danger {
204
+ --checkbox-background-check: var( --checkbox-danger-color );
205
+ --checkbox-background-check-hover: var( --checkbox-danger-color );
206
+ --checkbox-button-color-checked: var( --checkbox-danger-color );
207
+ --checkbox-button-background-checked: var( --checkbox-danger-background );
208
+ --checkbox-button-border-checked: var( --checkbox-danger-color );
143
209
  }
@@ -21,7 +21,8 @@ interface CheckBoxEvents extends ComponentEvents {
21
21
  */
22
22
 
23
23
  interface CheckboxProps extends ComponentProps {
24
- label: string; // The text label for the checkbox.
24
+ label?: string; // The text label for the checkbox; none: its icon alone (with a tooltip)
25
+ icon?: string; // an icon before its label
25
26
  checked?: boolean; // Optional boolean indicating if the checkbox is checked by default.
26
27
  value?: boolean | number | string; // Optional value associated with the checkbox.
27
28
  name?: string;
@@ -30,7 +31,13 @@ interface CheckboxProps extends ComponentProps {
30
31
 
31
32
  /**
32
33
  * Checkbox component that can be checked or unchecked.
33
- *
34
+ * Its look, by its class (cls): "left" (its label on the left), "button" (a button, "button
35
+ * outline" rounded), "danger" (checked in red, its box or its button). Without the box, its
36
+ * input is still reached by Tab.
37
+ *
38
+ * A toggle button is a Checkbox with the class "button":
39
+ * new Checkbox( { cls: "button", icon: icon_bold, tooltip: "Bold", change: ( ev ) => setBold( ev.value ) } )
40
+ *
34
41
  * @cssvar
35
42
  * ```
36
43
  * --checkbox-background-check
@@ -41,6 +48,14 @@ interface CheckboxProps extends ComponentProps {
41
48
  * --checkbox-check-border
42
49
  * --checkbox-check-border-hover
43
50
  * --checkbox-check-color
51
+ * --checkbox-focus
52
+ * --checkbox-button-border
53
+ * --checkbox-button-color
54
+ * --checkbox-button-color-checked
55
+ * --checkbox-button-background-checked
56
+ * --checkbox-button-border-checked
57
+ * --checkbox-danger-color
58
+ * --checkbox-danger-background
44
59
  * ```
45
60
  */
46
61
 
@@ -80,11 +95,12 @@ export class Checkbox extends Component<CheckboxProps,CheckBoxEvents> {
80
95
  })
81
96
  ]
82
97
  }),
83
- new Label( {
98
+ new Label( {
84
99
  tag: 'label',
85
- text: props.label,
86
- labelFor: inputId,
87
- id: undefined
100
+ text: props.label,
101
+ icon: props.icon,
102
+ labelFor: inputId,
103
+ id: undefined
88
104
  } ),
89
105
  ])
90
106
 
@@ -71,6 +71,7 @@
71
71
  margin: 0;
72
72
  height: 100%;
73
73
  flex-grow: 1;
74
+ min-width: 0; // narrower than its default size in a narrow box: it does not overflow
74
75
  }
75
76
 
76
77
  // the eye dropper: as high as the field, and wide enough to be aimed at
@@ -72,7 +72,10 @@
72
72
  &> #label {
73
73
 
74
74
  &> .x4label {
75
- padding: 0 6px 0 0;
75
+ // its text on the right of its width (labelWidth), its colon against the gap of the box:
76
+ // in a form, under the one of a TextEdit
77
+ justify-content: end;
78
+ padding: 0;
76
79
  border-bottom: 1px solid transparent;
77
80
 
78
81
  height: 100%;
@@ -73,6 +73,11 @@
73
73
 
74
74
  background-color: var( --gridview-background );
75
75
  border: 1px solid var( --gridview-border );
76
+
77
+ // disabled: faded as a Box (a Gridview is not one)
78
+ &[disabled] {
79
+ opacity: var( --box-disabled-opacity );
80
+ }
76
81
  color: var( --gridview-cell-color );
77
82
 
78
83
  .cell {
@@ -159,6 +164,7 @@
159
164
  }
160
165
 
161
166
  .percent {
167
+ align-self: center; // a Box stretches (top of the cell): centered in the row
162
168
  width: 100%;
163
169
  border: 1px solid var( --border );
164
170
  height: 8px;
@@ -20,7 +20,7 @@
20
20
 
21
21
  --input-sel-background: var( --accent-background );
22
22
  --input-sel-color: var( --accent-color );
23
- --input-placeholder: var( --disabled-background );
23
+ --input-placeholder: var( --disabled-color-dark ); // a text colour: seen on the background of the field, in both themes
24
24
  --input-error: var( --alert-background );
25
25
 
26
26
  --input-color-disabled: var( --disabled-color-dark );
@@ -46,7 +46,9 @@
46
46
 
47
47
  & .x4form {
48
48
  min-width: 400px;
49
- .x4icon {
49
+
50
+ // the icon of the box only, not those of its message
51
+ > .x4hbox > .x4icon {
50
52
  color: var( --msgbox-icon-color );
51
53
  height: 96px;
52
54
  }
@@ -57,7 +59,7 @@
57
59
  }
58
60
  }
59
61
 
60
- &.question .x4form .x4icon {
62
+ &.question .x4form > .x4hbox > .x4icon {
61
63
  color: var( --msgbox-question-icon-color );
62
64
  }
63
65
  }
@@ -68,7 +70,9 @@
68
70
 
69
71
  & .x4form {
70
72
  min-width: 400px;
71
- .x4icon {
73
+
74
+ // the icon of the box only, not those of its editor (the stars of a Rating...)
75
+ > .x4hbox > .x4icon {
72
76
  color: var( --msgbox-question-icon-color );
73
77
  height: 64px;
74
78
  }
@@ -141,7 +145,8 @@
141
145
  }
142
146
  }
143
147
 
144
- .x4icon {
148
+ // the spinner of the box only, not the icons of the lines of its log
149
+ > .x4hbox > .x4icon {
145
150
  height: 64px;
146
151
  animation: rotating 3s linear infinite;
147
152
  fill: var(--accent-background);
@@ -9,7 +9,7 @@
9
9
  --propertygrid-gadget-background: var( --background-primary );
10
10
  --propertygrid-gadget-color: var( --text-primary );
11
11
 
12
- --propertygrid-odd-background: var(--color-primary-a5);
12
+ --propertygrid-odd-background: var( --background-odd );
13
13
  --propertygrid-even-background: transparent;
14
14
  --propertygrid-disabled-background: rgb(170 170 170 / 14%);
15
15
  --propertygrid-hover-background: var(--color-primary-a10);
@@ -54,8 +54,6 @@
54
54
  padding: 6px 4px 4px 4px;
55
55
  min-height: 2em;
56
56
  background-color: var( --propertygrid-group-background );
57
- margin-bottom: 8px;
58
- box-shadow: 4px 3px 8px rgba(0, 0, 0, 0.4);
59
57
  color: var( --propertygrid-group-color );
60
58
  display: flex;
61
59
  align-items: center;
@@ -81,9 +79,14 @@
81
79
  font-weight: bold;
82
80
  }
83
81
 
82
+ // the button that folds the group: as high as the title, its icon in the colour
83
+ // of the group when it has the focus (its background stays transparent)
84
84
  &>.x4button {
85
+ --button-color-focus: var( --propertygrid-group-color );
85
86
  background-color: transparent;
86
87
  color: var( --propertygrid-group-color );
88
+ margin: 0;
89
+ padding: 0;
87
90
  }
88
91
 
89
92
  &>.x4label {
@@ -99,14 +102,19 @@
99
102
  }
100
103
  }
101
104
 
102
- &>.desc {
105
+ // under the title, in the box of the header
106
+ .desc {
103
107
  font-style: italic;
104
108
  font-weight: normal;
105
- font-size: 90%;
109
+ font-size: 80%;
110
+ padding-top: 4px;
111
+ padding-left: 1em;
106
112
  color: var( --text-ternary );
107
113
  }
108
114
 
115
+ // the space is between the last row of a group and the next group, none under the title
109
116
  &:not(:first-child ) {
117
+ margin-top: 8px;
110
118
  //border-top: 1px solid var(--color-primary-a50);
111
119
  }
112
120
 
@@ -16,8 +16,36 @@
16
16
 
17
17
  :root {
18
18
  --radio-background: var( --background-primary );
19
+ --radio-check-background: var( --accent-background );
19
20
  --radio-check-color: var( --accent-color );
20
- --radio-square-border-checked: var( --accent-color );
21
+ --radio-focus: var( --accent-background );
22
+
23
+ --radio-button-border: var( --border );
24
+ --radio-button-color: var( --text-ternary );
25
+ --radio-button-color-checked: var( --accent-background-active );
26
+ --radio-button-background-checked: var( --background-secondary );
27
+ --radio-button-border-checked: var( --accent-background );
28
+
29
+ --radio-danger-color: var( --alert-background );
30
+ --radio-danger-background: var( --alert-background-light );
31
+ }
32
+
33
+ // button: the round hidden, its input still in the page (not display: none),
34
+ // so that Tab and the arrows reach it; the focus of the keyboard shown around the radio
35
+ %radio-no-round {
36
+ gap: 0; // no space kept for the round
37
+
38
+ .inner {
39
+ width: 0;
40
+ height: 0;
41
+ overflow: hidden;
42
+ opacity: 0;
43
+ }
44
+
45
+ &:has(input:focus-visible) {
46
+ outline: 1px dotted var( --radio-focus );
47
+ outline-offset: 2px;
48
+ }
21
49
  }
22
50
 
23
51
  .x4radio {
@@ -69,7 +97,7 @@
69
97
  height: 100%;
70
98
 
71
99
  .fa-primary {
72
- fill: var( --accent-background)
100
+ fill: var( --radio-check-background );
73
101
  }
74
102
 
75
103
  .fa-secondary {
@@ -98,20 +126,20 @@
98
126
  }
99
127
  }
100
128
 
101
- .x4radio.button {
129
+ // square: its former name, kept for the code that has it (not documented)
130
+ .x4radio.button,
131
+ .x4radio.square {
102
132
  --size: 24px;
103
133
 
104
134
  &.outline {
105
135
  border-radius: 22px;;
106
136
  }
107
137
 
108
- border: 1px solid transparent;
138
+ border: 1px solid var( --radio-button-border );
109
139
  padding: 8px;
110
140
  margin: 0;
111
-
112
- .inner {
113
- display: none;
114
- }
141
+
142
+ @extend %radio-no-round;
115
143
 
116
144
  label {
117
145
  display: flex;
@@ -121,49 +149,44 @@
121
149
  #text {
122
150
  flex-grow: 1;
123
151
  text-align: center;
124
- color: var( --text-ternary );
152
+ color: var( --radio-button-color );
125
153
  }
126
-
154
+
155
+ // fill and color: an icon is drawn in its fill, or in currentColor
127
156
  #icon {
128
- fill: var( --text-ternary );
157
+ fill: var( --radio-button-color );
158
+ color: var( --radio-button-color );
129
159
  width: var( --size );
130
160
  height: var( --size );
131
161
  }
132
162
  }
133
163
 
134
164
  &:has(input:checked) {
135
- background-color: var( --background-secondary );
136
- border-color: var( --accent-background );
165
+ background-color: var( --radio-button-background-checked );
166
+ border-color: var( --radio-button-border-checked );
137
167
 
138
168
  .x4label {
169
+ // no bold: the width of the button would change with its state
139
170
  #text {
140
- color: var( --accent-background-active );
141
- font-weight: bold;
171
+ color: var( --radio-button-color-checked );
142
172
  }
143
173
 
144
174
  #icon {
145
- fill: var( --accent-background-active );
175
+ fill: var( --radio-button-color-checked );
176
+ color: var( --radio-button-color-checked );
146
177
  }
147
178
 
148
179
  .fa-primary {
149
- fill: var( --accent-background-active );
180
+ fill: var( --radio-button-color-checked );
150
181
  }
151
182
  }
152
183
  }
153
184
  }
154
185
 
155
- .x4radio.square {
156
- padding: 5px;
157
- border: 1px solid rgba(255,255,255,0.3);
158
- margin: 0;
159
-
160
- .inner {
161
- display: none;
162
- }
163
-
164
- &:has(input:checked) {
165
- background-color: var( --accent-background-active );
166
- border-color: var( --radio-square-border-checked );
167
- font-weight: 700;
168
- }
186
+ // danger: checked in the red of the alerts, its round or its button
187
+ .x4radio.danger {
188
+ --radio-check-background: var( --radio-danger-color );
189
+ --radio-button-color-checked: var( --radio-danger-color );
190
+ --radio-button-background-checked: var( --radio-danger-background );
191
+ --radio-button-border-checked: var( --radio-danger-color );
169
192
  }
@@ -47,12 +47,23 @@ interface RadioProps extends ComponentProps {
47
47
  }
48
48
 
49
49
  /**
50
+ * A radio: the ones with the same name are a group. Its look, by its class (cls): "button"
51
+ * (a button, "button outline" rounded), "danger" (checked in red, its round or its button).
52
+ * Without the round, its input is still reached by Tab.
50
53
  *
51
54
  * @cssvar
52
55
  * ```
53
56
  * --radio-background
57
+ * --radio-check-background
54
58
  * --radio-check-color
55
- * --radio-square-border-checked
59
+ * --radio-focus
60
+ * --radio-button-border
61
+ * --radio-button-color
62
+ * --radio-button-color-checked
63
+ * --radio-button-background-checked
64
+ * --radio-button-border-checked
65
+ * --radio-danger-color
66
+ * --radio-danger-background
56
67
  * ```
57
68
  */
58
69
 
@@ -167,7 +167,9 @@ export class CSizer extends Component<ComponentProps,CSizerEvent> {
167
167
  if( this._type.includes("-prev") || this._type.includes("-next") ) {
168
168
  const neighbour = this._type.includes("-prev") ? this.prevElement( ) : this.nextElement( );
169
169
  if( neighbour ) {
170
- this._ref.setStyleValue( "flex", `0 0 ${size}px` );
170
+ // shrink 1: no larger than the space the box has left (the flex neighbour at its
171
+ // padding); 0 at least, a negative basis is not a value
172
+ this._ref.setStyleValue( "flex", `0 1 ${Math.max( 0, size )}px` );
171
173
  }
172
174
  else {
173
175
  this._ref.setStyle( nr );
@@ -185,18 +187,28 @@ export class CSizer extends Component<ComponentProps,CSizerEvent> {
185
187
  }
186
188
  }
187
189
 
190
+ /**
191
+ * a sizer between two children of a HBox: it resizes the one before it or the one after it
192
+ * (a boolean, as in older code: true after, false before)
193
+ */
194
+
188
195
  export class HSizer extends CSizer {
189
196
  static classPrefix = "x4";
190
197
 
191
- constructor( next = true ) {
192
- super( next ? "hsize-next" : "hsize-prev" );
198
+ constructor( side: "before" | "after" | boolean = "after" ) {
199
+ super( side === "before" || side === false ? "hsize-prev" : "hsize-next" );
193
200
  }
194
201
  }
195
202
 
203
+ /**
204
+ * a sizer between two children of a VBox: it resizes the one before it (above) or the one
205
+ * after it (under); a boolean as for HSizer
206
+ */
207
+
196
208
  export class VSizer extends CSizer {
197
209
  static classPrefix = "x4";
198
210
 
199
- constructor( next = true ) {
200
- super( next ? "vsize-next" : "vsize-prev" );
211
+ constructor( side: "before" | "after" | boolean = "after" ) {
212
+ super( side === "before" || side === false ? "vsize-prev" : "vsize-next" );
201
213
  }
202
214
  }
@@ -72,6 +72,11 @@
72
72
  background-color: var( --spreadsheet-background );
73
73
  border: 1px solid var( --spreadsheet-border );
74
74
  color: var( --spreadsheet-cell-color );
75
+
76
+ // disabled: faded as a Box (a Spreadsheet is not one)
77
+ &[disabled] {
78
+ opacity: var( --box-disabled-opacity );
79
+ }
75
80
 
76
81
  .cell {
77
82
  padding: 4px;
@@ -179,6 +184,7 @@
179
184
  }
180
185
 
181
186
  .percent {
187
+ align-self: center; // a Box stretches (top of the cell): centered in the row
182
188
  width: 100%;
183
189
  border: 1px solid var( --border );
184
190
  height: 8px;
@@ -6,6 +6,7 @@
6
6
  --tab-color-hover: var( --text-primary );
7
7
  --tab-color: var( --text-primary );
8
8
  --tab-color-selected: var( --text-primary );
9
+ --tab-icon-color: var( --icon-color );
9
10
  }
10
11
 
11
12
  .x4tabs {
@@ -24,6 +25,7 @@
24
25
  border: none;
25
26
  border-bottom: 2px solid transparent;
26
27
  color: var( --tab-color );
28
+ --ol-button-icon-color: var( --tab-icon-color );
27
29
  margin: 0;
28
30
  outline: none;
29
31
  transition: border-color 0.2s ease;
@@ -149,6 +149,7 @@ interface TabsProps extends Omit<ComponentProps,"content"> {
149
149
  * --tab-color-hover
150
150
  * --tab-color
151
151
  * --tab-color-selected
152
+ * --tab-icon-color
152
153
  * ```
153
154
  */
154
155
 
@@ -23,6 +23,7 @@
23
23
  --textarea-selection-color: var( --accent-color );
24
24
  --textarea-background: var( --background-input );
25
25
  --textarea-color: var( --text-primary );
26
+ --textarea-required: var( --alert-background );
26
27
  }
27
28
 
28
29
  .x4simpletextarea {
@@ -77,6 +78,14 @@
77
78
  display: none;
78
79
  }
79
80
 
81
+ // required: a "*" before the label, as the one of a TextEdit
82
+ &:has( > .x4simpletextarea[required] ) > .x4label::before {
83
+ content: "*";
84
+ font-weight: bold;
85
+ color: var( --textarea-required );
86
+ margin-right: 2px;
87
+ }
88
+
80
89
  .x4simpletextarea {
81
90
  // its height of rows when the TextArea has none, the TextArea's height when it has one;
82
91
  // a basis of 0 would leave it the height of its padding: a textarea scrolls, its min-height is 0
@@ -139,6 +139,7 @@ export class SimpleTextArea extends Component<SimpleTextAreaProps,TextAreaEvents
139
139
  * --textarea-selection-color
140
140
  * --textarea-background
141
141
  * --textarea-color
142
+ * --textarea-required
142
143
  * ```
143
144
  */
144
145
 
@@ -52,6 +52,7 @@ $color--interval: 8.4%;
52
52
  --background-primary: var(--color-primary-a0);
53
53
  --background-secondary: var(--color-primary-a10);
54
54
  --background-ternary: color-mix(in srgb, var(--color-primary-a100) 4.3%, var(--color-primary-a0)); // #f4f4f4
55
+ --background-odd: color-mix(in srgb, var(--color-primary-a100) 2.4%, var(--color-primary-a0)); // #f9f9f9, a row out of two
55
56
  --background-hover: color-mix(in srgb, var(--color-primary-a100) 6.7%, var(--color-primary-a0)); // #eee
56
57
  --background-active: color-mix(in srgb, var(--color-primary-a100) 20%, var(--color-primary-a0)); // #ccc
57
58
  --background-input: color-mix(in srgb, var(--color-primary-a0) 60%, transparent); // white at 60%
@@ -20,6 +20,7 @@
20
20
  --treeview-background: var( --background-primary );
21
21
 
22
22
  --treeitem-color: var( --text-primary );
23
+ --treeitem-icon-color: var( --icon-color );
23
24
 
24
25
  --treeitem-backgound-hover: var( --background-secondary );
25
26
  --treeitem-color-hover: var( --text-primary );
@@ -79,7 +80,7 @@
79
80
 
80
81
  &>.label {
81
82
  --label-color: var( --treeitem-color );
82
- --label-icon-color: var( --treeitem-color );
83
+ --label-icon-color: var( --treeitem-icon-color );
83
84
 
84
85
  gap: 4px;
85
86
 
@@ -210,6 +211,10 @@
210
211
  border-color: var( --border );
211
212
  color: var( --treeview-item-color-disabled );
212
213
 
214
+ // the texts and the icons of the items
215
+ --treeitem-color: var( --treeview-item-color-disabled );
216
+ --treeitem-icon-color: var( --treeview-item-color-disabled );
217
+
213
218
  &>.body {
214
219
  .x4viewport {
215
220
  pointer-events: none;
@@ -73,6 +73,7 @@ export interface TreeviewEvents extends ComponentEvents {
73
73
  * ```
74
74
  * --treeview-background
75
75
  * --treeitem-color
76
+ * --treeitem-icon-color
76
77
  * --treeitem-backgound-hover
77
78
  * --treeitem-color-hover
78
79
  * --treeitem-background-sel
@@ -20,11 +20,14 @@
20
20
  background-color: black;
21
21
  position: relative;
22
22
 
23
+ // the size of the box, whatever the size of the stream (a <video> keeps its own size
24
+ // with only left / right): the image fitted in it, black bands around
23
25
  .vplayer {
24
26
  position: absolute;
25
27
  left: 0;
26
28
  top: 0;
27
- right: 0;
28
- bottom: 0;
29
+ width: 100%;
30
+ height: 100%;
31
+ object-fit: contain;
29
32
  }
30
33
  }
@@ -23,7 +23,8 @@ interface VideoProps extends ComponentProps {
23
23
  autoplay?: boolean;
24
24
  muted?: boolean;
25
25
  playsInline?: boolean;
26
- stream: MediaStream;
26
+ src?: string; // a file to play (url); a stream, when there is one, is shown instead
27
+ stream?: MediaStream; // a camera, a screen, a canvas (captureStream)
27
28
  }
28
29
 
29
30
  /**
@@ -55,6 +56,10 @@ export class Video extends Box {
55
56
  this.playsInline = props.playsInline;
56
57
  }
57
58
 
59
+ if( props.src ) {
60
+ this.src = props.src;
61
+ }
62
+
58
63
  this.stream = props.stream;
59
64
  }
60
65
 
@@ -86,6 +91,14 @@ export class Video extends Box {
86
91
  this.player.playsInline = set;
87
92
  }
88
93
 
94
+ get src( ) {
95
+ return this.player.src;
96
+ }
97
+
98
+ set src( url: string ) {
99
+ this.player.src = url;
100
+ }
101
+
89
102
  set stream( stream: MediaStream ) {
90
103
  this.player.srcObject = stream;
91
104
  }
@@ -1543,6 +1543,23 @@ export class X4PDFBuilder {
1543
1543
 
1544
1544
  return result
1545
1545
  }
1546
+
1547
+ /**
1548
+ * the document as a file, of type application/pdf (build, its streams not compressed)
1549
+ */
1550
+
1551
+ buildBlob(): Blob {
1552
+ return new Blob([this.build() as BlobPart], { type: 'application/pdf' })
1553
+ }
1554
+
1555
+ /**
1556
+ * an object url of the document: the src of an iframe, the href of a link, window.open.
1557
+ * URL.revokeObjectURL( url ) frees it when it is no longer shown
1558
+ */
1559
+
1560
+ buildURL(): string {
1561
+ return URL.createObjectURL(this.buildBlob())
1562
+ }
1546
1563
  }
1547
1564
 
1548
1565
  /*
@@ -13,25 +13,19 @@
13
13
  * Use of this source code is governed by an MIT-style license
14
14
  * that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
15
15
  *
16
- * to use you must:
17
- *
18
- * 1. add this to your tsconfig.json
16
+ * JSX in .tsx files:
17
+ *
18
+ * 1. in tsconfig.json (esbuild reads it too):
19
19
  * "compilerOptions": {
20
- * ...
21
- * "jsx": "preserve",
22
- * ...
23
- * }
24
- *
25
- * 2. be sure that esbuild has this:
26
- * jsxFactory: "x4create_element",
27
- * loader: { ".ts": "tsx" }
28
- *
29
- *
30
- * after that, all things like that will be ok
31
- *
32
- * import { x4_create_element } from "x4react"
33
- * const xx = <h1>This is a title</h1>
34
- *
20
+ * "jsx": "react",
21
+ * "jsxFactory": "x4_react.create_element"
22
+ * },
23
+ * "files": [ "node_modules/x4js/src/x4tsx.d.ts" ] // the JSX types of x4 (ref)
24
+ *
25
+ * 2. in each .tsx file, the factory imported:
26
+ * import { Label, VBox, x4_react } from "x4js";
27
+ * const box = <VBox><Label text="Hello" /></VBox>;
28
+ *
35
29
  */
36
30
 
37
31
  import { Component } from './component';
@@ -557,7 +557,8 @@ export class SvgText extends SvgItem {
557
557
  * @returns The current `SvgText` instance for chaining.
558
558
  */
559
559
  fontWeight( weight: 'light' | 'normal' | 'bold' ): this {
560
- return this.setAttr( 'font-weight', weight );
560
+ // "light" is no value of font-weight: its number
561
+ return this.setAttr( 'font-weight', weight=='light' ? '300' : weight );
561
562
  }
562
563
 
563
564
  /**
@@ -583,23 +584,25 @@ export class SvgText extends SvgItem {
583
584
  }
584
585
 
585
586
  /**
586
- * change the vertical alignment
587
+ * change the vertical alignment: y is the top, the middle, the bottom or the baseline
588
+ * of the text. dominant-baseline, read by all the browsers (alignment-baseline is not
589
+ * by Firefox before 149)
587
590
  */
588
591
  verticalAlign( align: 'top' | 'center' | 'bottom' | 'baseline' ): this {
589
592
 
590
593
  let al;
591
594
  switch( align ) {
592
595
  case 'top': al = 'hanging'; break;
593
- case 'center': al = 'middle'; break;
594
- case 'bottom': al = 'baseline'; break;
595
- case 'baseline': al = 'mathematical'; break;
596
+ case 'center': al = 'central'; break;
597
+ case 'bottom': al = 'ideographic'; break;
598
+ case 'baseline': al = 'alphabetic'; break;
596
599
  default:
597
600
  // If an invalid alignment is provided, do nothing and return this for chaining.
598
601
  console.warn(`Invalid verticalAlign value: ${align}. Must be 'top', 'center', 'bottom', or 'baseline'.`);
599
602
  return this;
600
603
  }
601
604
 
602
- return this.setAttr( 'alignment-baseline', al );
605
+ return this.setAttr( 'dominant-baseline', al );
603
606
  }
604
607
  }
605
608