@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
@@ -3,6 +3,8 @@
3
3
  :host {
4
4
  --slash-menu-width: 320px;
5
5
  --slash-menu-max-height: 260px;
6
+ --slash-menu-border-radius: 12px;
7
+ --slash-menu-item-border-radius: 8px;
6
8
 
7
9
  display: contents;
8
10
  }
@@ -20,24 +22,32 @@
20
22
  width: var(--slash-menu-width);
21
23
  max-width: var(--auto-size-available-width, none);
22
24
  max-height: min(var(--slash-menu-max-height), var(--auto-size-available-height, 100vh));
23
- overflow-y: auto;
24
- overscroll-behavior: none;
25
25
  z-index: var(--zn-z-index-dropdown);
26
- padding: var(--zn-spacing-2x-small, 4px) 0;
26
+ overflow: hidden;
27
27
  background: var(--zn-panel-background-color);
28
28
  border: solid var(--zn-panel-border-width, 1px) var(--zn-panel-border-color);
29
- border-radius: var(--zn-border-radius, 6px);
29
+ border-radius: var(--slash-menu-border-radius);
30
30
  box-shadow: var(--zn-shadow-medium);
31
31
  font-family: var(--zn-font-sans);
32
32
  font-size: var(--zn-font-size-medium);
33
33
  color: rgb(var(--zn-text));
34
34
  }
35
35
 
36
- .slash-menu__heading,
36
+ .slash-menu__list {
37
+ display: flex;
38
+ flex: 1 1 auto;
39
+ flex-direction: column;
40
+ gap: var(--zn-spacing-3x-small, 2px);
41
+ min-height: 0;
42
+ overflow-y: auto;
43
+ overscroll-behavior: none;
44
+ padding: var(--zn-spacing-2x-small, 4px) var(--zn-spacing-x-small, 8px);
45
+ }
46
+
37
47
  .slash-menu__group-heading,
38
48
  .slash-menu__footer,
39
49
  .slash-menu__empty {
40
- padding: var(--zn-spacing-2x-small, 4px) var(--zn-spacing-small, 12px);
50
+ padding: var(--zn-spacing-2x-small, 4px) var(--zn-spacing-x-small, 8px);
41
51
  font-size: var(--zn-font-size-x-small);
42
52
  color: var(--zn-color-neutral-500);
43
53
  user-select: none;
@@ -45,9 +55,11 @@
45
55
  }
46
56
 
47
57
  .slash-menu__group-heading {
58
+ padding-top: var(--zn-spacing-x-small, 8px);
48
59
  font-weight: var(--zn-font-weight-semibold);
49
60
  text-transform: uppercase;
50
- letter-spacing: 0.04em;
61
+ letter-spacing: 0.06em;
62
+ color: var(--zn-color-neutral-400);
51
63
  }
52
64
 
53
65
  .slash-menu__footer {
@@ -55,24 +67,72 @@
55
67
  margin-top: var(--zn-spacing-2x-small, 4px);
56
68
  }
57
69
 
70
+ .slash-menu__divider {
71
+ height: var(--zn-panel-border-width, 1px);
72
+ margin: var(--zn-spacing-2x-small, 4px) 0;
73
+ background: var(--zn-panel-border-color);
74
+ }
75
+
76
+ .slash-menu__hints {
77
+ display: flex;
78
+ flex: 0 0 auto;
79
+ align-items: center;
80
+ gap: var(--zn-spacing-small, 12px);
81
+ padding: var(--zn-spacing-x-small, 8px) var(--zn-spacing-small, 12px);
82
+ border-top: solid var(--zn-panel-border-width, 1px) var(--zn-panel-border-color);
83
+ font-size: var(--zn-font-size-x-small);
84
+ color: var(--zn-color-neutral-500);
85
+ user-select: none;
86
+ -webkit-user-select: none;
87
+ }
88
+
89
+ .slash-menu__hint {
90
+ display: flex;
91
+ align-items: center;
92
+ gap: var(--zn-spacing-2x-small, 4px);
93
+ }
94
+
95
+ .slash-menu__key {
96
+ display: inline-flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ min-width: 22px;
100
+ height: 22px;
101
+ padding: 0 var(--zn-spacing-2x-small, 4px);
102
+ border: solid var(--zn-panel-border-width, 1px) var(--zn-panel-border-color);
103
+ border-radius: var(--zn-border-radius, 6px);
104
+ background: var(--zn-panel-background-color);
105
+ font-family: var(--zn-font-sans);
106
+ font-size: var(--zn-font-size-2x-small);
107
+ line-height: 1;
108
+ color: var(--zn-color-neutral-600);
109
+ }
110
+
58
111
  .slash-menu__item {
59
112
  display: flex;
60
113
  align-items: center;
61
- gap: var(--zn-spacing-x-small, 8px);
114
+ gap: var(--zn-spacing-small, 12px);
62
115
  width: 100%;
63
- padding: var(--zn-spacing-x-small, 8px) var(--zn-spacing-small, 12px);
116
+ padding: var(--zn-spacing-2x-small, 4px) var(--zn-spacing-x-small, 8px);
64
117
  border: none;
118
+ border-radius: var(--slash-menu-item-border-radius);
65
119
  background: transparent;
66
120
  color: inherit;
67
121
  font: inherit;
68
122
  text-align: left;
69
123
  cursor: pointer;
70
124
 
71
- &--active,
72
125
  &:hover:not(&--disabled) {
73
126
  background: var(--zn-input-background-color-hover);
74
127
  }
75
128
 
129
+ // The active item is where Enter lands, so it is tinted rather than merely shaded like a hover.
130
+ // The tints throughout are translucent, so one set of them suits a light and a dark panel alike.
131
+ &--active,
132
+ &--active:hover:not(&--disabled) {
133
+ background: rgba(var(--zn-color-primary), 0.08);
134
+ }
135
+
76
136
  &--disabled {
77
137
  opacity: 0.5;
78
138
  cursor: not-allowed;
@@ -83,8 +143,25 @@
83
143
  display: flex;
84
144
  align-items: center;
85
145
  justify-content: center;
86
- flex: 0 0 16px;
87
- color: var(--zn-color-neutral-500);
146
+ flex: 0 0 auto;
147
+ width: 32px;
148
+ height: 32px;
149
+ border: solid var(--zn-panel-border-width, 1px) rgba(var(--zn-color-primary), 0.18);
150
+ border-radius: var(--slash-menu-item-border-radius);
151
+ background: rgba(var(--zn-color-primary), 0.06);
152
+ color: var(--zn-color-neutral-700);
153
+
154
+ // Icons are all-or-nothing across a list, so an item without one still holds the column
155
+ &--empty {
156
+ border-color: transparent;
157
+ background: none;
158
+ }
159
+
160
+ .slash-menu__item--active & {
161
+ border-color: rgba(var(--zn-color-primary), 0.3);
162
+ background: rgba(var(--zn-color-primary), 0.14);
163
+ color: rgb(var(--zn-color-primary));
164
+ }
88
165
  }
89
166
 
90
167
  .slash-menu__text {
@@ -95,6 +172,7 @@
95
172
  }
96
173
 
97
174
  .slash-menu__label {
175
+ font-weight: var(--zn-font-weight-semibold);
98
176
  overflow: hidden;
99
177
  text-overflow: ellipsis;
100
178
  white-space: nowrap;
@@ -81,6 +81,113 @@ describe('<zn-slash-menu>', () => {
81
81
  expect(reachedDocument, 'mousedown does not bubble to document').to.be.false;
82
82
  });
83
83
 
84
+ it('shows the keyboard hints unless they are hidden', async () => {
85
+ const el = await fixture<ZnSlashMenu>(html`
86
+ <zn-slash-menu></zn-slash-menu>`);
87
+ el.items = [{label: 'One'}];
88
+ el.show();
89
+ await el.updateComplete;
90
+
91
+ const hints = el.shadowRoot!.querySelector('[part="hints"]')!;
92
+ expect(hints).to.exist;
93
+ expect(hints.textContent).to.contain('navigate');
94
+ expect(hints.textContent).to.contain('select');
95
+ expect(hints.textContent).to.contain('dismiss');
96
+ expect(hints.querySelectorAll('kbd').length).to.equal(4);
97
+
98
+ el.hideHints = true;
99
+ await el.updateComplete;
100
+ expect(el.shadowRoot!.querySelector('[part="hints"]')).to.not.exist;
101
+ });
102
+
103
+ describe('recently used', () => {
104
+ const key = 'slash-menu-test';
105
+ const labelsOf = (el: ZnSlashMenu) =>
106
+ [...el.shadowRoot!.querySelectorAll('[data-slash-item] .slash-menu__label')].map(node => node.textContent);
107
+
108
+ const choose = async (el: ZnSlashMenu, label: string) => {
109
+ const buttons = [...el.shadowRoot!.querySelectorAll<HTMLElement>('[data-slash-item]')];
110
+ const button = buttons.find(item => item.querySelector('.slash-menu__label')?.textContent === label);
111
+ button!.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, cancelable: true}));
112
+ await el.updateComplete;
113
+ };
114
+
115
+ const items = [
116
+ {label: 'One', value: '1'},
117
+ {label: 'Two', value: '2'},
118
+ {label: 'Three', value: '3'}
119
+ ];
120
+
121
+ beforeEach(() => localStorage.removeItem('zn-slash-recent:' + key));
122
+
123
+ it('lists the items chosen here first, newest first, and remembers them', async () => {
124
+ const el = await fixture<ZnSlashMenu>(html`
125
+ <zn-slash-menu recent-key=${key} max-recent="2"></zn-slash-menu>`);
126
+ el.items = items;
127
+ el.show();
128
+ await el.updateComplete;
129
+
130
+ expect(labelsOf(el), 'nothing has been chosen yet').to.eql(['One', 'Two', 'Three']);
131
+
132
+ await choose(el, 'Three');
133
+ await choose(el, 'Two');
134
+
135
+ expect(labelsOf(el)).to.eql(['Two', 'Three', 'One', 'Two', 'Three']);
136
+ expect(el.shadowRoot!.querySelector('[part="group-heading"]')!.textContent).to.contain('Recently used');
137
+ expect(el.activeItem?.label, 'starts on the most recent item').to.equal('Two');
138
+
139
+ const other = await fixture<ZnSlashMenu>(html`
140
+ <zn-slash-menu recent-key=${key} max-recent="2" recent-heading="Your favourites"></zn-slash-menu>`);
141
+ other.items = items;
142
+ other.show();
143
+ await other.updateComplete;
144
+
145
+ expect(labelsOf(other), 'the list outlives the element').to.eql(['Two', 'Three', 'One', 'Two', 'Three']);
146
+ expect(other.shadowRoot!.querySelector('[part="group-heading"]')!.textContent).to.contain('Your favourites');
147
+
148
+ other.clearRecent();
149
+ await other.updateComplete;
150
+ expect(labelsOf(other)).to.eql(['One', 'Two', 'Three']);
151
+ });
152
+
153
+ it('closes the section with a rule only when the items below it have no heading', async () => {
154
+ const el = await fixture<ZnSlashMenu>(html`
155
+ <zn-slash-menu recent-key=${key}></zn-slash-menu>`);
156
+ el.items = items;
157
+ el.show();
158
+ await el.updateComplete;
159
+
160
+ await choose(el, 'Two');
161
+ expect(el.shadowRoot!.querySelector('[part="divider"]'), 'ungrouped items get a rule').to.exist;
162
+
163
+ el.items = items.map(item => ({...item, group: 'Basic blocks'}));
164
+ await el.updateComplete;
165
+
166
+ expect(el.shadowRoot!.querySelector('[part="divider"]'), 'a group heading separates them already').to.not.exist;
167
+ expect(el.shadowRoot!.querySelectorAll('[part="group-heading"]').length).to.equal(2);
168
+ });
169
+
170
+ it('stands aside while the user is searching, and stays off without a key', async () => {
171
+ const el = await fixture<ZnSlashMenu>(html`
172
+ <zn-slash-menu recent-key=${key}></zn-slash-menu>`);
173
+ el.items = items;
174
+ el.show();
175
+ await el.updateComplete;
176
+
177
+ await choose(el, 'Three');
178
+ el.query = 'o';
179
+ await el.updateComplete;
180
+
181
+ expect(labelsOf(el), 'the ranked matches are the answer to a query').to.eql(['One', 'Two', 'Three']);
182
+
183
+ el.recentKey = '';
184
+ el.query = '';
185
+ await el.updateComplete;
186
+
187
+ expect(labelsOf(el)).to.eql(['One', 'Two', 'Three']);
188
+ });
189
+ });
190
+
84
191
  it('reports how many matches were not rendered', async () => {
85
192
  const el = await fixture<ZnSlashMenu>(html`
86
193
  <zn-slash-menu max-items="2"></zn-slash-menu>`);
@@ -200,12 +200,18 @@ export default class ZnTextarea extends ZincElement implements ZincFormControl {
200
200
  /** The characters that open the slash menu. */
201
201
  @property({attribute: 'slash-trigger'}) slashTrigger = '/';
202
202
 
203
- /** The heading shown above the slash menu's items. */
203
+ /** The name the slash menu's list is announced by. */
204
204
  @property({attribute: 'slash-heading'}) slashHeading = 'Insert';
205
205
 
206
206
  /** Hides the insertion keys normally shown against the slash menu's items. */
207
207
  @property({attribute: 'slash-hide-keys', type: Boolean}) slashHideKeys = false;
208
208
 
209
+ /**
210
+ * Lists the items most recently chosen here above the rest, remembered in `localStorage` under this
211
+ * key. Share a key between the fields that should share a history; leave unset to offer no such list.
212
+ */
213
+ @property({attribute: 'slash-recent-key'}) slashRecentKey = '';
214
+
209
215
  /**
210
216
  * Resolves additional items each time the menu opens, for lists that come from elsewhere (e.g. an
211
217
  * API). Receives the current query and may return a promise. JavaScript only.
@@ -587,7 +593,11 @@ export default class ZnTextarea extends ZincElement implements ZincFormControl {
587
593
  </div>
588
594
  ${this.hasSlashMenu
589
595
  ? html`
590
- <zn-slash-menu part="slash-menu" heading=${this.slashHeading} ?hide-keys=${this.slashHideKeys}></zn-slash-menu>`
596
+ <zn-slash-menu
597
+ part="slash-menu"
598
+ heading=${this.slashHeading}
599
+ recent-key=${this.slashRecentKey}
600
+ ?hide-keys=${this.slashHideKeys}></zn-slash-menu>`
591
601
  : ''}
592
602
  <slot name="slash-menu"></slot>
593
603
  <slot name="slash-items" hidden></slot>
@@ -151,8 +151,8 @@ describe('<zn-textarea>', () => {
151
151
 
152
152
  expect(el.shadowRoot!.querySelector('zn-slash-menu'), 'no second menu is created').to.be.null;
153
153
  expect(slotted.items.map(item => item.label)).to.deep.equal(['Brand name', 'Support email']);
154
- expect(slotted.shadowRoot!.querySelector('[part="heading"]')?.textContent?.trim())
155
- .to.contain('Replacements');
154
+ expect(slotted.shadowRoot!.querySelector('[part="list"]')?.getAttribute('aria-label'))
155
+ .to.equal('Replacements');
156
156
  expect(menuItemLabels(slotted), 'the author\'s max-items is respected').to.deep.equal(['Brand name']);
157
157
 
158
158
  press(el, 'Enter');
@@ -69,12 +69,15 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
69
69
  /** The characters that open the slash menu. */
70
70
  @property({attribute: 'slash-trigger'}) slashTrigger = '/';
71
71
 
72
- /** The heading shown above the slash menu's items. */
72
+ /** The name the slash menu's list is announced by. */
73
73
  @property({attribute: 'slash-heading'}) slashHeading = 'Insert';
74
74
 
75
75
  /** Hides the insertion keys normally shown against the slash menu's items. */
76
76
  @property({attribute: 'slash-hide-keys', type: Boolean}) slashHideKeys = false;
77
77
 
78
+ /** Lists the slash menu items most recently chosen here above the rest, remembered under this key. */
79
+ @property({attribute: 'slash-recent-key'}) slashRecentKey = '';
80
+
78
81
  /** Resolves additional slash menu items each time the menu opens. JavaScript only. */
79
82
  @property({attribute: false}) slashItemsProvider?: (query: string) => SlashMenuItem[] | Promise<SlashMenuItem[]>;
80
83
 
@@ -447,6 +450,7 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
447
450
  slash-trigger="${this.slashTrigger}"
448
451
  slash-heading="${this.slashHeading}"
449
452
  slash-preset="${this.slashPreset}"
453
+ slash-recent-key="${this.slashRecentKey}"
450
454
  ?slash-hide-keys="${this.slashHideKeys}"
451
455
  .slashItems="${this.slashItems}"
452
456
  .slashItemsProvider="${this.slashItemsProvider}"