x4js 2.3.27 → 2.3.29

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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/aicontext.md +412 -24
  4. package/package.json +8 -3
  5. package/src/components/autocomplete/autocomplete.ts +5 -1
  6. package/src/components/bargraph/bargraph.module.scss +44 -0
  7. package/src/components/bargraph/bargraph.ts +362 -0
  8. package/src/components/base.scss +71 -1
  9. package/src/components/boxes/boxes.ts +2 -2
  10. package/src/components/button/button.module.scss +4 -3
  11. package/src/components/calendar/calendar.module.scss +6 -5
  12. package/src/components/calendar/calendar.ts +14 -8
  13. package/src/components/canvas/canvas.ts +5 -4
  14. package/src/components/carousel/carousel.module.scss +165 -0
  15. package/src/components/carousel/carousel.ts +423 -0
  16. package/src/components/chart/chart.module.scss +8 -0
  17. package/src/components/chart/chart.ts +170 -12
  18. package/src/components/checkbox/checkbox.module.scss +67 -1
  19. package/src/components/checkbox/checkbox.ts +22 -6
  20. package/src/components/colorinput/colorinput.module.scss +1 -0
  21. package/src/components/combobox/combobox.module.scss +6 -2
  22. package/src/components/components.ts +3 -0
  23. package/src/components/gantt/gantt.module.scss +462 -0
  24. package/src/components/gantt/gantt.ts +1254 -0
  25. package/src/components/gridview/gridview.module.scss +6 -0
  26. package/src/components/input/input.module.scss +1 -1
  27. package/src/components/knob/knob.module.scss +113 -0
  28. package/src/components/knob/knob.ts +355 -0
  29. package/src/components/mapview/mapview.module.scss +177 -0
  30. package/src/components/mapview/mapview.ts +764 -0
  31. package/src/components/messages/messages.module.scss +9 -4
  32. package/src/components/planner/planner.module.scss +688 -0
  33. package/src/components/planner/planner.ts +1792 -0
  34. package/src/components/propgrid/progrid.module.scss +13 -5
  35. package/src/components/radio/radio.module.scss +58 -32
  36. package/src/components/radio/radio.ts +12 -1
  37. package/src/components/sizers/sizer.ts +17 -5
  38. package/src/components/slider/slider.module.scss +22 -20
  39. package/src/components/spreadsheet/spreadsheet.module.scss +6 -0
  40. package/src/components/switch/switch.module.scss +68 -44
  41. package/src/components/switch/switch.ts +6 -3
  42. package/src/components/tabs/tabs.module.scss +2 -0
  43. package/src/components/tabs/tabs.ts +1 -0
  44. package/src/components/tag/tag.module.scss +4 -4
  45. package/src/components/textarea/textarea.module.scss +9 -0
  46. package/src/components/textarea/textarea.ts +1 -0
  47. package/src/components/textedit/textedit.module.scss +5 -2
  48. package/src/components/themes.scss +20 -6
  49. package/src/components/treeview/treeview.module.scss +6 -1
  50. package/src/components/treeview/treeview.ts +1 -0
  51. package/src/components/video/video.module.scss +5 -2
  52. package/src/components/video/video.ts +14 -1
  53. package/src/core/core_i18n.ts +65 -0
  54. package/src/core/core_pdf.ts +17 -0
  55. package/src/core/core_react.ts +12 -18
  56. package/src/core/core_svg.ts +9 -6
  57. package/src/core/core_tools.ts +44 -0
  58. package/src/x4doc.ts +3 -0
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "x4js",
3
- "version": "2.3.27",
3
+ "version": "2.3.29",
4
+ "license": "MIT",
4
5
  "type": "module",
5
6
  "main": "src/x4.ts",
6
7
  "module": "src/x4.ts",
7
8
  "types": "src/x4.ts",
8
9
  "exports": {
9
10
  ".": "./src/x4.ts",
10
- "./monaco": "./src/components/monaco/monaco.ts"
11
+ "./monaco": "./src/components/monaco/monaco.ts",
12
+ "./gantt": "./src/components/gantt/gantt.ts",
13
+ "./planner": "./src/components/planner/planner.ts",
14
+ "./mapview": "./src/components/mapview/mapview.ts"
11
15
  },
12
16
  "bin": {
13
17
  "x4js": "./bin/x4js.mjs"
@@ -15,7 +19,8 @@
15
19
  "scripts": {
16
20
  "build-publish": "deno run --allow-all ./publish.ts",
17
21
  "build-doc": "typedoc",
18
- "build-types": "npx tsc --declaration --emitDeclarationOnly --outFile types/x4.d.ts"
22
+ "build-types": "npx tsc --declaration --emitDeclarationOnly --outFile types/x4.d.ts",
23
+ "lint": "tsc --noEmit && node demo/gen-sources.mjs && tsc --noEmit -p demo/tsconfig.local.json"
19
24
  },
20
25
  "files": [
21
26
  "src",
@@ -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",
@@ -0,0 +1,44 @@
1
+ /**
2
+ * ___ ___ __
3
+ * \ \/ / / _
4
+ * \ / /_| |_
5
+ * / \____ _|
6
+ * /__/\__\ |_|
7
+ *
8
+ * @file bargraph.module.scss
9
+ * @author Etienne Cochard
10
+ *
11
+ * @copyright (c) 2026 R-libre ingenierie
12
+ *
13
+ * Use of this source code is governed by an MIT-style license
14
+ * that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
15
+ **/
16
+
17
+ // read by the code when it draws: a change is seen at the next change of theme, of colors, or of disabled
18
+ :root {
19
+ // no colors given: from start (min) to end (max)
20
+ --bargraph-color-start: color-mix( in srgb, var( --accent-background ) 35%, var( --background-primary ) );
21
+ --bargraph-color-end: var( --accent-background );
22
+
23
+ // the segments from the overshoot value
24
+ --bargraph-overshoot: var( --alert-background );
25
+
26
+ // an unlit segment: its color at --bargraph-off-mix in --bargraph-off
27
+ --bargraph-off: var( --border );
28
+ --bargraph-off-mix: 15%;
29
+ --bargraph-gap: 2px;
30
+ }
31
+
32
+ // the canvas fills it (Canvas sets its size)
33
+ .x4bargraph {
34
+ position: relative;
35
+ flex-shrink: 0;
36
+ width: 200px;
37
+ height: 12px;
38
+
39
+ // from the bottom
40
+ &.vertical {
41
+ width: 12px;
42
+ height: 200px;
43
+ }
44
+ }
@@ -0,0 +1,362 @@
1
+ /**
2
+ * ___ ___ __
3
+ * \ \/ / / _
4
+ * \ / /_| |_
5
+ * / \____ _|
6
+ * /__/\__\ |_|
7
+ *
8
+ * @file bargraph.ts
9
+ * @author Etienne Cochard
10
+ *
11
+ * @copyright (c) 2026 R-libre ingenierie
12
+ *
13
+ * Use of this source code is governed by an MIT-style license
14
+ * that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
15
+ **/
16
+
17
+ import { Application } from '../../core/core_application';
18
+ import { Color } from '../../core/core_colors';
19
+ import { clamp } from '../../core/core_tools';
20
+
21
+ import { Canvas, CanvasProps } from '../canvas/canvas';
22
+ import { CanvasEx } from '../canvas/canvas_ex';
23
+
24
+ import './bargraph.module.scss';
25
+
26
+ // no colors given: the accent, pale to full (variables: the theme gives them)
27
+ const accentStops = [
28
+ 'var(--bargraph-color-start)',
29
+ 'var(--bargraph-color-end)',
30
+ ];
31
+
32
+ const PEAK_HOLD = 1000; // ms the peak stays
33
+ const PEAK_FALL = 1500; // ms to fall the whole range
34
+
35
+ export interface BargraphProps extends Omit<CanvasProps, "paint" | "paint_cb" | "clear"> {
36
+ value?: number;
37
+ /** 0 to 100 by default */
38
+ min?: number;
39
+ max?: number;
40
+ /** the segments go up from the bottom; else from the left */
41
+ vertical?: boolean;
42
+ /** the count of segments, 20 by default */
43
+ segments?: number;
44
+ /** the colors from min to max, as Gauge (linearColorStops: green to red); one color: a plain bar.
45
+ * none: the accent, pale to full */
46
+ colors?: string[];
47
+ /** the segments that start at this value or above have the overshoot color (alert); the colors go under it */
48
+ overshoot?: number;
49
+ /** the bar goes up quickly and down slowly, in a few frames; false: at once (true by default) */
50
+ animated?: boolean;
51
+ /** the highest segment reached stays lit a second, then falls */
52
+ peak?: boolean;
53
+ }
54
+
55
+ /**
56
+ * a bar of segments, lit from min up to the value (a level meter), drawn in a canvas.
57
+ *
58
+ * @example
59
+ * new Bargraph( { vertical: true, overshoot: 85, peak: true } )
60
+ * meter.setValue( level ) // as often as needed
61
+ * each segment has the color of its place between the colors; an unlit one, a trace of it.
62
+ *
63
+ * setValue can be called many times a second: the bars are drawn once a frame, all together,
64
+ * and only when a segment changes.
65
+ *
66
+ * @cssvar
67
+ * ```
68
+ * --bargraph-color-start
69
+ * --bargraph-color-end
70
+ * --bargraph-overshoot
71
+ * --bargraph-off
72
+ * --bargraph-off-mix
73
+ * --bargraph-gap
74
+ * ```
75
+ */
76
+
77
+ export class Bargraph extends Canvas<BargraphProps> {
78
+ static classPrefix = "x4";
79
+
80
+ private _value = 0; // the value given
81
+ private _shown = 0; // the value drawn: it goes to _value when animated
82
+ private _peak = 0; // the peak drawn
83
+ private _peakTime = 0;
84
+
85
+ // what is drawn: the counts of segments
86
+ private _lit = 0;
87
+ private _peakLit = 0;
88
+
89
+ // the colors of the segments, read from the css; null: to read again
90
+ private _on: string[] = null;
91
+ private _off: string[] = null;
92
+ private _theme = ""; // the theme of the colors read
93
+ private _gap = 2;
94
+
95
+ constructor( props: BargraphProps ) {
96
+ super( props );
97
+
98
+ if( props.vertical ) {
99
+ this.addClass( 'vertical' );
100
+ }
101
+
102
+ this.setAttribute( "role", "meter" );
103
+
104
+ this._value = this._shown = this._peak = props.value ?? this._min( );
105
+ this._aria( );
106
+ this._count( );
107
+ }
108
+
109
+ private _min( ) { return this.props.min ?? 0; }
110
+ private _max( ) { return this.props.max ?? 100; }
111
+ private _segments( ) { return Math.max( 1, this.props.segments ?? 20 ); }
112
+
113
+ private _aria( ) {
114
+ this.setAria( "aria-valuemin", this._min( ) );
115
+ this.setAria( "aria-valuemax", this._max( ) );
116
+ this.setAria( "aria-valuenow", this._value );
117
+ }
118
+
119
+ // :: animation: one loop for all the bargraphs, while one of them moves :::::::
120
+
121
+ private static _moving = new Set<Bargraph>( );
122
+ private static _raf = 0;
123
+ private static _last = 0;
124
+
125
+ private static _tick( now: number ) {
126
+ const dt = Bargraph._last ? Math.min( now - Bargraph._last, 100 ) : 16.7;
127
+ Bargraph._last = now;
128
+ Bargraph._raf = 0;
129
+
130
+ for( const b of Bargraph._moving ) {
131
+ if( !b._step( now, dt ) ) {
132
+ Bargraph._moving.delete( b );
133
+ }
134
+ }
135
+
136
+ if( Bargraph._moving.size ) {
137
+ Bargraph._raf = requestAnimationFrame( Bargraph._tick );
138
+ }
139
+ else {
140
+ Bargraph._last = 0;
141
+ }
142
+ }
143
+
144
+ private _animate( ) {
145
+ Bargraph._moving.add( this );
146
+ if( !Bargraph._raf ) {
147
+ Bargraph._raf = requestAnimationFrame( Bargraph._tick );
148
+ }
149
+ }
150
+
151
+ // a frame: false when it does not move anymore
152
+ private _step( now: number, dt: number ) {
153
+ const range = Math.abs( this._max( ) - this._min( ) ) || 1;
154
+ const delta = this._value - this._shown;
155
+
156
+ if( this.props.animated===false ) {
157
+ this._shown = this._value;
158
+ }
159
+ else {
160
+ // up quickly, down slowly: as a level meter
161
+ this._shown += delta * ( 1 - Math.pow( delta>0 ? 0.5 : 0.8, dt/16.7 ) );
162
+ if( Math.abs( this._value - this._shown ) < range/1000 ) {
163
+ this._shown = this._value;
164
+ }
165
+ }
166
+
167
+ let moving = this._shown!==this._value;
168
+
169
+ if( this.props.peak ) {
170
+ if( this._shown>=this._peak ) {
171
+ this._peak = this._shown;
172
+ this._peakTime = now;
173
+ }
174
+ else if( now - this._peakTime > PEAK_HOLD ) {
175
+ this._peak = Math.max( this._shown, this._peak - range * dt / PEAK_FALL );
176
+ }
177
+
178
+ moving ||= this._peak>this._shown;
179
+ }
180
+
181
+ if( this._count( ) ) {
182
+ this.redraw( );
183
+ }
184
+
185
+ return moving;
186
+ }
187
+
188
+ // the counts of segments lit; true when they changed
189
+ private _count( ) {
190
+ const min = this._min( );
191
+ const max = this._max( );
192
+ const n = this._segments( );
193
+ const at = ( v: number ) => max>min ? Math.round( clamp( (v-min) / (max-min), 0, 1 ) * n ) : 0;
194
+
195
+ const lit = at( this._shown );
196
+ const peak = this.props.peak ? at( this._peak ) : 0;
197
+
198
+ if( lit==this._lit && peak==this._peakLit ) {
199
+ return false;
200
+ }
201
+
202
+ this._lit = lit;
203
+ this._peakLit = peak;
204
+ return true;
205
+ }
206
+
207
+ // :: colors ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
208
+
209
+ private _recolor( ) {
210
+ this._on = null;
211
+ this.redraw( );
212
+ }
213
+
214
+ // the variables as the bargraph sees them: a box may give its own
215
+ private _readColors( ) {
216
+ const style = getComputedStyle( this.dom );
217
+ this._theme = Application.getTheme( );
218
+ const read = ( value: string ) => new Color( value.replace( /var\(\s*(--[\w-]+)\s*\)/g, ( _, name ) => style.getPropertyValue( name ).trim( ) ) );
219
+ const copy = ( c: Color ) => { const x = c.toRgb( ); return new Color( x.red, x.green, x.blue, x.alpha ); };
220
+
221
+ const n = this._segments( );
222
+ const min = this._min( );
223
+ const size = ( this._max( ) - min ) / n;
224
+ const overshoot = this.props.overshoot;
225
+
226
+ // the segments under the overshoot: the colors are spread on them only
227
+ let low = n;
228
+ if( overshoot!==undefined ) {
229
+ low = 0;
230
+ while( low<n && min + low*size < overshoot ) {
231
+ low++;
232
+ }
233
+ }
234
+
235
+ const stops = ( this.props.colors?.length ? this.props.colors : accentStops ).map( read );
236
+ const over = read( "var(--bargraph-overshoot)" );
237
+ const off = read( "var(--bargraph-off)" );
238
+ const offMix = ( parseFloat( style.getPropertyValue( "--bargraph-off-mix" ) ) || 15 ) / 100;
239
+ const disabled = this.isDisabled( ) ? read( "var(--disabled-color-light)" ) : null;
240
+
241
+ // the color at pos (0 to 1) between the stops
242
+ const colorAt = ( pos: number ) => {
243
+ if( stops.length==1 ) {
244
+ return copy( stops[0] );
245
+ }
246
+
247
+ const at = pos * (stops.length-1);
248
+ const k = Math.min( Math.floor( at ), stops.length-2 );
249
+ return copy( stops[k] ).mix( stops[k+1], at-k );
250
+ };
251
+
252
+ this._on = [];
253
+ this._off = [];
254
+
255
+ for( let i=0; i<n; i++ ) {
256
+ const on = i<low ? colorAt( low>1 ? i/(low-1) : 0 ) : copy( over );
257
+
258
+ // a trace of its color
259
+ this._off.push( copy( off ).mix( on, offMix ).toRgbString( true ) );
260
+ this._on.push( ( disabled ?? on ).toRgbString( true ) );
261
+ }
262
+
263
+ this._gap = parseFloat( style.getPropertyValue( "--bargraph-gap" ) ) || 0;
264
+ }
265
+
266
+ // :: drawing :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
267
+
268
+ // in pixels of the screen, not of the page: the segments stay sharp at any zoom.
269
+ // the theme has no event: it is compared at each drawing (a bargraph that does not move
270
+ // keeps the colors it has until it is drawn again: refresh)
271
+ protected override paint( ctx: CanvasEx ) {
272
+ if( !this._on || this._theme!=Application.getTheme( ) ) {
273
+ this._readColors( );
274
+ }
275
+
276
+ const canvas = this.canvas.dom as HTMLCanvasElement;
277
+ const W = canvas.width;
278
+ const H = canvas.height;
279
+
280
+ ctx.setTransform( 1, 0, 0, 1, 0, 0 );
281
+ ctx.clearRect( 0, 0, W, H );
282
+
283
+ const vertical = !!this.props.vertical;
284
+ const n = this._on.length;
285
+ const gap = Math.round( this._gap * ( window.devicePixelRatio || 1 ) );
286
+ const len = vertical ? H : W;
287
+
288
+ for( let i=0; i<n; i++ ) {
289
+ const a = Math.round( i * (len+gap) / n );
290
+ const b = Math.round( (i+1) * (len+gap) / n ) - gap;
291
+ const lit = i<this._lit || i==this._peakLit-1;
292
+
293
+ ctx.fillStyle = lit ? this._on[i] : this._off[i];
294
+
295
+ if( vertical ) {
296
+ ctx.fillRect( 0, H-b, W, b-a );
297
+ }
298
+ else {
299
+ ctx.fillRect( a, 0, b-a, H );
300
+ }
301
+ }
302
+ }
303
+
304
+ // :: api :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
305
+
306
+ /**
307
+ * drawn at the next frame: call it as often as needed
308
+ */
309
+
310
+ setValue( v: number ) {
311
+ this._value = v;
312
+ this.setAria( "aria-valuenow", v );
313
+ this._animate( );
314
+ }
315
+
316
+ getValue( ) {
317
+ return this._value;
318
+ }
319
+
320
+ setMin( min: number ) {
321
+ this.props.min = min;
322
+ this._changed( );
323
+ }
324
+
325
+ setMax( max: number ) {
326
+ this.props.max = max;
327
+ this._changed( );
328
+ }
329
+
330
+ setColorStops( colors: string[] ) {
331
+ this.props.colors = colors;
332
+ this._changed( );
333
+ }
334
+
335
+ /** undefined: no overshoot */
336
+ setOvershoot( overshoot: number ) {
337
+ this.props.overshoot = overshoot;
338
+ this._changed( );
339
+ }
340
+
341
+ /**
342
+ * read the colors again and draw: after a change of the css variables, of the theme
343
+ */
344
+
345
+ refresh( ) {
346
+ this._recolor( );
347
+ }
348
+
349
+ // disabled: the segments lit take the disabled color
350
+ override enable( ena = true ): this {
351
+ super.enable( ena );
352
+ this._recolor( );
353
+ return this;
354
+ }
355
+
356
+ private _changed( ) {
357
+ this._aria( );
358
+ this._on = null;
359
+ this._count( );
360
+ this.redraw( );
361
+ }
362
+ }
@@ -108,4 +108,74 @@
108
108
  option:checked {
109
109
  background-color: var( --accent-background );
110
110
  color: var( --accent-color );
111
- }
111
+ }
112
+
113
+
114
+ // :: label-top ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
115
+ // The label above its field, smaller and discreet, darker while the field has the focus: the
116
+ // class label-top on a TextEdit (an AutoComplete), a Combobox, a TextArea, or on any box that
117
+ // holds them (a Form, the main view: all the application). labelWidth is not used.
118
+
119
+ :root {
120
+ --label-top-color: var( --text-secondary );
121
+ --label-top-color-focus: var( --text-primary ); // the field has the focus: darker, lighter in a dark theme
122
+ --label-top-size: 0.85em;
123
+ --label-top-gap: 2px; // between the label and the field
124
+ --label-top-gap-textarea: 4px; // the same, above a TextArea
125
+ --label-top-suffix: ":"; // after the label, as when it is on the left
126
+ }
127
+
128
+ // TextEdit (and AutoComplete), Combobox: the line becomes a column
129
+ :is( .label-top .x4textedit, .x4textedit.label-top, .label-top .x4combobox, .x4combobox.label-top ) {
130
+ flex-direction: column;
131
+ align-items: stretch;
132
+ gap: var( --label-top-gap );
133
+
134
+ // labelWidth is not used: the label has all the width
135
+ & > #label {
136
+ width: auto !important;
137
+ justify-content: start;
138
+ font-size: var( --label-top-size );
139
+
140
+ & > .x4label {
141
+ width: auto !important;
142
+ height: auto;
143
+ justify-content: start;
144
+ font-weight: normal;
145
+ --label-color: var( --label-top-color );
146
+ }
147
+ }
148
+
149
+ & > #edit {
150
+ flex: none;
151
+ }
152
+
153
+ &:focus-within > #label > .x4label {
154
+ --label-color: var( --label-top-color-focus );
155
+ }
156
+ }
157
+
158
+ // the colon of a TextEdit is after its #label, the one of a Combobox after its text
159
+ :is( .label-top .x4textedit, .x4textedit.label-top ) > #label::after {
160
+ content: var( --label-top-suffix );
161
+ }
162
+
163
+ :is( .label-top .x4combobox, .x4combobox.label-top ) > #label > .x4label #text:not(.empty)::after {
164
+ content: var( --label-top-suffix );
165
+ }
166
+
167
+ // TextArea: its label is above already; the same look
168
+ :is( .label-top .x4textarea, .x4textarea.label-top ) > .x4label {
169
+ font-size: var( --label-top-size );
170
+ font-weight: normal;
171
+ padding: 0 0 var( --label-top-gap-textarea );
172
+ --label-color: var( --label-top-color );
173
+
174
+ &::after {
175
+ content: var( --label-top-suffix );
176
+ }
177
+ }
178
+
179
+ :is( .label-top .x4textarea, .x4textarea.label-top ):focus-within > .x4label {
180
+ --label-color: var( --label-top-color-focus );
181
+ }
@@ -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]) {
@@ -36,6 +36,7 @@
36
36
 
37
37
  --calendar-header-color: var( --text-primary );
38
38
  --calendar-out-color: var( --disabled-color-dark );
39
+ --calendar-disabled-background: var( --background-ternary );
39
40
  --calendar-day-color: var( --text-primary );
40
41
  }
41
42
 
@@ -142,7 +143,6 @@
142
143
 
143
144
  .out {
144
145
  color: var( --calendar-out-color );
145
- font-size: 80%;
146
146
  }
147
147
 
148
148
  .day:hover {
@@ -152,16 +152,17 @@
152
152
  }
153
153
  }
154
154
 
155
- // out of minDate..maxDate: cannot be chosen
155
+ // out of minDate..maxDate: cannot be chosen; a background on all their cell (a band over the
156
+ // days closed), their text as the other days
156
157
  .disabled {
157
- color: var( --calendar-out-color );
158
- text-decoration: line-through;
158
+ background-color: var( --calendar-disabled-background );
159
159
  cursor: default;
160
160
  }
161
161
 
162
+ // the hover of a day: none on them
162
163
  .disabled:hover .text {
163
164
  background-color: transparent;
164
- color: var( --calendar-out-color );
165
+ color: inherit;
165
166
  }
166
167
  }
167
168
 
@@ -15,7 +15,7 @@
15
15
  **/
16
16
 
17
17
  import { Component, ComponentEvents, ComponentProps, EvChange, Flex } from '../../core/component'
18
- import { date_clone, date_hash, formatIntlDate, Point, unsafeHtml } from "../../core/core_tools"
18
+ import { date_clone, date_hash, date_week_start, formatIntlDate, Point, unsafeHtml } from "../../core/core_tools"
19
19
  import { _tr } from '../../core/core_i18n';
20
20
  import { EventCallback } from '../../core/core_events';
21
21
 
@@ -66,6 +66,7 @@ interface CalendarProps extends ComponentProps {
66
66
  * --calendar-today-border
67
67
  * --calendar-header-color
68
68
  * --calendar-out-color
69
+ * --calendar-disabled-background
69
70
  * --calendar-day-color
70
71
  * ```
71
72
  */
@@ -89,16 +90,11 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
89
90
 
90
91
  private _update( ) {
91
92
 
93
+ // the first line: the week of the 1st, from its monday
92
94
  let month_start = date_clone(this.m_date);
93
95
  month_start.setDate(1);
94
96
 
95
- let day = month_start.getDay();
96
- if (day == 0) {
97
- day = 7;
98
- }
99
-
100
- month_start.setDate(-day + 1 + 1);
101
- let dte = date_clone(month_start);
97
+ let dte = date_week_start(month_start);
102
98
 
103
99
  let selection = date_hash( this.m_date );
104
100
  let today = date_hash( new Date() );
@@ -287,7 +283,17 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
287
283
  let items: MenuItem[] = [];
288
284
 
289
285
  if (type == 'month') {
286
+ // the months of the year shown that have days between minDate and maxDate (as year * 12 + month)
287
+ const year = this.m_date.getFullYear( );
288
+ const { minDate, maxDate } = this.props;
289
+ const first = minDate ? minDate.getFullYear( ) * 12 + minDate.getMonth( ) : -Infinity;
290
+ const last = maxDate ? maxDate.getFullYear( ) * 12 + maxDate.getMonth( ) : Infinity;
291
+
290
292
  for (let m = 0; m < 12; m++) {
293
+ if( year * 12 + m < first || year * 12 + m > last ) {
294
+ continue;
295
+ }
296
+
291
297
  items.push(({
292
298
  text: _tr.global.month_long[m],
293
299
  click: () => { this._goto( this.m_date.getFullYear( ), m ); }
@@ -24,7 +24,7 @@ export interface EvPaint extends ComponentEvent {
24
24
  ctx: CanvasEx;
25
25
  }
26
26
 
27
- interface CanvasEventMap extends ComponentEvents {
27
+ export interface CanvasEventMap extends ComponentEvents {
28
28
  paint: EvPaint;
29
29
  }
30
30
 
@@ -41,10 +41,11 @@ export interface CanvasProps extends ComponentProps {
41
41
  // ============================================================================
42
42
 
43
43
  /**
44
- * Standard Canvas
44
+ * Standard Canvas; a class derived from it gives its props and its events:
45
+ * class Scope extends Canvas<ScopeProps, ScopeEvents>
45
46
  */
46
47
 
47
- export class Canvas extends Component<CanvasProps, CanvasEventMap> {
48
+ export class Canvas<P extends CanvasProps = CanvasProps, E extends CanvasEventMap = CanvasEventMap> extends Component<P, E> {
48
49
  static classPrefix = "x4";
49
50
 
50
51
  private m_iwidth: number = -1;
@@ -52,7 +53,7 @@ export class Canvas extends Component<CanvasProps, CanvasEventMap> {
52
53
  private m_scale = 1.0;
53
54
  private m_canvas: Component;
54
55
 
55
- constructor(props: CanvasProps) {
56
+ constructor(props: P) {
56
57
  super(props);
57
58
 
58
59
  this.mapPropEvents( props, 'paint' );