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
@@ -0,0 +1,178 @@
1
+ /**
2
+ * ___ ___ __
3
+ * \ \/ / / _
4
+ * \ / /_| |_
5
+ * / \____ _|
6
+ * /__/\__\ |_|
7
+ *
8
+ * @file settingsview.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
+ :root {
18
+ --settingsview-toc-width: 210px;
19
+ --settingsview-section-background: var( --color-primary-a10 );
20
+ --settingsview-section-title-background: var( --color-primary-a20 );
21
+ --settingsview-hover-background: color-mix( in srgb, var( --color-primary-a20 ) 60%, transparent );
22
+ --settingsview-marker: var( --accent-background );
23
+ }
24
+
25
+ .x4settingsview {
26
+ min-height: 0;
27
+
28
+ > .search {
29
+ flex-shrink: 0;
30
+ padding: 12px 32px 12px calc( var( --settingsview-toc-width ) + 32px );
31
+ border-bottom: 1px solid var( --border );
32
+
33
+ > .x4input {
34
+ flex: 1;
35
+ max-width: 900px;
36
+ margin: 0;
37
+ padding: 6px 10px;
38
+ border: 1px solid var( --border );
39
+ border-radius: 4px;
40
+
41
+ &:focus {
42
+ border-color: var( --settingsview-marker );
43
+ }
44
+ }
45
+ }
46
+
47
+ > .body {
48
+ min-height: 0;
49
+ align-items: stretch;
50
+ }
51
+
52
+ // the summary: the section at the top in the color of the text, marked on its left
53
+ .toc {
54
+ flex-shrink: 0;
55
+ width: var( --settingsview-toc-width );
56
+ margin: 0;
57
+ padding: 12px 0;
58
+ border: none;
59
+ border-right: 1px solid var( --border );
60
+ border-radius: 0;
61
+ background: transparent;
62
+
63
+ .x4item {
64
+ padding: 4px 4px 4px 16px;
65
+ border-left: 2px solid transparent;
66
+ border-radius: 0;
67
+ --label-color: var( --text-secondary );
68
+
69
+ &:hover {
70
+ background: transparent;
71
+ --label-color: var( --text-primary );
72
+ }
73
+
74
+ &.selected {
75
+ background: transparent;
76
+ border-left-color: var( --settingsview-marker );
77
+ --label-color: var( --text-primary );
78
+ font-weight: 600;
79
+ }
80
+ }
81
+ }
82
+
83
+ .sections {
84
+ align-items: flex-start;
85
+ gap: 24px;
86
+ min-height: 0;
87
+ overflow-y: auto;
88
+ padding: 20px 32px 32px;
89
+
90
+ > .nomatch {
91
+ padding: 8px;
92
+ --label-color: var( --text-secondary );
93
+ }
94
+ }
95
+
96
+ // a section: a panel, its title on a band
97
+ .section {
98
+ flex-shrink: 0;
99
+ width: 100%;
100
+ max-width: 900px;
101
+ padding: 0 8px 10px;
102
+ border: 1px solid var( --border );
103
+ border-radius: 8px;
104
+ background: var( --settingsview-section-background );
105
+ overflow: hidden;
106
+ scroll-margin-top: 20px; // brought to the top: below the padding of the view
107
+
108
+ // the last one shown: as high as the view, a click in the summary brings it to the top too
109
+ &.last {
110
+ min-height: 100%;
111
+ }
112
+
113
+ > .title {
114
+ margin: 0 -8px 6px;
115
+ padding: 12px 16px;
116
+ border-bottom: 1px solid var( --border );
117
+ background: var( --settingsview-section-title-background );
118
+ font-size: 1.1rem;
119
+ font-weight: 600;
120
+ --label-color: var( --text-primary );
121
+ }
122
+
123
+ > .description {
124
+ padding: 10px 8px 4px;
125
+ white-space: normal;
126
+ --label-color: var( --text-secondary );
127
+ }
128
+
129
+ // a section is as high as its content: what it holds takes its own height,
130
+ // a flex child (a Form) would get none
131
+ > * {
132
+ flex: none;
133
+ }
134
+
135
+ // a form of settings: the settings set the spaces
136
+ > .x4form {
137
+ padding: 0;
138
+ }
139
+ }
140
+
141
+ // a setting: its name, what it does, its controls; marked on its left while one has the focus
142
+ .x4settingitem {
143
+ flex-shrink: 0;
144
+ gap: 4px;
145
+ padding: 10px 8px 12px;
146
+ border-left: 2px solid transparent;
147
+ border-radius: 4px;
148
+
149
+ &:hover {
150
+ background: var( --settingsview-hover-background );
151
+ }
152
+
153
+ &:focus-within {
154
+ border-left-color: var( --settingsview-marker );
155
+ }
156
+
157
+ > .name {
158
+ font-weight: 600;
159
+ --label-color: var( --text-primary );
160
+ }
161
+
162
+ > .description {
163
+ white-space: normal;
164
+ --label-color: var( --text-secondary );
165
+ }
166
+
167
+ > .controls {
168
+ align-items: center;
169
+ gap: 10px;
170
+ padding-top: 4px;
171
+ flex-wrap: wrap;
172
+
173
+ > * {
174
+ margin: 0;
175
+ }
176
+ }
177
+ }
178
+ }
@@ -0,0 +1,289 @@
1
+ /**
2
+ * ___ ___ __
3
+ * \ \/ / / _
4
+ * \ / /_| |_
5
+ * / \____ _|
6
+ * /__/\__\ |_|
7
+ *
8
+ * @file settingsview.ts
9
+ * @author Etienne Cochard
10
+ *
11
+ * @copyright (c) 2026 R-libre ingenierie
12
+ *
13
+ * Use of this source code is governed by an MIT-style license
14
+ * that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
15
+ **/
16
+
17
+ import { Component, ComponentEvent, ComponentEvents } from '../../core/component';
18
+ import { EventCallback } from '../../core/core_events';
19
+ import { class_ns } from '../../core/core_tools';
20
+
21
+ import { BoxProps, HBox, VBox } from '../boxes/boxes';
22
+ import { Input } from '../input/input';
23
+ import { Label } from '../label/label';
24
+ import { Listbox } from '../listbox/listbox';
25
+
26
+ import { _tr } from '../../x4.js';
27
+
28
+ import "./settingsview.module.scss"
29
+
30
+ /**
31
+ *
32
+ */
33
+
34
+ export interface SettingItemProps extends BoxProps {
35
+ title: string;
36
+ description?: string;
37
+ content: Component | Component[]; // its controls
38
+ }
39
+
40
+ /**
41
+ * one setting: its name, what it does, then its controls on a line that wraps.
42
+ * It has no value of its own: its controls keep theirs. To read and write them
43
+ * together, put the items in a Form and give the controls a name.
44
+ */
45
+
46
+ @class_ns( "x4" )
47
+ export class SettingItem extends VBox<SettingItemProps> {
48
+
49
+ constructor( props: SettingItemProps ) {
50
+ // the content goes under the name and the description, not in the box itself
51
+ super( { ...props, content: undefined } );
52
+
53
+ this.setContent( [
54
+ new Label( { cls: "name", text: props.title } ),
55
+ props.description ? new Label( { cls: "description", text: props.description } ) : null,
56
+ new HBox( { cls: "controls", content: props.content } ),
57
+ ] );
58
+ }
59
+
60
+ /**
61
+ * the text a search looks in: the name and the description
62
+ */
63
+
64
+ getSearchText( ) {
65
+ return `${this.props.title} ${this.props.description ?? ""}`;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * a section: a panel, its title on a band, then its content
71
+ */
72
+
73
+ export interface SettingsSection {
74
+ id: string;
75
+ title: string;
76
+ description?: string; // under the title
77
+ content: Component[]; // SettingItem, Form of SettingItem, or any component
78
+ }
79
+
80
+ export interface EvSectionChange extends ComponentEvent {
81
+ readonly section: string; // the id of the section now at the top
82
+ }
83
+
84
+ export interface SettingsViewProps extends BoxProps {
85
+ sections?: SettingsSection[];
86
+ searchable?: boolean; // a search field above the sections
87
+ searchPlaceholder?: string; // _tr.global.settings.search when missing
88
+ noMatch?: string; // shown when the search finds nothing; _tr.global.settings.nomatch when missing
89
+ sectionChange?: EventCallback<EvSectionChange>;
90
+ }
91
+
92
+ export interface SettingsViewEvents extends ComponentEvents {
93
+ sectionChange: EvSectionChange;
94
+ }
95
+
96
+ /**
97
+ * text compared without case nor accents
98
+ */
99
+
100
+ function normalize( text: string ) {
101
+ return text.normalize( "NFD" ).replace( /[̀-ͯ]/g, "" ).toLowerCase( );
102
+ }
103
+
104
+ /**
105
+ * a page of settings, as the settings of VS Code: the summary of the sections
106
+ * on the left, the sections on the right, one after the other. A click in the
107
+ * summary brings its section to the top; the summary follows the scroll.
108
+ * With searchable, a field above them filters the settings.
109
+ *
110
+ * @cssvar
111
+ * ```
112
+ * --settingsview-toc-width
113
+ * --settingsview-section-background
114
+ * --settingsview-section-title-background
115
+ * --settingsview-hover-background
116
+ * --settingsview-marker
117
+ * ```
118
+ */
119
+
120
+ @class_ns( "x4" )
121
+ export class SettingsView<P extends SettingsViewProps = SettingsViewProps> extends VBox<P,SettingsViewEvents> {
122
+
123
+ // fields, not refs: refs belong to the classes that derive from this one
124
+ private _search: Input = null;
125
+ private _toc: Listbox;
126
+ private _view: VBox;
127
+ private _noMatch: Label;
128
+
129
+ // shown: kept by the search (isVisible is false too while the view is hidden)
130
+ private _sections: { section: SettingsSection, box: VBox, shown: boolean }[] = [];
131
+ private _current = ""; // the id of the section at the top
132
+
133
+ constructor( props: P ) {
134
+ super( props );
135
+ this.mapPropEvents( props, "sectionChange" );
136
+
137
+ this.setContent( [
138
+ props.searchable ? new HBox( {
139
+ cls: "search",
140
+ content: this._search = new Input( {
141
+ type: "text",
142
+ placeholder: props.searchPlaceholder ?? _tr.global.settings.search,
143
+ change: ( ) => this._filter( ),
144
+ } ),
145
+ } ) : null,
146
+
147
+ new HBox( {
148
+ cls: "body",
149
+ flex: true,
150
+ content: [
151
+ this._toc = new Listbox( {
152
+ cls: "toc",
153
+ items: [],
154
+ selectionChange: ( ev ) => this.showSection( ev.selection[0] as string ),
155
+ } ),
156
+ this._view = new VBox( { cls: "sections", flex: true } ),
157
+ ],
158
+ } ),
159
+ ] );
160
+
161
+ this._noMatch = new Label( { cls: "nomatch", text: props.noMatch ?? _tr.global.settings.nomatch, hidden: true } );
162
+ this._view.addDOMEvent( "scroll", ( ) => this._followScroll( ) );
163
+
164
+ // Escape empties the search
165
+ if( this._search ) {
166
+ this._search.addDOMEvent( "keydown", ( ev ) => {
167
+ if( ev.key == "Escape" && this._search.getValue( ) ) {
168
+ this._search.setValue( "" );
169
+ this._filter( );
170
+ }
171
+ } );
172
+ }
173
+
174
+ if( props.sections ) {
175
+ this.setSections( props.sections );
176
+ }
177
+ }
178
+
179
+ /**
180
+ * the sections, in their order; for a class that builds them in its
181
+ * constructor, after super( )
182
+ */
183
+
184
+ setSections( sections: SettingsSection[] ) {
185
+ this._sections = sections.map( section => ( {
186
+ section,
187
+ shown: true,
188
+ box: new VBox( {
189
+ cls: "section",
190
+ content: [
191
+ new Label( { cls: "title", text: section.title } ),
192
+ section.description ? new Label( { cls: "description", text: section.description } ) : null,
193
+ ...section.content,
194
+ ],
195
+ } ),
196
+ } ) );
197
+
198
+ this._view.setContent( [ ...this._sections.map( s => s.box ), this._noMatch ] );
199
+ this._current = "";
200
+ this._filter( );
201
+ }
202
+
203
+ /**
204
+ * brings a section to the top of the view
205
+ */
206
+
207
+ showSection( id: string, smooth = true ) {
208
+ this._sections.find( s => s.section.id == id )?.box.scrollIntoView( { behavior: smooth ? "smooth" : "auto", block: "start" } );
209
+ }
210
+
211
+ /**
212
+ * the id of the section at the top of the view
213
+ */
214
+
215
+ getSection( ) {
216
+ return this._current;
217
+ }
218
+
219
+ /**
220
+ * shows what the search finds; everything when it is empty. A section
221
+ * whose title or description holds the words is shown whole; otherwise
222
+ * only its settings that hold them, and what holds those settings.
223
+ */
224
+
225
+ private _filter( ) {
226
+ const text = this._search ? this._search.getValue( ) : "";
227
+ const words = normalize( text ).split( /\s+/ ).filter( w => w );
228
+ const matches = ( text: string ) => {
229
+ const t = normalize( text );
230
+ return words.every( w => t.includes( w ) );
231
+ };
232
+
233
+ let found = false;
234
+ for( const s of this._sections ) {
235
+ const { section, box } = s;
236
+ const whole = !words.length || matches( `${section.title} ${section.description ?? ""}` );
237
+ let shown = whole;
238
+
239
+ for( const c of section.content ) {
240
+ const items = c instanceof SettingItem ? [c] : c.enumChildComponents( true ).filter( x => x instanceof SettingItem ) as SettingItem[];
241
+ let some = false;
242
+ for( const item of items ) {
243
+ const ok = whole || matches( item.getSearchText( ) );
244
+ item.show( ok );
245
+ some ||= ok;
246
+ }
247
+ // what holds no setting (a table, a text) is shown with its whole section only
248
+ c.show( whole || some );
249
+ shown ||= some;
250
+ }
251
+
252
+ s.shown = shown;
253
+ box.show( shown );
254
+ found ||= shown;
255
+ }
256
+
257
+ this._noMatch.show( !found );
258
+
259
+ // the summary lists the sections shown; the last one is as high as the
260
+ // view at least: a click in the summary can bring every section to the top
261
+ const shown = this._sections.filter( s => s.shown );
262
+ this._sections.forEach( s => s.box.setClass( "last", s == shown[shown.length - 1] ) );
263
+ this._toc.clearSelection( false );
264
+ this._toc.setItems( shown.map( s => ( { id: s.section.id, text: s.section.title } ) ) );
265
+ this._current = "";
266
+ this._followScroll( );
267
+ }
268
+
269
+ /**
270
+ * the summary on the section at the top of the view; the first one while the view is hidden
271
+ */
272
+
273
+ private _followScroll( ) {
274
+ const view = this._view.getBoundingRect( );
275
+
276
+ let current = "";
277
+ for( const { section, box, shown } of this._sections ) {
278
+ if( shown && ( !current || ( view.height && box.getBoundingRect( ).top <= view.top + 40 ) ) ) {
279
+ current = section.id;
280
+ }
281
+ }
282
+
283
+ this._toc.select( current, false );
284
+ if( current != this._current ) {
285
+ this._current = current;
286
+ this.fire( "sectionChange", { section: current } );
287
+ }
288
+ }
289
+ }
@@ -16,30 +16,31 @@
16
16
 
17
17
 
18
18
  :root {
19
- --spreadsheet-background: white;
19
+ // from the theme: a dark theme (its scale --color-primary-* reversed) gives a dark grid
20
+ --spreadsheet-background: var( --background-primary );
20
21
  --spreadsheet-border: var( --border );
21
22
 
22
- --spreadsheet-header-cell-background: white;
23
- --spreadsheet-header-cell-color: black;
24
- --spreadsheet-header-cell-vline: #f0f0f0;
25
- --spreadsheet-header-cell-border: #cccccc;
23
+ --spreadsheet-header-cell-background: var( --background-primary );
24
+ --spreadsheet-header-cell-color: var( --text-primary );
25
+ --spreadsheet-header-cell-vline: var( --border );
26
+ --spreadsheet-header-cell-border: var( --border );
26
27
 
27
28
  --spreadsheet-check-background: var( --accent-background );
28
- --spreadsheet-check-color: white;
29
- --spreadsheet-check-background-hover: white;
29
+ --spreadsheet-check-color: var( --accent-color );
30
+ --spreadsheet-check-background-hover: var( --background-primary );
30
31
  --spreadsheet-check-color-hover: var( --accent-background );
31
32
 
32
33
  --spreadsheet-perc-background: var( --accent-background );
33
- --spreadsheet-perc-color: white;
34
- --spreadsheet-perc-background-hover: white;
34
+ --spreadsheet-perc-color: var( --accent-color );
35
+ --spreadsheet-perc-background-hover: var( --background-primary );
35
36
  --spreadsheet-perc-color-hover: var( --accent-background );
36
37
 
37
- --spreadsheet-cell-color: black;
38
- --spreadsheet-cell-color-sel: white;
38
+ --spreadsheet-cell-color: var( --text-primary );
39
+ --spreadsheet-cell-color-sel: var( --accent-color );
39
40
  --spreadsheet-cell-vline: var( --border );
40
41
 
41
- --spreadsheet-row-background: white;
42
- --spreadsheet-row-odd-background: white;
42
+ --spreadsheet-row-background: var( --background-primary );
43
+ --spreadsheet-row-odd-background: var( --background-primary );
43
44
  --spreadsheet-row-border: var( --border );
44
45
 
45
46
  --spreadsheet-row-background-hover: var( --background-secondary );
@@ -70,6 +71,7 @@
70
71
 
71
72
  background-color: var( --spreadsheet-background );
72
73
  border: 1px solid var( --spreadsheet-border );
74
+ color: var( --spreadsheet-cell-color );
73
75
 
74
76
  .cell {
75
77
  padding: 4px;
@@ -180,7 +182,7 @@
180
182
  width: 100%;
181
183
  border: 1px solid var( --border );
182
184
  height: 8px;
183
- background-color: white;
185
+ background-color: var( --spreadsheet-background );
184
186
  overflow: hidden;
185
187
 
186
188
  div {
@@ -234,7 +236,7 @@
234
236
  display: inline-flex;
235
237
  align-items: center;
236
238
 
237
- color: black;
239
+ color: var( --spreadsheet-header-cell-color );
238
240
  font-weight: 500;
239
241
  justify-content: center;
240
242
 
@@ -307,7 +309,7 @@
307
309
  width: var( --fixed-width );
308
310
  bottom: 0;
309
311
 
310
- background-color: white;
312
+ background-color: var( --spreadsheet-background );
311
313
  z-index: 1;
312
314
 
313
315
  .row {
@@ -27,6 +27,12 @@
27
27
  flex-shrink: 0; // alone in a box, it keeps its height: the box does not crush it
28
28
  resize: none; // no handle unless the prop resize asks for one
29
29
  padding: 4px;
30
+
31
+ // never smaller than one character, its padding and border included (border-box):
32
+ // 10px = 2 x 4px of padding + 2 x 1px of border
33
+ min-height: calc( 1lh + 10px );
34
+ min-width: calc( 1ch + 10px );
35
+
30
36
  outline: none;
31
37
  font: inherit;
32
38
 
@@ -65,7 +71,8 @@
65
71
  }
66
72
 
67
73
  .x4simpletextarea {
68
- @extend %flex;
69
-
70
- }
74
+ // its height of rows when the TextArea has none, the TextArea's height when it has one;
75
+ // a basis of 0 would leave it the height of its padding: a textarea scrolls, its min-height is 0
76
+ flex: 1 0 auto;
77
+ }
71
78
  }
@@ -14,7 +14,9 @@
14
14
  * that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
15
15
  **/
16
16
 
17
- import { Box, Component, ComponentProps } from 'x4js';
17
+ import { Component, ComponentProps } from '../../core/component';
18
+ import { class_ns } from '../../core/core_tools';
19
+ import { Box } from '../boxes/boxes';
18
20
 
19
21
  import "./video.module.scss"
20
22
 
@@ -29,6 +31,7 @@ interface VideoProps extends ComponentProps {
29
31
  *
30
32
  */
31
33
 
34
+ @class_ns( "x4" )
32
35
  export class Video extends Box {
33
36
 
34
37
  private _vplayer: HTMLVideoElement;
@@ -124,7 +127,18 @@ export class Video extends Box {
124
127
  this.player.pause( );
125
128
  }
126
129
 
127
- play( ) {
128
- this.player.play( );
130
+ /**
131
+ * @returns false when the playback could not start: the browser wants a click
132
+ * of the user first, or the source cannot be played
133
+ */
134
+
135
+ async play( ): Promise<boolean> {
136
+ try {
137
+ await this.player.play( );
138
+ return true;
139
+ }
140
+ catch( e ) {
141
+ return false;
142
+ }
129
143
  }
130
144
  }
@@ -83,8 +83,6 @@ export class Color {
83
83
 
84
84
  setValue(value: string): this {
85
85
 
86
- this.invalid = false;
87
-
88
86
  if( value.startsWith('#') ) {
89
87
  if (value.length == 7 && /#[0-9a-fA-F]{6}/.test(value)) {
90
88
  const hex = parseInt(value.slice(1), 16);
@@ -139,8 +137,10 @@ export class Color {
139
137
  }
140
138
  }
141
139
 
140
+ // red, to be seen, and marked as invalid
141
+ this.setRgb(255, 0, 0, 1);
142
142
  this.invalid = true;
143
- return this.setRgb(255, 0, 0, 1);
143
+ return this;
144
144
  }
145
145
 
146
146
  /**
@@ -205,23 +205,34 @@ export class Color {
205
205
  * @param r - Red component (0-255).
206
206
  * @param g - Green component (0-255).
207
207
  * @param b - Blue component (0-255).
208
- * @param a - Alpha component (0-1).
208
+ * @param a - Alpha component (0-1, default 1.0).
209
209
  * @returns The current Color instance.
210
210
  */
211
- setRgb(r: number, g: number, b: number, a: number): this {
212
- this.rgb = [clamp(r | 0, 0, 255), clamp(g | 0, 0, 255), clamp(b | 0, 0, 255), clamp(a, 0, 1)];
211
+ setRgb(r: number, g: number, b: number, a = 1.0): this {
212
+ // a new color: valid
213
+ this.invalid = false;
214
+ // rounded: hsv gives 123.999 for 124; "| 0" makes a NaN 0
215
+ const c = ( v: number ) => clamp( Math.round( v ) | 0, 0, 255 );
216
+ this.rgb = [c(r), c(g), c(b), clamp(a, 0, 1)];
213
217
  return this;
214
218
  }
215
219
 
216
220
  /**
217
221
  * Returns the CSS string representation of the color.
218
222
  *
219
- * @param withAlpha - Whether to include the alpha channel (default: true if alpha < 1).
220
- * @returns The CSS color string (e.g. "rgb(255,0,0)" or "rgba(255,0,0,0.5)").
223
+ * @param withAlpha - false (default): never the alpha; true: the alpha when the color is not opaque.
224
+ * @returns The CSS color string (e.g. "rgb(255,0,0)" or "rgba(255,0,0,0.500)").
221
225
  */
222
- toRgbString(withAlpha?: boolean): string {
226
+ toRgbString(withAlpha = false): string {
223
227
  const _ = this.rgb;
224
- return withAlpha === false || _[3] == 1 ? `rgb(${round(_[0])},${round(_[1])},${round(_[2])})` : `rgba(${round(_[0])},${round(_[1])},${round(_[2])},${_[3].toFixed(3)})`
228
+ const rgb = `${round(_[0])},${round(_[1])},${round(_[2])}`;
229
+
230
+ // an opaque color has no alpha to show
231
+ if( !withAlpha || _[3] >= 1 ) {
232
+ return `rgb(${rgb})`;
233
+ }
234
+
235
+ return `rgba(${rgb},${_[3].toFixed(3)})`;
225
236
  }
226
237
 
227
238
  /**
@@ -175,12 +175,13 @@ class DragManager {
175
175
  */
176
176
 
177
177
  private _startCheck() {
178
+ clearInterval( this.timer );
178
179
 
179
- if (this.timer) {
180
- clearInterval(this.timer);
181
- this._check( );
182
- }
180
+ // the target is told at once (and the one that was left)
181
+ this._check( );
183
182
 
183
+ // then checked again: over the children of a target, dragleave and dragover come
184
+ // in turn; it is left when no dragover came back in between
184
185
  this.timer = setInterval( () => this._check(), 300 );
185
186
  }
186
187