@kubex/zinc 1.1.69 → 1.1.71
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 +307 -12
- package/dist/vscode.html-custom-data.json +13 -4
- package/dist/web-types.json +49 -11
- package/dist/zn.d.ts +75 -7
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +298 -245
- package/docs/pages/components/icon-picker.md +14 -0
- package/docs/pages/components/input.md +19 -1
- package/docs/pages/components/theme-editor.md +60 -18
- package/package.json +1 -1
- package/scss/_global-spacing.scss +7 -0
- package/src/components/button/button.scss +1 -1
- package/src/components/collapsible/collapsible.scss +0 -10
- 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/theme-editor/theme-editor.component.ts +194 -35
- package/src/components/theme-editor/theme-editor.scss +47 -1
- package/src/components/theme-editor/theme-editor.test.ts +168 -0
|
@@ -87,6 +87,20 @@ Use the `avatar` library to generate initials from a name.
|
|
|
87
87
|
<zn-icon-picker name="av" label="Avatar" icon="John Smith" library="avatar" no-color></zn-icon-picker>
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
### Upload Image
|
|
91
|
+
|
|
92
|
+
By default the picker only offers library icons. Add the `allow-upload` attribute to enable a secondary "Upload Image" mode in the dialog, letting users upload an image file instead of picking an icon. Use the `accept` attribute to restrict the accepted file types (defaults to `image/*`).
|
|
93
|
+
|
|
94
|
+
An uploaded file replaces the icon value: on submit, the file is sent under `name` in place of the icon string — the two are mutually exclusive. The chosen file is also available via the element's `file` property.
|
|
95
|
+
|
|
96
|
+
```html:preview
|
|
97
|
+
<zn-icon-picker name="logo" label="Logo" allow-upload></zn-icon-picker>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```html:preview
|
|
101
|
+
<zn-icon-picker name="photo" label="Photo (PNG only)" allow-upload accept="image/png"></zn-icon-picker>
|
|
102
|
+
```
|
|
103
|
+
|
|
90
104
|
### Help Text
|
|
91
105
|
|
|
92
106
|
Add descriptive text below the picker with the `help-text` attribute.
|
|
@@ -136,7 +136,7 @@ the [Properties table](#properties) for the full list of options.
|
|
|
136
136
|
|
|
137
137
|
#### Color Input
|
|
138
138
|
|
|
139
|
-
The color input type displays a clickable color swatch on the
|
|
139
|
+
The color input type displays a clickable color swatch on the right that opens the browser's native color picker. The color value is shown as editable text in the input field, allowing users to either pick a color visually or enter a color value directly.
|
|
140
140
|
|
|
141
141
|
Use the `color-format` attribute to specify the format: `hex` (default), `rgb`, or `oklch`.
|
|
142
142
|
|
|
@@ -159,6 +159,24 @@ Use the `color-format` attribute to specify the format: `hex` (default), `rgb`,
|
|
|
159
159
|
<br />
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
+
#### Range Input
|
|
163
|
+
|
|
164
|
+
The range input type renders as a bare track with no field chrome. The current value is displayed above the track, on
|
|
165
|
+
the right, and updates as the thumb is dragged. Use `value-suffix` to append a unit to that value, and `min`, `max` and
|
|
166
|
+
`step` to control the range.
|
|
167
|
+
|
|
168
|
+
```html:preview
|
|
169
|
+
<zn-input type="range" label="Base radius" min="0" max="2" step="0.125" value="0.5" value-suffix="rem"></zn-input>
|
|
170
|
+
<br />
|
|
171
|
+
<zn-input type="range" label="Volume" min="0" max="100" value="70" value-suffix="%">
|
|
172
|
+
<div slot="help-text">Set <code>context-note</code> to replace the value readout with your own text.</div>
|
|
173
|
+
</zn-input>
|
|
174
|
+
<br />
|
|
175
|
+
<zn-input type="range" label="Small" size="small" min="0" max="10" value="4"></zn-input>
|
|
176
|
+
<br />
|
|
177
|
+
<zn-input type="range" label="Disabled" min="0" max="10" value="4" disabled></zn-input>
|
|
178
|
+
```
|
|
179
|
+
|
|
162
180
|
#### Other Input Types
|
|
163
181
|
|
|
164
182
|
```html:preview
|
|
@@ -29,8 +29,17 @@ The preview always fills its column, leaving no dead space beneath it.
|
|
|
29
29
|
it's still forwarded to the [preview frame](/components/preview-frame/), which
|
|
30
30
|
uses it the same way.
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
The preview column alone sets the editor's height — the controls never stretch
|
|
33
|
+
it, however many sections are expanded, they just scroll in place. So the
|
|
34
|
+
editor is as tall as its parent when that parent has a definite height, and
|
|
35
|
+
`min-height` tall otherwise; either way the controls and the preview scroll
|
|
36
|
+
independently while the header, toolbar and footer rows stay put. Stacked
|
|
37
|
+
(under 768px) there's no second column to take the height from, so both flow
|
|
38
|
+
with the page and there's only ever one scrollbar.
|
|
39
|
+
|
|
40
|
+
Controls organize themselves into tabs with collapsible groups inside each:
|
|
41
|
+
give each one a `group` and a `category` and the editor builds the structure
|
|
42
|
+
around them, no slot names required.
|
|
34
43
|
|
|
35
44
|
```html:preview
|
|
36
45
|
<zn-theme-editor
|
|
@@ -38,19 +47,10 @@ Controls can be organized into tabs with collapsible groups inside each —
|
|
|
38
47
|
src="/components/preview-frame-demo/"
|
|
39
48
|
min-height="420"
|
|
40
49
|
controls-caption="Theme Builder"
|
|
41
|
-
preview-caption="Live Preview"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{"name":"background","caption":"Background"}
|
|
46
|
-
]},
|
|
47
|
-
{"name":"shapes","caption":"Shapes","groups":[
|
|
48
|
-
{"name":"radius","caption":"Radius"}
|
|
49
|
-
]}
|
|
50
|
-
]'>
|
|
51
|
-
<zn-input slot="brand" name="accent" label="Accent" value="#6936f5" dark-value="#f5c542" type="color"></zn-input>
|
|
52
|
-
<zn-input slot="background" name="background" label="Background" value="#ffffff" dark-value="#18181b" type="color"></zn-input>
|
|
53
|
-
<zn-input slot="radius" name="radius" label="Corner radius" type="number" value="4"></zn-input>
|
|
50
|
+
preview-caption="Live Preview">
|
|
51
|
+
<zn-input group="Colors" category="Brand" name="accent" label="Accent" value="#6936f5" dark-value="#f5c542" type="color"></zn-input>
|
|
52
|
+
<zn-input group="Colors" category="Background" name="background" label="Background" value="#ffffff" dark-value="#18181b" type="color"></zn-input>
|
|
53
|
+
<zn-input group="Shapes" category="Radius" name="radius" label="Corner radius" type="number" value="4"></zn-input>
|
|
54
54
|
</zn-theme-editor>
|
|
55
55
|
|
|
56
56
|
<script>
|
|
@@ -172,9 +172,44 @@ Set the controls column width with `--zn-theme-editor-controls-width`
|
|
|
172
172
|
(default `343px`, matching page-builder's palette). Below 768px the columns
|
|
173
173
|
stack.
|
|
174
174
|
|
|
175
|
-
## Grouping controls
|
|
175
|
+
## Grouping controls with `group` and `category`
|
|
176
|
+
|
|
177
|
+
The simplest way to structure an editor is to let the controls describe their
|
|
178
|
+
own place in it. Leave `sections` unset and give each control a `group` (which
|
|
179
|
+
becomes a tab) and a `category` (a collapsible inside that tab) — the editor
|
|
180
|
+
builds the tabs and collapsibles from those labels and slots each control into
|
|
181
|
+
the right one for you, so there are no slot names to keep in sync by hand:
|
|
182
|
+
|
|
183
|
+
```html
|
|
184
|
+
<zn-theme-editor src="/embed?t=..." frame-origin="https://pay.example">
|
|
185
|
+
<zn-color-select group="Background & Foreground" category="Colors"
|
|
186
|
+
name="background" label="Background" value="#ffffff"></zn-color-select>
|
|
187
|
+
<zn-color-select group="Background & Foreground" category="Colors"
|
|
188
|
+
name="foreground" label="Foreground" value="#18181b"></zn-color-select>
|
|
189
|
+
<zn-input group="Background & Foreground" category="Spacing"
|
|
190
|
+
name="gap" label="Gap" type="number" value="8"></zn-input>
|
|
191
|
+
<zn-input group="Typography" category="Family"
|
|
192
|
+
name="font" label="Font" value="Inter"></zn-input>
|
|
193
|
+
</zn-theme-editor>
|
|
194
|
+
```
|
|
176
195
|
|
|
177
|
-
|
|
196
|
+
That renders a **Background & Foreground** tab holding *Colors* and *Spacing*
|
|
197
|
+
collapsibles, and a **Typography** tab holding *Family*. Labels are free text —
|
|
198
|
+
they're slugged into slot names internally, and two tabs can each hold a
|
|
199
|
+
category of the same name without colliding.
|
|
200
|
+
|
|
201
|
+
Either attribute works on its own: a control with only `group` sits directly in
|
|
202
|
+
its tab above any collapsibles, and one with only `category` becomes its own
|
|
203
|
+
top-level section. A control with neither stays in the default slot, ungrouped
|
|
204
|
+
above everything else. Tabs and collapsibles appear in the order the controls
|
|
205
|
+
first mention them, and controls added after mount are derived and slotted the
|
|
206
|
+
same way.
|
|
207
|
+
|
|
208
|
+
### Declaring the structure explicitly with `sections`
|
|
209
|
+
|
|
210
|
+
Set `sections` to take full control instead — it disables the attribute
|
|
211
|
+
derivation entirely, and is the way to set a group's `description` or have it
|
|
212
|
+
render `open`. It takes a JSON array of `{name, caption, groups}`. Each section
|
|
178
213
|
becomes a `zn-tabs` tab; each entry in its `groups` — `{name, caption,
|
|
179
214
|
description?, open?}` — becomes a collapsible inside that tab, and a control
|
|
180
215
|
is assigned to a group with `slot="<group-name>"`:
|
|
@@ -200,6 +235,12 @@ config" rule flat sections already followed. Every tab's panel stays mounted
|
|
|
200
235
|
while hidden (`zn-tabs` toggles visibility, never removes a panel), so
|
|
201
236
|
switching tabs never drops a value out of the theme, a preview push or a save.
|
|
202
237
|
|
|
238
|
+
The editor never shows a tab with everything shut: on load, and again whenever
|
|
239
|
+
a tab is clicked, its first collapsible expands unless one in that tab is
|
|
240
|
+
already open. `open: true` therefore only matters for picking *which* group
|
|
241
|
+
opens — and closing them all yourself sticks, since nothing reopens until the
|
|
242
|
+
next tab click.
|
|
243
|
+
|
|
203
244
|
### Flat sections (no groups)
|
|
204
245
|
|
|
205
246
|
A section can omit `groups` and just take controls directly via
|
|
@@ -207,7 +248,8 @@ A section can omit `groups` and just take controls directly via
|
|
|
207
248
|
then decides the presentation — stacked `zn-collapsible`s (`"collapsible"`,
|
|
208
249
|
the default) or a `zn-tabs` strip (`"tabs"`) — and is otherwise ignored: once
|
|
209
250
|
*any* section has a populated `groups`, every section renders as a nested tab
|
|
210
|
-
regardless of `section-layout`.
|
|
251
|
+
regardless of `section-layout`. Stacked sections get the same load-time
|
|
252
|
+
expansion as tabs: the first one opens unless another already has `open`.
|
|
211
253
|
|
|
212
254
|
```html
|
|
213
255
|
<zn-theme-editor
|
package/package.json
CHANGED
|
@@ -69,6 +69,13 @@
|
|
|
69
69
|
margin-top: var(--zn-base-gap);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
.form-actions {
|
|
73
|
+
display: flex;
|
|
74
|
+
justify-content: flex-end;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: var(--zn-spacing-small);
|
|
77
|
+
}
|
|
78
|
+
|
|
72
79
|
zn-form-group ~ zn-form-group {
|
|
73
80
|
margin-top: var(--zn-base-gap);
|
|
74
81
|
padding-top: var(--zn-base-gap);
|
|
@@ -60,13 +60,6 @@
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
// flush means flush on every edge, matching zn-pane/zn-panel/zn-tabs; without
|
|
64
|
-
// this the caption stays indented while the content sits at the edge.
|
|
65
|
-
:host([flush]) .header {
|
|
66
|
-
padding-left: 0;
|
|
67
|
-
padding-right: 0;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
63
|
:host([error-label]) {
|
|
71
64
|
.header {
|
|
72
65
|
.caption {
|
|
@@ -88,9 +81,6 @@
|
|
|
88
81
|
}
|
|
89
82
|
|
|
90
83
|
.content {
|
|
91
|
-
display: flex;
|
|
92
|
-
flex-direction: column;
|
|
93
|
-
gap: var(--zn-spacing-medium);
|
|
94
84
|
transition: .1s all ease-out;
|
|
95
85
|
}
|
|
96
86
|
|
|
@@ -590,6 +590,21 @@ export default class ZnFile extends ZincElement implements ZincFormControl {
|
|
|
590
590
|
this.emit('zn-clear');
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
+
/**
|
|
594
|
+
* Programmatically clears the selected file and any external `src` without
|
|
595
|
+
* confirmation. Unlike a user-initiated clear, this does not flag the control
|
|
596
|
+
* to submit an empty value.
|
|
597
|
+
*/
|
|
598
|
+
clear() {
|
|
599
|
+
if (this.input) {
|
|
600
|
+
this.input.value = '';
|
|
601
|
+
}
|
|
602
|
+
this.files = null;
|
|
603
|
+
this.src = '';
|
|
604
|
+
this.updatePreview();
|
|
605
|
+
this.formControlController.updateValidity();
|
|
606
|
+
}
|
|
607
|
+
|
|
593
608
|
private updatePreview() {
|
|
594
609
|
if (this.fileObjectUrl) {
|
|
595
610
|
URL.revokeObjectURL(this.fileObjectUrl);
|
|
@@ -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);
|