@kubex/zinc 1.1.53 → 1.1.55

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.
@@ -0,0 +1,30 @@
1
+ {
2
+ "rows": [
3
+ {
4
+ "id": "1",
5
+ "cells": [
6
+ {"text": "1", "column": "id"},
7
+ {"text": "Alice Johnson", "column": "name"},
8
+ {"text": "alice@example.com", "column": "email"},
9
+ {"text": "Active", "column": "status", "chipColor": "success"}
10
+ ]
11
+ },
12
+ {
13
+ "id": "2",
14
+ "cells": [
15
+ {"text": "2", "column": "id"},
16
+ {"text": "Bob Smith", "column": "name"},
17
+ {"text": "bob@example.com", "column": "email"},
18
+ {"text": "Active", "column": "status", "chipColor": "success"}
19
+ ]
20
+ }
21
+ ],
22
+ "page": 1,
23
+ "perPage": 10,
24
+ "total": 2,
25
+ "error": {
26
+ "text": "Results have been limited to 10 rows, please filter for more accuracy",
27
+ "level": "primary",
28
+ "icon": "info-circle"
29
+ }
30
+ }
@@ -113,6 +113,14 @@ Use the `size` attribute to control the alert's size. Available sizes are `small
113
113
  <zn-alert caption="Large Alert" size="large" level="warning">This is a large-sized alert.</zn-alert>
114
114
  ```
115
115
 
116
+ ### Centered Text
117
+
118
+ Use the `center` attribute to center the alert's text content.
119
+
120
+ ```html:preview
121
+ <zn-alert level="warning" center>This alert's text is centered.</zn-alert>
122
+ ```
123
+
116
124
  ### Complex Content
117
125
 
118
126
  Alerts can contain rich HTML content including lists, links, and formatted text.
@@ -89,6 +89,7 @@ countdown overlay. When the window elapses the tile auto-accepts via `accept-uri
89
89
  available
90
90
  incoming
91
91
  rejectable
92
+ accept-icon="phone_callback"
92
93
  item-id="chat-5500"
93
94
  title="Incoming chat"
94
95
  subtitle="Auto-accepting…"
@@ -624,6 +624,39 @@ This preview uses GET against a static file. In production, POST requests send p
624
624
  </zn-data-table>
625
625
  ```
626
626
 
627
+ ### Response Errors
628
+
629
+ Include an `error` object in the response to display a message inside the table, between the column headers and the first record, using the alert component. The error can be returned alongside row data — useful when results are valid but limited, or when a filter is invalid. The object supports `text`, an optional `icon`, and an optional `level` to control the alert level (defaults to `error`).
630
+
631
+ ```html:preview
632
+ <zn-data-table
633
+ data-uri="/data/data-table-error.json"
634
+ method="GET"
635
+ headers='[
636
+ {"key":"id","label":"ID"},
637
+ {"key":"name","label":"Name"},
638
+ {"key":"email","label":"Email"},
639
+ {"key":"status","label":"Status"}
640
+ ]'>
641
+ </zn-data-table>
642
+ ```
643
+
644
+ ```json
645
+ {
646
+ "rows": [...],
647
+ "page": 1,
648
+ "perPage": 10,
649
+ "total": 2,
650
+ "error": {
651
+ "text": "Results have been limited to 10 rows, please filter for more accuracy",
652
+ "icon": "warning",
653
+ "level": "warning"
654
+ }
655
+ }
656
+ ```
657
+
658
+ If the response contains an error and no rows, the error is shown above the empty state.
659
+
627
660
  ### Cell Styling
628
661
 
629
662
  Cells support various styling options through the data response format. Each cell can have colors, icons, links, chips, and hover content.
@@ -771,10 +804,20 @@ The data table expects responses in the following format:
771
804
  ],
772
805
  "page": 1,
773
806
  "perPage": 10,
774
- "total": 100
807
+ "total": 100,
808
+ "error": {
809
+ "text": "Optional message displayed below the column headers",
810
+ "icon": "warning",
811
+ "level": "warning"
812
+ }
775
813
  }
776
814
  ```
777
815
 
816
+ - `error`: Optional object displayed as an alert row below the column headers, alongside any returned rows
817
+ - `text`: The message to display
818
+ - `icon`: Optional icon shown in the alert
819
+ - `level`: Optional alert level (`primary`, `error`, `info`, `success`, `warning`, `note`), defaults to `error`
820
+
778
821
  ## Request Format
779
822
 
780
823
  For POST requests, the table sends:
@@ -427,3 +427,23 @@ Displays time.
427
427
  help-text="Select date and time">
428
428
  </zn-datepicker>
429
429
  ```
430
+
431
+ ### Dialogs & Shadow DOM
432
+
433
+ The calendar popup renders at the document level, so it is never clipped by ancestor shadow roots or
434
+ `overflow` containers. When the datepicker sits inside a modal dialog or popover, the calendar mounts
435
+ inside that element instead so it stays visible and interactive above the top layer.
436
+
437
+ ```html:preview
438
+ <zn-button id="datepicker-dialog-opener">Open Dialog</zn-button>
439
+ <zn-dialog id="datepicker-dialog" label="Book a meeting">
440
+ <zn-datepicker label="Meeting date"></zn-datepicker>
441
+ </zn-dialog>
442
+
443
+ <script>
444
+ const dialog = document.querySelector('#datepicker-dialog');
445
+ document.querySelector('#datepicker-dialog-opener').addEventListener('click', () => dialog.show());
446
+ </script>
447
+ ```
448
+
449
+ Use the `container` attribute only if you need to mount the calendar somewhere specific.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.53",
3
+ "version": "1.1.55",
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
@@ -305,3 +305,55 @@ ul.disc {
305
305
  ul.square {
306
306
  list-style-type: square;
307
307
  }
308
+
309
+ // Aura — soft, light-based theme with a pastel violet identity.
310
+ // Appended last so it wins source-order over the :root/light defaults on the
311
+ // document element (which carries the `t="aura"` attribute *and* matches :root).
312
+ // Only the source colour tokens and the theme-specific leaf tokens are set;
313
+ // derived tokens (--zn-body, --zn-text, --zn-panel ...) reference these via
314
+ // var() and follow automatically.
315
+ [t="aura"], .theme-aura {
316
+ --zn-backdrop-color: rgba(30, 27, 60, 0.45);
317
+
318
+ --zn-color-text: 42, 32, 89;
319
+ --zn-color-muted-text: 128, 118, 158;
320
+
321
+ --zn-color-base: 255, 255, 255;
322
+ --zn-color-body: 250, 248, 255;
323
+ --zn-color-navigation: var(--zn-color-base);
324
+ --zn-color-navigation-hightlight: 245, 242, 255;
325
+ --zn-color-border: 228, 222, 246;
326
+ --zn-color-border-active: 214, 206, 240;
327
+ --zn-color-tab: 240, 235, 252;
328
+ --zn-color-panel: 255, 255, 255;
329
+
330
+ --zn-color-primary: 139, 110, 245;
331
+ --zn-color-info: 53, 104, 195;
332
+ --zn-color-success: 98, 177, 121;
333
+ --zn-color-warning: 241, 168, 80;
334
+ --zn-color-error: 236, 68, 91;
335
+ --zn-color-disabled: 181, 181, 181;
336
+
337
+ // Theme-specific leaf tokens (light/dark declare these in their own blocks,
338
+ // so aura must supply its own — they are not inherited from :root).
339
+ --zn-panel-opacity: 1;
340
+ --zn-shadow: 232, 226, 250;
341
+ --zn-panel-secondary: 248 246 255;
342
+ --zn-panel-highlight: 245, 242, 255;
343
+ --zn-panel-highlight-opacity: 1;
344
+ --zn-input-bg-accent: 240, 236, 252;
345
+ --zn-bright-bg: 255, 255, 255;
346
+ --zn-light-text: 130, 120, 160;
347
+ --zn-dark-text: 60, 44, 110;
348
+ --zn-purple-text: 124, 92, 246;
349
+ --zn-accent: 200, 130, 230;
350
+
351
+ // Frosted-glass inactive tabs for the zn-page tab navbar (consumed in
352
+ // components/navbar/navbar.scss, scoped there to :host-context(zn-page)).
353
+ --navbar-page-inactive-tab-background: rgba(255, 255, 255, 0.5);
354
+ --navbar-page-inactive-tab-filter: blur(2px);
355
+
356
+ .zn-primary {
357
+ color: rgb(var(--zn-color-primary));
358
+ }
359
+ }
@@ -0,0 +1,35 @@
1
+ // Aura — a soft, light-based theme with a pastel violet identity.
2
+ // It inherits the light theme primitives (defined on :root) and only
3
+ // re-points the primary ramp to a softer, more pastel violet so the
4
+ // whole UI takes on the "aura" glow without redefining every token.
5
+ [t="aura"],
6
+ .theme-aura {
7
+ color-scheme: light;
8
+
9
+ /**************************************************************
10
+ * Theme Tokens — soft violet primary
11
+ *************************************************************/
12
+
13
+ --zn-color-primary-50: hsl(258, 100%, 97%);
14
+ --zn-color-primary-100: hsl(256, 95%, 94%);
15
+ --zn-color-primary-200: hsl(255, 90%, 90%);
16
+ --zn-color-primary-300: hsl(255, 88%, 83%);
17
+ --zn-color-primary-400: hsl(256, 85%, 75%);
18
+ --zn-color-primary-500: hsl(255, 82%, 68%);
19
+ --zn-color-primary-600: hsl(256, 78%, 62%);
20
+ --zn-color-primary-700: hsl(257, 70%, 54%);
21
+ --zn-color-primary-800: hsl(258, 68%, 46%);
22
+ --zn-color-primary-900: hsl(259, 66%, 38%);
23
+
24
+ /* Soft, faintly violet-tinted elevations */
25
+ --zn-shadow-x-small: 0 1px 4px rgba(60, 44, 110, 0.08);
26
+ --zn-shadow-small: 0 3px 9px rgba(60, 44, 110, 0.12);
27
+ --zn-shadow-medium: 0 6px 18px rgba(60, 44, 110, 0.12);
28
+ --zn-shadow-large: 0 6px 20px rgba(60, 44, 110, 0.14);
29
+ --zn-shadow-x-large: 0 6px 20px rgba(60, 44, 110, 0.22);
30
+
31
+ --zn-border-color: 218, 212, 246;
32
+
33
+ /* Focus ring follows the softer primary */
34
+ --zn-input-focus-ring-color: hsl(255 82% 68% / 40%);
35
+ }
@@ -1,5 +1,6 @@
1
1
  @use "light";
2
2
  @use "dark";
3
+ @use "aura";
3
4
  @use "spacing";
4
5
  @use "typography";
5
6
  @use "utility";
@@ -30,6 +30,7 @@ export default class ZnAlert extends ZincElement {
30
30
  @property({type: Boolean}) collapse: boolean = false;
31
31
  @property({type: String, reflect: true}) appearance: 'transparent' | 'solid' = 'transparent';
32
32
  @property({type: String}) level: 'primary' | 'error' | 'info' | 'success' | 'warning' | 'note' | 'cosmic' = 'info';
33
+ @property({type: Boolean, reflect: true}) center: boolean = false;
33
34
  @property({type: String}) size: 'small' | 'medium' | 'large' = 'medium';
34
35
 
35
36
  render() {
@@ -44,6 +44,10 @@ p {
44
44
  }
45
45
  }
46
46
 
47
+ :host([center]) .content {
48
+ text-align: center;
49
+ }
50
+
47
51
  :host([caption]) zn-icon {
48
52
  align-self: flex-start;
49
53
  margin-top: 6px;
@@ -1,6 +1,6 @@
1
- import {classMap} from 'lit/directives/class-map.js';
2
- import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
3
- import {property} from 'lit/decorators.js';
1
+ import { classMap } from 'lit/directives/class-map.js';
2
+ import { type CSSResultGroup, html, type PropertyValues, unsafeCSS } from 'lit';
3
+ import { property } from 'lit/decorators.js';
4
4
  import ZincElement from '../../internal/zinc-element';
5
5
 
6
6
  import styles from './channel-tile.scss';
@@ -38,13 +38,13 @@ export default class ZnChannelTile extends ZincElement {
38
38
  static styles: CSSResultGroup = unsafeCSS(styles);
39
39
 
40
40
  /** Renders the empty/available face instead of the active face. */
41
- @property({type: Boolean, reflect: true}) available: boolean = false;
41
+ @property({ type: Boolean, reflect: true }) available: boolean = false;
42
42
 
43
43
  /** (Available only) the tile is reserving an incoming item awaiting acceptance. */
44
- @property({type: Boolean, reflect: true}) incoming: boolean = false;
44
+ @property({ type: Boolean, reflect: true }) incoming: boolean = false;
45
45
 
46
46
  /** Free-form grouping/theming key (reflected so consumers can query/style by it). */
47
- @property({reflect: true}) variant: string = '';
47
+ @property({ reflect: true }) variant: string = '';
48
48
 
49
49
  /** Top-bar text (e.g. brand). */
50
50
  @property() header: string = '';
@@ -53,7 +53,7 @@ export default class ZnChannelTile extends ZincElement {
53
53
  @property() icon: string = '';
54
54
 
55
55
  /** Accent color driving the leading icon and `--channel-tile-color`. */
56
- @property({reflect: true}) color: ChannelTileColor = 'default';
56
+ @property({ reflect: true }) color: ChannelTileColor = 'default';
57
57
 
58
58
  /** Primary line. Defaults to "Available" in the available state when unset. */
59
59
  @property() title: string = '';
@@ -62,37 +62,37 @@ export default class ZnChannelTile extends ZincElement {
62
62
  @property() subtitle: string = '';
63
63
 
64
64
  /** (Active only) progress bar fill, 0–100. */
65
- @property({type: Number}) progress: number = 0;
65
+ @property({ type: Number }) progress: number = 0;
66
66
 
67
67
  /** (Active only) CSS color for the progress bar fill. */
68
- @property({attribute: 'progress-color'}) progressColor: string = '';
68
+ @property({ attribute: 'progress-color' }) progressColor: string = '';
69
69
 
70
70
  /** Identifier carried in `zn-accept` / `zn-reject` event details. */
71
- @property({attribute: 'item-id'}) itemId: string = '';
71
+ @property({ attribute: 'item-id' }) itemId: string = '';
72
72
 
73
73
  /** (Available only) when set, accepting fetches this URI unless `zn-accept` is canceled. */
74
- @property({attribute: 'accept-uri'}) acceptUri: string = '';
74
+ @property({ attribute: 'accept-uri' }) acceptUri: string = '';
75
75
 
76
76
  /** (Available/incoming) epoch (seconds or millis) at which the reservation window ends. */
77
- @property({type: Number, attribute: 'reserved-until'}) reservedUntil: number = 0;
77
+ @property({ type: Number, attribute: 'reserved-until' }) reservedUntil: number = 0;
78
78
 
79
79
  /** (Available/incoming) auto-accept window length in milliseconds. */
80
- @property({type: Number, attribute: 'auto-accept-delay'}) autoAcceptDelay: number = 0;
80
+ @property({ type: Number, attribute: 'auto-accept-delay' }) autoAcceptDelay: number = 0;
81
81
 
82
82
  /** (Available/incoming) whether a reject control is offered. */
83
- @property({type: Boolean, reflect: true}) rejectable: boolean = false;
83
+ @property({ type: Boolean, reflect: true }) rejectable: boolean = false;
84
84
 
85
85
  /** Icon for the built-in accept button. */
86
- @property({attribute: 'accept-icon'}) acceptIcon: string = 'plus@lu';
86
+ @property({ attribute: 'accept-icon' }) acceptIcon: string = 'plus@lu';
87
87
 
88
88
  /** Optional analytics id forwarded to the built-in accept button. */
89
- @property({attribute: 'accept-gaid'}) acceptGaid: string = '';
89
+ @property({ attribute: 'accept-gaid' }) acceptGaid: string = '';
90
90
 
91
91
  /** Icon for the reject control. */
92
- @property({attribute: 'reject-icon'}) rejectIcon: string = 'call_end';
92
+ @property({ attribute: 'reject-icon' }) rejectIcon: string = 'close';
93
93
 
94
94
  /** Accessible label for the reject control. */
95
- @property({attribute: 'reject-label'}) rejectLabel: string = 'Reject';
95
+ @property({ attribute: 'reject-label' }) rejectLabel: string = 'Reject';
96
96
 
97
97
  private _tickInterval: number | undefined;
98
98
  private _autoAcceptFired: string = '';
@@ -152,10 +152,10 @@ export default class ZnChannelTile extends ZincElement {
152
152
  private _accept(): void {
153
153
  const event = this.emit('zn-accept', {
154
154
  cancelable: true,
155
- detail: {itemId: this.itemId, acceptUri: this.acceptUri},
155
+ detail: { itemId: this.itemId, acceptUri: this.acceptUri },
156
156
  });
157
157
  if (event.defaultPrevented || !this.acceptUri) return;
158
- fetch(this.acceptUri, {credentials: 'same-origin'}).catch((err) => {
158
+ fetch(this.acceptUri, { credentials: 'same-origin' }).catch((err) => {
159
159
  console.error('Error accepting interaction', err);
160
160
  });
161
161
  }
@@ -186,7 +186,7 @@ export default class ZnChannelTile extends ZincElement {
186
186
  private _handleReject = (e: MouseEvent) => {
187
187
  e.preventDefault();
188
188
  e.stopPropagation();
189
- this.emit('zn-reject', {detail: {itemId: this.itemId, variant: this.variant}});
189
+ this.emit('zn-reject', { detail: { itemId: this.itemId, variant: this.variant } });
190
190
  };
191
191
 
192
192
  private _handleClick = (e: MouseEvent) => {
@@ -226,13 +226,19 @@ export default class ZnChannelTile extends ZincElement {
226
226
  ${remaining !== null
227
227
  ? this.title
228
228
  ? html`
229
- <h3 class="channel-tile__title"><slot name="title">${this.title}</slot></h3>
229
+ <h3 class="channel-tile__title">
230
+ <slot name="title">${this.title}</slot>
231
+ </h3>
230
232
  <p class="channel-tile__subtitle">${this.subtitle} (${remaining}s)</p>`
231
233
  : html`
232
234
  <h3 class="channel-tile__title">${this.subtitle} (${remaining}s)</h3>`
233
235
  : html`
234
- <h3 class="channel-tile__title"><slot name="title">${title}</slot></h3>
235
- <p class="channel-tile__subtitle"><slot name="subtitle">${this.subtitle}</slot></p>`}
236
+ <h3 class="channel-tile__title">
237
+ <slot name="title">${title}</slot>
238
+ </h3>
239
+ <p class="channel-tile__subtitle">
240
+ <slot name="subtitle">${this.subtitle}</slot>
241
+ </p>`}
236
242
  </div>
237
243
  <slot name="footer" class="channel-tile__footer"></slot>
238
244
  </div>
@@ -259,6 +265,9 @@ export default class ZnChannelTile extends ZincElement {
259
265
  icon-size="20"
260
266
  icon-button="round"
261
267
  color=${this.color}
268
+ class="${classMap({
269
+ [`channel-tile--${this.color}`]: true,
270
+ })}"
262
271
  no-hover></zn-button>`
263
272
  : ''}
264
273
  </slot>`;
@@ -145,13 +145,14 @@
145
145
  border: none;
146
146
  padding: 0;
147
147
  margin: 0;
148
- background-color: rgb(var(--zn-panel));
148
+ background-color: rgba(var(--zn-color-panel));
149
149
  color: rgb(var(--zn-color-error));
150
150
  cursor: pointer;
151
151
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
152
152
 
153
153
  &:hover {
154
- background-color: rgba(var(--zn-color-error), 0.1);
154
+ background-color: rgba(var(--zn-color-error));
155
+ color: rgb(var(--zn-color-panel));
155
156
  }
156
157
  }
157
158
 
@@ -195,3 +196,7 @@
195
196
  .channel-tile--disabled {
196
197
  --channel-tile-color: var(--zn-color-disabled, 204, 204, 204);
197
198
  }
199
+
200
+ .channel-tile--incoming .channel-tile__body zn-button {
201
+ --zn-color-tab: var(--zn-color-panel);
202
+ }
@@ -10,6 +10,7 @@ import {unsafeHTML} from "lit/directives/unsafe-html.js";
10
10
  import {type ZnFilterChangeEvent} from "../../events/zn-filter-change";
11
11
  import {type ZnSearchChangeEvent} from "../../events/zn-search-change";
12
12
  import ZincElement from '../../internal/zinc-element';
13
+ import ZnAlert from "../alert";
13
14
  import ZnButton from "../button";
14
15
  import ZnButtonGroup from "../button-group";
15
16
  import ZnChip from "../chip";
@@ -60,11 +61,18 @@ interface Row {
60
61
  cells: Cell[];
61
62
  }
62
63
 
64
+ interface ResponseError {
65
+ text: string;
66
+ icon?: string;
67
+ level?: 'primary' | 'error' | 'info' | 'success' | 'warning' | 'note';
68
+ }
69
+
63
70
  interface Response {
64
71
  rows: Row[];
65
72
  perPage: number;
66
73
  total: number;
67
74
  page: number;
75
+ error?: ResponseError;
68
76
  }
69
77
 
70
78
  export enum ActionSlots {
@@ -147,6 +155,7 @@ type AllowedInputElement =
147
155
  * @status experimental
148
156
  * @since 1.0
149
157
  *
158
+ * @dependency zn-alert
150
159
  * @dependency zn-button
151
160
  * @dependency zn-empty-state
152
161
  * @dependency zn-chip
@@ -177,6 +186,7 @@ type AllowedInputElement =
177
186
  export default class ZnDataTable extends ZincElement {
178
187
  static styles: CSSResultGroup = unsafeCSS(styles);
179
188
  static dependencies = {
189
+ 'zn-alert': ZnAlert,
180
190
  'zn-button': ZnButton,
181
191
  'zn-empty-state': ZnEmptyState,
182
192
  'zn-chip': ZnChip,
@@ -503,12 +513,15 @@ export default class ZnDataTable extends ZincElement {
503
513
  }
504
514
 
505
515
  renderTable(data: Response) {
506
- this.itemsPerPage = Math.max(1, data.perPage ?? DEFAULT_PER_PAGE);
516
+ this.itemsPerPage = Math.max(DEFAULT_PER_PAGE, data.perPage ?? DEFAULT_PER_PAGE);
507
517
  this.page = Math.max(1, data.page ?? DEFAULT_PAGE);
508
518
  this.totalPages = Math.ceil(Math.max(1, data.total) / this.itemsPerPage);
509
519
 
520
+ const error = data?.error?.text ? data.error : undefined;
521
+
510
522
  if (!data?.rows || data.rows.length === 0) {
511
- return this.emptyState();
523
+ return html`${error ? html`
524
+ <div class="table__error">${this.renderErrorAlert(error)}</div>` : nothing}${this.emptyState()}`;
512
525
  }
513
526
 
514
527
  this._rows = this.getRows(data);
@@ -573,10 +586,10 @@ export default class ZnDataTable extends ZincElement {
573
586
  // render a table for each group
574
587
  return html`
575
588
  <zn-sp flush>
576
- ${Object.keys(groupedRows).map((groupKey) => html`
589
+ ${Object.keys(groupedRows).map((groupKey, index) => html`
577
590
  <div class="table-group">
578
591
  <h3 class="table-group__title">${(groupKey === "Ungrouped" || groupKey === "*") ? "" : groupKey}</h3>
579
- ${this.renderTableData(groupedRows[groupKey])}
592
+ ${this.renderTableData(groupedRows[groupKey], index === 0 ? error : undefined)}
580
593
  </div>`
581
594
  )}
582
595
  </zn-sp>
@@ -584,14 +597,22 @@ export default class ZnDataTable extends ZincElement {
584
597
  }
585
598
 
586
599
 
587
- return this.renderTableData(this._rows);
600
+ return this.renderTableData(this._rows, error);
601
+ }
602
+
603
+ private renderErrorAlert(error: ResponseError) {
604
+ return html`
605
+ <zn-alert level="${error.level ?? 'error'}"
606
+ icon="${ifDefined(error.icon)}"
607
+ size="small"
608
+ center>${error.text}</zn-alert>`;
588
609
  }
589
610
 
590
611
  public humanize(str: string) {
591
612
  return str.charAt(0).toUpperCase() + str.slice(1);
592
613
  }
593
614
 
594
- public renderTableData(data: any) {
615
+ public renderTableData(data: any, error?: ResponseError) {
595
616
  // Primary (visible) headers exclude those explicitly hidden and those marked as secondary
596
617
  const filteredHeaders = Object.values(this.headers).filter((header: HeaderConfig) => {
597
618
  if (header.hideHeader || header.hideColumn) return false;
@@ -656,6 +677,10 @@ export default class ZnDataTable extends ZincElement {
656
677
  </tr>
657
678
  </thead>
658
679
  <tbody>
680
+ ${error ? html`
681
+ <tr class="table__row--error">
682
+ <td colspan="${colCount}">${this.renderErrorAlert(error)}</td>
683
+ </tr>` : nothing}
659
684
  ${(data as Row[]).map((row: Row) => html`
660
685
  <tr class="${classMap({
661
686
  'table__row--selected': this.isRowSelected(row),
@@ -924,7 +949,7 @@ export default class ZnDataTable extends ZincElement {
924
949
  return;
925
950
  }
926
951
 
927
- const rows = Array.from(this.renderRoot.querySelectorAll('tbody tr:not([data-details="true"])'));
952
+ const rows = Array.from(this.renderRoot.querySelectorAll('tbody tr.table__row--data'));
928
953
  const index = rows.indexOf(parent as HTMLTableRowElement);
929
954
  if (index === -1) return;
930
955
 
@@ -422,6 +422,14 @@ table:has(tbody tr:first-of-type.table__row--selected) thead tr th {
422
422
  text-overflow: ellipsis;
423
423
  }
424
424
 
425
+ tbody tr.table__row--error td {
426
+ padding-block: var(--zn-spacing-x-small);
427
+ }
428
+
429
+ .table__error {
430
+ padding-bottom: var(--zn-spacing-small);
431
+ }
432
+
425
433
  .table__collum--datetime{
426
434
  display:flex;
427
435
  flex-direction:column;
@@ -1,5 +1,5 @@
1
1
  import '../../../dist/zn.min.js';
2
- import { expect, fixture, html } from '@open-wc/testing';
2
+ import { expect, fixture, html, waitUntil } from '@open-wc/testing';
3
3
  import type ZnDataTable from './data-table.component';
4
4
 
5
5
  describe('<zn-data-table>', () => {
@@ -24,6 +24,31 @@ describe('<zn-data-table>', () => {
24
24
  expect(() => (el as unknown as {requestUpdate: () => void}).requestUpdate()).not.to.throw();
25
25
  });
26
26
 
27
+ it('renders an error alert from the fetch response alongside the table content', async () => {
28
+ const originalFetch = window.fetch;
29
+ window.fetch = () => Promise.resolve(new Response(JSON.stringify({
30
+ rows: [{id: '1', cells: [{text: 'Row 1', column: 'name'}]}],
31
+ page: 1,
32
+ perPage: 10,
33
+ total: 1,
34
+ error: {text: 'Results limited to 10 rows, please filter for more accuracy', level: 'warning'},
35
+ }), {status: 200, headers: {'Content-Type': 'application/json'}}));
36
+
37
+ try {
38
+ const el = await fixture<ZnDataTable>(html`
39
+ <zn-data-table data-uri="/test-data" headers='{"name": {"key": "name", "label": "Name"}}'></zn-data-table>`);
40
+ await waitUntil(() => el.shadowRoot?.querySelector('zn-alert'));
41
+
42
+ const alert = el.shadowRoot!.querySelector('tbody tr:first-child zn-alert')!;
43
+ expect(alert).to.exist;
44
+ expect(alert.textContent).to.contain('limited to 10 rows');
45
+ expect(alert.getAttribute('level')).to.equal('warning');
46
+ expect(el.shadowRoot!.querySelector('tbody tr.table__row--data')).to.exist;
47
+ } finally {
48
+ window.fetch = originalFetch;
49
+ }
50
+ });
51
+
27
52
  it('exposes displayTemplates as an object property', async () => {
28
53
  const el = await fixture<ZnDataTable>(html` <zn-data-table></zn-data-table> `);
29
54
  const templates = (el as unknown as {displayTemplates: Record<string, unknown>}).displayTemplates;