@kubex/zinc 1.1.68 → 1.1.70
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/dist/custom-elements.json +1719 -282
- package/dist/vscode.html-custom-data.json +160 -29
- package/dist/web-types.json +395 -70
- package/dist/zn.d.ts +395 -16
- package/dist/zn.min.js +580 -408
- package/docs/pages/components/collapsible.md +6 -2
- package/docs/pages/components/icon-picker.md +14 -0
- package/docs/pages/components/input.md +19 -1
- package/docs/pages/components/preview-frame-demo.njk +33 -4
- package/docs/pages/components/preview-frame.md +20 -0
- package/docs/pages/components/theme-editor.md +303 -0
- package/docs/superpowers/plans/2026-08-03-theme-editor.md +1536 -0
- package/docs/superpowers/specs/2026-08-03-theme-editor-design.md +327 -0
- package/package.json +1 -1
- package/src/components/collapsible/collapsible.component.ts +21 -23
- package/src/components/dropdown/dropdown.component.ts +9 -0
- package/src/components/editor/editor.component.ts +20 -21
- package/src/components/file/file.component.ts +15 -0
- package/src/components/icon-picker/icon-picker.component.ts +150 -16
- package/src/components/icon-picker/icon-picker.scss +32 -0
- package/src/components/input/input.component.ts +44 -21
- package/src/components/input/input.scss +134 -42
- package/src/components/input/input.test.ts +45 -0
- package/src/components/preview-frame/preview-frame.component.ts +126 -16
- package/src/components/preview-frame/preview-frame.scss +27 -0
- package/src/components/preview-frame/preview-frame.test.ts +253 -0
- package/src/components/theme-editor/index.ts +12 -0
- package/src/components/theme-editor/theme-editor.component.ts +918 -0
- package/src/components/theme-editor/theme-editor.scss +309 -0
- package/src/components/theme-editor/theme-editor.test.ts +1752 -0
- package/src/events/events.ts +2 -0
- package/src/events/zn-theme-change.ts +13 -0
- package/src/events/zn-theme-submit.ts +9 -0
- package/src/types/web-test-runner-commands.d.ts +6 -0
- package/src/zinc.ts +1 -0
- package/tsconfig.json +7 -1
- package/web-test-runner.config.js +3 -1
|
@@ -26,19 +26,27 @@ export default class ZnIconPicker extends ZincElement implements ZincFormControl
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
private readonly formControlController = new FormControlController(this, {
|
|
29
|
-
assumeInteractionOn: ['zn-change']
|
|
29
|
+
assumeInteractionOn: ['zn-change'],
|
|
30
|
+
// With allow-upload, a chosen file is submitted under `name` in place of
|
|
31
|
+
// the icon string — the two are mutually exclusive.
|
|
32
|
+
value: (el: ZnIconPicker) => el._file ?? el.icon
|
|
30
33
|
});
|
|
31
34
|
|
|
32
35
|
@property() name = '';
|
|
33
36
|
@property() icon = '';
|
|
34
37
|
@property() label = '';
|
|
35
|
-
|
|
38
|
+
// Matches zn-icon's defaultLibrary so the picked icon previews the same way
|
|
39
|
+
// it will render wherever the stored name is displayed.
|
|
40
|
+
@property() library: string = 'material-symbols-outlined';
|
|
36
41
|
@property() color: string = '';
|
|
37
42
|
@property({type: Boolean, attribute: 'no-color'}) noColor: boolean = false;
|
|
38
43
|
@property({type: Boolean, attribute: 'no-library'}) noLibrary: boolean = false;
|
|
39
44
|
@property({attribute: 'help-text'}) helpText: string = '';
|
|
40
45
|
@property({type: Boolean, reflect: true}) disabled = false;
|
|
41
46
|
@property({type: Boolean, reflect: true}) required = false;
|
|
47
|
+
@property({type: Boolean, attribute: 'trigger-submit'}) triggerSubmit = false;
|
|
48
|
+
@property({type: Boolean, attribute: 'allow-upload'}) allowUpload = false;
|
|
49
|
+
@property() accept = 'image/*';
|
|
42
50
|
@property({reflect: true}) form: string;
|
|
43
51
|
|
|
44
52
|
@defaultValue() defaultValue = '';
|
|
@@ -52,8 +60,16 @@ export default class ZnIconPicker extends ZincElement implements ZincFormControl
|
|
|
52
60
|
@state() private _pendingIcon = '';
|
|
53
61
|
@state() private _pendingLibrary = '';
|
|
54
62
|
@state() private _pendingColor = '';
|
|
63
|
+
@state() private _pendingFile: File | null = null;
|
|
64
|
+
@state() private _pendingFileUrl: string | null = null;
|
|
65
|
+
@state() private _mode: 'icon' | 'upload' = 'icon';
|
|
66
|
+
|
|
67
|
+
// Committed upload (allow-upload mode)
|
|
68
|
+
@state() private _file: File | null = null;
|
|
69
|
+
@state() private _fileUrl: string | null = null;
|
|
55
70
|
|
|
56
71
|
@query('zn-dialog') private _dialog: ZnDialog;
|
|
72
|
+
@query('.icon-picker__file-input') private _fileInput: HTMLInputElement;
|
|
57
73
|
|
|
58
74
|
get value(): string {
|
|
59
75
|
return this.icon;
|
|
@@ -63,8 +79,18 @@ export default class ZnIconPicker extends ZincElement implements ZincFormControl
|
|
|
63
79
|
this.icon = val;
|
|
64
80
|
}
|
|
65
81
|
|
|
82
|
+
/** The chosen file when the user uploaded an image instead of picking an icon. */
|
|
83
|
+
get file(): File | null {
|
|
84
|
+
return this._file;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** True when the current value renders as an image (an uploaded file or a URL) rather than a library icon. */
|
|
88
|
+
get isImageValue(): boolean {
|
|
89
|
+
return !!this._file || this.icon.includes('/');
|
|
90
|
+
}
|
|
91
|
+
|
|
66
92
|
get validity(): ValidityState {
|
|
67
|
-
if (this.required && !this.icon) {
|
|
93
|
+
if (this.required && !this.icon && !this._file) {
|
|
68
94
|
return {
|
|
69
95
|
valid: false,
|
|
70
96
|
valueMissing: true,
|
|
@@ -81,7 +107,7 @@ export default class ZnIconPicker extends ZincElement implements ZincFormControl
|
|
|
81
107
|
}
|
|
82
108
|
|
|
83
109
|
get validationMessage(): string {
|
|
84
|
-
return this.required && !this.icon ? 'Please select an icon.' : '';
|
|
110
|
+
return this.required && !this.icon && !this._file ? 'Please select an icon.' : '';
|
|
85
111
|
}
|
|
86
112
|
|
|
87
113
|
checkValidity(): boolean { return this.validity.valid; }
|
|
@@ -129,10 +155,11 @@ export default class ZnIconPicker extends ZincElement implements ZincFormControl
|
|
|
129
155
|
}
|
|
130
156
|
|
|
131
157
|
private async openDialog() {
|
|
132
|
-
this._pendingIcon = this.icon;
|
|
158
|
+
this._pendingIcon = this.isImageValue ? '' : this.icon;
|
|
133
159
|
this._pendingLibrary = this.library;
|
|
134
160
|
this._pendingColor = this.color;
|
|
135
161
|
this._searchQuery = '';
|
|
162
|
+
this._mode = this.allowUpload && this.isImageValue ? 'upload' : 'icon';
|
|
136
163
|
this._iconList = await this.getIconsForLibrary(this.library);
|
|
137
164
|
this._filteredIcons = this._iconList.slice(0, 200);
|
|
138
165
|
this._dialogOpen = true;
|
|
@@ -142,22 +169,74 @@ export default class ZnIconPicker extends ZincElement implements ZincFormControl
|
|
|
142
169
|
}
|
|
143
170
|
|
|
144
171
|
private closeDialog() {
|
|
172
|
+
this.discardPendingFile();
|
|
145
173
|
this._dialog.hide();
|
|
146
174
|
this._dialogOpen = false;
|
|
147
175
|
}
|
|
148
176
|
|
|
149
177
|
private handleConfirm() {
|
|
150
|
-
this.
|
|
151
|
-
|
|
152
|
-
|
|
178
|
+
if (this._mode === 'upload') {
|
|
179
|
+
if (this._pendingFile) {
|
|
180
|
+
if (this._fileUrl) {
|
|
181
|
+
URL.revokeObjectURL(this._fileUrl);
|
|
182
|
+
}
|
|
183
|
+
this._file = this._pendingFile;
|
|
184
|
+
this._fileUrl = this._pendingFileUrl;
|
|
185
|
+
this._pendingFile = null;
|
|
186
|
+
this._pendingFileUrl = null;
|
|
187
|
+
this.icon = '';
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
this.icon = this._pendingIcon;
|
|
191
|
+
this.library = this._pendingLibrary;
|
|
192
|
+
this.color = this._pendingColor;
|
|
193
|
+
this.clearFile();
|
|
194
|
+
}
|
|
153
195
|
this.closeDialog();
|
|
154
196
|
this.emit('zn-change');
|
|
197
|
+
if (this.triggerSubmit) {
|
|
198
|
+
this.formControlController.submit();
|
|
199
|
+
}
|
|
155
200
|
}
|
|
156
201
|
|
|
157
202
|
private handleCancel() {
|
|
158
203
|
this.closeDialog();
|
|
159
204
|
}
|
|
160
205
|
|
|
206
|
+
private handleFileSelect(e: Event) {
|
|
207
|
+
const input = e.target as HTMLInputElement;
|
|
208
|
+
const file = input.files?.[0] ?? null;
|
|
209
|
+
if (!file) return;
|
|
210
|
+
this.discardPendingFile();
|
|
211
|
+
this._pendingFile = file;
|
|
212
|
+
this._pendingFileUrl = URL.createObjectURL(file);
|
|
213
|
+
input.value = '';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private discardPendingFile() {
|
|
217
|
+
if (this._pendingFileUrl) {
|
|
218
|
+
URL.revokeObjectURL(this._pendingFileUrl);
|
|
219
|
+
}
|
|
220
|
+
this._pendingFile = null;
|
|
221
|
+
this._pendingFileUrl = null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private clearFile() {
|
|
225
|
+
if (this._fileUrl) {
|
|
226
|
+
URL.revokeObjectURL(this._fileUrl);
|
|
227
|
+
}
|
|
228
|
+
this._file = null;
|
|
229
|
+
this._fileUrl = null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
disconnectedCallback() {
|
|
233
|
+
super.disconnectedCallback();
|
|
234
|
+
this.discardPendingFile();
|
|
235
|
+
if (this._fileUrl) {
|
|
236
|
+
URL.revokeObjectURL(this._fileUrl);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
161
240
|
private handleSearchInput(e: Event) {
|
|
162
241
|
const input = e.target as HTMLInputElement;
|
|
163
242
|
this._searchQuery = input.value.toLowerCase();
|
|
@@ -209,7 +288,11 @@ export default class ZnIconPicker extends ZincElement implements ZincFormControl
|
|
|
209
288
|
e.stopPropagation();
|
|
210
289
|
this.icon = '';
|
|
211
290
|
this.color = '';
|
|
291
|
+
this.clearFile();
|
|
212
292
|
this.emit('zn-change');
|
|
293
|
+
if (this.triggerSubmit) {
|
|
294
|
+
this.formControlController.submit();
|
|
295
|
+
}
|
|
213
296
|
}
|
|
214
297
|
|
|
215
298
|
private _handleTriggerClick() {
|
|
@@ -220,7 +303,12 @@ export default class ZnIconPicker extends ZincElement implements ZincFormControl
|
|
|
220
303
|
render() {
|
|
221
304
|
const hasLabel = !!this.label;
|
|
222
305
|
const hasHelpText = !!this.helpText;
|
|
223
|
-
const
|
|
306
|
+
const hasValue = !!this.icon || !!this._file;
|
|
307
|
+
// Image values (uploaded file or URL) omit the library so zn-icon's URL
|
|
308
|
+
// auto-detection renders an <img> instead of a font ligature.
|
|
309
|
+
const triggerIcon = this._fileUrl ?? this.icon;
|
|
310
|
+
const triggerLibrary = hasValue && !this.isImageValue ? this.library : nothing;
|
|
311
|
+
const pendingPreviewUrl = this._pendingFileUrl ?? (this.isImageValue && !this._file ? this.icon : this._fileUrl);
|
|
224
312
|
|
|
225
313
|
return html`
|
|
226
314
|
<div part="form-control"
|
|
@@ -236,15 +324,15 @@ export default class ZnIconPicker extends ZincElement implements ZincFormControl
|
|
|
236
324
|
part="trigger"
|
|
237
325
|
class="icon-picker__trigger"
|
|
238
326
|
panel-bg
|
|
239
|
-
icon=${
|
|
240
|
-
icon-library=${
|
|
241
|
-
icon-color=${(
|
|
327
|
+
icon=${hasValue ? triggerIcon : nothing}
|
|
328
|
+
icon-library=${triggerLibrary}
|
|
329
|
+
icon-color=${(hasValue && !this.isImageValue && this.color) || nothing}
|
|
242
330
|
icon-size="24"
|
|
243
331
|
?disabled=${this.disabled}
|
|
244
332
|
@click=${this._handleTriggerClick}>
|
|
245
|
-
${
|
|
333
|
+
${hasValue ? 'Click to edit' : 'Set an icon'}
|
|
246
334
|
</zn-button>
|
|
247
|
-
${
|
|
335
|
+
${hasValue ? html`
|
|
248
336
|
<zn-button
|
|
249
337
|
class="icon-picker__clear"
|
|
250
338
|
color="transparent"
|
|
@@ -272,6 +360,38 @@ export default class ZnIconPicker extends ZincElement implements ZincFormControl
|
|
|
272
360
|
<zn-dialog size="custom" label="Select Icon" @zn-close=${this.handleCancel}>
|
|
273
361
|
<div class="icon-picker__dialog-layout">
|
|
274
362
|
<div class="icon-picker__dialog-main">
|
|
363
|
+
${this.allowUpload ? html`
|
|
364
|
+
<div class="icon-picker__mode-switch">
|
|
365
|
+
<zn-button
|
|
366
|
+
size="small"
|
|
367
|
+
color=${this._mode === 'icon' ? 'secondary' : 'transparent'}
|
|
368
|
+
icon="apps"
|
|
369
|
+
@click=${() => this._mode = 'icon'}>
|
|
370
|
+
Icon Library
|
|
371
|
+
</zn-button>
|
|
372
|
+
<zn-button
|
|
373
|
+
size="small"
|
|
374
|
+
color=${this._mode === 'upload' ? 'secondary' : 'transparent'}
|
|
375
|
+
icon="upload"
|
|
376
|
+
@click=${() => this._mode = 'upload'}>
|
|
377
|
+
Upload Image
|
|
378
|
+
</zn-button>
|
|
379
|
+
</div>
|
|
380
|
+
` : nothing}
|
|
381
|
+
|
|
382
|
+
${this._mode === 'upload' ? html`
|
|
383
|
+
<div class="icon-picker__upload">
|
|
384
|
+
<input class="icon-picker__file-input" type="file" accept=${this.accept} hidden
|
|
385
|
+
@change=${this.handleFileSelect}>
|
|
386
|
+
<zn-icon src="image" size="48"></zn-icon>
|
|
387
|
+
${this._pendingFile ? html`
|
|
388
|
+
<span class="icon-picker__upload-filename">${this._pendingFile.name}</span>
|
|
389
|
+
` : nothing}
|
|
390
|
+
<zn-button icon="upload" color="secondary" @click=${() => this._fileInput.click()}>
|
|
391
|
+
${pendingPreviewUrl ? 'Choose a different file' : 'Choose a file'}
|
|
392
|
+
</zn-button>
|
|
393
|
+
</div>
|
|
394
|
+
` : html`
|
|
275
395
|
<div class="icon-picker__dialog-controls">
|
|
276
396
|
${!this.noLibrary ? html`
|
|
277
397
|
<zn-select
|
|
@@ -354,10 +474,21 @@ export default class ZnIconPicker extends ZincElement implements ZincFormControl
|
|
|
354
474
|
`)}
|
|
355
475
|
</div>
|
|
356
476
|
`}
|
|
477
|
+
`}
|
|
357
478
|
</div>
|
|
358
479
|
|
|
359
480
|
<div class="icon-picker__dialog-sidebar">
|
|
360
|
-
${this.
|
|
481
|
+
${this._mode === 'upload' ? html`
|
|
482
|
+
<div class="icon-picker__preview ${pendingPreviewUrl ? '' : 'icon-picker__preview--empty'}">
|
|
483
|
+
${pendingPreviewUrl ? html`
|
|
484
|
+
<img class="icon-picker__upload-preview" src=${pendingPreviewUrl} alt="">
|
|
485
|
+
<span class="icon-picker__preview-name">${this._pendingFile?.name ?? ''}</span>
|
|
486
|
+
` : html`
|
|
487
|
+
<zn-icon src="image" size="48"></zn-icon>
|
|
488
|
+
<span class="icon-picker__preview-hint">Choose an image</span>
|
|
489
|
+
`}
|
|
490
|
+
</div>
|
|
491
|
+
` : this._pendingIcon ? html`
|
|
361
492
|
<div class="icon-picker__preview">
|
|
362
493
|
<zn-icon
|
|
363
494
|
src=${this._pendingIcon}
|
|
@@ -379,7 +510,10 @@ export default class ZnIconPicker extends ZincElement implements ZincFormControl
|
|
|
379
510
|
|
|
380
511
|
<div slot="footer">
|
|
381
512
|
<zn-button color="default" @click=${this.handleCancel}>Cancel</zn-button>
|
|
382
|
-
<zn-button color="primary" @click=${this.handleConfirm}
|
|
513
|
+
<zn-button color="primary" @click=${this.handleConfirm}
|
|
514
|
+
?disabled=${this._mode === 'upload' ? !this._pendingFile && !this.isImageValue : !this._pendingIcon}>
|
|
515
|
+
Confirm
|
|
516
|
+
</zn-button>
|
|
383
517
|
</div>
|
|
384
518
|
</zn-dialog>
|
|
385
519
|
` : nothing}
|
|
@@ -60,6 +60,38 @@ zn-dialog {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
.icon-picker__mode-switch {
|
|
64
|
+
display: flex;
|
|
65
|
+
gap: var(--zn-spacing-x-small);
|
|
66
|
+
margin-bottom: var(--zn-spacing-small);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.icon-picker__upload {
|
|
70
|
+
flex: 1;
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: center;
|
|
75
|
+
gap: var(--zn-spacing-small);
|
|
76
|
+
border: dashed 1px rgb(var(--zn-border-color));
|
|
77
|
+
border-radius: var(--zn-border-radius);
|
|
78
|
+
padding: var(--zn-spacing-large);
|
|
79
|
+
color: rgb(var(--zn-text-muted));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.icon-picker__upload-preview {
|
|
83
|
+
max-width: 100%;
|
|
84
|
+
max-height: 160px;
|
|
85
|
+
object-fit: contain;
|
|
86
|
+
border-radius: var(--zn-border-radius);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.icon-picker__upload-filename {
|
|
90
|
+
font-size: var(--zn-font-size-small);
|
|
91
|
+
color: rgb(var(--zn-text-muted));
|
|
92
|
+
word-break: break-all;
|
|
93
|
+
}
|
|
94
|
+
|
|
63
95
|
.icon-picker__dialog-controls {
|
|
64
96
|
display: flex;
|
|
65
97
|
gap: var(--zn-spacing-small);
|
|
@@ -33,7 +33,7 @@ import styles from './input.scss';
|
|
|
33
33
|
*
|
|
34
34
|
* @slot label - The input's label. Alternatively, you can use the `label` attribute.
|
|
35
35
|
* @slot label-tooltip - Used to add text that is displayed in a tooltip next to the label. Alternatively, you can use the `label-tooltip` attribute.
|
|
36
|
-
* @slot context-note - Used to add contextual text that is displayed above the input, on the right. Alternatively, you can use the `context-note` attribute.
|
|
36
|
+
* @slot context-note - Used to add contextual text that is displayed above the input, on the right. Alternatively, you can use the `context-note` attribute. Range inputs display their live value here unless this is set.
|
|
37
37
|
* @slot prefix - Used to prepend a presentational icon or similar element to the input.
|
|
38
38
|
* @slot suffix - Used to append a presentational icon or similar element to the input.
|
|
39
39
|
* @slot clear-icon - An icon to use in lieu of the default clear icon.
|
|
@@ -51,6 +51,12 @@ import styles from './input.scss';
|
|
|
51
51
|
* @csspart clear-button - The clear button.
|
|
52
52
|
* @csspart password-toggle-button - The password toggle button.
|
|
53
53
|
* @csspart suffix - The container that wraps the suffix.
|
|
54
|
+
*
|
|
55
|
+
* @cssproperty --zn-range-track-height - The height of a range input's track.
|
|
56
|
+
* @cssproperty --zn-range-track-color - The color of a range input's track.
|
|
57
|
+
* @cssproperty --zn-range-thumb-size - The diameter of a range input's thumb.
|
|
58
|
+
* @cssproperty --zn-range-thumb-color - The color of a range input's thumb.
|
|
59
|
+
* @cssproperty --zn-range-thumb-ring-width - The width of the ring drawn around a range input's thumb.
|
|
54
60
|
*/
|
|
55
61
|
export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
56
62
|
static styles = unsafeCSS(styles);
|
|
@@ -80,7 +86,7 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
80
86
|
* to `text`
|
|
81
87
|
*/
|
|
82
88
|
@property({reflect: true}) type: 'color' | 'currency' | 'date' | 'datetime-local' | 'email' | 'number' | 'password' |
|
|
83
|
-
'search' | 'tel' | 'text' | 'time' | 'url' = 'text';
|
|
89
|
+
'range' | 'search' | 'tel' | 'text' | 'time' | 'url' = 'text';
|
|
84
90
|
|
|
85
91
|
/** The name of the input, submitted as a name/value pair with form data. */
|
|
86
92
|
@property() name: string = "";
|
|
@@ -146,6 +152,12 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
146
152
|
/** The color format to display for color inputs. Only applies when type is 'color'. **/
|
|
147
153
|
@property({attribute: 'color-format'}) colorFormat: 'hex' | 'rgb' | 'hsl' | 'oklch' = 'hex';
|
|
148
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Appended to the value that range inputs display above the track, on the right, e.g. `rem`. Only applies when type
|
|
157
|
+
* is 'range' and no `context-note` is set.
|
|
158
|
+
*/
|
|
159
|
+
@property({attribute: 'value-suffix'}) valueSuffix: string = '';
|
|
160
|
+
|
|
149
161
|
/**
|
|
150
162
|
* By default, form-controls are associated with the nearest containing `<form>` element. This attribute allows you
|
|
151
163
|
* to place the form control outside a form and associate it with the form that has this `id`. The form must be
|
|
@@ -615,6 +627,13 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
615
627
|
}
|
|
616
628
|
}
|
|
617
629
|
|
|
630
|
+
protected firstUpdated() {
|
|
631
|
+
// A range input with no value resolves to the midpoint of min/max natively, so adopt it as our value
|
|
632
|
+
if (this.type === 'range' && this.value === '') {
|
|
633
|
+
this.value = this.input.value;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
618
637
|
@watch('disabled', {waitUntilFirstUpdate: true})
|
|
619
638
|
handleDisabledChange() {
|
|
620
639
|
// Disabled form controls are always valid
|
|
@@ -763,9 +782,10 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
763
782
|
const hasLabelTooltipSlot = this.hasSlotController.test('label-tooltip');
|
|
764
783
|
const hasContextNoteSlot = this.hasSlotController.test('context-note');
|
|
765
784
|
const hasHelpTextSlot = this.hasSlotController.test('help-text');
|
|
785
|
+
const isRange = this.type === 'range';
|
|
766
786
|
const hasLabel = this.label ? true : hasLabelSlot;
|
|
767
787
|
const hasLabelTooltip = this.labelTooltip ? true : hasLabelTooltipSlot;
|
|
768
|
-
const hasContextNote = this.contextNote ? true : hasContextNoteSlot;
|
|
788
|
+
const hasContextNote = this.contextNote || isRange ? true : hasContextNoteSlot;
|
|
769
789
|
const hasHelpText = this.helpText ? true : hasHelpTextSlot;
|
|
770
790
|
const hasClearIcon = this.clearable && !this.disabled && !this.readonly;
|
|
771
791
|
const hasOptionalIcon = this.optionalIcon;
|
|
@@ -803,7 +823,8 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
803
823
|
|
|
804
824
|
${hasContextNote
|
|
805
825
|
? html`
|
|
806
|
-
<span class="form-control__label-context-note"><slot name="context-note">${this.contextNote
|
|
826
|
+
<span class="form-control__label-context-note"><slot name="context-note">${this.contextNote ||
|
|
827
|
+
(isRange ? `${this.value ?? ''}${this.valueSuffix}` : '')}</slot></span>`
|
|
807
828
|
: ''}
|
|
808
829
|
|
|
809
830
|
<div part="form-control-input"
|
|
@@ -822,27 +843,12 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
822
843
|
'input--filled': this.filled,
|
|
823
844
|
'input--focused': this.hasFocus,
|
|
824
845
|
'input--empty': !this.value,
|
|
846
|
+
'input--range': isRange,
|
|
825
847
|
'input--no-spin-buttons': this.noSpinButtons && this.type !== 'currency',
|
|
826
848
|
})}>
|
|
827
849
|
|
|
828
850
|
<span part="prefix" class="input__prefix">
|
|
829
|
-
${this.type === '
|
|
830
|
-
? html`
|
|
831
|
-
<div
|
|
832
|
-
class="input__color-swatch"
|
|
833
|
-
@click=${this.handleColorSwatchClick}
|
|
834
|
-
style="--color-value: ${this.convertToHex(this.value) || '#000000'}">
|
|
835
|
-
<div class="input__color-swatch-inner"></div>
|
|
836
|
-
</div>
|
|
837
|
-
<input
|
|
838
|
-
class="input__color-picker"
|
|
839
|
-
type="color"
|
|
840
|
-
.value=${this.convertToHex(this.value) || '#000000'}
|
|
841
|
-
@change=${this.handleColorPickerChange}
|
|
842
|
-
@input=${this.handleColorPickerChange}
|
|
843
|
-
tabindex="-1"
|
|
844
|
-
/>`
|
|
845
|
-
: this.type === 'currency'
|
|
851
|
+
${this.type === 'currency'
|
|
846
852
|
? html`
|
|
847
853
|
<zn-icon class="input__prefix-default" src="payments"></zn-icon>`
|
|
848
854
|
: this.type === 'email' && hasOptionalIcon
|
|
@@ -933,6 +939,23 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
933
939
|
|
|
934
940
|
<span part="suffix" class="input__suffix">
|
|
935
941
|
<slot name="suffix"></slot>
|
|
942
|
+
${this.type === 'color'
|
|
943
|
+
? html`
|
|
944
|
+
<div
|
|
945
|
+
class="input__color-swatch"
|
|
946
|
+
@click=${this.handleColorSwatchClick}
|
|
947
|
+
style="--color-value: ${this.convertToHex(this.value) || '#000000'}">
|
|
948
|
+
<div class="input__color-swatch-inner"></div>
|
|
949
|
+
</div>
|
|
950
|
+
<input
|
|
951
|
+
class="input__color-picker"
|
|
952
|
+
type="color"
|
|
953
|
+
.value=${this.convertToHex(this.value) || '#000000'}
|
|
954
|
+
@change=${this.handleColorPickerChange}
|
|
955
|
+
@input=${this.handleColorPickerChange}
|
|
956
|
+
tabindex="-1"
|
|
957
|
+
/>`
|
|
958
|
+
: ''}
|
|
936
959
|
</span>
|
|
937
960
|
</div>
|
|
938
961
|
</div>
|
|
@@ -384,51 +384,163 @@ input[type='date']::-webkit-calendar-picker-indicator {
|
|
|
384
384
|
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" height="14" width="12.25" viewBox="0 0 448 512"><path fill="%236d7176" d="M128 0c13.3 0 24 10.7 24 24V64H296V24c0-13.3 10.7-24 24-24s24 10.7 24 24V64h40c35.3 0 64 28.7 64 64v16 48V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V192 144 128C0 92.7 28.7 64 64 64h40V24c0-13.3 10.7-24 24-24zM400 192H48V448c0 8.8 7.2 16 16 16H384c8.8 0 16-7.2 16-16V192zM112 256h96c8.8 0 16 7.2 16 16v96c0 8.8-7.2 16-16 16H112c-8.8 0-16-7.2-16-16V272c0-8.8 7.2-16 16-16z"/></svg>');
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
-
/* Color input styling */
|
|
387
|
+
/* Color input styling — the preview fills the trailing edge of the field */
|
|
388
388
|
.input__color-swatch {
|
|
389
389
|
display: inline-flex;
|
|
390
|
-
align-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
border-radius: var(--zn-border-radius);
|
|
395
|
-
border: solid 1px rgba(0, 0, 0, 0.15);
|
|
390
|
+
align-self: stretch;
|
|
391
|
+
border: none;
|
|
392
|
+
border-inline-start: solid var(--zn-input-border-width) var(--zn-input-border-color);
|
|
393
|
+
border-radius: 0;
|
|
396
394
|
background-color: var(--zn-color-neutral-0);
|
|
397
395
|
cursor: pointer;
|
|
398
|
-
transition: var(--zn-transition-fast) border-color, var(--zn-transition-fast) box-shadow;
|
|
399
396
|
overflow: hidden;
|
|
400
397
|
position: relative;
|
|
401
398
|
flex-shrink: 0;
|
|
402
|
-
margin
|
|
399
|
+
margin: 0;
|
|
403
400
|
|
|
404
|
-
&:hover {
|
|
405
|
-
|
|
406
|
-
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05);
|
|
401
|
+
&:hover .input__color-swatch-inner {
|
|
402
|
+
filter: brightness(0.95);
|
|
407
403
|
}
|
|
408
404
|
}
|
|
409
405
|
|
|
410
|
-
|
|
406
|
+
.input__color-swatch-inner {
|
|
407
|
+
width: 100%;
|
|
408
|
+
height: 100%;
|
|
409
|
+
background-color: var(--color-value, #000000);
|
|
410
|
+
transition: var(--zn-transition-fast) filter;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* Size adjustments for color inputs — .input clips the trailing corners */
|
|
411
414
|
.input--x-small .input__color-swatch {
|
|
412
|
-
|
|
415
|
+
width: calc(var(--zn-input-height-x-small) * 1.2);
|
|
413
416
|
}
|
|
414
417
|
|
|
415
418
|
.input--small .input__color-swatch {
|
|
416
|
-
|
|
419
|
+
width: calc(var(--zn-input-height-small) * 1.2);
|
|
417
420
|
}
|
|
418
421
|
|
|
419
422
|
.input--medium .input__color-swatch {
|
|
420
|
-
|
|
423
|
+
width: calc(var(--zn-input-height-medium) * 1.2);
|
|
421
424
|
}
|
|
422
425
|
|
|
423
426
|
.input--large .input__color-swatch {
|
|
424
|
-
|
|
427
|
+
width: calc(var(--zn-input-height-large) * 1.2);
|
|
425
428
|
}
|
|
426
429
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
430
|
+
/* Range inputs render as a bare track, with the value shown in the context note above */
|
|
431
|
+
/* The thumb already leaves clear space above the track, so drop the label/field gap */
|
|
432
|
+
:host([type='range']) .form-control--has-label .form-control__label,
|
|
433
|
+
:host([type='range']) .form-control__label-context-note {
|
|
434
|
+
margin-bottom: 0;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
:host([type='range']) .form-control-input {
|
|
438
|
+
margin-top: 0;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.input--standard.input--range {
|
|
442
|
+
--zn-range-track-height: 6px;
|
|
443
|
+
--zn-range-track-color: var(--zn-color-neutral-200);
|
|
444
|
+
--zn-range-thumb-size: 18px;
|
|
445
|
+
--zn-range-thumb-color: var(--zn-color-primary-600);
|
|
446
|
+
--zn-range-thumb-ring-width: 4px;
|
|
447
|
+
|
|
448
|
+
align-items: center;
|
|
449
|
+
height: var(--zn-range-thumb-size);
|
|
450
|
+
padding: 0;
|
|
451
|
+
overflow: visible;
|
|
452
|
+
cursor: pointer;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/* Matches the specificity of the hover/focus/disabled chrome rules above so the track stays bare */
|
|
456
|
+
.input--standard.input--range,
|
|
457
|
+
.input--standard.input--range:hover,
|
|
458
|
+
.input--standard.input--range.input--focused,
|
|
459
|
+
.input--standard.input--range.input--disabled {
|
|
460
|
+
border: none;
|
|
461
|
+
background: none;
|
|
462
|
+
box-shadow: none;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.input--standard.input--range.input--x-small {
|
|
466
|
+
--zn-range-track-height: 4px;
|
|
467
|
+
--zn-range-thumb-size: 14px;
|
|
468
|
+
--zn-range-thumb-ring-width: 3px;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.input--standard.input--range.input--small {
|
|
472
|
+
--zn-range-track-height: 5px;
|
|
473
|
+
--zn-range-thumb-size: 16px;
|
|
474
|
+
--zn-range-thumb-ring-width: 3px;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.input--standard.input--range.input--large {
|
|
478
|
+
--zn-range-track-height: 7px;
|
|
479
|
+
--zn-range-thumb-size: 20px;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.input--range .input__control {
|
|
483
|
+
-webkit-appearance: none;
|
|
484
|
+
appearance: none;
|
|
485
|
+
height: var(--zn-range-thumb-size);
|
|
486
|
+
padding: 0;
|
|
487
|
+
margin: 0;
|
|
488
|
+
background: none;
|
|
489
|
+
cursor: inherit;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.input--range .input__control::-webkit-slider-runnable-track {
|
|
493
|
+
height: var(--zn-range-track-height);
|
|
494
|
+
border-radius: calc(var(--zn-range-track-height) / 2);
|
|
495
|
+
background-color: var(--zn-range-track-color);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.input--range .input__control::-moz-range-track {
|
|
499
|
+
height: var(--zn-range-track-height);
|
|
500
|
+
border-radius: calc(var(--zn-range-track-height) / 2);
|
|
501
|
+
background-color: var(--zn-range-track-color);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.input--range .input__control::-webkit-slider-thumb {
|
|
505
|
+
-webkit-appearance: none;
|
|
506
|
+
appearance: none;
|
|
507
|
+
width: var(--zn-range-thumb-size);
|
|
508
|
+
height: var(--zn-range-thumb-size);
|
|
509
|
+
margin-top: calc((var(--zn-range-track-height) - var(--zn-range-thumb-size)) / 2);
|
|
510
|
+
border: var(--zn-range-thumb-ring-width) solid var(--zn-color-neutral-0);
|
|
511
|
+
border-radius: 50%;
|
|
512
|
+
background-color: var(--zn-range-thumb-color);
|
|
513
|
+
box-shadow: 0 0 0 1px var(--zn-input-border-color);
|
|
514
|
+
transition: var(--zn-transition-fast) box-shadow, var(--zn-transition-fast) background-color;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.input--range .input__control::-moz-range-thumb {
|
|
518
|
+
width: var(--zn-range-thumb-size);
|
|
519
|
+
height: var(--zn-range-thumb-size);
|
|
520
|
+
box-sizing: border-box;
|
|
521
|
+
border: var(--zn-range-thumb-ring-width) solid var(--zn-color-neutral-0);
|
|
522
|
+
border-radius: 50%;
|
|
523
|
+
background-color: var(--zn-range-thumb-color);
|
|
524
|
+
box-shadow: 0 0 0 1px var(--zn-input-border-color);
|
|
525
|
+
transition: var(--zn-transition-fast) box-shadow, var(--zn-transition-fast) background-color;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.input--range:hover:not(.input--disabled) .input__control::-webkit-slider-thumb {
|
|
529
|
+
background-color: var(--zn-color-primary-500);
|
|
530
|
+
box-shadow: 0 0 0 1px var(--zn-input-border-color-hover);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.input--range:hover:not(.input--disabled) .input__control::-moz-range-thumb {
|
|
534
|
+
background-color: var(--zn-color-primary-500);
|
|
535
|
+
box-shadow: 0 0 0 1px var(--zn-input-border-color-hover);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.input--range .input__control:focus-visible::-webkit-slider-thumb {
|
|
539
|
+
box-shadow: 0 0 0 var(--zn-focus-ring-width) var(--zn-input-focus-ring-color);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.input--range .input__control:focus-visible::-moz-range-thumb {
|
|
543
|
+
box-shadow: 0 0 0 var(--zn-focus-ring-width) var(--zn-input-focus-ring-color);
|
|
432
544
|
}
|
|
433
545
|
|
|
434
546
|
/* Hide the native color input but keep it functional */
|
|
@@ -444,23 +556,3 @@ input[type='date']::-webkit-calendar-picker-indicator {
|
|
|
444
556
|
visibility: hidden;
|
|
445
557
|
}
|
|
446
558
|
|
|
447
|
-
/* Size adjustments for color inputs */
|
|
448
|
-
.input--x-small .input__color-swatch {
|
|
449
|
-
width: 1.25rem;
|
|
450
|
-
height: 1.25rem;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
.input--small .input__color-swatch {
|
|
454
|
-
width: 1.5rem;
|
|
455
|
-
height: 1.5rem;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
.input--medium .input__color-swatch {
|
|
459
|
-
width: 1.75rem;
|
|
460
|
-
height: 1.75rem;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
.input--large .input__color-swatch {
|
|
464
|
-
width: 2rem;
|
|
465
|
-
height: 2rem;
|
|
466
|
-
}
|