@kubex/zinc 1.1.46 → 1.1.48

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.
@@ -52,6 +52,35 @@ layout: component
52
52
  </zn-content-block>
53
53
  ```
54
54
 
55
+ ### Subject
56
+
57
+ When a subject is available it is always shown in the header after the sender — expanded or
58
+ collapsed — in place of the truncated body preview. When `subject` is empty,
59
+ `subject-placeholder` is shown dimmed in its place. Omit both attributes to fall back to the
60
+ body preview (e.g. notes).
61
+
62
+ ```html:preview
63
+
64
+ <zn-content-block
65
+ time="16 Jun 2025, 13:00"
66
+ sender="John Smith"
67
+ avatar="JS"
68
+ subject="Re: Help with my order"
69
+ >
70
+ <div slot="text">Text formatted message goes here.</div>
71
+ </zn-content-block>
72
+
73
+ <zn-content-block
74
+ time="16 Jun 2025, 13:05"
75
+ sender="John Smith"
76
+ avatar="JS"
77
+ subject=""
78
+ subject-placeholder="This message has no subject."
79
+ >
80
+ <div slot="text">Text formatted message goes here.</div>
81
+ </zn-content-block>
82
+ ```
83
+
55
84
  ### Attachments
56
85
 
57
86
  Attachments render in a row at the bottom of the content and are only visible while the block
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.46",
3
+ "version": "1.1.48",
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",
@@ -176,6 +176,13 @@ export default class ZnChannelTile extends ZincElement {
176
176
  return Math.min(100, Math.max(0, (elapsed / total) * 100));
177
177
  }
178
178
 
179
+ private _remainingSeconds(): number | null {
180
+ // Only surfaced for auto-accept countdowns, not plain reservation windows.
181
+ const until = this._reservedUntilMs();
182
+ if (!until || !this.autoAcceptDelay) return null;
183
+ return Math.max(0, Math.ceil((until - Date.now()) / 1000));
184
+ }
185
+
179
186
  private _handleReject = (e: MouseEvent) => {
180
187
  e.preventDefault();
181
188
  e.stopPropagation();
@@ -193,6 +200,7 @@ export default class ZnChannelTile extends ZincElement {
193
200
  protected render(): unknown {
194
201
  const title = this.title || (this.available ? 'Available' : '');
195
202
  const countdown = this._isCountingDown() ? this._countdownPercent() : null;
203
+ const remaining = this._isCountingDown() ? this._remainingSeconds() : null;
196
204
 
197
205
  return html`
198
206
  <div
@@ -215,8 +223,12 @@ export default class ZnChannelTile extends ZincElement {
215
223
  <div class="channel-tile__body">
216
224
  ${this._renderLeading()}
217
225
  <div class="channel-tile__content">
218
- <h3 class="channel-tile__title"><slot name="title">${title}</slot></h3>
219
- <p class="channel-tile__subtitle"><slot name="subtitle">${this.subtitle}</slot></p>
226
+ ${remaining !== null
227
+ ? html`
228
+ <h3 class="channel-tile__title">${this.subtitle} (${remaining}s)</h3>`
229
+ : html`
230
+ <h3 class="channel-tile__title"><slot name="title">${title}</slot></h3>
231
+ <p class="channel-tile__subtitle"><slot name="subtitle">${this.subtitle}</slot></p>`}
220
232
  </div>
221
233
  <slot name="footer" class="channel-tile__footer"></slot>
222
234
  </div>
@@ -69,11 +69,11 @@
69
69
 
70
70
  .channel-tile__overlay {
71
71
  position: absolute;
72
- top: 0;
72
+ top: 24px; // sit below the header strip (fixed 24px)
73
73
  left: 0;
74
74
  bottom: 0;
75
75
  width: 0;
76
- background-color: rgba(var(--zn-color-success), 0.55);
76
+ background-color: rgb(154, 232, 162);
77
77
  transition: width 0.2s linear;
78
78
  pointer-events: none;
79
79
  z-index: 0;
@@ -35,6 +35,16 @@ export default class ContentBlock extends ZincElement {
35
35
  @property() sender = '';
36
36
  @property() avatar = '';
37
37
 
38
+ /** The message subject, shown in the header after the sender. */
39
+ @property() subject = '';
40
+
41
+ /**
42
+ * Placeholder shown dimmed in place of the subject when `subject` is empty
43
+ * (e.g. "This message has no subject."). Omit both for content without a
44
+ * subject line (e.g. notes).
45
+ */
46
+ @property({attribute: 'subject-placeholder'}) subjectPlaceholder = '';
47
+
38
48
  @property({type: Boolean, reflect: true}) outbound = false;
39
49
 
40
50
  @property({type: Boolean, attribute: "no-collapse"}) noCollapse = false;
@@ -225,7 +235,12 @@ export default class ContentBlock extends ZincElement {
225
235
  <span class="content-block-header__caption">
226
236
  <slot name="caption">${this.sender}</slot>
227
237
  </span>
228
- <span class="content-block-header__description">${this.truncateText()}</span>
238
+ ${this.subject || this.subjectPlaceholder ? html`
239
+ <span class="${classMap({
240
+ 'content-block-header__subject': true,
241
+ 'content-block-header__subject--empty': !this.subject
242
+ })}">${this.subject || this.subjectPlaceholder}</span>` : html`
243
+ <span class="content-block-header__description">${this.truncateText()}</span>`}
229
244
  ${this.attachmentCount > 0 ? html`
230
245
  <span class="content-block-header__attachments" part="header-attachments">
231
246
  <zn-icon src="paperclip@lu" size="14"></zn-icon>${this.attachmentCount}
@@ -6,6 +6,9 @@
6
6
  -webkit-font-smoothing: antialiased;
7
7
  -moz-osx-font-smoothing: grayscale;
8
8
 
9
+ background-color: rgba(var(--zn-panel), var(--zn-panel-opacity));
10
+ border-bottom: 1px solid rgb(var(--zn-border-color));
11
+
9
12
  padding-inline: calc(var(--zn-gutter, 0) + var(--zn-base-gap, 0));
10
13
 
11
14
  *, *::before, *::after {
@@ -58,6 +61,19 @@
58
61
  text-overflow: ellipsis;
59
62
  }
60
63
 
64
+ .content-block-header__subject {
65
+ flex: 1 1 auto;
66
+ min-width: 0;
67
+ font-size: var(--zn-font-size-medium);
68
+ overflow: hidden;
69
+ white-space: nowrap;
70
+ text-overflow: ellipsis;
71
+
72
+ &--empty {
73
+ opacity: 0.5;
74
+ }
75
+ }
76
+
61
77
  .content-block-header__description {
62
78
  flex: 1 1 auto;
63
79
  min-width: 0;
@@ -421,18 +421,26 @@ export default class ZnFile extends ZincElement implements ZincFormControl {
421
421
  // Use the transferred file list from the drag drop interface
422
422
  const hasTrigger = this.hasSlotController.test('trigger');
423
423
  if (!hasTrigger) {
424
- const disappearAnimation = getAnimation(this.inputChosen, 'file.text.disappear', {dir: this.localize.dir()});
425
- const appearAnimation = getAnimation(this.inputChosen, 'file.text.appear', {dir: this.localize.dir()});
424
+ const inputChosen = this.inputChosen;
425
+ const dropareaIcon = this.dropareaIcon;
426
426
 
427
- if (this.droparea) {
428
- const dropIconAnimation = getAnimation(this.dropareaIcon, 'file.iconDrop', {dir: this.localize.dir()});
427
+ if (this.droparea && dropareaIcon) {
428
+ const dropIconAnimation = getAnimation(dropareaIcon, 'file.iconDrop', {dir: this.localize.dir()});
429
429
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
430
- animateTo(this.dropareaIcon, dropIconAnimation.keyframes, dropIconAnimation.options);
430
+ animateTo(dropareaIcon, dropIconAnimation.keyframes, dropIconAnimation.options);
431
431
  }
432
- // eslint-disable-next-line max-len
433
- await animateTo(this.inputChosen, disappearAnimation.keyframes, disappearAnimation.options);
432
+
433
+ if (inputChosen) {
434
+ const disappearAnimation = getAnimation(inputChosen, 'file.text.disappear', {dir: this.localize.dir()});
435
+ await animateTo(inputChosen, disappearAnimation.keyframes, disappearAnimation.options);
436
+ }
437
+
434
438
  this.handleFiles(files);
435
- await animateTo(this.inputChosen, appearAnimation.keyframes, appearAnimation.options);
439
+
440
+ if (inputChosen) {
441
+ const appearAnimation = getAnimation(inputChosen, 'file.text.appear', {dir: this.localize.dir()});
442
+ await animateTo(inputChosen, appearAnimation.keyframes, appearAnimation.options);
443
+ }
436
444
  } else {
437
445
  this.handleFiles(files);
438
446
  }
@@ -1,10 +1,34 @@
1
1
  import '../../../dist/zn.min.js';
2
- import { expect, fixture, html } from '@open-wc/testing';
2
+ import {expect, fixture, html, oneEvent} from '@open-wc/testing';
3
+ import type ZnFile from './file.component';
3
4
 
4
- describe('<zn-drag-upload>', () => {
5
+ describe('<zn-file>', () => {
5
6
  it('should render a component', async () => {
6
- const el = await fixture(html` <zn-drag-upload></zn-drag-upload> `);
7
+ const el = await fixture(html`<zn-file></zn-file>`);
7
8
 
8
9
  expect(el).to.exist;
9
10
  });
11
+
12
+ for (const droparea of [false, true]) {
13
+ it(`accepts a drop when the ${droparea ? 'droparea' : 'standard input'} has no animation target`, async () => {
14
+ const el = await fixture<ZnFile>(html`<zn-file ?droparea=${droparea}></zn-file>`);
15
+ const dataTransfer = new DataTransfer();
16
+ dataTransfer.items.add(new File(['content'], 'example.txt', {type: 'text/plain'}));
17
+ const dropHandler = el as unknown as {
18
+ handleTransferItems: (items: DataTransferItemList | null) => Promise<FileList>;
19
+ };
20
+ dropHandler.handleTransferItems = () => Promise.resolve(dataTransfer.files);
21
+
22
+ const changed = oneEvent(el, 'zn-change');
23
+ el.shadowRoot!.querySelector<HTMLElement>('.form-control')!.dispatchEvent(new DragEvent('drop', {
24
+ bubbles: true,
25
+ cancelable: true,
26
+ dataTransfer
27
+ }));
28
+ await changed;
29
+
30
+ expect(el.files).to.have.length(1);
31
+ expect(el.files![0].name).to.equal('example.txt');
32
+ });
33
+ }
10
34
  });
@@ -164,4 +164,10 @@
164
164
  border: none !important;
165
165
  background-color: transparent !important;
166
166
  }
167
+
168
+ & .textarea__control {
169
+ background-color: transparent;
170
+ box-shadow: none;
171
+ border: none;
172
+ }
167
173
  }