@kubex/zinc 1.1.12 → 1.1.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 (36) hide show
  1. package/dist/custom-elements.json +1814 -1084
  2. package/dist/vscode.html-custom-data.json +228 -88
  3. package/dist/web-types.json +521 -213
  4. package/dist/zn.d.ts +180 -1
  5. package/dist/zn.min.css +1 -1
  6. package/dist/zn.min.js +522 -459
  7. package/docs/pages/components/channel-tile.md +168 -0
  8. package/docs/pages/components/chat-message-attachment.md +54 -0
  9. package/docs/pages/components/chat-message.md +18 -1
  10. package/docs/pages/components/data-table.md +1 -3
  11. package/package.json +1 -1
  12. package/scss/shared/_table.scss +9 -23
  13. package/scss/themes/_dark.scss +3 -0
  14. package/scss/themes/_light.scss +4 -0
  15. package/scss/themes/_typography.scss +1 -0
  16. package/src/components/button/button.scss +4 -0
  17. package/src/components/channel-tile/channel-tile.component.ts +280 -0
  18. package/src/components/channel-tile/channel-tile.scss +197 -0
  19. package/src/components/channel-tile/index.ts +12 -0
  20. package/src/components/chat-message/chat-message.component.ts +14 -5
  21. package/src/components/chat-message/chat-message.scss +37 -12
  22. package/src/components/chat-message-attachment/chat-message-attachment.component.ts +78 -0
  23. package/src/components/chat-message-attachment/chat-message-attachment.scss +32 -0
  24. package/src/components/chat-message-attachment/chat-message-attachment.test.ts +36 -0
  25. package/src/components/chat-message-attachment/index.ts +12 -0
  26. package/src/components/data-table/data-table.component.ts +65 -46
  27. package/src/components/data-table/data-table.scss +32 -58
  28. package/src/components/icon/icon.component.ts +1 -1
  29. package/src/components/icon/icon.scss +2 -1
  30. package/src/components/page/page.component.ts +32 -1
  31. package/src/components/page/page.scss +6 -5
  32. package/src/components/tooltip/tooltip.scss +1 -1
  33. package/src/events/events.ts +2 -0
  34. package/src/events/zn-accept.ts +7 -0
  35. package/src/events/zn-reject.ts +7 -0
  36. package/src/zinc.ts +2 -0
@@ -0,0 +1,168 @@
1
+ ---
2
+ meta:
3
+ title: Channel Tile
4
+ description: A channel/queue slot tile with an active (occupied) face showing an in-progress item, and an available (empty) face advertising capacity with optional auto-accept.
5
+ layout: component
6
+ ---
7
+
8
+ The channel tile represents a single slot in a queue or channel rail. It has two faces:
9
+
10
+ - **Active** — the slot is occupied by an in-progress item (header, leading icon, title/subtitle, and a progress/SLA bar).
11
+ - **Available** — the slot is empty and advertising capacity. When `incoming` is set it reserves an arriving item with a countdown that can auto-accept.
12
+
13
+ Tiles are designed to sit side by side in a horizontal rail; each is a fixed-width block with a right divider.
14
+
15
+ ```html:preview
16
+ <div style="display: flex;">
17
+ <zn-channel-tile
18
+ header="Acme Support"
19
+ icon="chat"
20
+ color="info"
21
+ title="Jane Cooper"
22
+ subtitle="Billing question"
23
+ progress="60"
24
+ progress-color="rgb(var(--zn-color-info))">
25
+ </zn-channel-tile>
26
+ <zn-channel-tile available header="Acme Support" subtitle="Waiting for the next chat"></zn-channel-tile>
27
+ </div>
28
+ ```
29
+
30
+ ## Examples
31
+
32
+ ### Active Tile
33
+
34
+ The default (occupied) face. Set `header` for the top bar, `icon` + `color` for the leading badge, and `title`/`subtitle` for the item. The `progress` value (0–100) fills the SLA bar along the bottom edge.
35
+
36
+ ```html:preview
37
+ <div style="display: flex;">
38
+ <zn-channel-tile
39
+ header="Acme Support"
40
+ icon="chat"
41
+ color="info"
42
+ title="Jane Cooper"
43
+ subtitle="Billing question"
44
+ progress="45"
45
+ progress-color="rgb(var(--zn-color-info))">
46
+ </zn-channel-tile>
47
+ </div>
48
+ ```
49
+
50
+ ### Available Tile
51
+
52
+ Set `available` to render the empty face. When `title` is omitted it defaults to "Available". The leading slot shows a built-in accept button.
53
+
54
+ ```html:preview
55
+ <div style="display: flex;">
56
+ <zn-channel-tile available subtitle="Ready for the next interaction"></zn-channel-tile>
57
+ </div>
58
+ ```
59
+
60
+ ### Accepting an Item
61
+
62
+ Provide `accept-uri` and the built-in accept button becomes a link to it. When a tile is accepted, a cancelable `zn-accept` event fires before the fetch — call `preventDefault()` on it to take over the accept yourself.
63
+
64
+ ```html:preview
65
+ <div style="display: flex;">
66
+ <zn-channel-tile
67
+ available
68
+ item-id="chat-4821"
69
+ accept-uri="/queue/accept/chat-4821"
70
+ subtitle="Click to accept">
71
+ </zn-channel-tile>
72
+ </div>
73
+ ```
74
+
75
+ ### Incoming with Auto-Accept
76
+
77
+ An `available` tile with `incoming` reserves an arriving item. Provide `reserved-until` (an epoch in seconds or milliseconds marking the end of the window) and `auto-accept-delay` (the window length in milliseconds) to drive the countdown overlay. When the window elapses the tile auto-accepts via `accept-uri`.
78
+
79
+ ```html:preview
80
+ <div style="display: flex;">
81
+ <zn-channel-tile
82
+ available
83
+ incoming
84
+ rejectable
85
+ item-id="chat-5500"
86
+ title="Incoming chat"
87
+ subtitle="Auto-accepting…"
88
+ accept-uri="/queue/accept/chat-5500"
89
+ auto-accept-delay="15000">
90
+ </zn-channel-tile>
91
+ </div>
92
+
93
+ <script>
94
+ // Set the reservation window relative to now so the countdown animates.
95
+ const tile = document.currentScript.previousElementSibling.querySelector('zn-channel-tile');
96
+ tile.reservedUntil = Date.now() + 15000;
97
+ </script>
98
+ ```
99
+
100
+ ### Rejectable
101
+
102
+ Add `rejectable` to an incoming tile to offer a reject control. Pressing it emits `zn-reject` (and suppresses accept).
103
+
104
+ ```html:preview
105
+ <div style="display: flex;">
106
+ <zn-channel-tile
107
+ available
108
+ incoming
109
+ rejectable
110
+ item-id="call-7700"
111
+ title="Incoming call"
112
+ subtitle="Reject to pass"
113
+ reject-icon="call_end"
114
+ reject-label="Reject call">
115
+ </zn-channel-tile>
116
+ </div>
117
+ ```
118
+
119
+ ### Colors
120
+
121
+ The `color` attribute sets the accent (the leading icon badge and the `--channel-tile-color` custom property). Available colors are: `default`, `primary`, `info`, `success`, `warning`, `error`, and `disabled`.
122
+
123
+ ```html:preview
124
+ <div style="display: flex; flex-wrap: wrap;">
125
+ <zn-channel-tile icon="chat" color="primary" title="Primary" subtitle="Accent example"></zn-channel-tile>
126
+ <zn-channel-tile icon="chat" color="info" title="Info" subtitle="Accent example"></zn-channel-tile>
127
+ <zn-channel-tile icon="chat" color="success" title="Success" subtitle="Accent example"></zn-channel-tile>
128
+ <zn-channel-tile icon="chat" color="warning" title="Warning" subtitle="Accent example"></zn-channel-tile>
129
+ <zn-channel-tile icon="chat" color="error" title="Error" subtitle="Accent example"></zn-channel-tile>
130
+ <zn-channel-tile icon="chat" color="disabled" title="Disabled" subtitle="Accent example"></zn-channel-tile>
131
+ </div>
132
+ ```
133
+
134
+ ### Custom Leading Content
135
+
136
+ Use the `leading` slot to replace the leading icon in the active state.
137
+
138
+ ```html:preview
139
+ <div style="display: flex;">
140
+ <zn-channel-tile header="Acme Support" title="Jane Cooper" subtitle="VIP customer">
141
+ <zn-icon slot="leading" library="avatar" src="Jane Cooper" size="36" round></zn-icon>
142
+ </zn-channel-tile>
143
+ </div>
144
+ ```
145
+
146
+ ### Custom Action
147
+
148
+ Use the `action` slot to replace the default accept button on an available tile — for example with a form.
149
+
150
+ ```html:preview
151
+ <div style="display: flex;">
152
+ <zn-channel-tile available subtitle="Pick a queue to join">
153
+ <zn-button slot="action" icon="login" icon-size="20" icon-button="round" color="success"></zn-button>
154
+ </zn-channel-tile>
155
+ </div>
156
+ ```
157
+
158
+ ### Footer
159
+
160
+ Use the `footer` slot for trailing content such as status badges in the active state.
161
+
162
+ ```html:preview
163
+ <div style="display: flex;">
164
+ <zn-channel-tile header="Acme Support" icon="chat" color="info" title="Jane Cooper" subtitle="Billing question">
165
+ <zn-chip slot="footer" type="warning">SLA</zn-chip>
166
+ </zn-channel-tile>
167
+ </div>
168
+ ```
@@ -0,0 +1,54 @@
1
+ ---
2
+ meta:
3
+ title: Chat Message Attachment
4
+ description: A file or link attachment for a chat message, rendered as an icon and label.
5
+ layout: component
6
+ ---
7
+
8
+ `zn-chat-message-attachment` represents a single file or link beneath a message. It is intended to be
9
+ used only inside a [`zn-chat-message`](/components/chat-message) — it automatically assigns itself to
10
+ that component's `attachments` slot, so you can drop it straight into the message.
11
+
12
+ ```html:preview
13
+ <zn-chat-message sender="Agent Name" time="1718193420">
14
+ Here are the documents you requested.
15
+ <zn-chat-message-attachment href="#" name="invoice.pdf"></zn-chat-message-attachment>
16
+ <zn-chat-message-attachment href="#" name="statement.pdf"></zn-chat-message-attachment>
17
+ </zn-chat-message>
18
+ ```
19
+
20
+ ## Examples
21
+
22
+ ### Custom Icon
23
+
24
+ Use the `icon` attribute to change the leading icon (any `zn-icon` name).
25
+
26
+ ```html:preview
27
+ <zn-chat-message sender="Agent Name" time="1718193420">
28
+ A couple of links for you.
29
+ <zn-chat-message-attachment href="#" name="View dashboard" icon="link"></zn-chat-message-attachment>
30
+ <zn-chat-message-attachment href="#" name="screenshot.png" icon="image"></zn-chat-message-attachment>
31
+ </zn-chat-message>
32
+ ```
33
+
34
+ ### Download
35
+
36
+ Set `download` to prompt a download rather than navigating to the link.
37
+
38
+ ```html:preview
39
+ <zn-chat-message sender="Agent Name" time="1718193420">
40
+ Your export is ready.
41
+ <zn-chat-message-attachment href="#" name="export.csv" download></zn-chat-message-attachment>
42
+ </zn-chat-message>
43
+ ```
44
+
45
+ ### Custom Label
46
+
47
+ The default slot overrides the `name` attribute when you need richer label content.
48
+
49
+ ```html:preview
50
+ <zn-chat-message sender="Agent Name" time="1718193420">
51
+ See attached.
52
+ <zn-chat-message-attachment href="#">contract_<strong>final</strong>.pdf</zn-chat-message-attachment>
53
+ </zn-chat-message>
54
+ ```
@@ -50,7 +50,24 @@ Use the `edit-dialog-trigger` slot to add an action to the end of the bubble, su
50
50
  </zn-chat-message>
51
51
  ```
52
52
 
53
- ### Custom Bubble Colour
53
+ ### Attachments
54
+
55
+ Use [`zn-chat-message-attachment`](/components/chat-message-attachment) to display files or links
56
+ beneath the message content. They are automatically placed in the `attachments` slot and wrap onto
57
+ multiple lines when they overflow.
58
+
59
+ ```html:preview
60
+ <zn-chat-message sender="Agent Name" time="1718193420">
61
+ The customer disconnected before I could answer his questions.
62
+ <zn-button slot="edit-dialog-trigger" icon="close" icon-button plain no-hover
63
+ color="error" tooltip="Remove Note"></zn-button>
64
+ <zn-chat-message-attachment href="#" name="this_is_an_attachment.pdf"></zn-chat-message-attachment>
65
+ <zn-chat-message-attachment href="#" name="quarterly_report.xlsx"></zn-chat-message-attachment>
66
+ <zn-chat-message-attachment href="#" name="screenshot.png"></zn-chat-message-attachment>
67
+ </zn-chat-message>
68
+ ```
69
+
70
+ ### Custom Bubble Color
54
71
 
55
72
  The bubble background can be themed with the `--message-background` custom property.
56
73
 
@@ -20,8 +20,6 @@ fullWidth: true
20
20
  {"key":"date","label":"Date Joined", "sortable":true}
21
21
  ]'>
22
22
 
23
- <zn-data-table-search slot="search" placeholder="Search records..."></zn-data-table-search>
24
-
25
23
  </zn-data-table>
26
24
  ```
27
25
 
@@ -792,4 +790,4 @@ For POST requests, the table sends:
792
790
  }
793
791
  ```
794
792
 
795
- Additional parameters from the `inputs` slot are merged into the request.
793
+ Additional parameters from the `inputs` slot are merged into the request.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.12",
3
+ "version": "1.1.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",
@@ -8,14 +8,16 @@ table:not(.remarkd-table) {
8
8
  max-width: 100%;
9
9
  table-layout: auto;
10
10
  text-align: left;
11
- font-size: var(--zn-text-table-content-font-size);
12
- line-height: var(--zn-text-table-content-line-height);
13
- font-weight: var(--zn-text-table-content-font-weight);
11
+ @include m.text-style(table-content);
14
12
 
15
13
  thead {
16
14
  font-weight: var(--zn-font-weight-medium);
17
15
  }
18
16
 
17
+ thead tr th {
18
+ background-color: rgb(var(--zn-table-header-background-color));
19
+ }
20
+
19
21
  tbody {
20
22
  //background-color: rgba(var(--zn-panel), var(--zn-panel-opacity));
21
23
 
@@ -33,28 +35,10 @@ table:not(.remarkd-table) {
33
35
  border-top: 0 !important;
34
36
  }
35
37
 
36
- tbody tr:nth-of-type(even) {
37
- background-color: rgba(var(--zn-body));
38
- }
39
-
40
- thead tr th:first-of-type,
41
- tbody tr td:first-of-type {
42
- padding-left: var(--zn-base-gap, 14px);
43
- }
44
-
45
- thead tr th:last-of-type,
46
- tbody tr td:last-of-type {
47
- padding-right: var(--zn-base-gap, 14px);
48
- }
49
-
50
38
  th, td {
51
- padding: m.spacing(sm);
39
+ padding: var(--zn-spacing-small) var(--zn-spacing-medium);
52
40
  min-width: 90px;
53
41
  white-space: nowrap;
54
-
55
- &:first-child {
56
- padding-left: 0;
57
- }
58
42
  }
59
43
 
60
44
  td > span {
@@ -62,7 +46,9 @@ table:not(.remarkd-table) {
62
46
  }
63
47
 
64
48
  th {
65
- padding-bottom: 0;
49
+ @include m.text-style(button-label);
50
+ text-transform: capitalize;
51
+ border-top: 1px solid rgb(var(--zn-border-color));
66
52
  border-bottom: 1px solid rgb(var(--zn-border-color));
67
53
  }
68
54
  }
@@ -274,4 +274,7 @@
274
274
 
275
275
  /* Tooltips */
276
276
  --zn-tooltip-background-color: var(--zn-color-neutral-800);
277
+
278
+ /* tables */
279
+ --zn-table-header-background-color: 27, 27, 30;
277
280
  }
@@ -298,4 +298,8 @@
298
298
  --zn-z-index-toast: 950;
299
299
  --zn-z-index-tooltip: 1000;
300
300
  --zn-z-index-editor-action: 1000000;
301
+
302
+
303
+ /* tables */
304
+ --zn-table-header-background-color: 252, 251, 254;
301
305
  }
@@ -24,6 +24,7 @@ $text-styles: (
24
24
  "h1": ("size": 16px, "line": 16px, "spacing": 18px, "weight": semibold, "color": default, "transform": none),
25
25
  "h2": ("size": 14px, "line": 16px, "spacing": 15px, "weight": semibold, "color": default, "transform": none),
26
26
  "h3": ("size": 11px, "line": 16px, "spacing": 15px, "weight": semibold, "color": default, "transform": none),
27
+ "heading-section": ("size": 14px, "line": 16px, "spacing": 15px, "weight": semibold, "color": default, "transform": none),
27
28
  "subheading": ("size": 14px, "line": 16px, "spacing": 15px, "weight": normal, "color": muted, "transform": none),
28
29
  "subheading-small": ("size": 13px, "line": 16px, "spacing": 15px, "weight": normal, "color": muted, "transform": none),
29
30
  "input-placeholder": ("size": 14px, "line": 24px, "spacing": 18px, "weight": normal, "color": muted, "transform": none),
@@ -319,6 +319,10 @@
319
319
  background: rgb(var(--zn-panel));
320
320
  }
321
321
 
322
+ &.button--icon-button-round:hover:not([disabled]), &.button--icon-button-round:active:not([disabled]) {
323
+ background: rgb(var(--zn-color-tab));
324
+ }
325
+
322
326
  &.button--plain:hover:not([disabled]), &.button--plain:active:not([disabled]) {
323
327
  background: transparent;
324
328
  }
@@ -0,0 +1,280 @@
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
+ import ZincElement from '../../internal/zinc-element';
5
+
6
+ import styles from './channel-tile.scss';
7
+
8
+ export type ChannelTileColor =
9
+ 'default' | 'primary' | 'info' | 'success' | 'warning' | 'error' | 'disabled';
10
+
11
+ /**
12
+ * @summary A channel/queue slot tile with two faces: an active (occupied) state
13
+ * showing an in-progress item, and an available (empty) state advertising
14
+ * capacity — optionally reserving an incoming item with an auto-accept countdown.
15
+ * @documentation https://zinc.style/components/channel-tile
16
+ * @status experimental
17
+ * @since 1.0
18
+ *
19
+ * @dependency zn-button
20
+ * @dependency zn-icon
21
+ *
22
+ * @event zn-accept - Emitted when an available tile is accepted (click or auto-accept).
23
+ * Cancelable — call `preventDefault()` to suppress the built-in `accept-uri` fetch.
24
+ * @event zn-reject - Emitted when the reject control is pressed.
25
+ *
26
+ * @slot leading - Replaces the leading icon in the active state.
27
+ * @slot action - Action content for the available state (e.g. a form). Falls back to a default accept button.
28
+ * @slot footer - Trailing content (e.g. status badges) in the active state.
29
+ *
30
+ * @csspart base - The component's base wrapper.
31
+ * @csspart progress - The progress indicator (incoming countdown overlay or active progress bar).
32
+ *
33
+ * @cssproperty --channel-tile-color - Resolved accent color (set from the `color` property).
34
+ */
35
+ export default class ZnChannelTile extends ZincElement {
36
+ static styles: CSSResultGroup = unsafeCSS(styles);
37
+
38
+ /** Renders the empty/available face instead of the active face. */
39
+ @property({type: Boolean, reflect: true}) available: boolean = false;
40
+
41
+ /** (Available only) the tile is reserving an incoming item awaiting acceptance. */
42
+ @property({type: Boolean, reflect: true}) incoming: boolean = false;
43
+
44
+ /** Free-form grouping/theming key (reflected so consumers can query/style by it). */
45
+ @property({reflect: true}) variant: string = '';
46
+
47
+ /** Top-bar text (e.g. brand). */
48
+ @property() header: string = '';
49
+
50
+ /** Leading icon (active state). */
51
+ @property() icon: string = '';
52
+
53
+ /** Accent color driving the leading icon and `--channel-tile-color`. */
54
+ @property({reflect: true}) color: ChannelTileColor = 'default';
55
+
56
+ /** Primary line. Defaults to "Available" in the available state when unset. */
57
+ @property() title: string = '';
58
+
59
+ /** Secondary line. */
60
+ @property() subtitle: string = '';
61
+
62
+ /** (Active only) progress bar fill, 0–100. */
63
+ @property({type: Number}) progress: number = 0;
64
+
65
+ /** (Active only) CSS color for the progress bar fill. */
66
+ @property({attribute: 'progress-color'}) progressColor: string = '';
67
+
68
+ /** Identifier carried in `zn-accept` / `zn-reject` event details. */
69
+ @property({attribute: 'item-id'}) itemId: string = '';
70
+
71
+ /** (Available only) when set, accepting fetches this URI unless `zn-accept` is canceled. */
72
+ @property({attribute: 'accept-uri'}) acceptUri: string = '';
73
+
74
+ /** (Available/incoming) epoch (seconds or millis) at which the reservation window ends. */
75
+ @property({type: Number, attribute: 'reserved-until'}) reservedUntil: number = 0;
76
+
77
+ /** (Available/incoming) auto-accept window length in milliseconds. */
78
+ @property({type: Number, attribute: 'auto-accept-delay'}) autoAcceptDelay: number = 0;
79
+
80
+ /** (Available/incoming) whether a reject control is offered. */
81
+ @property({type: Boolean, reflect: true}) rejectable: boolean = false;
82
+
83
+ /** Icon for the built-in accept button. */
84
+ @property({attribute: 'accept-icon'}) acceptIcon: string = 'plus@lu';
85
+
86
+ /** Optional analytics id forwarded to the built-in accept button. */
87
+ @property({attribute: 'accept-gaid'}) acceptGaid: string = '';
88
+
89
+ /** Icon for the reject control. */
90
+ @property({attribute: 'reject-icon'}) rejectIcon: string = 'call_end';
91
+
92
+ /** Accessible label for the reject control. */
93
+ @property({attribute: 'reject-label'}) rejectLabel: string = 'Reject';
94
+
95
+ private _tickInterval: number | undefined;
96
+ private _autoAcceptFired: string = '';
97
+
98
+ protected firstUpdated(changed: PropertyValues) {
99
+ super.firstUpdated(changed);
100
+ this._startTicker();
101
+ }
102
+
103
+ disconnectedCallback() {
104
+ this._stopTicker();
105
+ super.disconnectedCallback();
106
+ }
107
+
108
+ protected updated(changed: PropertyValues) {
109
+ super.updated(changed);
110
+ if (changed.has('itemId') && this.itemId !== this._autoAcceptFired) {
111
+ this._autoAcceptFired = '';
112
+ }
113
+ }
114
+
115
+ private _isCountingDown(): boolean {
116
+ return this.available && this.incoming;
117
+ }
118
+
119
+ private _startTicker() {
120
+ if (this._tickInterval !== undefined) return;
121
+ this._tickInterval = window.setInterval(() => {
122
+ // Only the incoming countdown needs a self-driven re-render; active
123
+ // tiles are refreshed by the consumer setting `progress`/`color`.
124
+ if (!this._isCountingDown()) return;
125
+ this.requestUpdate();
126
+ this._maybeAutoAccept();
127
+ }, 200);
128
+ }
129
+
130
+ private _stopTicker() {
131
+ if (this._tickInterval !== undefined) {
132
+ clearInterval(this._tickInterval);
133
+ this._tickInterval = undefined;
134
+ }
135
+ }
136
+
137
+ private _maybeAutoAccept() {
138
+ if (!this._isCountingDown()) return;
139
+ if (!this.acceptUri) return;
140
+ if (!this.autoAcceptDelay || !this.reservedUntil) return;
141
+ if (this._autoAcceptFired === this.itemId) return;
142
+
143
+ const until = this._reservedUntilMs();
144
+ if (!until || Date.now() < until) return;
145
+
146
+ this._autoAcceptFired = this.itemId;
147
+ this._accept();
148
+ }
149
+
150
+ private _accept(): void {
151
+ const event = this.emit('zn-accept', {
152
+ cancelable: true,
153
+ detail: {itemId: this.itemId, acceptUri: this.acceptUri},
154
+ });
155
+ if (event.defaultPrevented || !this.acceptUri) return;
156
+ fetch(this.acceptUri, {credentials: 'same-origin'}).catch((err) => {
157
+ console.error('Error accepting interaction', err);
158
+ });
159
+ }
160
+
161
+ private _reservedUntilMs(): number {
162
+ if (!this.reservedUntil) return 0;
163
+ return this.reservedUntil.toString().length === 10
164
+ ? this.reservedUntil * 1000
165
+ : this.reservedUntil;
166
+ }
167
+
168
+ private _countdownPercent(): number {
169
+ const until = this._reservedUntilMs();
170
+ if (!until || !this.autoAcceptDelay) return 0;
171
+ const total = this.autoAcceptDelay;
172
+ const remaining = Math.max(0, until - Date.now());
173
+ const elapsed = Math.max(0, total - remaining);
174
+ return Math.min(100, Math.max(0, (elapsed / total) * 100));
175
+ }
176
+
177
+ private _handleReject = (e: MouseEvent) => {
178
+ e.preventDefault();
179
+ e.stopPropagation();
180
+ this.emit('zn-reject', {detail: {itemId: this.itemId, variant: this.variant}});
181
+ };
182
+
183
+ private _handleClick = (e: MouseEvent) => {
184
+ if (!this._isCountingDown() || !this.acceptUri) return;
185
+ if (e.defaultPrevented) return;
186
+ e.preventDefault();
187
+ e.stopPropagation();
188
+ this._accept();
189
+ };
190
+
191
+ protected render(): unknown {
192
+ const title = this.title || (this.available ? 'Available' : '');
193
+ const countdown = this._isCountingDown() ? this._countdownPercent() : null;
194
+
195
+ return html`
196
+ <div
197
+ part="base"
198
+ class=${classMap({
199
+ 'channel-tile': true,
200
+ 'channel-tile--available': this.available,
201
+ 'channel-tile--active': !this.available,
202
+ 'channel-tile--incoming': this.available && this.incoming,
203
+ [`channel-tile--${this.color}`]: true,
204
+ })}
205
+ role="button"
206
+ tabindex="0"
207
+ @click=${this._handleClick}>
208
+ <div class="channel-tile__header">${this.header ? html`<h3>${this.header}</h3>` : ''}</div>
209
+ ${countdown !== null
210
+ ? html`
211
+ <div part="progress" class="channel-tile__overlay" style="width: ${countdown}%"></div>`
212
+ : ''}
213
+ <div class="channel-tile__body">
214
+ ${this._renderLeading()}
215
+ <div class="channel-tile__content">
216
+ <h3 class="channel-tile__title">${title}</h3>
217
+ <p class="channel-tile__subtitle">${this.subtitle}</p>
218
+ </div>
219
+ <slot name="footer" class="channel-tile__footer"></slot>
220
+ </div>
221
+ ${!this.available
222
+ ? html`
223
+ <div part="progress" class="channel-tile__bar">
224
+ <div class="channel-tile__bar-fill"
225
+ style="width: ${this.progress}%; background-color: ${this.progressColor || 'transparent'};"></div>
226
+ </div>`
227
+ : ''}
228
+ </div>`;
229
+ }
230
+
231
+ private _renderLeading() {
232
+ if (this.available) {
233
+ return this._renderAvailableAction();
234
+ }
235
+
236
+ return html`
237
+ <slot name="leading">
238
+ ${this.icon
239
+ ? html`
240
+ <zn-button icon=${this.icon}
241
+ icon-size="20"
242
+ icon-button="round"
243
+ color=${this.color}
244
+ no-hover></zn-button>`
245
+ : ''}
246
+ </slot>`;
247
+ }
248
+
249
+ private _renderAvailableAction() {
250
+ if (this.incoming && this.rejectable) {
251
+ return html`
252
+ <button class="channel-tile__reject" type="button" aria-label=${this.rejectLabel}
253
+ @click=${this._handleReject}>
254
+ <zn-icon src=${this.rejectIcon} size="22"></zn-icon>
255
+ </button>`;
256
+ }
257
+
258
+ if (this.acceptUri) {
259
+ return html`
260
+ <zn-button href=${this.acceptUri}
261
+ icon=${this.acceptIcon}
262
+ icon-size="20"
263
+ icon-button="round"
264
+ gaid=${this.acceptGaid}
265
+ loading-text="Accepting"
266
+ no-hover>
267
+ </zn-button>`;
268
+ }
269
+
270
+ return html`
271
+ <slot name="action">
272
+ <zn-button icon=${this.acceptIcon}
273
+ icon-size="20"
274
+ icon-button="round"
275
+ gaid=${this.acceptGaid}
276
+ no-hover>
277
+ </zn-button>
278
+ </slot>`;
279
+ }
280
+ }