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.
- package/aicontext.md +127 -32
- package/package.json +1 -1
- package/src/components/autocomplete/autocomplete.ts +3 -0
- package/src/components/boxes/boxes.ts +1 -1
- package/src/components/chart/chart.ts +12 -3
- package/src/components/colorinput/colorinput.module.scss +11 -2
- package/src/components/colorinput/colorinput.ts +37 -4
- package/src/components/colorpicker/colorpicker.module.scss +14 -2
- package/src/components/colorpicker/colorpicker.ts +57 -11
- package/src/components/components.ts +1 -0
- package/src/components/dialog/dialog.ts +16 -5
- package/src/components/form/form.ts +3 -4
- package/src/components/gridview/gridview.module.scss +18 -16
- package/src/components/gridview/gridview.ts +5 -0
- package/src/components/icon/icon.ts +34 -18
- package/src/components/keyboard/keyboard.ts +31 -5
- package/src/components/link/link.ts +18 -7
- package/src/components/messages/messages.ts +1 -1
- package/src/components/panel/panel.ts +3 -2
- package/src/components/settingsview/settingsview.module.scss +178 -0
- package/src/components/settingsview/settingsview.ts +289 -0
- package/src/components/spreadsheet/spreadsheet.module.scss +18 -16
- package/src/components/textarea/textarea.module.scss +10 -3
- package/src/components/video/video.ts +17 -3
- package/src/core/core_colors.ts +21 -10
- package/src/core/core_dragdrop.ts +5 -4
- package/src/core/core_i18n.ts +191 -54
- package/src/core/core_pdf.ts +15 -3
- package/src/core/core_shortcuts.ts +5 -1
- package/src/core/core_styles.ts +46 -25
- package/src/core/core_svg.ts +24 -16
- 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
|
-
--
|
|
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:
|
|
23
|
-
--spreadsheet-header-cell-color:
|
|
24
|
-
--spreadsheet-header-cell-vline:
|
|
25
|
-
--spreadsheet-header-cell-border:
|
|
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:
|
|
29
|
-
--spreadsheet-check-background-hover:
|
|
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:
|
|
34
|
-
--spreadsheet-perc-background-hover:
|
|
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:
|
|
38
|
-
--spreadsheet-cell-color-sel:
|
|
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:
|
|
42
|
-
--spreadsheet-row-odd-background:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
128
|
-
|
|
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
|
}
|
package/src/core/core_colors.ts
CHANGED
|
@@ -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
|
|
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
|
|
212
|
-
|
|
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 -
|
|
220
|
-
* @returns The CSS color string (e.g. "rgb(255,0,0)" or "rgba(255,0,0,0.
|
|
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
|
|
226
|
+
toRgbString(withAlpha = false): string {
|
|
223
227
|
const _ = this.rgb;
|
|
224
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
|