@kubex/zinc 1.1.69 → 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.
@@ -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 left 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.
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,9 @@ 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
- Controls can be organized into tabs with collapsible groups inside each
33
- `sections` with `groups` below is the canonical arrangement:
32
+ Controls organize themselves into tabs with collapsible groups inside each:
33
+ give each one a `group` and a `category` and the editor builds the structure
34
+ around them, no slot names required.
34
35
 
35
36
  ```html:preview
36
37
  <zn-theme-editor
@@ -38,19 +39,10 @@ Controls can be organized into tabs with collapsible groups inside each —
38
39
  src="/components/preview-frame-demo/"
39
40
  min-height="420"
40
41
  controls-caption="Theme Builder"
41
- preview-caption="Live Preview"
42
- sections='[
43
- {"name":"colors","caption":"Colors","groups":[
44
- {"name":"brand","caption":"Brand","open":true},
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>
42
+ preview-caption="Live Preview">
43
+ <zn-input group="Colors" category="Brand" name="accent" label="Accent" value="#6936f5" dark-value="#f5c542" type="color"></zn-input>
44
+ <zn-input group="Colors" category="Background" name="background" label="Background" value="#ffffff" dark-value="#18181b" type="color"></zn-input>
45
+ <zn-input group="Shapes" category="Radius" name="radius" label="Corner radius" type="number" value="4"></zn-input>
54
46
  </zn-theme-editor>
55
47
 
56
48
  <script>
@@ -172,9 +164,44 @@ Set the controls column width with `--zn-theme-editor-controls-width`
172
164
  (default `343px`, matching page-builder's palette). Below 768px the columns
173
165
  stack.
174
166
 
175
- ## Grouping controls into tabs and collapsibles
167
+ ## Grouping controls with `group` and `category`
168
+
169
+ The simplest way to structure an editor is to let the controls describe their
170
+ own place in it. Leave `sections` unset and give each control a `group` (which
171
+ becomes a tab) and a `category` (a collapsible inside that tab) — the editor
172
+ builds the tabs and collapsibles from those labels and slots each control into
173
+ the right one for you, so there are no slot names to keep in sync by hand:
174
+
175
+ ```html
176
+ <zn-theme-editor src="/embed?t=..." frame-origin="https://pay.example">
177
+ <zn-color-select group="Background &amp; Foreground" category="Colors"
178
+ name="background" label="Background" value="#ffffff"></zn-color-select>
179
+ <zn-color-select group="Background &amp; Foreground" category="Colors"
180
+ name="foreground" label="Foreground" value="#18181b"></zn-color-select>
181
+ <zn-input group="Background &amp; Foreground" category="Spacing"
182
+ name="gap" label="Gap" type="number" value="8"></zn-input>
183
+ <zn-input group="Typography" category="Family"
184
+ name="font" label="Font" value="Inter"></zn-input>
185
+ </zn-theme-editor>
186
+ ```
176
187
 
177
- Set `sections` to a JSON array of `{name, caption, groups}`. Each section
188
+ That renders a **Background & Foreground** tab holding *Colors* and *Spacing*
189
+ collapsibles, and a **Typography** tab holding *Family*. Labels are free text —
190
+ they're slugged into slot names internally, and two tabs can each hold a
191
+ category of the same name without colliding.
192
+
193
+ Either attribute works on its own: a control with only `group` sits directly in
194
+ its tab above any collapsibles, and one with only `category` becomes its own
195
+ top-level section. A control with neither stays in the default slot, ungrouped
196
+ above everything else. Tabs and collapsibles appear in the order the controls
197
+ first mention them, and controls added after mount are derived and slotted the
198
+ same way.
199
+
200
+ ### Declaring the structure explicitly with `sections`
201
+
202
+ Set `sections` to take full control instead — it disables the attribute
203
+ derivation entirely, and is the way to set a group's `description` or have it
204
+ render `open`. It takes a JSON array of `{name, caption, groups}`. Each section
178
205
  becomes a `zn-tabs` tab; each entry in its `groups` — `{name, caption,
179
206
  description?, open?}` — becomes a collapsible inside that tab, and a control
180
207
  is assigned to a group with `slot="<group-name>"`:
@@ -200,6 +227,12 @@ config" rule flat sections already followed. Every tab's panel stays mounted
200
227
  while hidden (`zn-tabs` toggles visibility, never removes a panel), so
201
228
  switching tabs never drops a value out of the theme, a preview push or a save.
202
229
 
230
+ The editor never shows a tab with everything shut: on load, and again whenever
231
+ a tab is clicked, its first collapsible expands unless one in that tab is
232
+ already open. `open: true` therefore only matters for picking *which* group
233
+ opens — and closing them all yourself sticks, since nothing reopens until the
234
+ next tab click.
235
+
203
236
  ### Flat sections (no groups)
204
237
 
205
238
  A section can omit `groups` and just take controls directly via
@@ -207,7 +240,8 @@ A section can omit `groups` and just take controls directly via
207
240
  then decides the presentation — stacked `zn-collapsible`s (`"collapsible"`,
208
241
  the default) or a `zn-tabs` strip (`"tabs"`) — and is otherwise ignored: once
209
242
  *any* section has a populated `groups`, every section renders as a nested tab
210
- regardless of `section-layout`.
243
+ regardless of `section-layout`. Stacked sections get the same load-time
244
+ expansion as tabs: the first one opens unless another already has `open`.
211
245
 
212
246
  ```html
213
247
  <zn-theme-editor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.69",
3
+ "version": "1.1.70",
4
4
  "description": "A collection of web components for building web applications based off of @shoelace-style/Shoelace",
5
5
  "keywords": [
6
6
  "web components",
@@ -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
- @property() library: string = 'material';
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.icon = this._pendingIcon;
151
- this.library = this._pendingLibrary;
152
- this.color = this._pendingColor;
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 hasIcon = !!this.icon;
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=${hasIcon ? this.icon : nothing}
240
- icon-library=${hasIcon ? this.library : nothing}
241
- icon-color=${(hasIcon && this.color) || nothing}
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
- ${hasIcon ? 'Click to edit' : 'Set an icon'}
333
+ ${hasValue ? 'Click to edit' : 'Set an icon'}
246
334
  </zn-button>
247
- ${hasIcon ? html`
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._pendingIcon ? html`
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} ?disabled=${!this._pendingIcon}>Confirm</zn-button>
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);