@kubex/zinc 1.1.84 → 1.1.86
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 +378 -367
- package/dist/vscode.html-custom-data.json +16 -16
- package/dist/web-types.json +41 -41
- package/dist/zn.d.ts +10 -1
- package/dist/zn.min.js +105 -104
- package/package.json +1 -1
- package/src/components/flow-builder/flow-builder.scss +6 -0
- package/src/components/preview-frame/preview-frame.component.ts +25 -25
- package/src/components/preview-frame/preview-frame.test.ts +48 -0
- package/src/components/theme-editor/theme-editor.component.ts +21 -17
- package/src/components/theme-editor/theme-editor.scss +17 -34
package/package.json
CHANGED
|
@@ -141,6 +141,12 @@
|
|
|
141
141
|
// rather than the navbar's default body colour.
|
|
142
142
|
--zn-active-nav-background: var(--zn-panel);
|
|
143
143
|
|
|
144
|
+
// Themes with a glass page tab bar (e.g. Aura) frost inactive tabs on every
|
|
145
|
+
// navbar under a zn-page — over the panel the translucent white just reads
|
|
146
|
+
// as white. Pin the standard tab tint instead.
|
|
147
|
+
--navbar-page-inactive-tab-background: rgb(var(--zn-color-tab));
|
|
148
|
+
--navbar-page-inactive-tab-filter: none;
|
|
149
|
+
|
|
144
150
|
border-right: 1px solid rgb(var(--zn-border-color));
|
|
145
151
|
|
|
146
152
|
// The registry-driven steps panel fills the column below the title block.
|
|
@@ -62,6 +62,11 @@ export default class ZnPreviewFrame extends ZincElement {
|
|
|
62
62
|
* refresh the preview. These are never intercepted: the shell submits them
|
|
63
63
|
* (so its own response handling — alerts, refreshes — runs as normal) and the
|
|
64
64
|
* preview re-fetches its config once the shell reports the save complete.
|
|
65
|
+
* Matched by delegation on the shell's bubbled `complete` event rather than
|
|
66
|
+
* by attaching to the forms themselves, so a save anywhere on the page
|
|
67
|
+
* refreshes the preview — including forms in a different DOM root, e.g. a
|
|
68
|
+
* page-level form saved while the preview sits inside a tab panel's shadow
|
|
69
|
+
* root (a page's Template select lives on one tab, its preview on another).
|
|
65
70
|
* Set empty to disable.
|
|
66
71
|
*/
|
|
67
72
|
@property({attribute: 'refresh-on'}) refreshOn = 'form';
|
|
@@ -141,8 +146,13 @@ export default class ZnPreviewFrame extends ZincElement {
|
|
|
141
146
|
private _contentObserver: ResizeObserver | undefined;
|
|
142
147
|
|
|
143
148
|
private readonly _watchedForms = new Set<HTMLFormElement>();
|
|
144
|
-
private readonly _refreshForms = new Set<HTMLFormElement>();
|
|
145
149
|
private readonly _debounceTimers = new Map<HTMLFormElement, number>();
|
|
150
|
+
// 'complete' delegation targets: the document always, plus the component's
|
|
151
|
+
// own shadow root when it has one (a non-composed event never reaches the
|
|
152
|
+
// document). A composed event inside the shadow root hits both, so the
|
|
153
|
+
// handler dedupes by event object.
|
|
154
|
+
private _shellSaveRoots: EventTarget[] = [];
|
|
155
|
+
private _lastShellSave: Event | undefined;
|
|
146
156
|
|
|
147
157
|
// Forms are siblings in light DOM and get replaced when other content
|
|
148
158
|
// re-renders; re-resolve them whenever the surrounding DOM changes.
|
|
@@ -158,6 +168,8 @@ export default class ZnPreviewFrame extends ZincElement {
|
|
|
158
168
|
window.addEventListener('message', this._onMessage);
|
|
159
169
|
this._attachForms();
|
|
160
170
|
const root = this.getRootNode();
|
|
171
|
+
this._shellSaveRoots = root instanceof ShadowRoot ? [document, root] : [document];
|
|
172
|
+
this._shellSaveRoots.forEach(target => target.addEventListener('complete', this._onShellSave));
|
|
161
173
|
if (root instanceof Document) {
|
|
162
174
|
this._formObserver.observe(root.body);
|
|
163
175
|
} else if (root instanceof ShadowRoot) {
|
|
@@ -178,8 +190,8 @@ export default class ZnPreviewFrame extends ZincElement {
|
|
|
178
190
|
this._contentObserver = undefined;
|
|
179
191
|
this._watchedForms.forEach(form => this._detachForm(form));
|
|
180
192
|
this._watchedForms.clear();
|
|
181
|
-
this.
|
|
182
|
-
this.
|
|
193
|
+
this._shellSaveRoots.forEach(target => target.removeEventListener('complete', this._onShellSave));
|
|
194
|
+
this._shellSaveRoots = [];
|
|
183
195
|
}
|
|
184
196
|
|
|
185
197
|
private readonly _onMessage = (e: MessageEvent) => {
|
|
@@ -315,30 +327,18 @@ export default class ZnPreviewFrame extends ZincElement {
|
|
|
315
327
|
form.addEventListener('zn-input', this._onChange);
|
|
316
328
|
form.addEventListener('change', this._onChange);
|
|
317
329
|
});
|
|
318
|
-
|
|
319
|
-
// Shell-saved forms: the shell fires 'complete' on the form it submitted.
|
|
320
|
-
const refreshMatched = new Set<HTMLFormElement>();
|
|
321
|
-
if (this.refreshOn) {
|
|
322
|
-
root.querySelectorAll(this.refreshOn).forEach(node => {
|
|
323
|
-
if (node instanceof HTMLFormElement && !matched.has(node)) refreshMatched.add(node);
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
this._refreshForms.forEach(form => {
|
|
328
|
-
if (!refreshMatched.has(form)) {
|
|
329
|
-
form.removeEventListener('complete', this._onShellSave);
|
|
330
|
-
this._refreshForms.delete(form);
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
refreshMatched.forEach(form => {
|
|
335
|
-
if (this._refreshForms.has(form)) return;
|
|
336
|
-
this._refreshForms.add(form);
|
|
337
|
-
form.addEventListener('complete', this._onShellSave);
|
|
338
|
-
});
|
|
339
330
|
}
|
|
340
331
|
|
|
341
|
-
|
|
332
|
+
// Shell-saved forms: the shell fires a bubbling 'complete' on the form it
|
|
333
|
+
// submitted. composedPath()[0] recovers the form when the event was
|
|
334
|
+
// retargeted crossing a shadow boundary on its way to the document.
|
|
335
|
+
private readonly _onShellSave = (e: Event) => {
|
|
336
|
+
if (e === this._lastShellSave) return;
|
|
337
|
+
this._lastShellSave = e;
|
|
338
|
+
if (!this.refreshOn) return;
|
|
339
|
+
const origin = e.composedPath()[0];
|
|
340
|
+
if (!(origin instanceof HTMLFormElement) || !origin.matches(this.refreshOn)) return;
|
|
341
|
+
if (this._watchedForms.has(origin)) return; // auto-saved forms refresh via _save
|
|
342
342
|
void this._sendConfig();
|
|
343
343
|
};
|
|
344
344
|
|
|
@@ -248,6 +248,54 @@ describe('<zn-preview-frame>', () => {
|
|
|
248
248
|
expect(posted[0]['type']).to.equal('hp-preview:config');
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
+
// A uri tab panel puts the preview in the tabs component's shadow root while
|
|
252
|
+
// page-level forms (e.g. the page's Template select) stay in the document —
|
|
253
|
+
// a save out there must still refresh the preview.
|
|
254
|
+
it('refreshes on a shell save of a form outside the component\'s own root', async () => {
|
|
255
|
+
const wrapper = await fixture(html`
|
|
256
|
+
<div>
|
|
257
|
+
<form action="/save" method="post"><input name="template" value="flow"></form>
|
|
258
|
+
<div id="host"></div>
|
|
259
|
+
</div>`);
|
|
260
|
+
|
|
261
|
+
const shadow = wrapper.querySelector('#host')!.attachShadow({mode: 'open'});
|
|
262
|
+
const el = document.createElement('zn-preview-frame');
|
|
263
|
+
el.setAttribute('src', 'about:blank');
|
|
264
|
+
el.setAttribute('frame-origin', 'https://site.example');
|
|
265
|
+
el.setAttribute('data-uri', '/payload');
|
|
266
|
+
shadow.appendChild(el);
|
|
267
|
+
await (el as HTMLElement & {updateComplete: Promise<boolean>}).updateComplete;
|
|
268
|
+
|
|
269
|
+
wrapper.querySelector('form')!.dispatchEvent(
|
|
270
|
+
new CustomEvent('complete', {bubbles: true, detail: {}}));
|
|
271
|
+
|
|
272
|
+
await waitUntil(() => fetchCalls.length === 1);
|
|
273
|
+
expect(fetchCalls[0].uri).to.equal('/payload');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('refreshes once for a composed complete event inside its own shadow root', async () => {
|
|
277
|
+
const wrapper = await fixture(html`<div><div id="host"></div></div>`);
|
|
278
|
+
|
|
279
|
+
const shadow = wrapper.querySelector('#host')!.attachShadow({mode: 'open'});
|
|
280
|
+
const form = document.createElement('form');
|
|
281
|
+
form.setAttribute('action', '/save');
|
|
282
|
+
shadow.appendChild(form);
|
|
283
|
+
const el = document.createElement('zn-preview-frame');
|
|
284
|
+
el.setAttribute('src', 'about:blank');
|
|
285
|
+
el.setAttribute('frame-origin', 'https://site.example');
|
|
286
|
+
el.setAttribute('data-uri', '/payload');
|
|
287
|
+
shadow.appendChild(el);
|
|
288
|
+
await (el as HTMLElement & {updateComplete: Promise<boolean>}).updateComplete;
|
|
289
|
+
|
|
290
|
+
// the shell fires composed events, so this reaches both the shadow root
|
|
291
|
+
// and the document — it must trigger a single refresh
|
|
292
|
+
form.dispatchEvent(new CustomEvent('complete', {bubbles: true, composed: true, detail: {}}));
|
|
293
|
+
|
|
294
|
+
await waitUntil(() => fetchCalls.length === 1);
|
|
295
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
296
|
+
expect(fetchCalls.length).to.equal(1);
|
|
297
|
+
});
|
|
298
|
+
|
|
251
299
|
it('does not intercept the submit of a refresh-on form', async () => {
|
|
252
300
|
const wrapper = await fixture(html`
|
|
253
301
|
<div>
|
|
@@ -5,6 +5,7 @@ import { MutationController } from '@lit-labs/observers/mutation-controller.js';
|
|
|
5
5
|
import { property, query, queryAll, state } from 'lit/decorators.js';
|
|
6
6
|
import ZincElement from '../../internal/zinc-element';
|
|
7
7
|
import ZnButton from '../button';
|
|
8
|
+
import ZnButtonGroup from '../button-group';
|
|
8
9
|
import ZnCollapsible from '../collapsible';
|
|
9
10
|
import ZnIcon from '../icon';
|
|
10
11
|
import ZnNavbar from '../navbar';
|
|
@@ -84,6 +85,7 @@ interface DerivedStructure {
|
|
|
84
85
|
* @dependency zn-preview-frame
|
|
85
86
|
* @dependency zn-icon
|
|
86
87
|
* @dependency zn-button
|
|
88
|
+
* @dependency zn-button-group
|
|
87
89
|
* @dependency zn-tabs
|
|
88
90
|
* @dependency zn-navbar
|
|
89
91
|
* @dependency zn-select
|
|
@@ -135,6 +137,7 @@ export default class ZnThemeEditor extends ZincElement {
|
|
|
135
137
|
'zn-preview-frame': ZnPreviewFrame,
|
|
136
138
|
'zn-icon': ZnIcon,
|
|
137
139
|
'zn-button': ZnButton,
|
|
140
|
+
'zn-button-group': ZnButtonGroup,
|
|
138
141
|
'zn-tabs': ZnTabs,
|
|
139
142
|
'zn-navbar': ZnNavbar,
|
|
140
143
|
'zn-select': ZnSelect,
|
|
@@ -822,13 +825,14 @@ export default class ZnThemeEditor extends ZincElement {
|
|
|
822
825
|
<div class="editor__controls-inner">
|
|
823
826
|
<div part="controls-header" class="editor__controls-header">
|
|
824
827
|
${this.controlsCaption ? html`<span class="editor__caption">${this.controlsCaption}</span>` : nothing}
|
|
825
|
-
<button
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
828
|
+
<zn-button class="editor__mode"
|
|
829
|
+
icon-button="small"
|
|
830
|
+
icon-size="20"
|
|
831
|
+
icon="${this.mode === 'dark' ? 'sun@lu' : 'moon@lu'}"
|
|
832
|
+
data-mode-toggle
|
|
833
|
+
tooltip="${this.mode === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}"
|
|
834
|
+
aria-label="${this.mode === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}"
|
|
835
|
+
@click="${this._toggleMode}"></zn-button>
|
|
832
836
|
</div>
|
|
833
837
|
<div class="editor__controls-body">
|
|
834
838
|
<div
|
|
@@ -870,17 +874,17 @@ export default class ZnThemeEditor extends ZincElement {
|
|
|
870
874
|
<div part="toolbar" class="editor__toolbar">
|
|
871
875
|
${this.previewCaption ? html`<span class="editor__caption">${this.previewCaption}</span>` : nothing}
|
|
872
876
|
<div class="editor__toolbar-actions">
|
|
873
|
-
<
|
|
877
|
+
<zn-button-group class="editor__devices" aria-label="Preview width">
|
|
874
878
|
${DEVICES.map(d => html`
|
|
875
|
-
<button
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
</
|
|
879
|
+
<zn-button class="editor__device"
|
|
880
|
+
icon-button="small"
|
|
881
|
+
icon-size="20"
|
|
882
|
+
icon="${d.icon}@lu"
|
|
883
|
+
data-device="${d.id}"
|
|
884
|
+
aria-label="${d.label}"
|
|
885
|
+
aria-pressed="${this.device === d.id ? 'true' : 'false'}"
|
|
886
|
+
@click="${() => this._setDevice(d.id)}"></zn-button>`)}
|
|
887
|
+
</zn-button-group>
|
|
884
888
|
${this._sourcesSafe().length > 0 ? html`
|
|
885
889
|
<zn-select
|
|
886
890
|
class="editor__sources"
|
|
@@ -70,6 +70,12 @@
|
|
|
70
70
|
// rather than the navbar's default body colour (flow-builder pattern).
|
|
71
71
|
--zn-active-nav-background: var(--zn-panel);
|
|
72
72
|
|
|
73
|
+
// Themes with a glass page tab bar (e.g. Aura) frost inactive tabs on every
|
|
74
|
+
// navbar under a zn-page — over the editor's opaque panel the translucent
|
|
75
|
+
// white just reads as white. Pin the standard tab tint instead.
|
|
76
|
+
--navbar-page-inactive-tab-background: rgb(var(--zn-color-tab));
|
|
77
|
+
--navbar-page-inactive-tab-filter: none;
|
|
78
|
+
|
|
73
79
|
position: relative;
|
|
74
80
|
display: flex;
|
|
75
81
|
flex-direction: column;
|
|
@@ -235,44 +241,21 @@
|
|
|
235
241
|
}
|
|
236
242
|
}
|
|
237
243
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
border: 1px solid rgb(var(--zn-border-color));
|
|
241
|
-
border-radius: 4px;
|
|
242
|
-
overflow: hidden;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
.editor__device,
|
|
246
|
-
.editor__mode {
|
|
247
|
-
display: flex;
|
|
248
|
-
align-items: center;
|
|
249
|
-
justify-content: center;
|
|
250
|
-
width: 30px;
|
|
251
|
-
height: 28px;
|
|
252
|
-
padding: 0;
|
|
253
|
-
border: 0;
|
|
254
|
-
background: transparent;
|
|
255
|
-
color: rgb(var(--zn-text));
|
|
256
|
-
cursor: pointer;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
.editor__device + .editor__device {
|
|
260
|
-
border-left: 1px solid rgb(var(--zn-border-color));
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
.editor__device:hover,
|
|
264
|
-
.editor__mode:hover {
|
|
265
|
-
background: rgba(var(--zn-border-color), 0.5);
|
|
266
|
-
}
|
|
267
|
-
|
|
244
|
+
// The device switcher is zn-button[icon-button="small"]s joined into a
|
|
245
|
+
// segmented control by zn-button-group; only the active fill is styled here.
|
|
268
246
|
.editor__device[aria-pressed="true"] {
|
|
269
|
-
|
|
270
|
-
|
|
247
|
+
// The button's shadow border-color is !important, so it can only be
|
|
248
|
+
// overridden through its own custom property.
|
|
249
|
+
--zn-button-border-color: rgb(var(--zn-primary));
|
|
250
|
+
|
|
251
|
+
// !important so the fill also beats the button's shadow hover background.
|
|
252
|
+
&::part(base) {
|
|
253
|
+
background: rgb(var(--zn-primary)) !important;
|
|
254
|
+
color: white !important;
|
|
255
|
+
}
|
|
271
256
|
}
|
|
272
257
|
|
|
273
258
|
.editor__mode {
|
|
274
|
-
border: 1px solid rgb(var(--zn-border-color));
|
|
275
|
-
border-radius: 4px;
|
|
276
259
|
// Sole occupant of .editor__controls-header's right side, beside the caption.
|
|
277
260
|
margin-left: auto;
|
|
278
261
|
}
|