@kubex/zinc 1.0.18 → 1.0.20

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 (32) hide show
  1. package/dist/custom-elements.json +721 -8
  2. package/dist/vscode.html-custom-data.json +98 -3
  3. package/dist/web-types.json +195 -5
  4. package/dist/zn.d.ts +156 -4
  5. package/dist/zn.min.js +744 -668
  6. package/docs/pages/components/input-group.md +61 -0
  7. package/docs/pages/components/translations.md +53 -0
  8. package/package.json +1 -1
  9. package/src/components/audio-select/audio-select.component.ts +131 -0
  10. package/src/components/audio-select/audio-select.scss +4 -0
  11. package/src/components/audio-select/audio-select.test.ts +10 -0
  12. package/src/components/audio-select/index.ts +12 -0
  13. package/src/components/checkbox/checkbox.component.ts +66 -11
  14. package/src/components/checkbox/checkbox.scss +102 -15
  15. package/src/components/data-table/data-table.component.ts +7 -0
  16. package/src/components/inline-edit/inline-edit.component.ts +2 -1
  17. package/src/components/input-group/index.ts +12 -0
  18. package/src/components/input-group/input-group.component.ts +73 -0
  19. package/src/components/input-group/input-group.scss +65 -0
  20. package/src/components/input-group/input-group.test.ts +98 -0
  21. package/src/components/item/item.component.ts +5 -1
  22. package/src/components/item/item.scss +4 -0
  23. package/src/components/navbar/navbar.component.ts +21 -6
  24. package/src/components/navbar/navbar.scss +17 -1
  25. package/src/components/select/select.component.ts +7 -11
  26. package/src/components/select/select.scss +3 -3
  27. package/src/components/tabs/tabs.component.ts +33 -24
  28. package/src/components/translations/index.ts +6 -0
  29. package/src/components/translations/translations.component.ts +214 -0
  30. package/src/components/translations/translations.scss +15 -0
  31. package/src/components/translations/translations.test.ts +39 -0
  32. package/src/zinc.ts +3 -0
@@ -0,0 +1,73 @@
1
+ import {classMap} from 'lit/directives/class-map.js';
2
+ import {type CSSResultGroup, html, unsafeCSS} from 'lit';
3
+ import {HasSlotController} from '../../internal/slot';
4
+ import {property, query} from 'lit/decorators.js';
5
+ import ZincElement from '../../internal/zinc-element';
6
+
7
+ import styles from './input-group.scss';
8
+
9
+ /**
10
+ * @summary A wrapper component that groups inputs and selects visually.
11
+ * @documentation https://zinc.style/components/input-group
12
+ * @status experimental
13
+ * @since 1.0
14
+ *
15
+ * @slot - The default slot for inputs and selects.
16
+ * @slot label - The input group's label. Alternatively, you can use the `label` attribute.
17
+ *
18
+ * @csspart base - The component's base wrapper.
19
+ * @csspart form-control - The form control wrapper.
20
+ * @csspart form-control-label - The label's wrapper.
21
+ * @csspart form-control-input - The input group container.
22
+ */
23
+ export default class ZnInputGroup extends ZincElement {
24
+ static styles: CSSResultGroup = unsafeCSS(styles);
25
+
26
+ private readonly hasSlotController = new HasSlotController(this, 'label');
27
+
28
+ @query('slot:not([name])') defaultSlot: HTMLSlotElement;
29
+
30
+ /** The input group's label. If you need to display HTML, use the `label` slot. */
31
+ @property() label = '';
32
+
33
+ private handleSlotChange() {
34
+ const slottedElements = [...this.defaultSlot.assignedElements({flatten: true})] as HTMLElement[];
35
+
36
+ // Filter for inputs and selects
37
+ const supportedTags = ['ZN-INPUT', 'ZN-SELECT', 'ZN-BUTTON'];
38
+ const controls = slottedElements.filter(el => supportedTags.includes(el.tagName));
39
+
40
+ controls.forEach(el => {
41
+ const index = controls.indexOf(el);
42
+
43
+ el.toggleAttribute('data-zn-input-group__input', true);
44
+ el.toggleAttribute('data-zn-input-group__input--first', index === 0);
45
+ el.toggleAttribute('data-zn-input-group__input--inner', index > 0 && index < controls.length - 1);
46
+ el.toggleAttribute('data-zn-input-group__input--last', index === controls.length - 1);
47
+ });
48
+ }
49
+
50
+ render() {
51
+ const hasLabelSlot = this.hasSlotController.test('label');
52
+ const hasLabel = this.label ? true : hasLabelSlot;
53
+
54
+ return html`
55
+ <div part="form-control"
56
+ class="${classMap({
57
+ 'form-control': true,
58
+ 'form-control--has-label': hasLabel,
59
+ })}">
60
+
61
+ <label part="form-control-label"
62
+ id="label"
63
+ class="form-control__label"
64
+ aria-hidden=${hasLabel ? 'false' : 'true'}>
65
+ <slot name="label">${this.label}</slot>
66
+ </label>
67
+
68
+ <div part="base form-control-input" class="input-group" role="group" aria-labelledby="label">
69
+ <slot @slotchange=${this.handleSlotChange}></slot>
70
+ </div>
71
+ </div>`;
72
+ }
73
+ }
@@ -0,0 +1,65 @@
1
+ @use "../../wc";
2
+ @use "../../form-control";
3
+
4
+ :host {
5
+ display: block;
6
+ }
7
+
8
+ .input-group {
9
+ display: flex;
10
+ flex-wrap: nowrap;
11
+ align-items: center;
12
+ width: 100%;
13
+ }
14
+
15
+ /* Common styles for slotted inputs */
16
+ ::slotted([data-zn-input-group__input]) {
17
+ /* Overlap borders */
18
+ margin-inline-end: -1px;
19
+
20
+ /* Ensure focus ring is above */
21
+ position: relative;
22
+ z-index: 1;
23
+ }
24
+
25
+ ::slotted([data-zn-input-group__input]:focus-within) {
26
+ z-index: 2;
27
+ }
28
+
29
+ ::slotted([data-zn-input-group__input--first]) {
30
+ --zn-input-border-radius-x-small: var(--zn-border-radius-medium) 0 0 var(--zn-border-radius-medium);
31
+ --zn-input-border-radius-small: var(--zn-border-radius-medium) 0 0 var(--zn-border-radius-medium);
32
+ --zn-input-border-radius-medium: var(--zn-border-radius-medium) 0 0 var(--zn-border-radius-medium);
33
+ --zn-input-border-radius-large: var(--zn-border-radius-medium) 0 0 var(--zn-border-radius-medium);
34
+ }
35
+
36
+ ::slotted([data-zn-input-group__input--inner]) {
37
+ --zn-input-border-radius-x-small: 0;
38
+ --zn-input-border-radius-small: 0;
39
+ --zn-input-border-radius-medium: 0;
40
+ --zn-input-border-radius-large: 0;
41
+ }
42
+
43
+ ::slotted([data-zn-input-group__input--last]) {
44
+ --zn-input-border-radius-x-small: 0 var(--zn-border-radius-medium) var(--zn-border-radius-medium) 0;
45
+ --zn-input-border-radius-small: 0 var(--zn-border-radius-medium) var(--zn-border-radius-medium) 0;
46
+ --zn-input-border-radius-medium: 0 var(--zn-border-radius-medium) var(--zn-border-radius-medium) 0;
47
+ --zn-input-border-radius-large: 0 var(--zn-border-radius-medium) var(--zn-border-radius-medium) 0;
48
+
49
+ margin-inline-end: 0;
50
+ }
51
+
52
+ /* Button overrides */
53
+ ::slotted(zn-button[data-zn-input-group__input])::part(base) {
54
+ border-radius: 0;
55
+ }
56
+
57
+ ::slotted(zn-button[data-zn-input-group__input--first])::part(base) {
58
+ border-start-start-radius: var(--zn-border-radius-medium);
59
+ border-end-start-radius: var(--zn-border-radius-medium);
60
+ }
61
+
62
+ ::slotted(zn-button[data-zn-input-group__input--last])::part(base) {
63
+ border-start-end-radius: var(--zn-border-radius-medium);
64
+ border-end-end-radius: var(--zn-border-radius-medium);
65
+ }
@@ -0,0 +1,98 @@
1
+ import '../../components/input/index';
2
+ import '../../components/select/index';
3
+ import '../../components/button/index';
4
+ import './index';
5
+ import {expect, fixture, html} from '@open-wc/testing';
6
+ import type ZnInputGroup from './input-group.component';
7
+
8
+ // We need to ensure components are defined for the test
9
+ customElements.define('zn-input-group-test-input', class extends HTMLElement {
10
+ });
11
+
12
+ describe('<zn-input-group>', () => {
13
+ it('should render a component', async () => {
14
+ const el = await fixture(html`
15
+ <zn-input-group></zn-input-group> `);
16
+ expect(el).to.exist;
17
+ });
18
+
19
+ it('should apply data attributes to slotted inputs', async () => {
20
+ const el = await fixture<ZnInputGroup>(html`
21
+ <zn-input-group>
22
+ <zn-input></zn-input>
23
+ <zn-input></zn-input>
24
+ <zn-input></zn-input>
25
+ </zn-input-group>
26
+ `);
27
+
28
+ await el.updateComplete;
29
+ await new Promise(r => setTimeout(r, 100));
30
+
31
+ const inputs = el.querySelectorAll('zn-input');
32
+ expect(inputs[0]).to.have.attribute('data-zn-input-group__input');
33
+ expect(inputs[0]).to.have.attribute('data-zn-input-group__input--first');
34
+
35
+ expect(inputs[1]).to.have.attribute('data-zn-input-group__input');
36
+ expect(inputs[1]).to.have.attribute('data-zn-input-group__input--inner');
37
+
38
+ expect(inputs[2]).to.have.attribute('data-zn-input-group__input');
39
+ expect(inputs[2]).to.have.attribute('data-zn-input-group__input--last');
40
+ });
41
+
42
+ it('should handle mixed inputs and selects', async () => {
43
+ const el = await fixture<ZnInputGroup>(html`
44
+ <zn-input-group>
45
+ <zn-input></zn-input>
46
+ <zn-select></zn-select>
47
+ </zn-input-group>
48
+ `);
49
+
50
+ await new Promise(r => setTimeout(r, 100));
51
+
52
+ const controls = el.querySelectorAll('zn-input, zn-select');
53
+ expect(controls[0]).to.have.attribute('data-zn-input-group__input--first');
54
+ expect(controls[1]).to.have.attribute('data-zn-input-group__input--last');
55
+ });
56
+
57
+ it('should handle button at the end', async () => {
58
+ const el = await fixture<ZnInputGroup>(html`
59
+ <zn-input-group>
60
+ <zn-input></zn-input>
61
+ <zn-button>Submit</zn-button>
62
+ </zn-input-group>
63
+ `);
64
+
65
+ await new Promise(r => setTimeout(r, 100));
66
+
67
+ const input = el.querySelector('zn-input');
68
+ const button = el.querySelector('zn-button');
69
+
70
+ expect(input).to.have.attribute('data-zn-input-group__input--first');
71
+ expect(button).to.have.attribute('data-zn-input-group__input--last');
72
+ expect(button).to.have.attribute('data-zn-input-group__input');
73
+ });
74
+
75
+ it('should render a label when the label attribute is set', async () => {
76
+ const el = await fixture<ZnInputGroup>(html`
77
+ <zn-input-group label="Test Label"></zn-input-group>
78
+ `);
79
+ await el.updateComplete;
80
+ const label = el.shadowRoot?.querySelector('.form-control__label');
81
+ expect(label).to.exist;
82
+ expect(label).to.contain.text('Test Label');
83
+ });
84
+
85
+ it('should render a label from the label slot', async () => {
86
+ const el = await fixture<ZnInputGroup>(html`
87
+ <zn-input-group>
88
+ <span slot="label">Slot Label</span>
89
+ </zn-input-group>
90
+ `);
91
+ await el.updateComplete;
92
+ const label = el.shadowRoot?.querySelector('.form-control__label');
93
+ expect(label).to.exist;
94
+ const slot = label?.querySelector('slot');
95
+ expect(slot).to.exist;
96
+ // Note: checking slot content in shadow DOM is tricky, but existence of label wrapper is enough to prove logic works
97
+ });
98
+ });
@@ -51,6 +51,9 @@ export default class ZnItem extends ZincElement {
51
51
  // align items to the right
52
52
  @property({type: Boolean, attribute: 'align-end'}) alignEnd: boolean;
53
53
 
54
+ // align items in the center
55
+ @property({type: Boolean, attribute: 'align-center'}) alignCenter: boolean;
56
+
54
57
  connectedCallback() {
55
58
  super.connectedCallback();
56
59
  this.setAttribute('role', 'listitem');
@@ -95,7 +98,8 @@ export default class ZnItem extends ZincElement {
95
98
  'item--large': this.size === 'large',
96
99
  'item--has-icon': hasIcon,
97
100
  'item--no-padding': this.noPadding,
98
- 'item--align-end': this.alignEnd
101
+ 'item--align-end': this.alignEnd,
102
+ 'item--align-center': this.alignCenter
99
103
  })}"
100
104
  part="base">
101
105
 
@@ -41,6 +41,10 @@
41
41
  text-align: right;
42
42
  }
43
43
 
44
+ &--align-center {
45
+ align-items: center;
46
+ }
47
+
44
48
  &__header {
45
49
  display: flex;
46
50
  align-items: center;
@@ -40,6 +40,7 @@ export default class ZnNavbar extends ZincElement {
40
40
  @property({type: Boolean}) stacked: boolean;
41
41
  @property({type: Array}) dropdown = [];
42
42
  @property({attribute: "no-pad", type: Boolean}) noPad: false
43
+ @property({attribute: 'manual-add-items', type: Boolean}) manualAddItems = false;
43
44
 
44
45
  private _preItems: NodeListOf<Element>;
45
46
  private _postItems: NodeListOf<Element>;
@@ -126,16 +127,21 @@ export default class ZnNavbar extends ZincElement {
126
127
  this._navItems?.classList.toggle('has-hidden', hasHidden && hideRemaining)
127
128
  }
128
129
 
129
- public addItem(item: any) {
130
- if (this._openedTabs.includes(item.getAttribute('tab-uri'))) {
130
+ public addItem(item: Element): void {
131
+ const tabUri = item.getAttribute('tab-uri');
132
+ if (typeof tabUri !== 'string' || this._openedTabs.includes(tabUri)) {
131
133
  return;
132
134
  }
133
- this._openedTabs.push(item.getAttribute('tab-uri'));
135
+ this._openedTabs.push(tabUri);
134
136
  const ul = this.shadowRoot?.querySelector('ul');
135
137
  const dropdown = this.shadowRoot?.querySelector('[id="dropdown-item"]');
136
138
  // @ts-expect-error
137
139
  dropdown.querySelector('zn-dropdown').hide();
138
- return dropdown ? ul?.insertBefore(item, dropdown) : ul?.appendChild(item);
140
+ if (dropdown) {
141
+ ul?.insertBefore(item, dropdown);
142
+ } else {
143
+ ul?.appendChild(item);
144
+ }
139
145
  }
140
146
 
141
147
  protected firstUpdated(_changedProperties: PropertyValues) {
@@ -166,6 +172,9 @@ export default class ZnNavbar extends ZincElement {
166
172
  const menu = this.shadowRoot?.querySelector('zn-menu');
167
173
  if (menu) {
168
174
  menu.addEventListener('zn-menu-select', (e: ZnMenuSelectEvent) => {
175
+ if (this.manualAddItems) {
176
+ return;
177
+ }
169
178
  const element = e.detail.element;
170
179
  if (element.hasAttribute('data-path')) {
171
180
  const li = document.createElement('li');
@@ -197,13 +206,19 @@ export default class ZnNavbar extends ZincElement {
197
206
  }
198
207
 
199
208
  return html`
200
- <div class="navbar__container">
209
+ <div class="${classMap({
210
+ 'navbar__container': true,
211
+ 'navbar__container--stacked': this.stacked,
212
+ 'navbar__container--icon-bar': this.iconBar
213
+ })}">
201
214
  <ul class="${classMap({
202
215
  'navbar': true,
203
216
  'navbar--slim': this.slim,
204
217
  'navbar--border': this.border,
205
218
  'navbar--flush': this.flush,
206
- 'navbar--no-pad': this.noPad
219
+ 'navbar--no-pad': this.noPad,
220
+ 'navbar--stacked': this.stacked,
221
+ 'navbar--icon-bar': this.iconBar
207
222
  })}">
208
223
  ${this._preItems}
209
224
  ${this.navigation.map((item: any) => {
@@ -146,6 +146,22 @@ ul.navbar {
146
146
 
147
147
  }
148
148
 
149
+ .navbar__container--stacked {
150
+ display: inline;
151
+ }
152
+
153
+ ul.navbar.navbar--stacked.navbar--icon-bar {
154
+ flex-direction: column;
155
+ padding: 0;
156
+ gap: 0;
157
+
158
+ li {
159
+ border-radius: 0;
160
+ align-items: flex-start;
161
+ padding: 8px;
162
+ }
163
+ }
164
+
149
165
 
150
166
  :host(:not([stacked]):not([icon-bar])) {
151
167
  ul.navbar {
@@ -236,7 +252,7 @@ ul.navbar {
236
252
  height: 2px;
237
253
  }
238
254
 
239
- &:hover:before {
255
+ &:hover:before:not(#dropdown-item) {
240
256
  // TODO Hover Before colors
241
257
  left: -2px;
242
258
  right: -2px;
@@ -98,7 +98,7 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
98
98
  @state() currentOption: ZnOption;
99
99
  @state() selectedOptions: ZnOption[] = [];
100
100
  @state() private valueHasChanged: boolean = false;
101
- @state() private hasCheckboxPrefix: boolean = false;
101
+ @state() private inputPrefix: boolean = false;
102
102
 
103
103
  /** The name of the select, submitted as a name/value pair with form data. */
104
104
  @property() name = '';
@@ -271,13 +271,9 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
271
271
  }
272
272
  }
273
273
 
274
- private updateHasCheckboxPrefix() {
275
- try {
276
- const assigned = this.prefixSlot?.assignedElements({flatten: true}) || [];
277
- this.hasCheckboxPrefix = assigned.some(el => el.tagName === 'ZN-CHECKBOX');
278
- } catch {
279
- this.hasCheckboxPrefix = false;
280
- }
274
+ private updateHasInputPrefix() {
275
+ const assigned = this.prefixSlot?.assignedElements({flatten: true}) || [];
276
+ this.inputPrefix = assigned.length > 0;
281
277
  }
282
278
 
283
279
  private addOpenListeners() {
@@ -772,7 +768,7 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
772
768
  });
773
769
  }
774
770
 
775
- this.updateHasCheckboxPrefix();
771
+ this.updateHasInputPrefix();
776
772
  }
777
773
 
778
774
  @watch('disabled', {waitUntilFirstUpdate: true})
@@ -971,7 +967,7 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
971
967
  'select--small': this.size === 'small',
972
968
  'select--medium': this.size === 'medium',
973
969
  'select--large': this.size === 'large',
974
- 'select--has-checkbox-prefix': this.hasCheckboxPrefix
970
+ 'select--has-input-prefix': this.inputPrefix
975
971
  })}
976
972
  placement=${this.placement}
977
973
  strategy=${this.hoist ? 'fixed' : 'absolute'}
@@ -989,7 +985,7 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
989
985
  <slot part="prefix"
990
986
  name="prefix"
991
987
  class="select__prefix"
992
- @slotchange=${() => this.updateHasCheckboxPrefix()}></slot>
988
+ @slotchange=${() => this.updateHasInputPrefix()}></slot>
993
989
 
994
990
  <input
995
991
  part="display-input"
@@ -183,7 +183,7 @@
183
183
  padding-inline: var(--zn-input-spacing-medium);
184
184
  }
185
185
 
186
- .select--has-checkbox-prefix.select--medium .select__combobox {
186
+ .select--has-input-prefix.select--medium .select__combobox {
187
187
  padding-inline-start: 0;
188
188
  }
189
189
 
@@ -212,7 +212,7 @@
212
212
  padding-inline: var(--zn-input-spacing-large);
213
213
  }
214
214
 
215
- .select--has-checkbox-prefix.select--large .select__combobox {
215
+ .select--has-input-prefix.select--large .select__combobox {
216
216
  padding-inline-start: 0;
217
217
  }
218
218
 
@@ -258,7 +258,7 @@
258
258
  }
259
259
 
260
260
  /* When a checkbox is used in the prefix, remove left padding so the prefix joins flush */
261
- .select--has-checkbox-prefix.select--standard .select__combobox {
261
+ .select--has-input-prefix.select--standard .select__combobox {
262
262
  padding-inline-start: 0;
263
263
  }
264
264
 
@@ -254,7 +254,12 @@ export default class ZnTabs extends ZincElement {
254
254
  // ts-ignore
255
255
  const target = (event.relatedTarget ?? event.target) as HTMLElement;
256
256
  if (target) {
257
- this.clickTab(target, event.altKey);
257
+ if ('startViewTransition' in document) {
258
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
259
+ (document as any).startViewTransition(() => this.clickTab(target, event.altKey));
260
+ } else {
261
+ this.clickTab(target, event.altKey)
262
+ }
258
263
  }
259
264
  }
260
265
 
@@ -333,8 +338,9 @@ export default class ZnTabs extends ZincElement {
333
338
  return false;
334
339
  }
335
340
 
341
+ const sameTab = this._current === tabName;
336
342
  // Multi click on an active tab will refresh the tab
337
- if (this._current === tabName) {
343
+ if (sameTab) {
338
344
  this._activeClicks++;
339
345
  if (this._activeClicks > 2) {
340
346
  refresh = true;
@@ -345,32 +351,35 @@ export default class ZnTabs extends ZincElement {
345
351
  }
346
352
 
347
353
  let inSlot = true;
348
- this._panels.forEach((elements, key) => {
349
- const isActive = key === tabName;
350
- elements.forEach((element) => {
351
- if (isActive && element.parentNode !== this) {
352
- inSlot = false;
353
- }
354
- element.toggleAttribute('selected', isActive);
355
- if (isActive && refresh) {
356
- let uri = "";
357
- let gaid = "";
358
- if (this._activeTab && this._activeTab.hasAttribute('tab-uri')) {
359
- uri = this._activeTab.getAttribute('tab-uri')!;
360
- gaid = this._activeTab.getAttribute('gaid')!;
354
+ const updateTabs = () => {
355
+ this._panels.forEach((elements, key) => {
356
+ const isActive = key === tabName;
357
+ elements.forEach((element) => {
358
+ if (isActive && element.parentNode !== this) {
359
+ inSlot = false;
361
360
  }
362
- document.dispatchEvent(new CustomEvent('zn-refresh-element', {
363
- detail: {element: element, uri: uri, gaid: gaid}
364
- }));
365
- }
361
+ element.toggleAttribute('selected', isActive);
362
+ if (isActive && refresh) {
363
+ let uri = "";
364
+ let gaid = "";
365
+ if (this._activeTab && this._activeTab.hasAttribute('tab-uri')) {
366
+ uri = this._activeTab.getAttribute('tab-uri')!;
367
+ gaid = this._activeTab.getAttribute('gaid')!;
368
+ }
369
+ document.dispatchEvent(new CustomEvent('zn-refresh-element', {
370
+ detail: {element: element, uri: uri, gaid: gaid}
371
+ }));
372
+ }
373
+ });
366
374
  });
367
- });
368
375
 
369
- if (this._panel) {
370
- this._panel.classList.toggle('contents-slot', !inSlot);
371
- }
376
+ if (this._panel) {
377
+ this._panel.classList.toggle('contents-slot', !inSlot);
378
+ }
379
+ this._current = tabName;
380
+ };
372
381
 
373
- this._current = tabName;
382
+ updateTabs();
374
383
 
375
384
  return true;
376
385
  }
@@ -0,0 +1,6 @@
1
+ import ZnTranslations from './translations.component';
2
+
3
+ export * from './translations.component';
4
+ export default ZnTranslations;
5
+
6
+ ZnTranslations.define('zn-translations');