x4js 2.3.20 → 2.3.22

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 (32) hide show
  1. package/aicontext.md +127 -32
  2. package/package.json +1 -1
  3. package/src/components/autocomplete/autocomplete.ts +3 -0
  4. package/src/components/boxes/boxes.ts +1 -1
  5. package/src/components/chart/chart.ts +12 -3
  6. package/src/components/colorinput/colorinput.module.scss +11 -2
  7. package/src/components/colorinput/colorinput.ts +37 -4
  8. package/src/components/colorpicker/colorpicker.module.scss +14 -2
  9. package/src/components/colorpicker/colorpicker.ts +57 -11
  10. package/src/components/components.ts +1 -0
  11. package/src/components/dialog/dialog.ts +16 -5
  12. package/src/components/form/form.ts +3 -4
  13. package/src/components/gridview/gridview.module.scss +18 -16
  14. package/src/components/gridview/gridview.ts +5 -0
  15. package/src/components/icon/icon.ts +34 -18
  16. package/src/components/keyboard/keyboard.ts +31 -5
  17. package/src/components/link/link.ts +18 -7
  18. package/src/components/messages/messages.ts +1 -1
  19. package/src/components/panel/panel.ts +3 -2
  20. package/src/components/settingsview/settingsview.module.scss +178 -0
  21. package/src/components/settingsview/settingsview.ts +289 -0
  22. package/src/components/spreadsheet/spreadsheet.module.scss +18 -16
  23. package/src/components/textarea/textarea.module.scss +10 -3
  24. package/src/components/video/video.ts +17 -3
  25. package/src/core/core_colors.ts +21 -10
  26. package/src/core/core_dragdrop.ts +5 -4
  27. package/src/core/core_i18n.ts +191 -54
  28. package/src/core/core_pdf.ts +15 -3
  29. package/src/core/core_shortcuts.ts +5 -1
  30. package/src/core/core_styles.ts +46 -25
  31. package/src/core/core_svg.ts +24 -16
  32. package/src/core/core_tools.ts +18 -9
package/aicontext.md CHANGED
@@ -41,7 +41,7 @@ CoreElement<E> named timers + event surface (no DOM)
41
41
  │ ├── CSizer → HSizer / VSizer
42
42
  │ ├── FileDialog, ScrollView, Viewport, DockingView, Flex, Space
43
43
  │ ├── HBox-based: ColorInput, Rating, Switch, Tag, Keyboard, TextEdit → AutoComplete, Breadcrumbs
44
- │ └── VBox-based: Calendar, ColorPicker, FileDrop, Panel, PropertyGrid, TextArea
44
+ │ └── VBox-based: Calendar, ColorPicker, FileDrop, Panel, PropertyGrid, TextArea, SettingsView, SettingItem
45
45
  └── Application<E> singleton, no DOM of its own
46
46
 
47
47
  EventSource<E> standalone event registry
@@ -50,7 +50,7 @@ EventSource<E> standalone event registry
50
50
  └── StateManager<T>
51
51
 
52
52
  CoreElement-based (non-DOM): DataProxy, DataView, Store
53
- Standalone: Rect, Timer, Color, DataModel, Shortcuts, Stylesheet, ComputedStyle,
53
+ Standalone: Rect, Timer, Color, DataModel, Shortcuts, Stylesheet (deprecated: CSS variables), ComputedStyle,
54
54
  SvgBuilder, X4PDFBuilder, x4_react, CMover, UnsafeHtml (extends String)
55
55
  ```
56
56
 
@@ -499,10 +499,11 @@ interface LinkProps extends ComponentProps {
499
499
  text?: string | UnsafeHtml
500
500
  icon?: string
501
501
  align?: "left" | "center" | "right"
502
- click?: EventCallback<EvClick>
502
+ click?: EventCallback<EvClick> // ev.context = href; the link is followed unless ev.preventDefault()
503
+ // content: the link is its content (no label: text, icon, align are not used)
503
504
  }
504
505
  interface LinkEvents extends ComponentEvents { click: EvClick }
505
- class Link extends Component { setText(text: string|UnsafeHtml): void }
506
+ class Link extends Component { setText(text: string|UnsafeHtml): void } // the text of its label: the icon stays
506
507
  ```
507
508
 
508
509
  ### `Icon`
@@ -510,9 +511,10 @@ class Link extends Component { setText(text: string|UnsafeHtml): void }
510
511
  ```ts
511
512
  interface IconProps extends ComponentProps { iconId?: string }
512
513
  class Icon extends Component { setIcon(iconId: string): void }
513
- // iconId resolution: "var:name" → CSS var | "data:image/svg+xml,<svg..." → inline
514
+ // iconId resolution: "var:name" or "var:--name" → the variable --name set on <html> by the code
515
+ // (document.documentElement.style.setProperty("--name", svg); not one of a stylesheet) | "data:image/svg+xml,<svg..." → inline
514
516
  // | "<svg..." → raw inline | "*.svg" → async load + cache | else → <img src>
515
- export const svgLoader: SvgLoader // shared SVG cache
517
+ export const svgLoader: SvgLoader // shared SVG cache: load(url): Promise<string>; a file not found rejects, and is not asked again
516
518
  ```
517
519
 
518
520
  ### `Image`
@@ -647,12 +649,12 @@ interface SimpleTextAreaProps extends BaseProps { // BaseProps: see Input (nam
647
649
  }
648
650
  interface TextAreaProps extends SimpleTextAreaProps { label?: string }
649
651
 
650
- class SimpleTextArea extends Component { // the <textarea> alone
652
+ class SimpleTextArea extends Component { // the <textarea> alone; never smaller than one character (min-height/min-width, padding included)
651
653
  getText(): string; setText(text: string): void
652
654
  scrollToBottom(): void
653
655
  isValid(): boolean // false when it is required and empty
654
656
  }
655
- class TextArea extends VBox { // a Label above a SimpleTextArea
657
+ class TextArea extends VBox { // a Label above a SimpleTextArea; without a height, the textarea has its 2 rows (and the height the handle gives); with one, it fills it
656
658
  getText(): string; setText(text: string): void
657
659
  scrollToBottom(): void
658
660
  isValid(): boolean
@@ -771,7 +773,7 @@ interface AutoCompleteProps extends TextEditProps { // label, labelWidth, requ
771
773
  strict?: boolean // only a picked item (or empty) is accepted; default false
772
774
  itemValue?: "text" | "id" // what goes into the field when an item is picked; default "text"
773
775
  delay?: number // ms before search, default 250
774
- minChars?: number // default 1
776
+ minChars?: number // default 1; ↓ searches whatever the length (ev.text may be "")
775
777
  renderer?: (item: ListItem) => Component
776
778
  }
777
779
  class AutoComplete extends TextEdit {
@@ -788,12 +790,15 @@ class AutoComplete extends TextEdit {
788
790
  interface ColorInputProps extends BoxProps {
789
791
  color: Color | string // null: no color shown
790
792
  name?: string
791
- format?: "rgb" | "hex" // text of the field, "rgb" by default
793
+ format?: "rgb" | "hex" // text of the field, "rgb" by default; "rgba(...)" / "#rrggbbaa" when not opaque
792
794
  picker?: boolean // a click on the swatch opens a ColorPicker
793
795
  swatches?: (Color|string)[] | (() => (Color|string)[]) // a function is called each time the picker opens
794
796
  nullable?: boolean // "no color" can be chosen
797
+ alpha?: boolean // false: colors kept opaque, no alpha slider in the picker (default true)
798
+ placeholder?: string // text of the empty field
795
799
  change?: EventCallback<EvColorChange> // user changes only, not setColor
796
800
  }
801
+ // typing: the text is not rewritten while typed; on blur it is written from the color in the typed form (#... or rgb...)
797
802
  class ColorInput extends HBox { // queryInterface("form-element"): "#rrggbb" or null
798
803
  getColor(): Color; getValue(): string
799
804
  setColor(color: Color | string): void; setSwatches(swatches): void
@@ -870,7 +875,7 @@ class Gridview extends Component {
870
875
  getView(): DataView
871
876
  getSelection(): DataRecord[] // null when nothing is selected
872
877
  getFirstSel(): DataRecord // null when nothing is selected
873
- selectItem(id, ensureVisible?: boolean): void
878
+ selectItem(id, ensureVisible?: boolean): void // may be called before the grid is shown (then nothing is scrolled)
874
879
  clearSelection(): void
875
880
  selectAll(): void // multisel only; keeps the id of every record: costs memory on a very large view
876
881
  navigate(sens: kbNav): boolean
@@ -954,7 +959,7 @@ interface ChartProps extends SvgProps {
954
959
  type?: "line" | "area" | "bar" | "pie" | "donut" // default "line"
955
960
  series: ChartSerie[] // required; pie & donut use the first serie
956
961
  labels?: string[] // default: 1, 2, 3...
957
- xaxis?: ChartAxis; yaxis?: ChartAxis // { name?, ticks?, min?, max?, step? }, all computed by default
962
+ xaxis?: ChartAxis; yaxis?: ChartAxis // { name?, ticks?, min?, max?, step? }, all computed by default; on x: max = number of points, step = points between labels (>= 1), min not used
958
963
  legend?: boolean // default: shown when 2+ named series
959
964
  stacked?: boolean
960
965
  formatter?: (value: number) => string // default: chartFormatUnit (1.5k, 2M...)
@@ -1077,6 +1082,7 @@ interface DialogProps extends PopupProps {
1077
1082
  title: string // required
1078
1083
  buttons: BtnGroupItem[] // required
1079
1084
  icon?: string; form?: Form
1085
+ content?: ComponentContent // without form: the content of its form
1080
1086
  closable?: boolean | string; modal?: boolean
1081
1087
  btnclick?: EventCallback<EvBtnClick>
1082
1088
  }
@@ -1087,6 +1093,7 @@ class Dialog extends Popup {
1087
1093
  getButton(name: string): Button
1088
1094
  getBtnBar(): BtnGroup
1089
1095
  setTitle(title: string): void
1096
+ show(): this // each time: the focus on its first field
1090
1097
  showAsync(): Promise<string> // the name of the clicked button; null when closed without a button (Escape, close box)
1091
1098
  }
1092
1099
  ```
@@ -1137,7 +1144,7 @@ interface FormProps extends BoxProps { autoComplete?: boolean }
1137
1144
  class Form extends Box {
1138
1145
  setValues(values: FormValues): void
1139
1146
  getValues<T extends FormValues>(): T
1140
- setAutoComplete(on?: boolean): void
1147
+ setAutoComplete(on?: boolean): void // attribute on the <form>: fields without their own follow it, added later too
1141
1148
  setValidator(validator: (values: FormValues, is_valid: boolean) => boolean): void // a new call replaces the validator
1142
1149
  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)
1143
1150
  }
@@ -1146,6 +1153,78 @@ class Form extends Box {
1146
1153
 
1147
1154
  ---
1148
1155
 
1156
+ ## `SettingsView` — a page of settings, as the settings of VS Code
1157
+
1158
+ The summary of the sections on the left, the sections on the right, one after the other, each a panel with its title on a band; in a section, each `SettingItem` with its name, what it does, then its controls. A click in the summary brings its section to the top; the summary follows the scroll and marks the section at the top of the view.
1159
+
1160
+ ```ts
1161
+ interface SettingItemProps extends BoxProps {
1162
+ title: string
1163
+ description?: string // under the name
1164
+ content: Component | Component[] // its controls, on a line that wraps
1165
+ }
1166
+ class SettingItem extends VBox {
1167
+ getSearchText(): string // the title and the description: what a search looks in
1168
+ }
1169
+ // A SettingItem has no value of its own: its controls keep theirs. To read and write them together,
1170
+ // put the items in a Form (a section can hold a Form of SettingItem), give the controls a name,
1171
+ // and use getValues / setValues. The item is marked on its left while one of its controls has the focus.
1172
+
1173
+ interface SettingsSection {
1174
+ id: string
1175
+ title: string
1176
+ description?: string // under the title
1177
+ content: Component[] // SettingItem, Form of SettingItem, or any component
1178
+ }
1179
+ interface SettingsViewProps extends BoxProps {
1180
+ sections?: SettingsSection[]
1181
+ searchable?: boolean // a search field above the sections
1182
+ searchPlaceholder?: string // _tr.global.settings.search when missing
1183
+ noMatch?: string // shown when the search finds nothing; _tr.global.settings.nomatch when missing
1184
+ sectionChange?: EventCallback<EvSectionChange>
1185
+ }
1186
+ interface EvSectionChange extends ComponentEvent { readonly section: string } // the id of the section at the top
1187
+ class SettingsView<P extends SettingsViewProps = SettingsViewProps> extends VBox<P, SettingsViewEvents> {
1188
+ setSections(sections: SettingsSection[]): void // replaces the sections
1189
+ showSection(id: string, smooth = true): void // brings a section to the top
1190
+ getSection(): string // the id of the section at the top
1191
+ }
1192
+ // Search: the words typed are looked for without case nor accents; an item is shown when its title or
1193
+ // its description holds all of them. A section whose title or description holds them is shown whole;
1194
+ // otherwise only its items that hold them, and what holds those items (a Form, a box). What holds no
1195
+ // item (a table, a text) is shown with its whole section only. Escape empties the field.
1196
+ // The section marked in the summary is the last one whose top is above the top of the view (40px of
1197
+ // margin); the first one while the view is hidden (in a tab not shown). The last section shown is at
1198
+ // least as high as the view (class "last"): a click in the summary can bring any section to the top.
1199
+ // Classes: .x4settingsview > .search > .x4input, > .body > .toc (a Listbox), > .body > .sections;
1200
+ // .section(.last) > .title, .description; .sections > .nomatch; .x4settingitem > .name, .description, .controls
1201
+ // CSS variables: --settingsview-toc-width, --settingsview-section-background,
1202
+ // --settingsview-section-title-background, --settingsview-hover-background, --settingsview-marker
1203
+ ```
1204
+
1205
+ A page that builds its sections with references to its own components (`this.refs.x = ...`) cannot give them to `super()`: `this` is not there yet. It calls `setSections` after `super()`. `SettingsView` keeps its own parts in private fields, not in `refs`: `refs` is left to the classes that derive from it.
1206
+
1207
+ ```ts
1208
+ class MySettings extends SettingsView {
1209
+ declare protected refs: { size: Input }
1210
+
1211
+ constructor(props: BoxProps) {
1212
+ super({ ...props, searchable: true })
1213
+
1214
+ this.setSections([
1215
+ { id: "editor", title: "Editor", content: [
1216
+ new Form({ content: [
1217
+ new SettingItem({ title: "Font size", description: "The size of the text, in pixels.",
1218
+ content: this.refs.size = new Input({ type: "number", name: "fontSize", value: 14 }) }),
1219
+ ] }),
1220
+ ] },
1221
+ ])
1222
+ }
1223
+ }
1224
+ ```
1225
+
1226
+ ---
1227
+
1149
1228
  ## Sizers / viewport / misc
1150
1229
 
1151
1230
  ```ts
@@ -1162,8 +1241,9 @@ class VSizer extends CSizer // vertical splitter
1162
1241
  class ScrollView extends Component { getViewport(): Component }
1163
1242
  class Viewport extends Component
1164
1243
  class Canvas extends Component { getContext(): CanvasRenderingContext2D; paint(): void }
1165
- class Video extends Box {
1166
- play(); pause(); mute(set: boolean)
1244
+ class Video extends Box { // class x4video
1245
+ play(): Promise<boolean> // false: could not start (a click of the user wanted, a source not playable); no source: waits for one
1246
+ pause(); mute(set: boolean)
1167
1247
  get player(): HTMLVideoElement; get duration(): number; get paused(): boolean
1168
1248
  get volume(): number; get videoWidth(): number; get videoHeight(): number
1169
1249
  set stream(stream: MediaStream)
@@ -1171,12 +1251,15 @@ class Video extends Box {
1171
1251
  class Calendar extends VBox { // props: date?, minDate?, maxDate? (days out of them cannot be chosen), change ("change": the day clicked)
1172
1252
  getDate(): Date; setDate(date: Date): void // copies: the calendar keeps its own date
1173
1253
  }
1174
- class ColorPicker extends VBox { // props: color, swatches?, nullable?; "change": EvColorChange, also fired once when built
1254
+ class ColorPicker extends VBox { // props: color, swatches?, nullable?, alpha? (false: no alpha slider, opaque colors); "change": EvColorChange, also fired once when built
1175
1255
  getColor(): Color; setColor(color: Color | string): void; setSwatches(swatches): void
1176
1256
  }
1177
- class Panel extends VBox { setTitle(title: string): void }
1257
+ class Panel extends VBox { setTitle(title: string): void } // the text of its title: its icon stays
1178
1258
  class Breadcrumbs extends HBox { setItems(elements): void }
1179
- interface KeyboardProps extends BoxProps { locale?: string } // "fr-FR" (default), "de-CH", "pl"...
1259
+ interface KeyboardProps extends BoxProps {
1260
+ locale?: string // "fr-FR" (default), "de-CH", "pl"...
1261
+ numpadOnly?: boolean // number or date field: the pad has no "abc" key back to the letters
1262
+ }
1180
1263
  class Keyboard extends HBox { // virtual keyboard, shown while an <input> has the focus
1181
1264
  setLocale(locale: string): void
1182
1265
  setZoom(perc: number): void
@@ -1184,6 +1267,7 @@ class Keyboard extends HBox { // virtual keyboard, shown while an
1184
1267
  }
1185
1268
  // languages: cs da de en es et fi fr ga hr hu it lt lv mt nl pl pt ro sk sl sv
1186
1269
  // bottom row: 123 (numeric pad), the letters of the language (éè, none in en), the signs (#+=); Abc comes back
1270
+ // a key acts on pointerdown; the keys that draw the keyboard again (shift, 123, abc, accents, signs) on the click
1187
1271
  class TickLine extends Component
1188
1272
  class Header extends HBox // resizable column header; updates target style "--{name}-width"
1189
1273
  ```
@@ -1287,7 +1371,7 @@ parseRoute(str: string|RegExp, loose?: boolean): Segment
1287
1371
 
1288
1372
  ## Shortcuts (`core_shortcuts`)
1289
1373
 
1290
- Keyboard shortcuts. A sequence is written as it is read: `"Mod+C"`, `"Shift+Mod+Z"`, `"Delete"`, `"Alt+ArrowLeft"`.
1374
+ Keyboard shortcuts. A sequence is written as it is read: `"Mod+C"`, `"Shift+Mod+Z"`, `"Delete"`, `"Alt+ArrowLeft"`, `"Ctrl+Space"`.
1291
1375
  Modifiers: `Shift`, `Ctrl`, `Cmd`, `Alt`, and `Mod` = the key of the commands (Ctrl on Windows and Linux, Cmd on a Mac). Prefer `Mod`.
1292
1376
 
1293
1377
  ```ts
@@ -1393,7 +1477,7 @@ dragManager.registerDropTarget(column, (cmd, el, infos) => {
1393
1477
 
1394
1478
  - `registerDraggableElement`: while it is dragged, the component has the class `dragging`, and a copy of it with the class `dragged`, added to the body, follows the pointer. Nothing else is done: no data of the application goes with the drag, the target gets the component itself.
1395
1479
  - The callback of a target: `enter` and `leave` when the pointer comes over it and goes away, `drag` again and again while it is over it, `drop`. Only `drag` and `drop` have `infos`; `pt` is in the coordinates of the page.
1396
- - `enter` and `leave` are found by a timer: they may come up to 300 ms late, and a `leave` also follows a `drop`. Between them the target has the class `drop-over`.
1480
+ - `enter` comes at once, before the first `drag`. `leave` is found by a timer: it comes up to 300 ms after the pointer went away (over the children of a target the browser sends dragleave and dragover in turn), and a `leave` also follows a `drop`. Between them the target has the class `drop-over`.
1397
1481
  - `el` in the callbacks is the dragged component, not the target. There is none for files that come from the desktop: read `infos.data.files`.
1398
1482
  - `Image` (`candrop`) and `FileDrop` already use it for files.
1399
1483
 
@@ -1401,13 +1485,15 @@ dragManager.registerDropTarget(column, (cmd, el, infos) => {
1401
1485
 
1402
1486
  ## i18n (`core_i18n`)
1403
1487
 
1404
- Two built-in languages (`fr` default, and `en`).
1488
+ Four built-in languages: `fr` (default), `en` (based on `fr`), `es` and `de` (based on `en`). Dates: `D/M/Y` in fr and es, `M/D/Y` in en, `D.M.Y` in de.
1405
1489
 
1406
1490
  ```ts
1407
1491
  export let _tr // current translation object (proxy)
1408
1492
  createLanguage(name: string, base: string): void
1409
- addTranslation(name: string, ...parts: any[]): void
1493
+ addTranslation(name: string, ...parts: any[]): void // seen at once, the language being selected or not
1410
1494
  selectLanguage(name: string): typeof _tr
1495
+ // a key missing in a language is taken from its base (createLanguage), at the same path;
1496
+ // a key no language has: console.error "I18N error: unable to find _tr.x.y", undefined
1411
1497
  getCurrentLanguage(): string
1412
1498
  getAvailableLanguages(): string[]
1413
1499
  isLanguage(name: string): boolean
@@ -1416,7 +1502,8 @@ isLanguage(name: string): boolean
1416
1502
  // search search_tip required_field invalid_format invalid_email invalid_number invalid_date
1417
1503
  // empty_list err_403 property value copy cut paste filedrop
1418
1504
  // diff_date_seconds/minutes/hours, date_input_formats, date_format, date_time_format,
1419
- // day_short[] day_long[] month_short[] month_long[], keyboard.{next,numeric,alpha}
1505
+ // day_short[] day_long[] month_short[] month_long[], keyboard.{next,numeric,alpha},
1506
+ // settings.{search,nomatch}
1420
1507
  ```
1421
1508
 
1422
1509
  ---
@@ -1426,7 +1513,7 @@ isLanguage(name: string): boolean
1426
1513
  ```ts
1427
1514
  class UnsafeHtml extends String // raw, unescaped HTML marker
1428
1515
  unsafeHtml(x: string): UnsafeHtml
1429
- unsafe(strings, ...values): UnsafeHtml // template tag: unsafe`<b>${x}</b>`
1516
+ unsafe(strings, ...values): UnsafeHtml // template tag: unsafe`<b>${x}</b>`; values escaped (an UnsafeHtml kept), null/undefined write nothing, 0 and false are written
1430
1517
  sanitizeHtml(input: string): string // escape &<>"' for safe text insertion
1431
1518
  safeText(v: string | UnsafeHtml): string // UnsafeHtml passes through; plain string is escaped
1432
1519
 
@@ -1443,10 +1530,12 @@ interface IRect { left; top; height; width: number }
1443
1530
  class Timer { setTimeout; clearTimeout; setInterval; clearInterval; clearAllTimeouts }
1444
1531
 
1445
1532
  class Color {
1446
- // new Color(value) | new Color(r,g,b,a?)
1447
- setValue(value): this; setRgb(r,g,b,a): this; setHsv(h,s,v,a?): this; setAlpha(a): this
1448
- getAlpha(): number; lighten(percent): this; isInvalid(): boolean
1449
- toHexString(): string; toRgbString(withAlpha?): string; toNumber(): number
1533
+ // new Color(value) | new Color(r,g,b,a?) a: 0..1, default 1
1534
+ setValue(value): this; setRgb(r,g,b,a?): this; setHsv(h,s,v,a?): this; setAlpha(a): this
1535
+ getAlpha(): number; lighten(percent): this
1536
+ isInvalid(): boolean // the last setValue could not be read (the color is red); setRgb/setHsv make it valid
1537
+ toHexString(): string; toNumber(): number
1538
+ toRgbString(withAlpha = false): string // "rgb(r,g,b)"; true: "rgba(r,g,b,0.500)" when not opaque, "rgb(...)" when opaque
1450
1539
  toRgb(): Rgb; toHsv(): Hsv
1451
1540
  }
1452
1541
 
@@ -1465,10 +1554,11 @@ pascalCase(s): string; camelCase(s): string; clamp<T>(v, min, max): T
1465
1554
  measureText(str, size, bold?): number // width in Helvetica, from core_pdf (see PDF)
1466
1555
 
1467
1556
  // Dates
1468
- date_format(date, options?): string
1469
- date_diff(date1, date2, options?): string
1557
+ date_format(date, options?): string // options not read yet: formatIntlDate(date)
1558
+ date_diff(date1, date2, options?): string // "30 secondes", "5 minutes", "2 heures 5 minutes" (either order); options not read
1470
1559
  date_to_sql(date, withHours): string; date_sql_utc(date): Date
1471
- date_hash(date): number; date_clone(date): Date; date_calc_weeknum(date): number
1560
+ date_hash(date): number; date_clone(date): Date
1561
+ date_calc_weeknum(date): number // ISO 8601: weeks from monday, week 1 holds the first thursday (29/12/2025 → 1)
1472
1562
  parseIntlDate(value, fmts?): Date; formatIntlDate(date, fmt?, utc?): string
1473
1563
  calcAge(birth, ref?): number
1474
1564
 
@@ -1504,7 +1594,7 @@ const makeUniqueComponentId // () => string, "x4-<n>" (exported as a value)
1504
1594
 
1505
1595
  ```
1506
1596
  d day (no zero) D day (2 digits) j short day (mon) J long day (monday)
1507
- w week number W week (2 digits) m month (no zero) M month (2 digits)
1597
+ w week number (ISO 8601) W week (2 digits) m month (no zero) M month (2 digits)
1508
1598
  o short month O long month y/Y year h 24h hour
1509
1599
  H hour (2 digits) i minutes I minutes (2 dig) s seconds
1510
1600
  S seconds (2 dig) a am/pm l ms L ms (3 digits)
@@ -1526,6 +1616,9 @@ class SvgBuilder extends SvgGroup {
1526
1616
  }
1527
1617
  // Items: SvgGroup, SvgPath (moveTo/lineTo/curveTo/arc/closePath), SvgText (font/fontSize/textAlign),
1528
1618
  // SvgShape, SvgIcon, SvgGradient (addStop)
1619
+ // arc(x, y, r, start, end): angles in degrees, 0 is the top, 90 the right; it starts a new subpath
1620
+ // addStop(offset, color): a number is a percentage (20 → "20%"), "50%" as it is
1621
+ // getNumAttr(name): 0 when not set; SvgIcon(svg): its svg needs xmlns="http://www.w3.org/2000/svg"
1529
1622
  ```
1530
1623
 
1531
1624
  ---
@@ -1793,6 +1886,8 @@ Optional keys: `external`, `define`, `esbuild`, `dev` (`host`, `port`, `https`,
1793
1886
  }
1794
1887
  ```
1795
1888
 
1889
+ `experimentalDecorators` is required: the decorators `@data.*` of a `DataModel` are written for it. Without it esbuild applies the standard JavaScript decorators, and the fields of a model are not declared.
1890
+
1796
1891
  ### `src/index.html`
1797
1892
 
1798
1893
  ```html
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x4js",
3
- "version": "2.3.20",
3
+ "version": "2.3.22",
4
4
  "type": "module",
5
5
  "main": "src/x4.ts",
6
6
  "module": "src/x4.ts",
@@ -158,6 +158,9 @@ export class AutoComplete extends TextEdit<AutoCompleteEvents> {
158
158
  if( text.length<this._min_chars ) {
159
159
  this.clearTimeout( "search" );
160
160
  this._popup.show( false );
161
+
162
+ // the suggestions were for another text: ↓ asks for new ones
163
+ this._count = 0;
161
164
  return;
162
165
  }
163
166
 
@@ -508,7 +508,7 @@ export class GridBox<P extends GridBoxProps=GridBoxProps,E extends ComponentEven
508
508
  */
509
509
 
510
510
  setTemplate( t: string[] ) {
511
- this.setAttribute( "grid-template-areas", t.map( x => '"' + x + '"' ).join(" ") );
511
+ this.setStyleValue( "gridTemplateAreas", t.map( x => '"' + x + '"' ).join(" ") );
512
512
  }
513
513
 
514
514
  /**
@@ -31,7 +31,7 @@ export interface ChartTick {
31
31
  export interface ChartAxis {
32
32
  name?: string;
33
33
  ticks?: ChartTick[];
34
- min?: number;
34
+ min?: number; // not used on x axis
35
35
  max?: number; // on x axis: number of points
36
36
  step?: number; // on x axis: one label every 'step' points
37
37
  }
@@ -102,6 +102,11 @@ export function chartFormatUnit( value: number, decimals = 2 ) {
102
102
 
103
103
  function niceScale( min: number, max: number, maxTicks: number ) {
104
104
 
105
+ // a bound given beyond the values: an empty range at that bound
106
+ if( max<min ) {
107
+ max = min;
108
+ }
109
+
105
110
  if( min==max ) {
106
111
  if( min==0 ) { max = 1; }
107
112
  else if( min>0 ) { min = 0; }
@@ -152,9 +157,12 @@ function computeStack( series: ChartSerie[], count: number ) {
152
157
  const neg = new Array<number>( count ).fill( 0 );
153
158
 
154
159
  return series.map( s => {
160
+ // a serie without values counts 0
161
+ const values = s.values || [];
162
+
155
163
  const out: { lo: number, hi: number }[] = [];
156
164
  for( let i=0; i<count; i++ ) {
157
- const v = isNumber( s.values[i] ) ? s.values[i] : 0;
165
+ const v = isNumber( values[i] ) ? values[i] : 0;
158
166
  const acc = v>=0 ? pos : neg;
159
167
 
160
168
  out.push( { lo: acc[i], hi: acc[i]+v } );
@@ -591,7 +599,8 @@ export class Chart extends SvgComponent<ChartProps,ChartEvents> {
591
599
  }
592
600
 
593
601
  // too many labels: show one every n
594
- const every = xaxis.step>0 ? Math.round( xaxis.step ) : Math.max( 1, Math.ceil( ( maxw+8 ) / ( xstep || plot.width ) ) );
602
+ // at least one point between two labels
603
+ const every = xaxis.step>0 ? Math.max( 1, Math.round( xaxis.step ) ) : Math.max( 1, Math.ceil( ( maxw+8 ) / ( xstep || plot.width ) ) );
595
604
  for( let i=0; i<count; i+=every ) {
596
605
  xticks.push( { value: i, text: this._label(i) } );
597
606
  }
@@ -19,6 +19,7 @@
19
19
  --colorinput-btn-color: var( --text-secondary );
20
20
  --colorinput-btn-color-hover: var( --text-primary );
21
21
  --colorinput-popup-background: var( --background-primary );
22
+ --colorinput-none: var( --alert-background );
22
23
  }
23
24
 
24
25
  .x4colorinput {
@@ -35,6 +36,10 @@
35
36
  width: 1rem;
36
37
  height: 1rem;
37
38
 
39
+ // its color over checkers: a color that is not opaque is seen as such
40
+ background: linear-gradient( var( --swatch-color ), var( --swatch-color ) ),
41
+ repeating-conic-gradient( #c8c8c8 0% 25%, #ffffff 0% 50% ) 0 0 / 6px 6px;
42
+
38
43
  // props.picker: a click opens the color picker. It is a button:
39
44
  // wide enough to be aimed at.
40
45
  &.picker {
@@ -50,10 +55,14 @@
50
55
  }
51
56
  }
52
57
 
53
- // props.nullable, no color: the checkerboard that stands for what is transparent
58
+ // no color: white, crossed out from bottom left to top right, the edges of the line
59
+ // blurred by 1px (smooth); checkers are for a transparent color
54
60
  &.none {
55
61
  border: 1px solid var( --border );
56
- background: repeating-conic-gradient( #c8c8c8 0% 25%, #ffffff 0% 50% ) 0 0 / 6px 6px;
62
+ background: linear-gradient( to bottom right,
63
+ transparent calc( 50% - 1.5px ), var( --colorinput-none ) calc( 50% - 0.5px ),
64
+ var( --colorinput-none ) calc( 50% + 0.5px ), transparent calc( 50% + 1.5px ) ),
65
+ white;
57
66
  }
58
67
  }
59
68
 
@@ -59,6 +59,12 @@ export interface ColorInputProps extends BoxProps {
59
59
  /** The field may have no color: emptied, or "no color" picked. */
60
60
  nullable?: boolean;
61
61
 
62
+ /** false: the colors are opaque, the picker has no alpha slider (default true). */
63
+ alpha?: boolean;
64
+
65
+ /** The text shown in the field while it is empty. */
66
+ placeholder?: string;
67
+
62
68
  /** The user changed the color (not fired by setColor). */
63
69
  change?: EventCallback<EvColorChange>;
64
70
  }
@@ -74,6 +80,7 @@ interface ColorInputEvents extends ComponentEvents {
74
80
  * --colorinput-btn-color
75
81
  * --colorinput-btn-color-hover
76
82
  * --colorinput-popup-background
83
+ * --colorinput-none
77
84
  * ```
78
85
  */
79
86
 
@@ -100,7 +107,7 @@ export class ColorInput extends HBox<ColorInputProps,ColorInputEvents> {
100
107
 
101
108
  this.setContent( [
102
109
  this._swatch = new Component( { cls: "swatch" } ),
103
- this._edit = new Input( { type: "text", value: "", spellcheck: false } ),
110
+ this._edit = new Input( { type: "text", value: "", spellcheck: false, placeholder: props.placeholder } ),
104
111
 
105
112
  isFeatureAvailable("eyedropper") ? new Button( { icon: icon, click: ( ) => {
106
113
  const eyeDropper = new (window as any).EyeDropper();
@@ -112,6 +119,7 @@ export class ColorInput extends HBox<ColorInputProps,ColorInputEvents> {
112
119
  } } ) : null
113
120
  ])
114
121
 
122
+ // while it is typed, the text is left alone: the color follows it
115
123
  this._edit.addDOMEvent( "input", ( ) => {
116
124
  const txt = this._edit.getValue( );
117
125
 
@@ -122,10 +130,16 @@ export class ColorInput extends HBox<ColorInputProps,ColorInputEvents> {
122
130
 
123
131
  const clr = new Color( txt );
124
132
  if( !clr.isInvalid() ) {
125
- this._changed( clr );
133
+ this._changed( clr, false );
126
134
  }
127
135
  });
128
136
 
137
+ // the field is left: the text is written from the color, in the form that was typed
138
+ // (a text that could not be read shows the color that was kept)
139
+ this._edit.addDOMEvent( "change", ( ) => {
140
+ this._show( this._color );
141
+ });
142
+
129
143
  if( props.picker ) {
130
144
  // A click on the swatch opens the picker, a second one closes it.
131
145
  // The picker closes by itself as soon as the button goes down
@@ -160,7 +174,19 @@ export class ColorInput extends HBox<ColorInputProps,ColorInputEvents> {
160
174
  }
161
175
 
162
176
  private _format( color: Color ): string {
163
- return this.props.format=="hex" ? color.toHexString( ) : color.toRgbString( false );
177
+ // the form of the text in the field: "#..." or "rgb...", else props.format
178
+ const text = this._edit.getValue( ).trim( ).toLowerCase( );
179
+
180
+ let hex = this.props.format=="hex";
181
+ if( text.startsWith( "#" ) ) {
182
+ hex = true;
183
+ }
184
+ else if( text.startsWith( "rgb" ) ) {
185
+ hex = false;
186
+ }
187
+
188
+ // rgba / #rrggbbaa when the color is not opaque
189
+ return hex ? color.toHexString( ) : color.toRgbString( true );
164
190
  }
165
191
 
166
192
  /**
@@ -169,10 +195,16 @@ export class ColorInput extends HBox<ColorInputProps,ColorInputEvents> {
169
195
  */
170
196
 
171
197
  private _show( color: Color, text = true ) {
198
+ // no alpha: every color shown is opaque, typed, picked or given
199
+ if( color && this.props.alpha===false ) {
200
+ color.setAlpha( 1 );
201
+ }
202
+
172
203
  this._color = color;
173
204
 
174
205
  this._swatch.setClass( "none", !color );
175
- this._swatch.setStyleValue( "backgroundColor", color ? color.toRgbString(false) : null );
206
+ // the stylesheet draws it over checkers
207
+ this._swatch.setStyleVariable( "--swatch-color", color ? color.toRgbString( true ) : "" );
176
208
 
177
209
  if( text ) {
178
210
  this._edit.setValue( color ? this._format( color ) : "" );
@@ -201,6 +233,7 @@ export class ColorInput extends HBox<ColorInputProps,ColorInputEvents> {
201
233
  color: this._color ? new Color( this._color.toHexString( ) ) : new Color( 255, 255, 255 ),
202
234
  swatches,
203
235
  nullable: this.props.nullable,
236
+ alpha: this.props.alpha,
204
237
  } );
205
238
 
206
239
  const popup = this._popup = new Popup( { cls: "x4colorinput-popup", autoClose: true, content: picker } );
@@ -16,6 +16,10 @@
16
16
 
17
17
  @use "../shared.scss";
18
18
 
19
+ :root {
20
+ --colorpicker-none: var( --alert-background );
21
+ }
22
+
19
23
  @mixin overlay-shadow {
20
24
  width: 12px;
21
25
  height: 12px;
@@ -143,13 +147,21 @@
143
147
  border-radius: 3px;
144
148
  cursor: pointer;
145
149
 
150
+ // its color over checkers: a color that is not opaque is seen as such
151
+ background: linear-gradient( var( --swatch-color ), var( --swatch-color ) ),
152
+ repeating-conic-gradient( #c8c8c8 0% 25%, #ffffff 0% 50% ) 0 0 / 6px 6px;
153
+
146
154
  &:hover {
147
155
  border-color: var( --border-focus );
148
156
  }
149
157
 
150
- // no color: the checkerboard that stands for what is transparent
158
+ // no color: white, crossed out from bottom left to top right, the edges of the line
159
+ // blurred by 1px (smooth); checkers are for a transparent color
151
160
  &.none {
152
- background: repeating-conic-gradient( #c8c8c8 0% 25%, #ffffff 0% 50% ) 0 0 / 6px 6px;
161
+ background: linear-gradient( to bottom right,
162
+ transparent calc( 50% - 1.5px ), var( --colorpicker-none ) calc( 50% - 0.5px ),
163
+ var( --colorpicker-none ) calc( 50% + 0.5px ), transparent calc( 50% + 1.5px ) ),
164
+ white;
153
165
  }
154
166
  }
155
167
  }