@kubex/zinc 1.0.12 → 1.0.14

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 (49) hide show
  1. package/dist/custom-elements.json +2611 -2187
  2. package/dist/vscode.html-custom-data.json +185 -155
  3. package/dist/web-types.json +420 -334
  4. package/dist/zn.d.ts +133 -15
  5. package/dist/zn.min.css +1 -1
  6. package/dist/zn.min.js +603 -551
  7. package/docs/pages/components/data-select.md +1 -1
  8. package/docs/pages/components/filter-container.md +72 -0
  9. package/docs/pages/components/filter-wrapper.md +2 -2
  10. package/docs/pages/components/hover-container.md +20 -0
  11. package/docs/pages/components/reveal.md +24 -0
  12. package/docs/pages/components/select.md +39 -1
  13. package/docs/pages/components/toggle.md +6 -3
  14. package/package.json +1 -1
  15. package/scss/_root.scss +4 -4
  16. package/src/components/button/button.component.ts +11 -5
  17. package/src/components/content-block/content-block.scss +35 -23
  18. package/src/components/data-select/data-select.component.ts +5 -2
  19. package/src/components/data-table/data-table.component.ts +128 -27
  20. package/src/components/data-table/data-table.scss +56 -10
  21. package/src/components/dropdown/dropdown.component.ts +3 -0
  22. package/src/components/editor/editor.component.ts +1 -1
  23. package/src/components/filter-container/filter-container.component.ts +112 -0
  24. package/src/components/filter-container/filter-container.scss +5 -0
  25. package/src/components/filter-container/filter-container.test.ts +10 -0
  26. package/src/components/filter-container/index.ts +12 -0
  27. package/src/components/filter-wrapper/filter-wrapper.component.ts +1 -1
  28. package/src/components/form-group/form-group.component.ts +26 -26
  29. package/src/components/header/header.scss +7 -0
  30. package/src/components/navbar/navbar.scss +4 -0
  31. package/src/components/note/note.component.ts +1 -0
  32. package/src/components/option/option.component.ts +3 -3
  33. package/src/components/option/option.scss +5 -9
  34. package/src/components/progress-bar/progress-bar.component.ts +1 -1
  35. package/src/components/progress-tile/progress-tile.component.ts +2 -2
  36. package/src/components/reveal/index.ts +12 -0
  37. package/src/components/reveal/reveal.component.ts +92 -0
  38. package/src/components/reveal/reveal.scss +18 -0
  39. package/src/components/reveal/reveal.test.ts +10 -0
  40. package/src/components/select/select.component.ts +63 -7
  41. package/src/components/sp/sp.component.ts +2 -0
  42. package/src/components/split-pane/split-pane.component.ts +2 -1
  43. package/src/components/tile/tile.component.ts +3 -1
  44. package/src/components/toggle/toggle.component.ts +33 -23
  45. package/src/components/tooltip/tooltip.component.ts +10 -5
  46. package/src/internal/form.ts +5 -0
  47. package/src/utilities/top-layer-manager.ts +32 -0
  48. package/src/wc.scss +9 -5
  49. package/src/zinc.ts +2 -0
@@ -8,7 +8,7 @@ layout: component
8
8
  ```html:preview
9
9
 
10
10
  <div class="form-spacing">
11
- <zn-data-select label="Country" provider="country" name="country"></zn-data-select>
11
+ <zn-data-select label="Country" provider="country" name="country" multiple></zn-data-select>
12
12
  <zn-data-select label="Currency" provider="currency" name="currency"></zn-data-select>
13
13
  <zn-data-select label="Color" provider="color" name="color"></zn-data-select>
14
14
  </div>
@@ -0,0 +1,72 @@
1
+ ---
2
+ meta:
3
+ title: Filter Container
4
+ description:
5
+ layout: component
6
+ ---
7
+
8
+ ```html:preview
9
+
10
+ <zn-filter-container>
11
+ <zn-sp flush>
12
+ <zn-panel caption="Section One">
13
+ <div style="display: flex; flex-direction: column;">
14
+ <div data-filter="John Doe"
15
+ style="padding: 10px 0; display: flex; flex-direction: column; border-bottom: 1px solid grey;">
16
+ <strong>John Doe</strong>
17
+ <span>john.doe@zinc.style</span>
18
+ </div>
19
+
20
+ <div data-filter="Gary Smith"
21
+ style="padding: 10px 0; display: flex; flex-direction: column; border-bottom: 1px solid grey;">
22
+ <strong>Gary Smith</strong>
23
+ <span>gary.smith@zinc.style</span>
24
+ </div>
25
+
26
+ <div data-filter="Lisa Wong"
27
+ style="padding: 10px 0; display: flex; flex-direction: column; border-bottom: 1px solid grey;">
28
+ <strong>Lisa Wong</strong>
29
+ <span>lisa.wong@zinc.style</span>
30
+ </div>
31
+
32
+ <div data-filter="Emma Johnson"
33
+ style="padding: 10px 0; display: flex; flex-direction: column; border-bottom: 1px solid grey;">
34
+ <strong>Emma Johnson</strong>
35
+ <span>emma.johnson@zinc.style</span>
36
+ </div>
37
+ </div>
38
+
39
+ </zn-panel>
40
+
41
+ <zn-panel caption="Section Two">
42
+ <div style="display: flex; flex-direction: column;">
43
+ <div data-filter="Michael Brown"
44
+ style="padding: 10px 0; display: flex; flex-direction: column; border-bottom: 1px solid grey;">
45
+ <strong>Michael Brown</strong>
46
+ <span>michael.brown@zinc.style</span>
47
+ </div>
48
+ <div data-filter="Sophia Davis"
49
+ style="padding: 10px 0; display: flex; flex-direction: column; border-bottom: 1px solid grey;">
50
+ <strong>Sophia Davis</strong>
51
+ <span>sophia.davis@zinc.style</span>
52
+ </div>
53
+ <div data-filter="Lisa Wong"
54
+ style="padding: 10px 0; display: flex; flex-direction: column; border-bottom: 1px solid grey;">
55
+ <strong>Lisa Wong</strong>
56
+ <span>lisa.wong@zinc.style</span>
57
+ </div>
58
+ </div>
59
+ </zn-panel>
60
+ </zn-sp>
61
+ </zn-filter-container>
62
+ ```
63
+
64
+ ## Examples
65
+
66
+ ### First Example
67
+
68
+ TODO
69
+
70
+ ### Second Example
71
+
72
+ TODO
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  meta:
3
- title: Query Wrapper
3
+ title: Filter Wrapper
4
4
  description:
5
5
  layout: component
6
6
  ---
7
7
 
8
8
  ```html:preview
9
- <zn-query-wrapper></zn-query-wrapper>
9
+ <zn-filter-wrapper></zn-filter-wrapper>
10
10
  ```
11
11
 
12
12
  ## Examples
@@ -0,0 +1,20 @@
1
+ ---
2
+ meta:
3
+ title: Hover Container
4
+ description:
5
+ layout: component
6
+ ---
7
+
8
+ ```html:preview
9
+ <zn-hover-container></zn-hover-container>
10
+ ```
11
+
12
+ ## Examples
13
+
14
+ ### First Example
15
+
16
+ TODO
17
+
18
+ ### Second Example
19
+
20
+ TODO
@@ -0,0 +1,24 @@
1
+ ---
2
+ meta:
3
+ title: Reveal
4
+ description:
5
+ layout: component
6
+ ---
7
+
8
+ ```html:preview
9
+
10
+ <zn-reveal duration="3000"
11
+ initial="******@hotmail.com"
12
+ revealed="john.doe@hotmail.com">
13
+ </zn-reveal>
14
+ ```
15
+
16
+ ## Examples
17
+
18
+ ### First Example
19
+
20
+ TODO
21
+
22
+ ### Second Example
23
+
24
+ TODO
@@ -39,6 +39,44 @@ This component works with standard `<form>` elements. Please refer to the sectio
39
39
  on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
40
40
  :::
41
41
 
42
+ ### Distinct Selects
43
+
44
+
45
+ ```html:preview
46
+
47
+ <div class="form-spacing">
48
+ <zn-select provider="country" multiple name="country-one" id="country-one" distinct="country-two">
49
+ <zn-option value="0">Option 0</zn-option>
50
+ <zn-option value="1">Option 1</zn-option>
51
+ <zn-option value="2">Option 2</zn-option>
52
+ </zn-select>
53
+ <zn-select provider="country" multiple name="country-two" id="country-two" distinct="country-one">
54
+ <zn-option value="0">Option 0</zn-option>
55
+ <zn-option value="1">Option 1</zn-option>
56
+ <zn-option value="2">Option 2</zn-option>
57
+ </zn-select>
58
+ </div>
59
+ ```
60
+
61
+ ### Conditional Selects
62
+
63
+
64
+ ```html:preview
65
+
66
+ <div class="form-spacing">
67
+ <zn-select provider="conditional" multiple name="conditional-one" id="conditional-one" conditional="conditional-two">
68
+ <zn-option value="0">Option 0</zn-option>
69
+ <zn-option value="1">Option 1</zn-option>
70
+ <zn-option value="2">Option 2</zn-option>
71
+ </zn-select>
72
+ <zn-select provider="conditional" multiple name="conditional-two" id="conditional-two" conditional="conditional-one">
73
+ <zn-option value="0">Option 0</zn-option>
74
+ <zn-option value="1">Option 1</zn-option>
75
+ <zn-option value="2">Option 2</zn-option>
76
+ </zn-select>
77
+ </div>
78
+ ```
79
+
42
80
  ### Help Text
43
81
 
44
82
  Add descriptive help text to a select with the `help-text` attribute. For help texts that contain HTML, use the
@@ -124,7 +162,7 @@ the most common for selects), include an empty option that people can select to
124
162
  :::
125
163
 
126
164
  ```html:preview
127
- <zn-select label="Clearable multi-choice select" clearable multiple value="option-1 option-2" help-text="For multi-choice selects only, display an icon button to let people clear their selections">
165
+ <zn-select label="Clearable multi-choice select" clearable multiple help-text="For multi-choice selects only, display an icon button to let people clear their selections">
128
166
  <zn-option value="option-1">Option 1</zn-option>
129
167
  <zn-option value="option-2">Option 2</zn-option>
130
168
  <zn-option value="option-3">Option 3</zn-option>
@@ -6,14 +6,17 @@ layout: component
6
6
  ---
7
7
 
8
8
  ```html:preview
9
- <zn-toggle on-text="On" off-text="off" label="Renew?"></zn-toggle>
9
+ <zn-toggle></zn-toggle>
10
10
  ```
11
11
 
12
12
  ## Examples
13
13
 
14
- ### First Example
14
+ ### With Tooltip
15
15
 
16
- TODO
16
+ ```html:preview
17
+
18
+ <zn-toggle on-text="Available" off-text="Unavailable" label="Status"></zn-toggle>
19
+ ```
17
20
 
18
21
  ### Second Example
19
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
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",
package/scss/_root.scss CHANGED
@@ -6,8 +6,8 @@
6
6
  :root {
7
7
  // Brand Colors
8
8
  --zn-red: 237, 84, 109;
9
- --zn-blue: 100, 131, 242;
10
- --zn-green: 41, 186, 181;
9
+ --zn-blue: 100, 117, 242;
10
+ --zn-green: 43, 192, 145;
11
11
  --zn-orange: 255, 136, 0;
12
12
  --zn-yellow: 255, 193, 7;
13
13
  --zn-indigo: 63, 81, 181;
@@ -18,10 +18,10 @@
18
18
  --zn-accent: 71, 214, 214;
19
19
 
20
20
  // Theme Colors
21
- --zn-color-info: 100, 131, 242;
21
+ --zn-color-info: 100, 117, 242;
22
22
  --zn-color-warning: 255, 136, 0;
23
23
  --zn-color-error: 237, 84, 109;
24
- --zn-color-success: 41, 186, 181;
24
+ --zn-color-success: 43, 192, 145;
25
25
  --zn-color-disabled: 181, 181, 181;
26
26
 
27
27
  // Common Sizes
@@ -5,6 +5,7 @@ import {HasSlotController} from '../../internal/slot';
5
5
  import {html, literal} from 'lit/static-html.js';
6
6
  import {ifDefined} from 'lit/directives/if-defined.js';
7
7
  import {property, query, queryAll} from 'lit/decorators.js';
8
+ import {watch} from '../../internal/watch';
8
9
  import ZincElement from '../../internal/zinc-element';
9
10
  import ZnDropdown from "../dropdown";
10
11
  import ZnIcon from "../icon";
@@ -137,13 +138,18 @@ export default class ZnButton extends ZincElement implements ZincFormControl {
137
138
  this.teardownAutoClick();
138
139
  }
139
140
 
141
+ @watch('autoClick')
142
+ async handleAutoClickChange(_old: boolean, value: boolean) {
143
+ if (value) {
144
+ await this.updateComplete;
145
+ this.setupAutoClick();
146
+ } else {
147
+ this.teardownAutoClick();
148
+ }
149
+ }
150
+
140
151
  protected updated(changedProps: Map<string, any>) {
141
152
  super.updated(changedProps);
142
- if (changedProps.has('autoClick')) {
143
- if (this.autoClick) {
144
- this.setupAutoClick();
145
- }
146
- }
147
153
  if (this.button) {
148
154
  const oldOverlay = this.button.querySelector('.button--loading-fill');
149
155
  if (oldOverlay) {
@@ -73,52 +73,59 @@ zn-panel:not(.content-block--short) zn-header::part(header-description) {
73
73
  display: none;
74
74
  }
75
75
 
76
- li[data-list="ordered"] {
77
- counter-increment: list-0 1;
76
+ ol {
77
+ counter-reset: list-0 0;
78
78
  }
79
79
 
80
80
  @supports (counter-set:none) {
81
- li[data-list] {
81
+ ol li, ul li {
82
82
  counter-set: list-1 0 list-2 0 list-3 0 list-4 0 list-5 0 list-6 0 list-7 0 list-8 0 list-9 0;
83
83
  }
84
84
  }
85
85
 
86
- li[data-list=bullet] > .ql-ui:before {
87
- content: "•";
86
+ ol, ul {
87
+ padding: 0;
88
+ margin: 0;
88
89
  }
89
90
 
90
- li[data-list=checked] > .ql-ui:before {
91
- content: "☑";
91
+ li[data-list=checked] > .ql-ui, li[data-list=unchecked] > .ql-ui {
92
+ color: #777;
92
93
  }
93
94
 
94
- li[data-list=unchecked] > .ql-ui:before {
95
- content: "☐";
95
+ li {
96
+ list-style-type: none;
97
+ padding-left: 1em;
98
+ position: relative;
96
99
  }
97
100
 
98
- li[data-list="ordered"] > .ql-ui::before {
99
- content: counter(list-0) ". ";
101
+ ul > li::before {
102
+ content: "";
100
103
  }
101
104
 
102
- ol {
103
- padding: 0;
104
- margin: 0;
105
+ ol > li {
106
+ counter-increment: list-0;
105
107
  }
106
108
 
107
- li[data-list=checked] > .ql-ui, li[data-list=unchecked] > .ql-ui {
108
- color: #777;
109
+ ol > li::before {
110
+ content: counter(list-0) ". ";
109
111
  }
110
112
 
111
- .ql-ui {
112
- position: absolute;
113
+ /* Checkbox list markers */
114
+ li[data-list="checked"]::before {
115
+ content: "☑";
113
116
  }
114
117
 
115
- li {
116
- list-style-type: none;
117
- padding-left: 1em;
118
- position: relative;
118
+ li[data-list="unchecked"]::before {
119
+ content: "☐";
120
+ }
121
+
122
+ li[data-list="checked"]::before,
123
+ li[data-list="unchecked"]::before {
124
+ color: #777;
119
125
  }
120
126
 
121
- li > .ql-ui::before {
127
+ ol li::before,
128
+ ul li::before {
122
129
  display: inline-block;
123
130
  margin-left: -1.5em;
124
131
  margin-right: 0.3em;
@@ -126,3 +133,8 @@ li > .ql-ui::before {
126
133
  white-space: nowrap;
127
134
  width: 1.2em;
128
135
  }
136
+
137
+ li:has(> .ql-ui)::before {
138
+ content: none;
139
+ display: none;
140
+ }
@@ -7,12 +7,12 @@ import {
7
7
  emptyDataProvider,
8
8
  type LocalDataProvider,
9
9
  } from "./providers/provider";
10
- import {type CSSResultGroup, html, nothing, unsafeCSS, PropertyValues} from 'lit';
10
+ import {type CSSResultGroup, html, nothing, type PropertyValues, unsafeCSS} from 'lit';
11
11
  import {FormControlController} from "../../internal/form";
12
12
  import {property, query} from 'lit/decorators.js';
13
13
  import {watch} from "../../internal/watch";
14
- import type {ZincFormControl} from '../../internal/zinc-element';
15
14
  import ZincElement from '../../internal/zinc-element';
15
+ import type {ZincFormControl} from '../../internal/zinc-element';
16
16
  import type ZnSelect from "../select";
17
17
 
18
18
  import styles from './data-select.scss';
@@ -93,6 +93,8 @@ export default class ZnDataSelect extends ZincElement implements ZincFormControl
93
93
 
94
94
  @property({attribute: "icon-only", type: Boolean, reflect: true}) iconOnly = false;
95
95
 
96
+ @property({type: Boolean}) multiple = false;
97
+
96
98
  protected readonly formControlController = new FormControlController(this);
97
99
 
98
100
  get validationMessage() {
@@ -215,6 +217,7 @@ export default class ZnDataSelect extends ZincElement implements ZincFormControl
215
217
  @zn-clear="${this.handleClear}"
216
218
  @blur=${this.blur}
217
219
  value="${this.value}"
220
+ ?multiple=${this.multiple}
218
221
  placeholder="${this.getPlaceholder(localProvider)}"
219
222
  exportparts="combobox,expand-icon,form-control-help-text,form-control-input,display-input">
220
223
  ${(this.iconPosition !== 'none' || this.iconOnly) ? html`
@@ -92,6 +92,7 @@ interface HeaderConfig {
92
92
  filterable?: boolean;
93
93
  hideHeader?: boolean;
94
94
  hideColumn?: boolean;
95
+ secondary?: boolean;
95
96
  }
96
97
 
97
98
  interface DataRequest {
@@ -252,7 +253,7 @@ export default class ZnDataTable extends ZincElement {
252
253
  const params: Record<string, any> = {};
253
254
  if (inputs) {
254
255
  inputs.forEach((input) => {
255
- const allowedInputs = ['zn-input', 'zn-select', 'zn-query-builder', 'zn-multiselect', 'zn-params-select', 'input', 'select', 'textarea'];
256
+ const allowedInputs = ['zn-input', 'zn-select', 'zn-query-builder', 'zn-multiselect', 'zn-params-select', 'zn-datepicker', 'input', 'select', 'textarea'];
256
257
  if (allowedInputs.includes(input.tagName.toLowerCase())) {
257
258
  const value = (input as AllowedInputElement).value as string || input.getAttribute('value');
258
259
  const name = (input as AllowedInputElement).name || input.getAttribute('name');
@@ -289,6 +290,10 @@ export default class ZnDataTable extends ZincElement {
289
290
 
290
291
  private rowHasActions: boolean = false;
291
292
 
293
+ private _expandedRows: Set<string> = new Set();
294
+ private _hiddenCells: Map<string, Cell[]> = new Map();
295
+ private _secondaryHeaders: HeaderConfig[];
296
+
292
297
  requestParams: Record<string, any> = {};
293
298
 
294
299
  refresh() {
@@ -458,22 +463,40 @@ export default class ZnDataTable extends ZincElement {
458
463
  }
459
464
 
460
465
  public renderTableData(data: any) {
466
+ // Primary (visible) headers exclude those explicitly hidden and those marked as secondary
461
467
  const filteredHeaders = Object.values(this.headers).filter((header: HeaderConfig) => {
462
- if (header.hideHeader || header.hideColumn) {
463
- return false;
464
- }
465
- return !Object.values(this.hiddenColumns).includes(header.key);
468
+ if (header.hideHeader || header.hideColumn) return false;
469
+
470
+ if (Object.values(this.hiddenColumns).includes(header.key)) return false;
471
+
472
+ return !header.secondary;
473
+ });
474
+
475
+ // Secondary headers (shown in expandable details)
476
+ this._secondaryHeaders = Object.values(this.headers).filter((header: HeaderConfig) => {
477
+ if (header.hideColumn) return false;
478
+
479
+ if (Object.values(this.hiddenColumns).includes(header.key)) return false;
480
+
481
+ return header.secondary === true;
466
482
  });
467
483
 
468
484
  this.rowHasActions = this._rows.some((row: Row) => row.actions && row.actions.length > 0);
469
485
 
470
- // To make sure the cells are in the same order as the headers, we need to map them cell.col to heading.key
486
+ // Compute and store hidden cells per row (secondary items only), and reorder visible cells to match header order
487
+ const secondaryKeys = new Set(this._secondaryHeaders.map(h => h.key));
488
+ this._hiddenCells.clear();
489
+ const visibleRowCells: Map<string, Cell[]> = new Map();
490
+
471
491
  this._rows.forEach((row: Row) => {
492
+ const originalCells = Array.isArray(row.cells) ? row.cells : [];
472
493
  const orderedCells: Cell[] = [];
473
- // const unorderedCells = row.cells.filter((c: Cell) => !filteredHeaders.some((h: HeaderConfig) => h.key === c.column));
494
+
495
+ const hiddenCells = originalCells.filter((c: Cell) => secondaryKeys.has(c.column));
496
+ this._hiddenCells.set(row.id, hiddenCells);
474
497
 
475
498
  filteredHeaders.forEach((header: HeaderConfig) => {
476
- const cell = row.cells.find((c: Cell) => c.column === header.key);
499
+ const cell = originalCells.find((c: Cell) => c.column === header.key);
477
500
  if (cell) {
478
501
  orderedCells.push(cell);
479
502
  } else {
@@ -481,9 +504,12 @@ export default class ZnDataTable extends ZincElement {
481
504
  }
482
505
  });
483
506
 
484
- row.cells = orderedCells;//.concat(unorderedCells);
507
+ visibleRowCells.set(row.id, orderedCells);
485
508
  });
486
509
 
510
+ const anyHidden = this.hasHiddenColumns();
511
+ const colCount = filteredHeaders.length + (this.rowHasActions ? 1 : 0) + (anyHidden ? 1 : 0);
512
+
487
513
  return html`
488
514
  <div style="overflow-x: auto">
489
515
  <table class="${classMap({
@@ -493,17 +519,27 @@ export default class ZnDataTable extends ZincElement {
493
519
  })}">
494
520
  <thead>
495
521
  <tr>
522
+ ${anyHidden ? html`
523
+ <th class="table__head table__head--expander"></th>` : nothing}
496
524
  ${filteredHeaders.map((header: HeaderConfig) => this.renderCellHeader(header))}
497
525
  ${this.rowHasActions ? html`
498
526
  <th></th>` : html``}
499
527
  </tr>
500
528
  </thead>
501
529
  <tbody>
502
- ${data.map((row: Row) => html`
503
- <tr class="${classMap({'table__row--selected': this.isRowSelected(row)})}">
504
- ${row.cells.map((value: Cell, index: number) => this.renderCellBody(index, value))}
505
- ${this.rowHasActions ? this.renderActions(row) : null}
506
- </tr>`)}
530
+ ${(data as Row[]).map((row: Row, rowIndex: number) => html`
531
+ <tr class="${classMap({
532
+ 'table__row--selected': this.isRowSelected(row),
533
+ 'table__row--data': true,
534
+ 'table__row--even': (rowIndex % 2) === 0,
535
+ 'table__row--odd': (rowIndex % 2) === 1,
536
+ })}" data-row-id="${row.id}">
537
+ ${anyHidden ? this.renderExpanderCell(row) : nothing}
538
+ ${(visibleRowCells.get(row.id) || row.cells).map((value: Cell, index: number) => this.renderCellBody(index, value))}
539
+ ${this.rowHasActions ? this.renderActions(row) : nothing}
540
+ </tr>
541
+ ${this._expandedRows.has(row.id) ? this.renderDetailsRow(row, colCount) : nothing}
542
+ `)}
507
543
  </tbody>
508
544
  </table>
509
545
  </div>
@@ -743,7 +779,7 @@ export default class ZnDataTable extends ZincElement {
743
779
  return;
744
780
  }
745
781
 
746
- const rows = Array.from(this.renderRoot.querySelectorAll('tbody tr'));
782
+ const rows = Array.from(this.renderRoot.querySelectorAll('tbody tr:not([data-details="true"])'));
747
783
  const index = rows.indexOf(parent as HTMLTableRowElement);
748
784
  if (index === -1) return;
749
785
 
@@ -900,15 +936,24 @@ export default class ZnDataTable extends ZincElement {
900
936
 
901
937
  private renderCellHeader(header: HeaderConfig) {
902
938
  const sortable = !Object.values(this.unsortableHeaders).includes(header.key) && !Object.values(this.hiddenHeaders).includes(header.key) && !this.unsortable;
903
- const lastHeader = Object.keys(this.headers).filter((key) => !Object.values(this.hiddenColumns).includes(key)).slice(-1)[0];
904
- const lastHeaderKey = lastHeader ? this.headers[lastHeader].key : '';
939
+
940
+ // Determine the last visible header considering secondary flags and hidden columns
941
+ const lastVisibleHeaderKey = Object.values(this.headers)
942
+ .filter((h: HeaderConfig) => {
943
+ if (h.hideHeader || h.hideColumn) return false;
944
+
945
+ if (Object.values(this.hiddenColumns).includes(h.key)) return false;
946
+
947
+ return !h.secondary;
948
+ })
949
+ .slice(-1)[0]?.key;
905
950
 
906
951
  return html`
907
952
  <th
908
953
  class="${classMap({
909
954
  'table__head': true,
910
955
  'table__head--wide': header.key === this.wideColumn,
911
- 'table__head--last': header.key === lastHeaderKey,
956
+ 'table__head--last': header.key === lastVisibleHeaderKey,
912
957
  'table__head--hidden': Object.values(this.hiddenHeaders).includes(header.key),
913
958
  })}"
914
959
  @click="${sortable ? this.updateSort(header.key) : undefined}">
@@ -922,16 +967,13 @@ export default class ZnDataTable extends ZincElement {
922
967
 
923
968
  private renderCellBody(index: number, value: Cell) {
924
969
  const filteredHeaders = Object.values(this.headers).filter((header: HeaderConfig) => {
925
- if (header.hideHeader || header.hideColumn) {
926
- return false;
927
- }
928
- return !Object.values(this.hiddenColumns).includes(header.key);
929
- });
970
+ if (header.hideHeader || header.hideColumn) return false;
971
+
972
+ if (Object.values(this.hiddenColumns).includes(header.key)) return false;
930
973
 
931
- const headerKeys: string[] = Object.keys(this.headers).filter(
932
- (key) => !Object.values(this.hiddenColumns).includes(key)
933
- );
934
- const headerKey: string = headerKeys[index];
974
+ return !header.secondary;
975
+ });
976
+ const headerKey: string = filteredHeaders[index]?.key;
935
977
 
936
978
  return html`
937
979
  <td
@@ -945,6 +987,65 @@ export default class ZnDataTable extends ZincElement {
945
987
  </td>`;
946
988
  }
947
989
 
990
+ private hasHiddenColumns(): boolean {
991
+ return this._secondaryHeaders.length > 0;
992
+ }
993
+
994
+ private renderExpanderCell(row: Row) {
995
+ const hasHidden = (this._hiddenCells.get(row.id) || []).length > 0;
996
+ if (!hasHidden) return html`
997
+ <td class="table__cell table__cell--expander"></td>`;
998
+
999
+ const expanded = this._expandedRows.has(row.id);
1000
+ const icon = expanded ? 'expand_more' : 'chevron_right';
1001
+
1002
+ return html`
1003
+ <td class="table__cell table__cell--expander">
1004
+ <zn-button
1005
+ color="transparent"
1006
+ size="x-small"
1007
+ icon="${icon}"
1008
+ icon-size="18"
1009
+ aria-label="${expanded ? 'Collapse row' : 'Expand row'}"
1010
+ @click="${(e: Event) => this.toggleRowExpansion(e, row)}">
1011
+ </zn-button>
1012
+ </td>`;
1013
+ }
1014
+
1015
+ private renderDetailsRow(row: Row, colSpan: number) {
1016
+ const hiddenCells = this._hiddenCells.get(row.id) || [];
1017
+ if (hiddenCells.length === 0) return html``;
1018
+
1019
+ return html`
1020
+ <tr class="table__row--details" data-details="true">
1021
+ <td colspan="${colSpan}">
1022
+ <div class="table__details">
1023
+ ${hiddenCells.map((cell: Cell) => {
1024
+ const header = Object.values(this.headers).find((h: HeaderConfig) => h.key === cell.column);
1025
+ const label = header?.label || this.humanize(cell.column);
1026
+ return html`
1027
+ <div class="table__details__item">
1028
+ <div class="table__details__label">${label}</div>
1029
+ <div class="table__details__value">${this.renderCell(cell)}</div>
1030
+ </div>`;
1031
+ })}
1032
+ </div>
1033
+ </td>
1034
+ </tr>`;
1035
+ }
1036
+
1037
+ private toggleRowExpansion(e: Event, row: Row) {
1038
+ e.stopPropagation();
1039
+
1040
+ if (this._expandedRows.has(row.id)) {
1041
+ this._expandedRows.delete(row.id);
1042
+ } else {
1043
+ this._expandedRows.add(row.id);
1044
+ }
1045
+
1046
+ this.requestUpdate();
1047
+ }
1048
+
948
1049
  private isRowSelected(row: Row): boolean {
949
1050
  return this.selectedRows.some((r: Row) => r.id === row.id);
950
1051
  }