@kubex/zinc 1.1.94 → 1.1.96

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.
Files changed (39) hide show
  1. package/dist/custom-elements.json +653 -89
  2. package/dist/vscode.html-custom-data.json +73 -18
  3. package/dist/web-types.json +145 -35
  4. package/dist/zn.d.ts +204 -39
  5. package/dist/zn.min.css +1 -1
  6. package/dist/zn.min.js +374 -308
  7. package/docs/pages/components/flow-builder.md +17 -1
  8. package/docs/pages/components/page-builder.md +31 -0
  9. package/docs/pages/components/slash-menu.md +132 -6
  10. package/docs/pages/components/textarea.md +16 -0
  11. package/package.json +1 -1
  12. package/scss/_root.scss +7 -1
  13. package/src/components/button/button.scss +5 -2
  14. package/src/components/flow-builder/flow-builder.component.ts +16 -1
  15. package/src/components/flow-builder/flow-builder.test.ts +206 -1
  16. package/src/components/flow-builder/flow-geometry.test.ts +102 -0
  17. package/src/components/flow-builder/flow.types.ts +82 -15
  18. package/src/components/flow-builder/modules/flow-canvas/flow-canvas.component.ts +163 -108
  19. package/src/components/flow-builder/modules/flow-step/flow-step.component.ts +2 -0
  20. package/src/components/inline-edit/inline-edit.component.ts +6 -1
  21. package/src/components/input/input.component.ts +12 -2
  22. package/src/components/page/page.scss +7 -2
  23. package/src/components/page-builder/modules/page-section-card/page-section-card.component.ts +16 -9
  24. package/src/components/page-builder/modules/page-section-card/page-section-card.scss +13 -0
  25. package/src/components/page-builder/modules/page-section-card/page-section-card.test.ts +9 -0
  26. package/src/components/page-builder/page-builder.component.ts +62 -4
  27. package/src/components/page-builder/page-builder.test.ts +94 -0
  28. package/src/components/remarkd-editor/remarkd-editor.component.ts +227 -12
  29. package/src/components/remarkd-editor/remarkd-editor.scss +81 -0
  30. package/src/components/remarkd-editor/remarkd-editor.test.ts +258 -0
  31. package/src/components/settings-container/settings-container.scss +2 -1
  32. package/src/components/slash-item/slash-item.component.ts +1 -1
  33. package/src/components/slash-menu/slash-menu-items.ts +48 -0
  34. package/src/components/slash-menu/slash-menu.component.ts +134 -27
  35. package/src/components/slash-menu/slash-menu.scss +90 -12
  36. package/src/components/slash-menu/slash-menu.test.ts +107 -0
  37. package/src/components/textarea/textarea.component.ts +12 -2
  38. package/src/components/textarea/textarea.test.ts +2 -2
  39. package/src/components/translations/translations.component.ts +5 -1
@@ -117,12 +117,15 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
117
117
  /** The characters that open the slash menu. */
118
118
  @property({ attribute: 'slash-trigger' }) slashTrigger = '/';
119
119
 
120
- /** The heading shown above the slash menu's items. */
120
+ /** The name the slash menu's list is announced by. */
121
121
  @property({ attribute: 'slash-heading' }) slashHeading = 'Insert';
122
122
 
123
123
  /** Hides the insertion keys normally shown against the slash menu's items. */
124
124
  @property({ attribute: 'slash-hide-keys', type: Boolean }) slashHideKeys = false;
125
125
 
126
+ /** Lists the slash menu items most recently chosen here above the rest, remembered under this key. */
127
+ @property({ attribute: 'slash-recent-key' }) slashRecentKey = '';
128
+
126
129
  /** Resolves additional slash menu items each time the menu opens. JavaScript only. */
127
130
  @property({ attribute: false }) slashItemsProvider?: (query: string) => SlashMenuItem[] | Promise<SlashMenuItem[]>;
128
131
 
@@ -466,6 +469,7 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
466
469
  slash-trigger="${this.slashTrigger}"
467
470
  slash-heading="${this.slashHeading}"
468
471
  slash-preset="${this.slashPreset}"
472
+ slash-recent-key="${this.slashRecentKey}"
469
473
  ?slash-hide-keys="${this.slashHideKeys}"
470
474
  .slashItems="${this.slashItems}"
471
475
  .slashItemsProvider="${this.slashItemsProvider}"
@@ -489,6 +493,7 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
489
493
  slash-trigger="${this.slashTrigger}"
490
494
  slash-heading="${this.slashHeading}"
491
495
  slash-preset="${this.slashPreset}"
496
+ slash-recent-key="${this.slashRecentKey}"
492
497
  ?slash-hide-keys="${this.slashHideKeys}"
493
498
  .slashItems="${this.slashItems}"
494
499
  .slashItemsProvider="${this.slashItemsProvider}"
@@ -276,12 +276,18 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
276
276
  /** The characters that open the slash menu. */
277
277
  @property({attribute: 'slash-trigger'}) slashTrigger = '/';
278
278
 
279
- /** The heading shown above the slash menu's items. */
279
+ /** The name the slash menu's list is announced by. */
280
280
  @property({attribute: 'slash-heading'}) slashHeading = 'Insert';
281
281
 
282
282
  /** Hides the insertion keys normally shown against the slash menu's items. */
283
283
  @property({attribute: 'slash-hide-keys', type: Boolean}) slashHideKeys = false;
284
284
 
285
+ /**
286
+ * Lists the items most recently chosen here above the rest, remembered in `localStorage` under this
287
+ * key. Share a key between the fields that should share a history; leave unset to offer no such list.
288
+ */
289
+ @property({attribute: 'slash-recent-key'}) slashRecentKey = '';
290
+
285
291
  /**
286
292
  * Resolves additional items each time the menu opens, for lists that come from elsewhere (e.g. an
287
293
  * API). Receives the current query and may return a promise. JavaScript only.
@@ -1100,7 +1106,11 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
1100
1106
  </div>
1101
1107
  ${this.hasSlashMenu
1102
1108
  ? html`
1103
- <zn-slash-menu part="slash-menu" heading=${this.slashHeading} ?hide-keys=${this.slashHideKeys}></zn-slash-menu>`
1109
+ <zn-slash-menu
1110
+ part="slash-menu"
1111
+ heading=${this.slashHeading}
1112
+ recent-key=${this.slashRecentKey}
1113
+ ?hide-keys=${this.slashHideKeys}></zn-slash-menu>`
1104
1114
  : ''}
1105
1115
  <slot name="slash-menu"></slot>
1106
1116
  <slot name="slash-items" hidden></slot>
@@ -93,8 +93,13 @@
93
93
  background-repeat: no-repeat;
94
94
  background-position: bottom center;
95
95
  background-size: cover;
96
- mix-blend-mode: color-burn;
97
- opacity: 0.2;
96
+ // PERF: mix-blend-mode forces .page__header .header — which contains the tab
97
+ // navbar — into its own render surface with a backdrop readback. Dropped in
98
+ // favour of plain alpha compositing. This is a small visual change to the
99
+ // aurora wave (it no longer darkens what it sits on); restore the blend mode
100
+ // if the look regresses, but expect the GPU cost back with it.
101
+ //mix-blend-mode: color-burn;
102
+ opacity: 0.04;
98
103
  pointer-events: none;
99
104
  }
100
105
 
@@ -39,6 +39,8 @@ export default class ZnPageSectionCard extends ZincElement {
39
39
  @property({type: Boolean, reflect: true}) selected = false;
40
40
  /** Set when the section's type has no registered template — renders greyed. */
41
41
  @property({type: Boolean, reflect: true}) unknown = false;
42
+ /** Set when the builder pins this section to the page — drops the remove action. */
43
+ @property({type: Boolean, reflect: true}) locked = false;
42
44
 
43
45
  protected updated(changed: PropertyValues) {
44
46
  super.updated(changed);
@@ -67,6 +69,10 @@ export default class ZnPageSectionCard extends ZincElement {
67
69
  <span class="card__label">${this.label}</span>
68
70
  ${this.summary ? html`<span class="card__summary">${this.summary}</span>` : ''}
69
71
  </span>
72
+ ${this.locked ? html`
73
+ <span class="card__lock" title="This section cannot be removed">
74
+ <zn-icon src="lock" size="14"></zn-icon>
75
+ </span>` : ''}
70
76
  <span class="card__actions" @keydown="${this._actionKeydown}">
71
77
  <zn-button
72
78
  class="card__action"
@@ -77,15 +83,16 @@ export default class ZnPageSectionCard extends ZincElement {
77
83
  title="Duplicate section"
78
84
  aria-label="Duplicate section"
79
85
  @click="${(e: Event) => this._action(e, 'page-card-duplicate')}"></zn-button>
80
- <zn-button
81
- class="card__action"
82
- icon-button="small"
83
- plain
84
- icon="delete"
85
- icon-size="14"
86
- title="Remove section"
87
- aria-label="Remove section"
88
- @click="${(e: Event) => this._action(e, 'page-card-remove')}"></zn-button>
86
+ ${this.locked ? '' : html`
87
+ <zn-button
88
+ class="card__action"
89
+ icon-button="small"
90
+ plain
91
+ icon="delete"
92
+ icon-size="14"
93
+ title="Remove section"
94
+ aria-label="Remove section"
95
+ @click="${(e: Event) => this._action(e, 'page-card-remove')}"></zn-button>`}
89
96
  </span>
90
97
  </div>
91
98
  `;
@@ -15,6 +15,11 @@
15
15
  cursor: grabbing;
16
16
  }
17
17
 
18
+ :host([locked]),
19
+ :host([locked]:active) {
20
+ cursor: default;
21
+ }
22
+
18
23
  .card {
19
24
  display: flex;
20
25
  align-items: center;
@@ -77,6 +82,14 @@
77
82
  text-overflow: ellipsis;
78
83
  }
79
84
 
85
+ // Always visible, unlike the hover actions — it is what explains their absence.
86
+ .card__lock {
87
+ display: flex;
88
+ flex: 0 0 auto;
89
+ align-items: center;
90
+ opacity: 0.45;
91
+ }
92
+
80
93
  // Space is always reserved (visibility, not display) so the card never
81
94
  // changes size or shifts its text when the actions appear on hover.
82
95
  .card__actions {
@@ -41,6 +41,15 @@ describe('<zn-page-section-card>', () => {
41
41
  expect(hostSawKeydown, 'host keydown suppressed for button activation keys').to.be.false;
42
42
  });
43
43
 
44
+ it('should drop the remove action and show a lock when locked', async () => {
45
+ const el = await fixture<ZnPageSectionCard>(html`
46
+ <zn-page-section-card label="Hero" locked></zn-page-section-card>`);
47
+ expect(el.hasAttribute('locked')).to.be.true;
48
+ expect(el.shadowRoot!.querySelector('zn-button[title="Remove section"]'), 'remove gone').to.not.exist;
49
+ expect(el.shadowRoot!.querySelector('zn-button[title="Duplicate section"]'), 'duplicate kept').to.exist;
50
+ expect(el.shadowRoot!.querySelector('.card__lock'), 'lock indicator').to.exist;
51
+ });
52
+
44
53
  it('should reflect selected and unknown states', async () => {
45
54
  const el = await fixture<ZnPageSectionCard>(html`
46
55
  <zn-page-section-card label="Hero" selected unknown></zn-page-section-card>`);
@@ -105,6 +105,14 @@ export default class ZnPageBuilder extends ZincElement {
105
105
  @property({ reflect: true }) heading = '';
106
106
  @property({ reflect: true }) subheading = '';
107
107
 
108
+ /**
109
+ * Section type key that must lead the page. The builder hoists an existing section of
110
+ * that type to the top, or inserts an empty one, and pins it there: it can't be
111
+ * removed, reordered or dragged into a slot, and nothing can be dropped above it.
112
+ * Its content stays fully editable in the inspector.
113
+ */
114
+ @property({ attribute: 'required-first', reflect: true }) requiredFirst = '';
115
+
108
116
  /** Section types to make available, registered into the internal registry. */
109
117
  @property({ attribute: false }) sectionTypes: PageSectionType[] = [];
110
118
 
@@ -224,6 +232,15 @@ export default class ZnPageBuilder extends ZincElement {
224
232
  }
225
233
  }
226
234
 
235
+ // Idempotent, so it does not matter whether this or handleConfigChange runs first.
236
+ @watch('requiredFirst')
237
+ handleRequiredFirstChange() {
238
+ const sections = this._requireFirst(this._state.sections);
239
+ if (sections === this._state.sections) return;
240
+ this._state = { sections };
241
+ this.defaultValue = JSON.stringify(this._state);
242
+ }
243
+
227
244
  @watch('sectionTypes')
228
245
  handleSectionTypesChange() {
229
246
  this.registry.registerAll(this.sectionTypes ?? []);
@@ -450,13 +467,48 @@ export default class ZnPageBuilder extends ZincElement {
450
467
  }
451
468
  this._history = [];
452
469
  this._redoStack = [];
453
- this._state = { sections };
470
+ this._state = { sections: this._requireFirst(sections) };
454
471
  this.defaultValue = JSON.stringify(this._state);
455
472
  this._selectedId = null;
456
473
  this._pickerIndex = null;
457
474
  this._offerRestoreIfNewer();
458
475
  }
459
476
 
477
+ // --- The pinned leading section (`required-first`) ---------------------------
478
+
479
+ /**
480
+ * Id of the section pinned to the top of the page, or null when `required-first`
481
+ * is unset. Derived from the state rather than stored on it, so nothing about the
482
+ * lock leaks into the persisted config.
483
+ */
484
+ private get _pinnedId(): string | null {
485
+ const first = this._state.sections[0];
486
+ return this.requiredFirst && first?.type === this.requiredFirst ? first.id : null;
487
+ }
488
+
489
+ private _isPinned(id: string): boolean {
490
+ return this._pinnedId === id;
491
+ }
492
+
493
+ /** Lowest top-level index a section may be added or moved to. */
494
+ private get _firstFreeIndex(): number {
495
+ return this._pinnedId === null ? 0 : 1;
496
+ }
497
+
498
+ /**
499
+ * Sections reordered so `required-first` leads the page: an existing section of that
500
+ * type is hoisted to the front, otherwise an empty one is prepended. Returns the
501
+ * argument unchanged when there is nothing to do, so callers can compare by identity.
502
+ */
503
+ private _requireFirst(sections: PageSection[]): PageSection[] {
504
+ if (!this.requiredFirst || sections[0]?.type === this.requiredFirst) return sections;
505
+ const at = sections.findIndex(s => s.type === this.requiredFirst);
506
+ if (at === -1) return [{ id: generateSectionId(), type: this.requiredFirst, data: {} }, ...sections];
507
+ const hoisted = [...sections];
508
+ hoisted.unshift(...hoisted.splice(at, 1));
509
+ return hoisted;
510
+ }
511
+
460
512
  /** Finds a section by id, searching top-level sections and slotted children. */
461
513
  private _findSection(id: string | null): PageSection | undefined {
462
514
  if (!id) return undefined;
@@ -551,7 +603,7 @@ export default class ZnPageBuilder extends ZincElement {
551
603
  this._pushHistory();
552
604
  const section: PageSection = { id: generateSectionId(), type, data: {} };
553
605
  const sections = [...this._state.sections];
554
- sections.splice(index ?? sections.length, 0, section);
606
+ sections.splice(Math.max(index ?? sections.length, this._firstFreeIndex), 0, section);
555
607
  this._commit({ sections });
556
608
  this._select(section.id);
557
609
  return section;
@@ -576,6 +628,7 @@ export default class ZnPageBuilder extends ZincElement {
576
628
  }
577
629
 
578
630
  private _removeSection(id: string) {
631
+ if (this._isPinned(id)) return;
579
632
  const [removed, sections] = this._extract(id);
580
633
  if (!removed) return;
581
634
  this._pushHistory();
@@ -620,6 +673,8 @@ export default class ZnPageBuilder extends ZincElement {
620
673
 
621
674
  /** Moves a section (top-level or slotted) to a top-level position. */
622
675
  private _moveSection(id: string, index: number) {
676
+ if (this._isPinned(id)) return;
677
+ index = Math.max(index, this._firstFreeIndex);
623
678
  const from = this._state.sections.findIndex(s => s.id === id);
624
679
  if (from !== -1) {
625
680
  const to = index > from ? index - 1 : index;
@@ -648,6 +703,7 @@ export default class ZnPageBuilder extends ZincElement {
648
703
  const container = this._findSection(containerId);
649
704
  const containerType = container ? this.registry.get(container.type) : undefined;
650
705
  if (!moved || !container || !containerType?.slots || id === containerId) return;
706
+ if (this._isPinned(id)) return; // the pinned section stays at the top of the page
651
707
  if (this.registry.get(moved.type)?.slots) return; // no containers inside slots
652
708
  if (containerType.accepts && !containerType.accepts.includes(moved.type)) return;
653
709
  if (slotIndex < 0 || slotIndex >= containerType.slots) return;
@@ -866,7 +922,7 @@ export default class ZnPageBuilder extends ZincElement {
866
922
  Drag sections here to build your page
867
923
  </div>` : ''}
868
924
  ${sections.map((section, i) => html`
869
- ${this._renderDropZone(i)}
925
+ ${i === 0 && this._pinnedId !== null ? '' : this._renderDropZone(i)}
870
926
  ${this._renderCard(section, i)}
871
927
  `)}
872
928
  ${this._renderDropZone(sections.length)}
@@ -881,10 +937,11 @@ export default class ZnPageBuilder extends ZincElement {
881
937
  extraClass = ''
882
938
  ) {
883
939
  const type = this.registry.get(section.type);
940
+ const pinned = this._isPinned(section.id);
884
941
  return html`
885
942
  <zn-page-section-card
886
943
  class="${extraClass}"
887
- draggable="true"
944
+ draggable="${pinned ? 'false' : 'true'}"
888
945
  tabindex="0"
889
946
  label="${section.label ?? type?.label ?? section.type}"
890
947
  summary="${type ? sectionSummary(section, type) : `Unknown type "${section.type}"`}"
@@ -893,6 +950,7 @@ export default class ZnPageBuilder extends ZincElement {
893
950
  color="${ifDefined(type?.color)}"
894
951
  ?selected="${this._selectedId === section.id}"
895
952
  ?unknown="${!type}"
953
+ ?locked="${pinned}"
896
954
  @click="${(e: Event) => {
897
955
  e.stopPropagation();
898
956
  this._select(section.id);
@@ -1,5 +1,6 @@
1
1
  import '../../../dist/zn.min.js';
2
2
  import {expect, fixture, html} from '@open-wc/testing';
3
+ import type {PageState} from './page.types';
3
4
  import type ZnPageBuilder from './page-builder.component';
4
5
 
5
6
  describe('<zn-page-builder>', () => {
@@ -575,6 +576,99 @@ describe('<zn-page-builder>', () => {
575
576
  expect(el.state.sections[0].children?.[0] ?? null, 'Delete removes the child').to.be.null;
576
577
  });
577
578
 
579
+ it('should pin a required-first section, inserting one when the page has none', async () => {
580
+ const el = await fixture<ZnPageBuilder>(html`
581
+ <zn-page-builder required-first="hero">
582
+ <template type="hero" slot="config" label="Hero"></template>
583
+ <template type="rich-text" slot="config" label="Rich Text"></template>
584
+ </zn-page-builder>`);
585
+ await el.updateComplete;
586
+
587
+ expect(el.state.sections.map(s => s.type), 'inserted into an empty page').to.deep.equal(['hero']);
588
+ // The submitted value carries the pinned section, so a save can't miss it.
589
+ expect((JSON.parse(el.value) as PageState).sections).to.have.length(1);
590
+
591
+ const card = el.shadowRoot!.querySelector('zn-page-section-card')!;
592
+ expect(card.hasAttribute('locked'), 'card is locked').to.be.true;
593
+ expect(card.getAttribute('draggable'), 'card is not draggable').to.equal('false');
594
+ // No "+" strip above it — the first drop zone follows the pinned card.
595
+ expect(el.shadowRoot!.querySelector('.canvas > *')?.tagName.toLowerCase()).to.equal('zn-page-section-card');
596
+ });
597
+
598
+ it('should hoist an existing required-first section to the top of the page', async () => {
599
+ const el = await fixture<ZnPageBuilder>(html`
600
+ <zn-page-builder required-first="hero"
601
+ config='{"sections":[{"id":"t","type":"rich-text","data":{}},{"id":"h","type":"hero","data":{"title":"Help"}}]}'>
602
+ <template type="hero" slot="config" label="Hero"></template>
603
+ <template type="rich-text" slot="config" label="Rich Text"></template>
604
+ </zn-page-builder>`);
605
+ await el.updateComplete;
606
+
607
+ expect(el.state.sections.map(s => s.id)).to.deep.equal(['h', 't']);
608
+ expect(el.state.sections[0].data, 'hoisted with its data intact').to.deep.equal({title: 'Help'});
609
+ });
610
+
611
+ it('should refuse to remove, reorder or displace the pinned section', async () => {
612
+ const el = await fixture<ZnPageBuilder>(html`
613
+ <zn-page-builder required-first="hero"
614
+ config='{"sections":[{"id":"h","type":"hero","data":{}},{"id":"t","type":"rich-text","data":{}}]}'>
615
+ <template type="hero" slot="config" label="Hero"></template>
616
+ <template type="rich-text" slot="config" label="Rich Text"></template>
617
+ </zn-page-builder>`);
618
+ await el.updateComplete;
619
+
620
+ const pinnedCard = el.shadowRoot!.querySelector('zn-page-section-card')!;
621
+ expect(pinnedCard.shadowRoot!.querySelector('zn-button[title="Remove section"]'), 'no remove action').to.not.exist;
622
+
623
+ pinnedCard.dispatchEvent(new CustomEvent('page-card-remove', {bubbles: true, composed: true}));
624
+ pinnedCard.dispatchEvent(new KeyboardEvent('keydown', {key: 'Delete', bubbles: true, cancelable: true}));
625
+ await el.updateComplete;
626
+ expect(el.state.sections.map(s => s.id), 'survives remove and Delete').to.deep.equal(['h', 't']);
627
+
628
+ // Dropping another section on the topmost zone lands it below the pinned one.
629
+ const dataTransfer = new DataTransfer();
630
+ dataTransfer.setData('application/x-zn-page-section', 't');
631
+ el.shadowRoot!.querySelector('.drop')!
632
+ .dispatchEvent(new DragEvent('drop', {dataTransfer, bubbles: true, cancelable: true}));
633
+ await el.updateComplete;
634
+ expect(el.state.sections.map(s => s.id), 'nothing lands above the pinned section').to.deep.equal(['h', 't']);
635
+
636
+ expect(el.addSection('rich-text', 0)).to.not.be.null;
637
+ expect(el.state.sections[0].id, 'addSection index 0 is clamped').to.equal('h');
638
+ });
639
+
640
+ it('should keep the pinned section out of container slots', async () => {
641
+ const el = await fixture<ZnPageBuilder>(html`
642
+ <zn-page-builder required-first="hero"
643
+ config='{"sections":[{"id":"h","type":"hero","data":{}},{"id":"grid","type":"article-grid","data":{}}]}'>
644
+ <template type="hero" slot="config" label="Hero"></template>
645
+ <template type="article-grid" slot="config" label="Article Grid" slots="2"></template>
646
+ </zn-page-builder>`);
647
+ await el.updateComplete;
648
+
649
+ const dataTransfer = new DataTransfer();
650
+ dataTransfer.setData('application/x-zn-page-section', 'h');
651
+ el.shadowRoot!.querySelector('.slot--empty')!
652
+ .dispatchEvent(new DragEvent('drop', {dataTransfer, bubbles: true, cancelable: true}));
653
+ await el.updateComplete;
654
+
655
+ expect(el.state.sections[0].id, 'still leading the page').to.equal('h');
656
+ expect(el.state.sections[1].children?.[0] ?? null, 'slot left empty').to.be.null;
657
+ });
658
+
659
+ it('should leave the page alone without required-first', async () => {
660
+ const el = await fixture<ZnPageBuilder>(html`
661
+ <zn-page-builder config='{"sections":[{"id":"t","type":"rich-text","data":{}}]}'>
662
+ <template type="rich-text" slot="config" label="Rich Text"></template>
663
+ </zn-page-builder>`);
664
+ await el.updateComplete;
665
+
666
+ expect(el.state.sections.map(s => s.type)).to.deep.equal(['rich-text']);
667
+ const card = el.shadowRoot!.querySelector('zn-page-section-card')!;
668
+ expect(card.hasAttribute('locked')).to.be.false;
669
+ expect(card.getAttribute('draggable')).to.equal('true');
670
+ });
671
+
578
672
  it('should accept a palette drop anywhere on the empty canvas', async () => {
579
673
  const el = await fixture<ZnPageBuilder>(html`
580
674
  <zn-page-builder>