@kubex/zinc 1.0.103 → 1.0.104

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.
@@ -0,0 +1,301 @@
1
+ ---
2
+ meta:
3
+ title: Markdown Editor
4
+ description: A markdown editor with live preview, split view, and fullscreen mode.
5
+ layout: component
6
+ ---
7
+
8
+ `zn-markdown-editor` wraps the full markdown authoring workflow in a single
9
+ form control: a textarea for writing, a live preview rendered with
10
+ [`marked`](https://marked.js.org/) (bundled with the component), toggleable
11
+ editor / split / preview modes, a fullscreen toggle, and `localStorage`
12
+ persistence for the selected view.
13
+
14
+ ```html:preview
15
+ <zn-markdown-editor
16
+ label="Content"
17
+ name="content"
18
+ help-text="Supports GitHub-flavored markdown"
19
+ value="# Hello
20
+
21
+ Write some **markdown** on the left — use the toolbar to switch to split or preview."
22
+ ></zn-markdown-editor>
23
+ ```
24
+
25
+ ## Examples
26
+
27
+ ### Initial Value via Attribute
28
+
29
+ Set the initial markdown with the `value` attribute.
30
+
31
+ ```html:preview
32
+ <zn-markdown-editor
33
+ label="Article"
34
+ name="article"
35
+ value="## Release notes
36
+
37
+ - New payments dashboard
38
+ - Faster settlement reporting"
39
+ ></zn-markdown-editor>
40
+ ```
41
+
42
+ ### Initial Value via Light DOM
43
+
44
+ If no `value` attribute is set, `zn-markdown-editor` reads its initial markdown
45
+ from its own text-node children. This is handy when rendering server-side
46
+ content into the tag without escaping quotes:
47
+
48
+ ```html:preview
49
+ <zn-markdown-editor name="content" label="Terms">
50
+ # Terms of Service
51
+
52
+ This copy was rendered from a server-side template and picked up automatically.
53
+ </zn-markdown-editor>
54
+ ```
55
+
56
+ ### Setting the Default View
57
+
58
+ Use the `view-mode` attribute to open directly in `editor` (default), `split`,
59
+ or `preview`.
60
+
61
+ ```html:preview
62
+ <zn-markdown-editor
63
+ name="content"
64
+ label="Split by default"
65
+ view-mode="split"
66
+ value="## Split view
67
+
68
+ See both at once."
69
+ ></zn-markdown-editor>
70
+
71
+ <br />
72
+
73
+ <zn-markdown-editor
74
+ name="content"
75
+ label="Preview by default"
76
+ view-mode="preview"
77
+ value="## Preview view
78
+
79
+ Toggle to the editor to edit."
80
+ ></zn-markdown-editor>
81
+ ```
82
+
83
+ ### Persisting the Selected View
84
+
85
+ The chosen view mode is persisted in `localStorage` under the key set by
86
+ `storage-key` (default `zn-markdown-editor-view-mode`). Use distinct keys
87
+ when multiple editors should remember their own preference.
88
+
89
+ ```html:preview
90
+ <zn-markdown-editor
91
+ name="email_template"
92
+ label="Email template"
93
+ storage-key="email-template-view"
94
+ ></zn-markdown-editor>
95
+ ```
96
+
97
+ To opt out of persistence, set `storage-key` to an empty string:
98
+
99
+ ```html
100
+
101
+ <zn-markdown-editor storage-key=""></zn-markdown-editor>
102
+ ```
103
+
104
+ ### Help Text
105
+
106
+ Add descriptive help text with the `help-text` attribute, or use the slot for
107
+ HTML content.
108
+
109
+ ```html:preview
110
+ <zn-markdown-editor
111
+ label="Release notes"
112
+ name="notes"
113
+ help-text="Use headings, lists, and code fences for emphasis."
114
+ ></zn-markdown-editor>
115
+
116
+ <br />
117
+
118
+ <zn-markdown-editor label="Release notes" name="notes">
119
+ <div slot="help-text">
120
+ See the <a href="https://commonmark.org/help/">CommonMark cheatsheet</a>
121
+ for a full syntax reference.
122
+ </div>
123
+ </zn-markdown-editor>
124
+ ```
125
+
126
+ ### Rows
127
+
128
+ Use `rows` to change the initial height of the editor before it overflows.
129
+ Default is `20`.
130
+
131
+ ```html:preview
132
+ <zn-markdown-editor label="Short editor" rows="6"></zn-markdown-editor>
133
+ ```
134
+
135
+ ### Required, Readonly, Disabled
136
+
137
+ `zn-markdown-editor` forwards standard form-control state onto its underlying
138
+ textarea.
139
+
140
+ ```html:preview
141
+ <zn-markdown-editor label="Required" name="content" required></zn-markdown-editor>
142
+
143
+ <br />
144
+
145
+ <zn-markdown-editor
146
+ label="Readonly"
147
+ name="content"
148
+ readonly
149
+ value="## Read-only content
150
+
151
+ Visible but not editable."
152
+ ></zn-markdown-editor>
153
+
154
+ <br />
155
+
156
+ <zn-markdown-editor
157
+ label="Disabled"
158
+ name="content"
159
+ disabled
160
+ value="Disabled editor."
161
+ ></zn-markdown-editor>
162
+ ```
163
+
164
+ ### Form Integration
165
+
166
+ `zn-markdown-editor` is a [form control](/getting-started/form-controls), so
167
+ its value is submitted with the surrounding form.
168
+
169
+ ```html:preview
170
+ <form class="markdown-editor-form">
171
+ <zn-markdown-editor
172
+ name="content"
173
+ label="Document"
174
+ required
175
+ value="## Getting started
176
+
177
+ Write your content here."
178
+ ></zn-markdown-editor>
179
+ <br />
180
+ <zn-button type="submit" color="success">Submit</zn-button>
181
+ <zn-button type="reset" color="secondary">Reset</zn-button>
182
+ </form>
183
+
184
+ <script type="module">
185
+ const form = document.querySelector('.markdown-editor-form');
186
+
187
+ await customElements.whenDefined('zn-button');
188
+ await customElements.whenDefined('zn-markdown-editor');
189
+
190
+ form.addEventListener('submit', (e) => {
191
+ e.preventDefault();
192
+ const data = Object.fromEntries(new FormData(form));
193
+ alert('Submitted!\n\n' + JSON.stringify(data, null, 2));
194
+ });
195
+ </script>
196
+ ```
197
+
198
+ ### Reacting to Events
199
+
200
+ Listen for content and view-mode changes.
201
+
202
+ ```html:preview
203
+ <zn-markdown-editor
204
+ id="event-editor"
205
+ name="content"
206
+ label="Event demo"
207
+ value="Type here..."
208
+ ></zn-markdown-editor>
209
+
210
+ <div id="event-log" style="margin-top: 1rem; padding: 1rem; background: var(--zn-color-neutral-100); border-radius: 4px; font-family: monospace; font-size: 0.875rem; max-height: 200px; overflow-y: auto;">
211
+ Events will appear here...
212
+ </div>
213
+
214
+ <script type="module">
215
+ const editor = document.getElementById('event-editor');
216
+ const log = document.getElementById('event-log');
217
+
218
+ await customElements.whenDefined('zn-markdown-editor');
219
+
220
+ function push(name, detail = '') {
221
+ const t = new Date().toLocaleTimeString();
222
+ log.innerHTML = `[${t}] ${name}${detail ? ': ' + detail : ''}<br>` + log.innerHTML;
223
+ }
224
+
225
+ editor.addEventListener('zn-input', () => push('zn-input', `length=${editor.value.length}`));
226
+ editor.addEventListener('zn-change', () => push('zn-change'));
227
+ editor.addEventListener('zn-view-mode-change', (e) => push('zn-view-mode-change', e.detail.mode));
228
+ </script>
229
+ ```
230
+
231
+ ### Programmatic Control
232
+
233
+ `focus()`, `blur()`, `checkValidity()`, and `reportValidity()` are all
234
+ available on the element.
235
+
236
+ ```html:preview
237
+ <zn-markdown-editor
238
+ id="control-editor"
239
+ name="content"
240
+ label="Controlled editor"
241
+ value="## Controlled"
242
+ ></zn-markdown-editor>
243
+ <br />
244
+ <zn-button id="editor-focus">Focus</zn-button>
245
+ <zn-button id="editor-blur">Blur</zn-button>
246
+ <zn-button id="editor-split">Switch to split</zn-button>
247
+ <zn-button id="editor-preview">Switch to preview</zn-button>
248
+ <zn-button id="editor-clear" color="error">Clear</zn-button>
249
+
250
+ <script type="module">
251
+ const editor = document.getElementById('control-editor');
252
+
253
+ await customElements.whenDefined('zn-button');
254
+ await customElements.whenDefined('zn-markdown-editor');
255
+
256
+ document.getElementById('editor-focus').addEventListener('click', () => editor.focus());
257
+ document.getElementById('editor-blur').addEventListener('click', () => editor.blur());
258
+ document.getElementById('editor-split').addEventListener('click', () => { editor.viewMode = 'split'; });
259
+ document.getElementById('editor-preview').addEventListener('click', () => { editor.viewMode = 'preview'; });
260
+ document.getElementById('editor-clear').addEventListener('click', () => { editor.value = ''; });
261
+ </script>
262
+ ```
263
+
264
+ ### Fullscreen Mode
265
+
266
+ Click the fullscreen icon in the top-right to expand the editor to cover its
267
+ nearest positioned ancestor. Click again to collapse. This is driven by the
268
+ `expanded` state on the element, so you can also toggle it programmatically:
269
+
270
+ ```html
271
+
272
+ <zn-markdown-editor id="fs-editor"></zn-markdown-editor>
273
+
274
+ <script>
275
+ document.getElementById('fs-editor').expanded = true;
276
+ </script>
277
+ ```
278
+
279
+ ## Events
280
+
281
+ | Event | Detail | Description |
282
+ |-----------------------|----------------------------------------------|---------------------------------------------|
283
+ | `zn-input` | – | Fired on every keystroke in the editor. |
284
+ | `zn-change` | – | Fired when the editor's value is committed. |
285
+ | `zn-view-mode-change` | `{ mode: 'editor' \| 'split' \| 'preview' }` | Fired when the user switches view modes. |
286
+
287
+ ## Slots
288
+
289
+ | Slot | Description |
290
+ |-------------|---------------------------------------------------------------------|
291
+ | `label` | Custom HTML label. Alternatively use the `label` attribute. |
292
+ | `help-text` | Custom HTML help text. Alternatively use the `help-text` attribute. |
293
+
294
+ ## CSS Parts
295
+
296
+ | Part | Description |
297
+ |-----------|-------------------------------------|
298
+ | `base` | The component's base wrapper. |
299
+ | `toolbar` | The view-mode / fullscreen toolbar. |
300
+ | `editor` | The textarea wrapper. |
301
+ | `preview` | The rendered markdown preview pane. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.0.103",
3
+ "version": "1.0.104",
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",
@@ -51,6 +51,7 @@
51
51
  "composed-offset-position": "^0.0.6",
52
52
  "emoji-mart": "^5.6.0",
53
53
  "lit": "^2.7.6",
54
+ "marked": "^18.0.2",
54
55
  "quill": "^2.0.3"
55
56
  },
56
57
  "devDependencies": {
@@ -0,0 +1,12 @@
1
+ import ZnMarkdownEditor from './markdown-editor.component';
2
+
3
+ export * from './markdown-editor.component';
4
+ export default ZnMarkdownEditor;
5
+
6
+ ZnMarkdownEditor.define('zn-markdown-editor');
7
+
8
+ declare global {
9
+ interface HTMLElementTagNameMap {
10
+ 'zn-markdown-editor': ZnMarkdownEditor;
11
+ }
12
+ }
@@ -0,0 +1,332 @@
1
+ import {classMap} from "lit/directives/class-map.js";
2
+ import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
3
+ import {defaultValue} from "../../internal/default-value";
4
+ import {FormControlController} from "../../internal/form";
5
+ import {HasSlotController} from "../../internal/slot";
6
+ import {marked} from "marked";
7
+ import {property, query, state} from 'lit/decorators.js';
8
+ import {watch} from "../../internal/watch";
9
+ import ZincElement from '../../internal/zinc-element';
10
+ import type {ZincFormControl} from '../../internal/zinc-element';
11
+ import type ZnTextarea from "../textarea";
12
+
13
+ import styles from './markdown-editor.scss';
14
+
15
+ type ViewMode = 'editor' | 'split' | 'preview';
16
+
17
+ const VIEW_MODES: {mode: ViewMode; icon: string; label: string}[] = [
18
+ {mode: 'editor', icon: 'edit_note', label: 'Editor'},
19
+ {mode: 'split', icon: 'vertical_split', label: 'Split'},
20
+ {mode: 'preview', icon: 'visibility', label: 'Preview'},
21
+ ];
22
+
23
+ /**
24
+ * @summary A markdown editor with live preview, split view, and a fullscreen mode.
25
+ * @documentation https://zinc.style/components/markdown-editor
26
+ * @status experimental
27
+ * @since 1.0
28
+ *
29
+ * @dependency zn-textarea
30
+ * @dependency zn-button-group
31
+ * @dependency zn-icon
32
+ *
33
+ * @event zn-change - Emitted when the markdown content changes.
34
+ * @event zn-input - Emitted on each keystroke in the editor.
35
+ * @event zn-view-mode-change - Emitted when the user switches between editor / split / preview.
36
+ *
37
+ * @slot label - The editor label. Alternatively, use the `label` attribute.
38
+ * @slot help-text - Help text shown below the editor. Alternatively, use the `help-text` attribute.
39
+ *
40
+ * @csspart base - The component's base wrapper.
41
+ * @csspart toolbar - The toolbar containing the view-mode and fullscreen controls.
42
+ * @csspart editor - The textarea wrapper.
43
+ * @csspart preview - The rendered markdown preview.
44
+ */
45
+ export default class ZnMarkdownEditor extends ZincElement implements ZincFormControl {
46
+ static styles: CSSResultGroup = unsafeCSS(styles);
47
+
48
+ private readonly formControlController = new FormControlController(this, {
49
+ assumeInteractionOn: ['zn-input', 'zn-change'],
50
+ });
51
+ private readonly hasSlotController = new HasSlotController(this, 'label', 'help-text');
52
+
53
+ private debounceTimer: ReturnType<typeof setTimeout> | null = null;
54
+
55
+ @query('zn-textarea') textarea: ZnTextarea;
56
+ @query('.markdown-editor__preview') previewEl: HTMLDivElement;
57
+
58
+ /** The name of the control, submitted as part of form data. */
59
+ @property() name = '';
60
+
61
+ /** The current markdown content. */
62
+ @property() value = '';
63
+
64
+ /** The default value — used when resetting the form. */
65
+ @defaultValue() defaultValue = '';
66
+
67
+ /** The control's label. If you need HTML, use the `label` slot. */
68
+ @property() label = '';
69
+
70
+ /** Help text displayed below the editor. If you need HTML, use the `help-text` slot. */
71
+ @property({attribute: 'help-text'}) helpText = '';
72
+
73
+ /** Placeholder text shown when the editor is empty. */
74
+ @property() placeholder = 'Enter markdown content...';
75
+
76
+ /** Number of rows for the textarea. */
77
+ @property({type: Number}) rows = 20;
78
+
79
+ /** Which view to show. */
80
+ @property({reflect: true, attribute: 'view-mode'})
81
+ viewMode: ViewMode = 'editor';
82
+
83
+ /**
84
+ * Key used to persist the selected view mode to `localStorage`. Set to an empty string to disable persistence.
85
+ */
86
+ @property({attribute: 'storage-key'}) storageKey = 'zn-markdown-editor-view-mode';
87
+
88
+ /** Makes the editor required for form submission. */
89
+ @property({type: Boolean, reflect: true}) required = false;
90
+
91
+ /** Makes the editor read-only. */
92
+ @property({type: Boolean, reflect: true}) readonly = false;
93
+
94
+ /** Disables the editor. */
95
+ @property({type: Boolean, reflect: true}) disabled = false;
96
+
97
+ /** Whether the editor is currently expanded to cover its containing positioned ancestor. */
98
+ @state() expanded = false;
99
+
100
+ get validity(): ValidityState {
101
+ return this.textarea?.validity;
102
+ }
103
+
104
+ get validationMessage(): string {
105
+ return this.textarea?.validationMessage ?? '';
106
+ }
107
+
108
+ checkValidity(): boolean {
109
+ return this.textarea?.checkValidity() ?? true;
110
+ }
111
+
112
+ getForm(): HTMLFormElement | null {
113
+ return this.formControlController.getForm();
114
+ }
115
+
116
+ reportValidity(): boolean {
117
+ return this.textarea?.reportValidity() ?? true;
118
+ }
119
+
120
+ setCustomValidity(message: string): void {
121
+ this.textarea?.setCustomValidity(message);
122
+ this.formControlController.updateValidity();
123
+ }
124
+
125
+ /** Sets focus on the editor. */
126
+ focus(options?: FocusOptions) {
127
+ this.textarea?.focus(options);
128
+ }
129
+
130
+ /** Removes focus from the editor. */
131
+ blur() {
132
+ this.textarea?.blur();
133
+ }
134
+
135
+ connectedCallback() {
136
+ super.connectedCallback();
137
+
138
+ // Pull initial value from light-DOM text content if no value attr/property is set.
139
+ if (!this.hasAttribute('value') && !this.value) {
140
+ const textNodes = Array.from(this.childNodes).filter(n => n.nodeType === Node.TEXT_NODE);
141
+ const raw = textNodes.map(n => n.textContent ?? '').join('');
142
+ const content = raw.replace(/\r\n/g, '\n').trim();
143
+ if (content.length > 0) {
144
+ this.value = content;
145
+ this.defaultValue = content;
146
+ textNodes.forEach(n => {
147
+ if ((n.textContent ?? '').trim().length > 0) n.parentNode?.removeChild(n);
148
+ });
149
+ }
150
+ }
151
+
152
+ const stored = this.readStoredViewMode();
153
+ if (stored) this.viewMode = stored;
154
+ }
155
+
156
+ disconnectedCallback() {
157
+ super.disconnectedCallback();
158
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
159
+ }
160
+
161
+ protected firstUpdated(_changedProperties: PropertyValues) {
162
+ void _changedProperties;
163
+ this.formControlController.updateValidity();
164
+ if (this.viewMode !== 'editor') this.renderPreview();
165
+ }
166
+
167
+ private readStoredViewMode(): ViewMode | null {
168
+ if (!this.storageKey) return null;
169
+ try {
170
+ const saved = localStorage.getItem(this.storageKey);
171
+ if (saved === 'editor' || saved === 'split' || saved === 'preview') return saved;
172
+ } catch {
173
+ // localStorage not available
174
+ }
175
+ return null;
176
+ }
177
+
178
+ private writeStoredViewMode(mode: ViewMode) {
179
+ if (!this.storageKey) return;
180
+ try {
181
+ localStorage.setItem(this.storageKey, mode);
182
+ } catch {
183
+ // ignore
184
+ }
185
+ }
186
+
187
+ private renderPreview() {
188
+ if (this.viewMode === 'editor') return;
189
+ if (!this.previewEl) return;
190
+ const parsed = marked.parse(this.value || '', {async: false});
191
+ this.previewEl.innerHTML = typeof parsed === 'string' ? parsed : '';
192
+ }
193
+
194
+ private handleInput = () => {
195
+ this.value = this.textarea.value;
196
+ this.emit('zn-input');
197
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
198
+ this.debounceTimer = setTimeout(() => this.renderPreview(), 150);
199
+ };
200
+
201
+ private handleChange = () => {
202
+ this.value = this.textarea.value;
203
+ this.emit('zn-change');
204
+ this.renderPreview();
205
+ };
206
+
207
+ private handleViewToggle = (e: Event) => {
208
+ const host = (e.target as HTMLElement).closest<HTMLElement>('[data-mode]');
209
+ const mode = host?.dataset.mode as ViewMode | undefined;
210
+ if (!mode || mode === this.viewMode) return;
211
+ this.viewMode = mode;
212
+ };
213
+
214
+ private handleExpandToggle = () => {
215
+ this.expanded = !this.expanded;
216
+ };
217
+
218
+ @watch('viewMode', {waitUntilFirstUpdate: true})
219
+ async handleViewModeChange() {
220
+ await this.updateComplete;
221
+ this.writeStoredViewMode(this.viewMode);
222
+ this.renderPreview();
223
+ this.dispatchEvent(new CustomEvent('zn-view-mode-change', {
224
+ bubbles: true,
225
+ composed: true,
226
+ detail: {mode: this.viewMode},
227
+ }));
228
+ }
229
+
230
+ @watch('value', {waitUntilFirstUpdate: true})
231
+ async handleValueChange() {
232
+ await this.updateComplete;
233
+ this.formControlController.updateValidity();
234
+ this.renderPreview();
235
+ }
236
+
237
+ @watch('markedReady', {waitUntilFirstUpdate: true})
238
+ handleMarkedReady() {
239
+ this.renderPreview();
240
+ }
241
+
242
+ render() {
243
+ const hasLabelSlot = this.hasSlotController.test('label');
244
+ const hasHelpSlot = this.hasSlotController.test('help-text');
245
+ const hasLabel = !!this.label || hasLabelSlot;
246
+ const hasHelpText = !!this.helpText || hasHelpSlot;
247
+ const showEditor = this.viewMode === 'editor' || this.viewMode === 'split';
248
+ const showPreview = this.viewMode === 'preview' || this.viewMode === 'split';
249
+
250
+ return html`
251
+ <div part="base"
252
+ class=${classMap({
253
+ 'markdown-editor': true,
254
+ 'markdown-editor--expanded': this.expanded,
255
+ 'markdown-editor--split': this.viewMode === 'split',
256
+ 'markdown-editor--preview-only': this.viewMode === 'preview',
257
+ })}>
258
+ ${hasLabel ? html`
259
+ <label class="markdown-editor__label">
260
+ <slot name="label">${this.label}</slot>
261
+ </label>` : ''}
262
+
263
+ <div part="toolbar" class="markdown-editor__toolbar">
264
+ <zn-button-group class="markdown-editor__view-toggle" @click=${this.handleViewToggle}>
265
+ ${VIEW_MODES.map(v => this.renderViewButton(v.mode, v.icon, v.label))}
266
+ </zn-button-group>
267
+ <zn-button
268
+ class="markdown-editor__expand-btn"
269
+ type="button"
270
+ size="medium"
271
+ color="secondary"
272
+ icon=${this.expanded ? 'close_fullscreen' : 'open_in_full'}
273
+ icon-size="18"
274
+ tooltip=${this.expanded ? 'Collapse' : 'Expand'}
275
+ @click=${this.handleExpandToggle}
276
+ ></zn-button>
277
+ </div>
278
+
279
+ <div class="markdown-editor__body">
280
+ <div part="editor"
281
+ class="markdown-editor__editor"
282
+ ?hidden=${!showEditor}>
283
+ <zn-textarea
284
+ .value=${this.value}
285
+ name=${this.name || ''}
286
+ placeholder=${this.placeholder}
287
+ rows=${this.rows}
288
+ resize="auto"
289
+ ?required=${this.required}
290
+ ?readonly=${this.readonly}
291
+ ?disabled=${this.disabled}
292
+ @zn-input=${this.handleInput}
293
+ @zn-change=${this.handleChange}
294
+ ></zn-textarea>
295
+ </div>
296
+ <div part="preview"
297
+ class="markdown-editor__preview-wrapper"
298
+ ?hidden=${!showPreview}>
299
+ <div class="markdown-editor__preview"></div>
300
+ </div>
301
+ </div>
302
+
303
+ ${hasHelpText ? html`
304
+ <div class="markdown-editor__help-text">
305
+ <slot name="help-text">${this.helpText}</slot>
306
+ </div>` : ''}
307
+ </div>
308
+ `;
309
+ }
310
+
311
+ private renderViewButton(mode: ViewMode, icon: string, label: string) {
312
+ const isActive = this.viewMode === mode;
313
+ return html`
314
+ <zn-button
315
+ class=${classMap({
316
+ 'markdown-editor__view-btn': true,
317
+ 'markdown-editor__view-btn--active': isActive,
318
+ })}
319
+ data-mode=${mode}
320
+ type="button"
321
+ size="medium"
322
+ color="default"
323
+ ?outline=${!isActive}
324
+ square
325
+ icon=${icon}
326
+ icon-size="18"
327
+ tooltip=${label}
328
+ aria-pressed=${isActive ? 'true' : 'false'}
329
+ ></zn-button>
330
+ `;
331
+ }
332
+ }