x4js 2.3.24 → 2.3.26

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 (68) hide show
  1. package/aicontext.md +101 -17
  2. package/aiguide.md +64 -15
  3. package/package.json +1 -1
  4. package/src/components/assets/icons.ts +2 -0
  5. package/src/components/autocomplete/autocomplete.ts +2 -2
  6. package/src/components/boxes/boxes.module.scss +16 -2
  7. package/src/components/boxes/boxes.ts +24 -11
  8. package/src/components/breadcrumb/breadcrumb.ts +1 -2
  9. package/src/components/btngroup/btngroup.ts +2 -2
  10. package/src/components/button/button.ts +7 -2
  11. package/src/components/calendar/calendar.ts +4 -2
  12. package/src/components/canvas/canvas.ts +1 -2
  13. package/src/components/chart/chart.ts +3 -3
  14. package/src/components/checkbox/checkbox.ts +2 -2
  15. package/src/components/colorinput/colorinput.ts +3 -2
  16. package/src/components/colorpicker/colorpicker.ts +5 -5
  17. package/src/components/combobox/combobox.ts +3 -3
  18. package/src/components/dialog/dialog.ts +8 -2
  19. package/src/components/dockingview/dockingview.ts +1 -2
  20. package/src/components/filedrop/filedrop.ts +3 -2
  21. package/src/components/form/form.module.scss +2 -1
  22. package/src/components/form/form.ts +7 -3
  23. package/src/components/gauge/gauge.ts +2 -2
  24. package/src/components/gridview/gridview.module.scss +8 -5
  25. package/src/components/gridview/gridview.ts +11 -13
  26. package/src/components/header/header.ts +2 -2
  27. package/src/components/icon/icon.ts +1 -2
  28. package/src/components/image/image.ts +1 -2
  29. package/src/components/input/input.ts +3 -2
  30. package/src/components/keyboard/keyboard.ts +2 -2
  31. package/src/components/label/label.ts +3 -3
  32. package/src/components/link/link.ts +3 -2
  33. package/src/components/listbox/listbox.module.scss +59 -2
  34. package/src/components/listbox/listbox.ts +133 -23
  35. package/src/components/menu/menu.ts +5 -4
  36. package/src/components/messages/messages.module.scss +95 -3
  37. package/src/components/messages/messages.ts +113 -27
  38. package/src/components/notification/notification.ts +6 -6
  39. package/src/components/panel/panel.ts +2 -2
  40. package/src/components/popup/popup.ts +17 -2
  41. package/src/components/progress/progress.ts +1 -2
  42. package/src/components/propgrid/progrid.module.scss +2 -3
  43. package/src/components/propgrid/propgrid.ts +32 -38
  44. package/src/components/radio/radio.ts +1 -2
  45. package/src/components/rating/rating.ts +2 -2
  46. package/src/components/select/select.ts +2 -2
  47. package/src/components/settingsview/settingsview.module.scss +6 -5
  48. package/src/components/settingsview/settingsview.ts +3 -3
  49. package/src/components/sizers/sizer.ts +21 -29
  50. package/src/components/slider/slider.ts +2 -2
  51. package/src/components/spreadsheet/spreadsheet.ts +8 -10
  52. package/src/components/switch/switch.ts +2 -2
  53. package/src/components/tabs/tabs.ts +4 -4
  54. package/src/components/tag/tag.ts +2 -2
  55. package/src/components/textarea/textarea.ts +3 -3
  56. package/src/components/textedit/textedit.ts +2 -2
  57. package/src/components/themes.scss +2 -0
  58. package/src/components/tickline/tickline.ts +3 -3
  59. package/src/components/tooltips/tooltips.ts +11 -2
  60. package/src/components/treeview/treeview.ts +3 -3
  61. package/src/components/video/video.ts +1 -2
  62. package/src/components/viewport/viewport.ts +4 -3
  63. package/src/core/component.ts +15 -24
  64. package/src/core/core_application.ts +36 -3
  65. package/src/core/core_events.ts +14 -2
  66. package/src/core/core_i18n.ts +67 -0
  67. package/src/core/core_svg.ts +1 -0
  68. package/src/core/core_tools.ts +83 -24
package/aicontext.md CHANGED
@@ -91,6 +91,8 @@ fire<K>(name: K, ev: E[K]): void
91
91
 
92
92
  - `capturing = true` inserts at the head of the listener list.
93
93
  - `fire()` automatically injects `source`, `type`, `stopPropagation()`, `preventDefault()` when absent.
94
+ - A listener that throws: `fire()` sends its error to `console.error`, calls the next listeners, and returns
95
+ normally; the one that fires never gets the error of a listener. (An `async` listener rejects its own promise.)
94
96
 
95
97
  ### `CoreEvent`
96
98
 
@@ -140,7 +142,7 @@ interface ComponentProps {
140
142
  flex?: number | boolean // true → "x4flex"; number → flex-grow
141
143
  disabled?: boolean
142
144
  hidden?: boolean
143
- tooltip?: string
145
+ tooltip?: string // shown as html: escape a text from a user (see initTooltips)
144
146
  existingDOM?: HTMLElement // wrap an existing DOM node
145
147
  }
146
148
 
@@ -167,6 +169,15 @@ toggleClass(cls: string): void
167
169
  setClass(cls: string, set?: boolean): this
168
170
  ```
169
171
 
172
+ A component gets one class for each class of its chain: its name in lower case, after the prefix that class
173
+ declares itself with `static classPrefix` (not the one of its parent), then `x4`. Every component of x4 declares
174
+ `static classPrefix = "x4"` (a Button: `x4button x4`); a class of the application declares its own, or none.
175
+
176
+ ```ts
177
+ class Avatar extends Image { static classPrefix = "my"; } // classes: myavatar x4image x4
178
+ class Badge extends Label { } // classes: badge x4label x4
179
+ ```
180
+
170
181
  ### HTML attributes & ARIA
171
182
 
172
183
  ```ts
@@ -220,7 +231,8 @@ childCount(): number
220
231
  enumChildComponents(recursive: boolean): Component[]
221
232
  enumChildNodes(recursive: boolean): Node[]
222
233
  visitChildren(cb: (el: Component) => boolean): void
223
- static parentElement<T>(dom: Node, cls?: Constructor<T>): T
234
+ static parentElement<T>(dom: Node, cls?: Constructor<T>): T // starts at the parent of dom, not at dom:
235
+ // test componentFromDOM(dom) first for the component of dom itself
224
236
  ```
225
237
 
226
238
  ### Visibility / state / focus / geometry
@@ -288,6 +300,11 @@ static fireGlobal(msg: string, params?: any): void
288
300
  static setTheme(name: string): void // the class x4theme-<name> on <html>: "" the light theme (default, no class),
289
301
  // "dark" the dark theme of x4, another name a theme of the application
290
302
  static getTheme(): string // "" for the light theme, "dark", or the name given to setTheme
303
+ static setLocale(locale: string, currency?: string): void // forces the locale of the formats (formatNumber, formatMoney,
304
+ // formatDate, formatDateTime) over the one of the language; null: the language's
305
+ // again. currency: "EUR" by default, kept when not given
306
+ static getLocale(): string // the forced one, else _tr.global.locale ("fr-FR" for fr, "en-US" for en)
307
+ static getCurrency(): string
291
308
  focusNext(next: boolean): boolean
292
309
  addShortcut(keys: string | string[], callback: (ev: KeyboardEvent) => void, options?: ShortcutOptions): void // see Shortcuts
293
310
  setupSocketMessaging(path?: string, looseCallback?: () => void): void
@@ -310,8 +327,14 @@ class VBox<P, E> extends Box<P, E>
310
327
  interface BoxProps extends ComponentProps { tag?: string }
311
328
  ```
312
329
 
330
+ A Box is a `<div>` (another element with `tag`). Disabled (`disable()`, the prop `disabled`), its content is inert
331
+ (no click, no focus, no input) and faded by `--box-disabled-opacity` (0.5); a disabled Box in it is not faded twice.
332
+
313
333
  ### `Box.refs` — typed directory of the children to reach again later
314
334
 
335
+ Only a `Box` has `refs`: a class derived from `Component` (not from `Box`) has none, and `this.refs.x = ...` there
336
+ compiles with a `declare refs` but throws a `TypeError` at run time.
337
+
315
338
  `Box` owns a `protected refs: Record<string, Component>`. Declare its shape in the subclass, then assign each child
316
339
  to `refs` while building the content: the assignment evaluates to the component, so it is written inline in the
317
340
  content array. Do not keep such children in separate private fields, and do not use `refs.x` for a child that was
@@ -826,29 +849,46 @@ class FileDialog extends Component {} // programmatic file picker
826
849
  interface ListItem {
827
850
  id: ListboxID // number | string
828
851
  text: string | UnsafeHtml
829
- iconId?: string; data?: any; cls?: string; checked?: boolean
852
+ sub_cols?: (string | UnsafeHtml | Component)[] // extra columns after text (9 at most, ref-c2...ref-c10, see Header);
853
+ // a null value makes no cell. A Treeview draws them too
854
+ iconId?: string; data?: any; cls?: string
855
+ checked?: boolean // checkable: the starting state of the case (getChecked() gives the current one)
830
856
  }
831
857
  interface ListboxProps extends Omit<ComponentProps,"content"> {
832
858
  items?: ListItem[]
833
859
  renderer?: (item: ListItem) => Component
834
860
  title?: string; icon?: string
835
861
  header?: Header; footer?: Component
836
- checkable?: true; multisel?: true
862
+ checkable?: boolean // a case at the start of each line
863
+ multisel?: true
864
+ reselect?: boolean // a click on the item already selected fires selectionChange again
865
+ // (lists linked together: the view shows what was clicked last)
837
866
  emptyMsg?: string // shown in place of the items while there are none
838
867
  dblClick?: EventCallback<EvDblClick>
839
868
  selectionChange?: EventCallback<EvSelectionChange>
840
869
  contextMenu?: EventCallback<EvContextMenu>
870
+ checkChange?: EventCallback<EvCheckChange> // EvCheckChange { id: ListboxID, checked: boolean }
841
871
  }
842
872
  // emptyMsg: without it the text is _tr.global.empty_list ("Empty list"). It is shown by setItems
843
873
  // (the constructor calls it) when there is no item. The listbox then has the class "empty".
874
+ // click: no prop; listbox.on( "click", ev => ... ), ev.context the id of the item. It is fired on the item
875
+ // already selected too, which fires no selectionChange.
876
+ // checkable: renderItem puts the case (.x4check) first in the line, with a renderer of yours too;
877
+ // a checked line has the class "checked". A click on the case, or Space for the last item
878
+ // selected, changes it and fires checkChange: no click, no selection. A double click on the
879
+ // case is ignored (its two clicks change it twice). setChecked fires nothing; updateItem keeps
880
+ // the state of the case. With a header, the columns start after the case.
844
881
  class Listbox extends Component {
845
- setItems(items: ListItem[], keepSel?: boolean): void
882
+ setItems(items: ListItem[], keepSel?: boolean): void // one selectionChange at the end, with the selection it ends
883
+ // with, when there was one before or there is one after (keepSel: the same ids too)
846
884
  appendItem(item: ListItem, prepend?: boolean, select?: boolean): void
847
885
  updateItem(id: any, item: ListItem): void
848
886
  getItem(id: ListboxID): ListItem
849
887
  select(ids: ListboxID | ListboxID[], notify?: boolean): void
850
888
  clearSelection(fireEvent?: boolean): void
851
889
  getSelection(): ListboxID[]
890
+ getChecked(): ListboxID[] // checkable: in the order of the list
891
+ setChecked(ids: ListboxID[]): void // these checked, the others not; no event
852
892
  filter(filter: string | RegExp): void
853
893
  navigate(sens: kbNav): boolean
854
894
  ensureSelectionVisible(): void
@@ -1036,6 +1076,7 @@ interface ButtonProps extends ComponentProps {
1036
1076
  menu?: MenuElement[] | (() => MenuElement[]) // a click opens this Menu under the button, in place of the click event;
1037
1077
  // a function is called at each click; the button gets the class "with-menu"
1038
1078
  }
1079
+ // its <button> is type="button": in a <form>, Enter in a field does not click it. attrs: { type: "submit" } to submit
1039
1080
  class Button extends Component {
1040
1081
  click(): void // simulate a click
1041
1082
  setText(text: string|UnsafeHtml): void
@@ -1078,6 +1119,8 @@ class Popup extends Box {
1078
1119
  isOpen(): boolean
1079
1120
  // show()/hide() inherited from Component; show() routes to displayCenter()
1080
1121
  }
1122
+ // A modal popup (a Dialog, a MessageBox...) keeps the element that had the focus when it opens and gives
1123
+ // it back the focus when it closes, unless the focus was put elsewhere meanwhile.
1081
1124
  ```
1082
1125
 
1083
1126
  ### `Dialog`
@@ -1101,6 +1144,10 @@ class Dialog extends Popup {
1101
1144
  show(): this // each time: the focus on its first field
1102
1145
  showAsync(): Promise<string> // the name of the clicked button; null when closed without a button (Escape, close box)
1103
1146
  }
1147
+ // Keys: Escape closes it (or clicks `closable` when it is a button name). Enter clicks its default
1148
+ // button ("ok.default" in buttons), except in a textarea or an element contenteditable, where Enter is
1149
+ // a new line and Ctrl+Enter (Cmd+Enter) clicks it. Out of a dialog, Enter goes to the next field,
1150
+ // except in a textarea or a contenteditable.
1104
1151
  ```
1105
1152
 
1106
1153
  ### `MessageBox` / `InputBox` / `PromptBox` / `ProgressionBox`
@@ -1120,10 +1167,24 @@ class PromptBox extends Dialog {
1120
1167
  class ProgressionBox extends Dialog {
1121
1168
  constructor(title: string)
1122
1169
  addText(text: string|UnsafeHtml, perc: number): void
1123
- addError(text: string|UnsafeHtml, perc: number): void
1124
- clear(): void
1125
- done(): void // hide after 5s if no error, else show OK button
1126
- }
1170
+ addError(text: string|UnsafeHtml, perc: number): void // red, on a light red line, a red bar on its left;
1171
+ // the title bar in red until clear()
1172
+ addWarning(text: string|UnsafeHtml, perc: number): void // the same in orange
1173
+ addSection(title: string|UnsafeHtml, perc: number): void // a section (<details>, open): the lines that follow go
1174
+ // in it; a click on its title folds it; its title has the icon
1175
+ // of the worst of its lines. Without any, the lines go in the log
1176
+ setText(text: string|UnsafeHtml, perc: number): void // in place of all the lines
1177
+ clear(): void // no line, no error, no button OK
1178
+ done(): void // shows OK: after an error or a warning it waits for a click,
1179
+ // their counts on the left of the bar ("2 errors / 1 warning",
1180
+ // from _tr.global.progression), the icon of the worst;
1181
+ // else it counts "OK (5s)" and closes the box at 0
1182
+ }
1183
+ // ProgressionBox: its lines in a <pre> in a fixed font, a <div> each (class "line", plus "error" or "warning"),
1184
+ // 10 lines high, scrolled both ways (the box keeps its size), every line as wide as the longest one;
1185
+ // a string is text, an UnsafeHtml html. Ctrl+A in the lines selects them only (a folded section too).
1186
+ // Colours: --msgbox-log-background, -border, -border-focus, -error-color, -error-background,
1187
+ // -warning-color, -warning-background, -scrollbar (the thumb of its scroll bars; their track is its background).
1127
1188
  ```
1128
1189
 
1129
1190
  ### `Notification`
@@ -1146,7 +1207,8 @@ class Notification extends Popup { display(time_in_s?: number): void } // 0: u
1146
1207
  ```ts
1147
1208
  type FormValues = Record<string, any>
1148
1209
  interface FormProps extends BoxProps { autoComplete?: boolean }
1149
- class Form extends Box {
1210
+ class Form<P extends FormProps = FormProps, E extends ComponentEvents = ComponentEvents> extends Box<P, E> {
1211
+ // a class derived from Form declares its events: class EntryForm extends Form<EntryProps, EntryEvents>
1150
1212
  setValues(values: FormValues): void
1151
1213
  getValues<T extends FormValues>(): T
1152
1214
  setAutoComplete(on?: boolean): void // attribute on the <form>: fields without their own follow it, added later too
@@ -1154,6 +1216,9 @@ class Form extends Box {
1154
1216
  validate(): FormValues // null if invalid; the values the validator changes are written back in their fields (any element with the name, every radio of a group)
1155
1217
  }
1156
1218
  // Collects all children (recursive) that have a "name" attribute and implement queryInterface("form-element").
1219
+ // Never sent by the browser: its "submit" is prevented (Enter in its only field would send it).
1220
+ // A Form is flex by default (it takes the free height of its box) and has a padding of 16px.
1221
+ // flex: false: the height of its content (a toolbar, a line); its padding set back by a css rule.
1157
1222
  ```
1158
1223
 
1159
1224
  ---
@@ -1240,9 +1305,11 @@ class VSplitBox extends VBox // panels stacked, resizable height:
1240
1305
  // new HSplitBox( { content: [ new Treeview( { width: 250 } ), new Panel( { flex: 1 } ) ] } )
1241
1306
 
1242
1307
  // Low level: only place sizers by hand when a SplitBox does not fit (popup edges, header cells...).
1243
- class CSizer extends Component // base splitter
1308
+ class CSizer extends Component // base splitter; events start, resize { size, width, height }, stop
1244
1309
  class HSizer extends CSizer // horizontal splitter
1245
1310
  class VSizer extends CSizer // vertical splitter
1311
+ // A pointercancel (a touch taken by the browser) ends the resize as a pointerup does: stop is fired.
1312
+ // The columns of a Gridview or a Spreadsheet are 16px wide at least (the min-width of a cell).
1246
1313
  class ScrollView extends Component { getViewport(): Component }
1247
1314
  class Viewport extends Component
1248
1315
  class Canvas extends Component { getContext(): CanvasRenderingContext2D; paint(): void }
@@ -1405,7 +1472,7 @@ class MyView extends VBox {
1405
1472
  }
1406
1473
  ```
1407
1474
 
1408
- - A component shortcut also works while the focus is nowhere, if the component is the last one with shortcuts that had the focus: of two views shown together, the one the user works in answers.
1475
+ - A component shortcut also works while the focus is nowhere, if the component is the last one with shortcuts that had the focus: of two views shown together, the one the user works in answers. Not while that component is hidden (in a tab not shown, for instance).
1409
1476
  - A key typed in an `input`, a `textarea` or a `select` is not a shortcut, unless `editable` is set.
1410
1477
  - A key that what has the focus already used (it called `preventDefault` or `stopPropagation`) is not a shortcut.
1411
1478
  - A shortcut always takes its key (`preventDefault`). For a key that must sometimes be left alone, use a `keydown` handler instead.
@@ -1507,10 +1574,18 @@ isLanguage(name: string): boolean
1507
1574
  // search search_tip required_field invalid_format invalid_email invalid_number invalid_date
1508
1575
  // empty_list err_403 property value copy cut paste filedrop
1509
1576
  // diff_date_seconds/minutes/hours, date_input_formats, date_format, date_time_format,
1577
+ // locale ("fr-FR", "en-US", "es-ES", "de-DE": the formats of numbers, amounts, dates),
1510
1578
  // day_short[] day_long[] month_short[] month_long[], keyboard.{next,numeric,alpha},
1511
- // settings.{search,nomatch}
1579
+ // settings.{search,nomatch}, progression.{error,errors,warning,warnings}
1580
+ getCurrentLocale(): string; getCurrentCurrency(): string // as Application.getLocale / getCurrency
1512
1581
  ```
1513
1582
 
1583
+ The language gives the texts and the locale of the formats: `selectLanguage("en")` changes both. A regional
1584
+ variant is a language based on another: `createLanguage("fr-ch", "fr")`, `addTranslation("fr-ch", { global: { locale: "fr-CH" } })`.
1585
+ `Application.setLocale(locale, currency)` forces the locale over the language's. The fields of date and the
1586
+ Calendar follow the patterns of the language (`date_format`...), not the locale. A field of type number is shown
1587
+ by the browser, in its own locale, whatever this one.
1588
+
1514
1589
  ---
1515
1590
 
1516
1591
  ## Theme (`themes.scss`)
@@ -1584,7 +1659,14 @@ pascalCase(s): string; camelCase(s): string; clamp<T>(v, min, max): T
1584
1659
  measureText(str, size, bold?): number // width in Helvetica, from core_pdf (see PDF)
1585
1660
 
1586
1661
  // Dates
1587
- date_format(date, options?): string // options not read yet: formatIntlDate(date)
1662
+ date_format(date, options?): string // options: Intl.DateTimeFormat in the locale; without: formatIntlDate(date)
1663
+
1664
+ // Formats in the current locale (Application.setLocale, else the one of the language), Intl made once by locale
1665
+ formatNumber(value, decimals?, grouping = true): string // "1 234,5" in fr-FR; decimals: the most shown
1666
+ formatMoney(value): string // "1 234,50 €": the currency of Application.getCurrency()
1667
+ formatDate(date): string // "22/10/2026"
1668
+ formatDateTime(date): string // "22/10/2026 14:05"
1669
+ // Gridview and Spreadsheet format their columns number, money, date, date-time with them (a formatter comes first)
1588
1670
  date_diff(date1, date2, options?): string // "30 secondes", "5 minutes", "2 heures 5 minutes" (either order); options not read
1589
1671
  date_to_sql(date, withHours): string; date_sql_utc(date): Date
1590
1672
  date_hash(date): number; date_clone(date): Date
@@ -1603,9 +1685,6 @@ getSystemMetrics(part: "scrollbar"|"zoom"): number
1603
1685
  setWaitCursor(wait: boolean): void; beep(): void
1604
1686
  isUnitLess(name): boolean; isFeatureAvailable(name: "eyedropper"): boolean
1605
1687
 
1606
- // Class decorator
1607
- class_ns(ns: string): ClassDecorator
1608
- const x4_class_ns_sym: symbol
1609
1688
  const unitless // set of unitless CSS props
1610
1689
 
1611
1690
  // System interfaces
@@ -1720,6 +1799,11 @@ createPainter(c2d: CanvasRenderingContext2D, w: number, h: number): CanvasEx /
1720
1799
  initTooltips(): void // initialize the global tooltip system
1721
1800
  ```
1722
1801
 
1802
+ No tooltip is shown until the application calls `initTooltips()` once (the prop `tooltip` only sets an attribute).
1803
+ `initTooltips()` shows the text of the attribute `tooltip` (the prop `tooltip`), or the one `getTip()`
1804
+ of a `"tip-handler"` gives, **as html**. A text that comes from a user is escaped by the application
1805
+ with `sanitizeHtml( text )`: `tooltip: \`Open <b>${sanitizeHtml( file.name )}</b>\``.
1806
+
1723
1807
  ---
1724
1808
 
1725
1809
  ## State (`core_state`)
package/aiguide.md CHANGED
@@ -20,7 +20,7 @@ Prefer:
20
20
  - plain objects for application data
21
21
  - `State` for observable application state
22
22
  - local events for things that happened locally
23
- - `GlobalEvent` for application-wide or distributed messages
23
+ - a global message (`Application.fireGlobal`) for application-wide or distributed messages
24
24
  - `DataStore` for specialized tabular data
25
25
  - persistent `Component` objects backed by the real DOM
26
26
  - CSS and CSS variables for presentation and theming
@@ -84,13 +84,13 @@ For a simple event payload, `context` can carry the relevant identity or value w
84
84
 
85
85
  Do not put transient actions into State merely to trigger behavior.
86
86
 
87
- ### GlobalEvent
87
+ ### Global messages
88
88
 
89
- Use `GlobalEvent` for application-wide messages.
89
+ Use a global message (`Application.fireGlobal`) for application-wide messages.
90
90
 
91
- A GlobalEvent may also be transported between processes, clients or a server.
91
+ A global message may also be transported between processes, clients or a server.
92
92
 
93
- Views should not care how a GlobalEvent is transported.
93
+ Views should not care how a global message is transported.
94
94
 
95
95
  In particular, do not expose WebSocket details to UI components merely because WebSocket is used as the transport.
96
96
 
@@ -295,9 +295,24 @@ Do not use events as a replacement for persistent application state.
295
295
 
296
296
  ---
297
297
 
298
- ## 9. GlobalEvent
298
+ ## 9. Global messages
299
299
 
300
- Use `GlobalEvent` for messages whose scope is the application or distributed system rather than a local component relationship.
300
+ A global message is sent by `Application.fireGlobal( msg, params )` and received by
301
+ `component.onGlobalEvent( ev => ... )`, `ev` an `EvMessage { msg, params }`. The listener of
302
+ `onGlobalEvent` is taken off when the component leaves the page.
303
+
304
+ ```ts
305
+ Application.fireGlobal( "device.updated", { id: deviceId } );
306
+
307
+ this.onGlobalEvent( ( ev ) => {
308
+ if( ev.msg == "device.updated" ) {
309
+ this.refresh( ev.params.id );
310
+ }
311
+ } );
312
+ ```
313
+
314
+ Use a global message for what concerns the application or a distributed system, rather than a
315
+ local component relationship.
301
316
 
302
317
  Global messages should normally use stable application-level names, for example:
303
318
 
@@ -318,11 +333,11 @@ The receiver can obtain the authoritative object through the application's model
318
333
 
319
334
  For high-frequency realtime information such as telemetry, carrying the actual data can be appropriate.
320
335
 
321
- Do not force every GlobalEvent into the same payload strategy.
336
+ Do not force every global message into the same payload strategy.
322
337
 
323
338
  Most importantly:
324
339
 
325
- > GlobalEvent represents the message, not its transport.
340
+ > A global message represents the message, not its transport.
326
341
 
327
342
  A view should normally not contain WebSocket-specific logic.
328
343
 
@@ -361,7 +376,7 @@ Model obtains or modifies domain data
361
376
  State represents current application state
362
377
  Component presents that state
363
378
  Events communicate local occurrences
364
- GlobalEvent communicates application-wide occurrences
379
+ Global msg communicates application-wide occurrences (Application.fireGlobal)
365
380
  ```
366
381
 
367
382
  Do not add additional layers unless they solve an actual problem.
@@ -500,11 +515,36 @@ Do not hide a control a component creates: configure the component through its p
500
515
 
501
516
  If the props cannot express it, say so instead of working around it.
502
517
 
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.
518
+ A theme is a class on `<html>`. `Application.setTheme( name )` puts `x4theme-<name>` there and
519
+ takes off the one before; `Application.setTheme( "" )` goes back to the light theme, the default;
520
+ `Application.getTheme( )` gives the name. x4 has one theme: `"dark"`, like Dark Modern of VS Code.
521
+
522
+ An application writes its own theme in its CSS, under `html.x4theme-<name>`, the way
523
+ `themes.scss` writes the dark one:
524
+
525
+ - the colour scale, `--color-primary-a0` (the background) to `--color-primary-a100` (the text).
526
+ The greys of x4 (backgrounds, texts, borders) are mixes of `a0` and `a100`: a scale whose
527
+ `a0` is dark and `a100` light gives them dark too;
528
+ - the global variables the scale does not give, when the theme wants other values:
529
+ `--background-*`, `--text-*`, `--border*`, `--disabled-*`, `--accent-*`, `--icon-color*`,
530
+ `--alert-*`, `--warning-*`, `--success-*`;
531
+ - `color-scheme: dark` for a dark theme: the native parts (select, scroll bars, spinners).
532
+
533
+ The variables of a component (`--listbox-*`, `--dialog-*`…, listed in the `@cssvar` of its
534
+ class) point to the global ones: the theme changes them all at once. Set one only when that
535
+ component must differ from the rest; do not recolour components one by one.
536
+
537
+ ```css
538
+ html.x4theme-pink {
539
+ --color-primary-a0: #fff5f8;
540
+ /* … a5 to a90 … */
541
+ --color-primary-a100: #3a1020;
542
+ }
543
+ ```
506
544
 
507
- A dark theme gets its own scale.
545
+ ```ts
546
+ Application.setTheme( "pink" );
547
+ ```
508
548
 
509
549
  ---
510
550
 
@@ -602,7 +642,7 @@ Component event
602
642
  ↓
603
643
  Application operation
604
644
  ↓
605
- State change / GlobalEvent
645
+ State change / global message
606
646
  ↓
607
647
  Existing Components update
608
648
  ```
@@ -643,6 +683,15 @@ const app_fr = { app: { open: "Ouvrir", close: "Fermer" } };
643
683
  const app_en: typeof app_fr = { app: { open: "Open", close: "Close" } };
644
684
  ```
645
685
 
686
+ `_tr` is typed with the texts of x4 only: `_tr.app.open` does not compile. Give the
687
+ application its own typed view of `_tr`, once, from its first language:
688
+
689
+ ```ts
690
+ export const tx = _tr as typeof _tr & typeof app_fr;
691
+
692
+ new Button( { label: tx.app.open } );
693
+ ```
694
+
646
695
  ---
647
696
 
648
697
  ## Final rule
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x4js",
3
- "version": "2.3.24",
3
+ "version": "2.3.26",
4
4
  "type": "module",
5
5
  "main": "src/x4.ts",
6
6
  "module": "src/x4.ts",
@@ -19,6 +19,8 @@ const x4icons = {
19
19
  danger: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24V264c0 13.3-10.7 24-24 24s-24-10.7-24-24V152c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>`,
20
20
  loading: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor"><!--!Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc.--><path d="M207.4 20.4c2.4 8.5-2.6 17.3-11.2 19.7C101.5 66.2 32 153 32 256c0 123.7 100.3 224 224 224s224-100.3 224-224c0-103-69.5-189.8-164.3-215.9c-8.5-2.4-13.5-11.2-11.2-19.7s11.2-13.5 19.7-11.2C432.5 39.1 512 138.2 512 256c0 141.4-114.6 256-256 256S0 397.4 0 256C0 138.2 79.5 39.1 187.7 9.2c8.5-2.4 17.3 2.6 19.7 11.2z"/></svg>`,
21
21
  close_box: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" fill="currentColor"><!--!Font Awesome Pro 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc.--><path d="M192 233.4L59.5 100.9 36.9 123.5 169.4 256 36.9 388.5l22.6 22.6L192 278.6 324.5 411.1l22.6-22.6L214.6 256 347.1 123.5l-22.6-22.6L192 233.4z"/></svg>`,
22
+ // a triangle, a "!" cut in it
23
+ warning: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor"><path fill-rule="evenodd" d="M8 1L15.5 14.5H.5zM7.25 5.5h1.5V10h-1.5zM7 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0z"/></svg>`,
22
24
  };
23
25
 
24
26
  export default x4icons;
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { Component, ComponentEvent, ComponentEvents, EvChange } from '../../core/component';
18
18
  import { EventCallback } from '../../core/core_events';
19
- import { class_ns, kbNav } from '../../core/core_tools';
19
+ import { kbNav } from '../../core/core_tools';
20
20
 
21
21
  import { HBox } from '../boxes/boxes';
22
22
  import { DropdownList } from '../combobox/combobox';
@@ -96,8 +96,8 @@ const render_id_item = ( item: ListItem ) => {
96
96
  * })
97
97
  */
98
98
 
99
- @class_ns( "x4" )
100
99
  export class AutoComplete extends TextEdit<AutoCompleteEvents> {
100
+ static classPrefix = "x4";
101
101
 
102
102
  private _popup: DropdownList;
103
103
 
@@ -16,16 +16,30 @@
16
16
 
17
17
  @use "../shared.scss";
18
18
 
19
+ :root {
20
+ --box-disabled-opacity: 0.5; // a disabled Box: its content faded
21
+ }
22
+
19
23
  .x4box {
20
- // reset fieldset
24
+ // reset a box built with tag: "fieldset"
21
25
  margin: 0;
22
- padding: 0;
26
+ padding: 0;
23
27
  border: none;
24
28
 
25
29
  min-width: 0;
26
30
  min-height: 0;
27
31
  flex-shrink: 0;
28
32
  align-self: stretch;
33
+
34
+ // disabled: its content inert (enable) and faded
35
+ &[disabled] {
36
+ opacity: var( --box-disabled-opacity );
37
+
38
+ // a disabled Box in a disabled Box: opacity 1 for it, so faded once, not twice
39
+ .x4box[disabled] {
40
+ opacity: 1;
41
+ }
42
+ }
29
43
  }
30
44
 
31
45
  .x4hbox {
@@ -14,7 +14,7 @@
14
14
  * that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
15
15
  **/
16
16
 
17
- import { asap, class_ns, isArray, isNumber } from '../../core/core_tools';
17
+ import { asap, 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
20
  import { HSizer, VSizer } from '../sizers/sizer';
@@ -28,6 +28,14 @@ export interface BoxProps extends ComponentProps {
28
28
 
29
29
  /**
30
30
  * A generic container component for grouping and laying out child components.
31
+ *
32
+ * A <div>. Disabled (disable( ), the prop disabled), its content is inert (no click, no
33
+ * focus, no input) and faded by --box-disabled-opacity.
34
+ *
35
+ * @cssvar
36
+ * ```
37
+ * --box-disabled-opacity
38
+ * ```
31
39
  *
32
40
  * The `refs` object is a typed directory of the children you need to reach
33
41
  * again later. Assign a child to `refs` as you build the content: the
@@ -60,11 +68,12 @@ export interface BoxProps extends ComponentProps {
60
68
  *
61
69
  */
62
70
 
63
- @class_ns( "x4" )
64
71
  export class Box<P extends BoxProps=BoxProps,E extends ComponentEvents=ComponentEvents> extends Component<P,E> {
72
+ static classPrefix = "x4";
73
+
65
74
  protected refs: Record<string,Component> = {};
66
75
  constructor( props: P ) {
67
- super( { tag: "fieldset", ...props } );
76
+ super( { tag: "div", ...props } );
68
77
  }
69
78
  }
70
79
 
@@ -74,8 +83,9 @@ export class Box<P extends BoxProps=BoxProps,E extends ComponentEvents=Component
74
83
  * Arranges child components in a horizontal line.
75
84
  */
76
85
 
77
- @class_ns( "x4" )
78
86
  export class HBox<P extends BoxProps=BoxProps,E extends ComponentEvents=ComponentEvents> extends Box<P,E> {
87
+ static classPrefix = "x4";
88
+
79
89
  }
80
90
 
81
91
  /**
@@ -84,8 +94,9 @@ export class HBox<P extends BoxProps=BoxProps,E extends ComponentEvents=Componen
84
94
  * The CSS class for this component is automatically generated as `x4vbox`.
85
95
  */
86
96
 
87
- @class_ns( "x4" )
88
97
  export class VBox<P extends BoxProps=BoxProps,E extends ComponentEvents=ComponentEvents> extends Box<P,E> {
98
+ static classPrefix = "x4";
99
+
89
100
  constructor( p: P ) {
90
101
  super( p );
91
102
  }
@@ -136,8 +147,9 @@ function split_content( panels: Component[], horz: boolean ) {
136
147
  * })
137
148
  */
138
149
 
139
- @class_ns( "x4" )
140
150
  export class HSplitBox<P extends SplitBoxProps=SplitBoxProps,E extends ComponentEvents=ComponentEvents> extends HBox<P,E> {
151
+ static classPrefix = "x4";
152
+
141
153
  constructor( props: P ) {
142
154
  super( { ...props, content: split_content( props.content, true ) } );
143
155
  }
@@ -149,8 +161,9 @@ export class HSplitBox<P extends SplitBoxProps=SplitBoxProps,E extends Component
149
161
  * The CSS class for this component is automatically generated as `x4vsplitbox`.
150
162
  */
151
163
 
152
- @class_ns( "x4" )
153
164
  export class VSplitBox<P extends SplitBoxProps=SplitBoxProps,E extends ComponentEvents=ComponentEvents> extends VBox<P,E> {
165
+ static classPrefix = "x4";
166
+
154
167
  constructor( props: P ) {
155
168
  super( { ...props, content: split_content( props.content, false ) } );
156
169
  }
@@ -206,8 +219,8 @@ interface StackItemEx extends StackItem {
206
219
  * The CSS class for this component is automatically generated as `x4stackbox`.
207
220
  */
208
221
 
209
- @class_ns( "x4" )
210
222
  export class StackBox<P extends StackBoxProps = StackBoxProps, E extends StackeBoxEvents = StackeBoxEvents> extends Box<StackBoxProps,StackeBoxEvents> {
223
+ static classPrefix = "x4";
211
224
 
212
225
  protected _items: StackItemEx[];
213
226
  protected _cur: number;
@@ -371,8 +384,8 @@ export class StackBox<P extends StackBoxProps = StackBoxProps, E extends StackeB
371
384
  * The CSS class for this component is automatically generated as `x4assistbox`.
372
385
  */
373
386
 
374
- @class_ns( "x4" )
375
387
  export class AssistBox extends StackBox {
388
+ static classPrefix = "x4";
376
389
 
377
390
  /**
378
391
  * Selects the next or previous page in the stack.
@@ -433,8 +446,8 @@ export interface GridBoxProps extends Omit<BoxProps,"content"> {
433
446
  * Auto-generates CSS class: `x4gridbox`.
434
447
  */
435
448
 
436
- @class_ns("x4")
437
449
  export class GridBox<P extends GridBoxProps=GridBoxProps,E extends ComponentEvents=ComponentEvents> extends Box<P,E> {
450
+ static classPrefix = "x4";
438
451
 
439
452
  constructor( props: P ) {
440
453
  super( props );
@@ -543,8 +556,8 @@ interface MasonryProps extends Omit<BoxProps,"content"> {
543
556
  * Auto-generates CSS class: `x4masonrybox`.
544
557
  */
545
558
 
546
- @class_ns("x4")
547
559
  export class MasonryBox extends Box<MasonryProps> {
560
+ static classPrefix = "x4";
548
561
 
549
562
  constructor(props: MasonryProps ) {
550
563
  super(props);
@@ -14,7 +14,6 @@
14
14
  * that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
15
15
  **/
16
16
 
17
- import { class_ns } from '../../core/core_tools';
18
17
  import { parseRoute, Router } from '../../core/core_router';
19
18
 
20
19
  import { BoxProps, Button, HBox, Icon } from '../components';
@@ -64,8 +63,8 @@ interface BreadcrumbsProps extends BoxProps{
64
63
  * ```
65
64
  */
66
65
 
67
- @class_ns( "x4" )
68
66
  export class Breadcrumbs extends HBox<BreadcrumbsProps,BreadcrumbEvents> {
67
+ static classPrefix = "x4";
69
68
 
70
69
  constructor( props: BreadcrumbsProps ) {
71
70
  super( props );
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { Component, ComponentEvents, ComponentProps, Flex, Space } from '../../core/component';
18
18
  import { EventCallback } from '../../core/core_events';
19
- import { class_ns, isString } from '../../core/core_tools';
19
+ import { isString } from '../../core/core_tools';
20
20
  import { _tr } from '../../core/core_i18n'
21
21
 
22
22
  import { Button } from '../button/button';
@@ -64,8 +64,8 @@ interface BtnGroupProps extends Omit<ComponentProps,"content"> {
64
64
  * a packed group draws its buttons with the --ol-button-* variables of Button
65
65
  */
66
66
 
67
- @class_ns( "x4" )
68
67
  export class BtnGroup extends Box<BtnGroupProps,BtnGroupEvents> {
68
+ static classPrefix = "x4";
69
69
 
70
70
  constructor( props: BtnGroupProps ) {
71
71
  super( props );