@kubex/zinc 1.0.7 → 1.0.8

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 (31) hide show
  1. package/dist/custom-elements.json +318 -10
  2. package/dist/vscode.html-custom-data.json +33 -5
  3. package/dist/web-types.json +80 -12
  4. package/dist/zn.d.ts +53 -3
  5. package/dist/zn.min.js +371 -311
  6. package/docs/data/products-table.json +203 -0
  7. package/docs/pages/components/button.md +25 -0
  8. package/docs/pages/components/data-table.md +21 -0
  9. package/docs/pages/components/header.md +7 -0
  10. package/docs/pages/components/order-table.md +82 -5
  11. package/package.json +1 -1
  12. package/src/components/button/button.component.ts +168 -10
  13. package/src/components/button/button.scss +115 -0
  14. package/src/components/content-block/content-block.component.ts +18 -7
  15. package/src/components/content-block/content-block.scss +4 -2
  16. package/src/components/data-select/data-select.component.ts +0 -1
  17. package/src/components/data-table/data-table.component.ts +112 -20
  18. package/src/components/data-table/data-table.scss +14 -3
  19. package/src/components/editor/editor.scss +1 -0
  20. package/src/components/expanding-action/expanding-action.component.ts +1 -1
  21. package/src/components/header/header.component.ts +8 -2
  22. package/src/components/header/header.scss +5 -1
  23. package/src/components/linked-select/linked-select.component.ts +1 -0
  24. package/src/components/menu/menu.component.ts +0 -1
  25. package/src/components/navbar/navbar.component.ts +1 -1
  26. package/src/components/note/note.component.ts +70 -2
  27. package/src/components/note/note.scss +26 -4
  28. package/src/components/order-table/order-table.component.ts +49 -28
  29. package/src/components/panel/panel.component.ts +3 -1
  30. package/src/components/rating/rating.component.ts +10 -9
  31. package/src/components/rating/rating.scss +15 -1
@@ -23,6 +23,7 @@
23
23
  border-color: transparent;
24
24
  height: 100%;
25
25
  display: inline-flex;
26
+ position: relative;
26
27
 
27
28
  &.button--info {
28
29
  --btn-color: var(--zn-color-info);
@@ -39,6 +40,10 @@
39
40
  &.button--success {
40
41
  --btn-color: var(--zn-color-success);
41
42
  }
43
+
44
+ &__label--hidden {
45
+ visibility: hidden;
46
+ }
42
47
  }
43
48
 
44
49
  .button.button--muted-notification {
@@ -306,6 +311,116 @@
306
311
  }
307
312
  }
308
313
 
314
+ .button--loading-bg {
315
+ background-color: #e0e0e0;
316
+
317
+ &-transparent {
318
+ background-color: transparent;
319
+ }
320
+ }
321
+
322
+ .button--loading {
323
+ &-fill {
324
+ pointer-events: none;
325
+ position: absolute;
326
+ z-index: 1;
327
+ background-color: rgba(var(--btn-color), .85);
328
+ border-radius: inherit;
329
+ transition: width 0s;
330
+ animation: loading-fill-grow var(--loading-duration, 2s) linear forwards;
331
+ inset: -1px;
332
+
333
+ &-transparent {
334
+ background-color: rgba(var(--zn-primary), 0.1);
335
+ }
336
+ }
337
+
338
+ &-container {
339
+ position: absolute;
340
+ top: 0;
341
+ left: 0;
342
+ width: 100%;
343
+ height: 100%;
344
+ display: flex;
345
+ align-items: center;
346
+ justify-content: center;
347
+ pointer-events: none;
348
+ }
349
+
350
+ &-text-bottom, &-text-top {
351
+ width: 100%;
352
+ height: 100%;
353
+ display: flex;
354
+ align-items: center;
355
+ justify-content: center;
356
+ font-size: inherit;
357
+ font-weight: inherit;
358
+ user-select: none;
359
+ position: absolute;
360
+ top: 0;
361
+ left: 0;
362
+ pointer-events: none;
363
+
364
+ .loading-countdown {
365
+ margin-left: 3px;
366
+ }
367
+
368
+ &-left {
369
+ justify-content: flex-start;
370
+ padding-inline-start: 7px;
371
+ }
372
+
373
+ &-right {
374
+ justify-content: flex-end;
375
+ padding-inline-end: 7px;
376
+ }
377
+ }
378
+
379
+ &-text-bottom {
380
+ z-index: 2;
381
+ color: rgba(var(--btn-color), .85);
382
+
383
+ &-transparent {
384
+ color: inherit;
385
+ }
386
+ }
387
+
388
+ &-text-top {
389
+ z-index: 3;
390
+ color: inherit;
391
+ /* Animate the reveal from left to right using a mask */
392
+ mask-image: linear-gradient(90deg, #000 0%, #000 100%);
393
+ mask-size: 0 100%;
394
+ mask-repeat: no-repeat;
395
+ animation: loading-text-reveal var(--loading-duration, 2s) linear forwards;
396
+ }
397
+ }
398
+
399
+ .button--relative-wrapper {
400
+ position: relative;
401
+ display: inline-flex;
402
+ height: 100%;
403
+ width: 100%;
404
+ }
405
+
406
+ @keyframes loading-fill-grow {
407
+ from {
408
+ width: 0;
409
+ }
410
+ to {
411
+ width: 100%;
412
+ }
413
+ }
414
+
415
+ @keyframes loading-text-reveal {
416
+ from {
417
+ mask-size: 0 100%;
418
+ }
419
+ to {
420
+ mask-size: 100% 100%;
421
+ }
422
+ }
423
+
309
424
  :host([vertical-align="center"]) {
310
425
  align-self: center;
311
426
  }
@@ -1,11 +1,11 @@
1
1
  import {classMap} from "lit/directives/class-map.js";
2
2
  import {deepQuerySelectorAll} from "../../utilities/query";
3
3
  import {HasSlotController} from "../../internal/slot";
4
+ import type {PropertyValues} from 'lit';
4
5
  import {html, nothing, unsafeCSS} from 'lit';
5
6
  import {property, queryAssignedNodes, queryAsync} from 'lit/decorators.js';
6
7
  import {unsafeHTML} from 'lit-html/directives/unsafe-html.js';
7
8
  import ZincElement from "../../internal/zinc-element";
8
- import type {PropertyValues} from 'lit';
9
9
  import type ZnTile from "../tile";
10
10
 
11
11
  import styles from './content-block.scss';
@@ -29,7 +29,11 @@ export default class ContentBlock extends ZincElement {
29
29
  @property() avatar = '';
30
30
 
31
31
  @property({type: Boolean, reflect: true}) outbound = false;
32
+
33
+ @property({type: Boolean, attribute: "no-collapse"}) noCollapse = false;
34
+
32
35
  @property({type: Boolean, reflect: true}) short = false;
36
+
33
37
  @property({attribute: 'default-display', reflect: true}) defaultDisplay: 'text' | 'html' = 'text';
34
38
 
35
39
  @queryAssignedNodes({slot: 'html', flatten: true}) htmlNodes!: Node[];
@@ -84,6 +88,9 @@ export default class ContentBlock extends ZincElement {
84
88
  }
85
89
 
86
90
  private _collapseContent(e: Event) {
91
+ if (this.noCollapse) {
92
+ return;
93
+ }
87
94
  const target = e.target as HTMLElement;
88
95
  const headerClick = target.classList.contains('content-block-header') ||
89
96
  target.parentElement?.classList.contains('content-block-header');
@@ -186,20 +193,24 @@ export default class ContentBlock extends ZincElement {
186
193
  flush-footer="${hasFooter || nothing}"
187
194
  class="${classMap({
188
195
  'content-block--outbound': this.outbound,
189
- 'content-block--short': this.short
196
+ 'content-block--short': this.short,
197
+ 'content-block--collapsible': !this.noCollapse
190
198
  })}">
191
199
 
192
200
  <zn-header class="content-block-header"
193
- caption="${this.sender}"
194
201
  description="${this.truncateText()}"
195
202
  @click="${this._collapseContent}">
196
203
 
197
- <zn-icon src="${this.avatar}"
198
- library="avatar"
199
- round></zn-icon>
204
+ <slot name="icon">
205
+ <zn-icon src="${this.avatar}"
206
+ library="avatar"
207
+ round></zn-icon>
208
+ </slot>
209
+
210
+ <slot name="caption" slot="caption">${this.sender}</slot>
200
211
 
201
212
  <div slot="actions">
202
- <small>${this.time}</small>
213
+ <slot name="actions"><small>${this.time}</small></slot>
203
214
  ${showActions ? html`
204
215
  <zn-dropdown>
205
216
  <zn-button slot="trigger" icon="more_vert" icon-size="24" color="transparent"
@@ -19,8 +19,10 @@ zn-header div[slot='actions'] {
19
19
  gap: 10px;
20
20
  }
21
21
 
22
- zn-header::part(base) {
23
- cursor: pointer;
22
+ .content-block--collapsible {
23
+ zn-header::part(base) {
24
+ cursor: pointer;
25
+ }
24
26
  }
25
27
 
26
28
  zn-header::part(header-left) {
@@ -231,7 +231,6 @@ export default class ZnDataSelect extends ZincElement implements ZincFormControl
231
231
  private _updatePrefix() {
232
232
  // Set the prefix of the select to the selected values prefix
233
233
  const selectedOption = this.select.selectedOptions[0];
234
- console.log('selectedOption', selectedOption);
235
234
  if (selectedOption && (this.iconPosition !== 'none' || this.iconOnly) && this.selectPrefix) {
236
235
  if (this.iconPosition === 'none' && this.iconOnly) {
237
236
  this.iconPosition = 'start';
@@ -2,7 +2,7 @@ import {classMap} from "lit/directives/class-map.js";
2
2
  import {type CSSResultGroup, html, nothing, type TemplateResult, unsafeCSS} from 'lit';
3
3
  import {HasSlotController} from "../../internal/slot";
4
4
  import {ifDefined} from "lit/directives/if-defined.js";
5
- import {property} from 'lit/decorators.js';
5
+ import {property, query} from 'lit/decorators.js';
6
6
  import {ref} from "lit/directives/ref.js";
7
7
  import {Task} from "@lit/task";
8
8
  import {unsafeHTML} from "lit/directives/unsafe-html.js";
@@ -184,7 +184,7 @@ export default class ZnDataTable extends ZincElement {
184
184
  @property({attribute: "empty-state-icon"}) emptyStateIcon: string = "data_alert";
185
185
 
186
186
  // Hide the checkbox column
187
- @property({attribute: 'hide-checkboxes', type: Boolean}) hideCheckboxes: boolean = true;
187
+ @property({attribute: 'hide-checkboxes', type: Boolean}) hideCheckboxes: boolean;
188
188
 
189
189
  @property() filters: [] = [];
190
190
 
@@ -192,6 +192,12 @@ export default class ZnDataTable extends ZincElement {
192
192
 
193
193
  @property({attribute: "no-initial-load", type: Boolean}) noInitialLoad: boolean = false;
194
194
 
195
+ @property({attribute: 'group-by'}) groupBy = '';
196
+
197
+ @property() groups = '';
198
+
199
+ @query('#select-all-rows') selectAllButton: ZnButton;
200
+
195
201
  // Data Table Properties
196
202
  private _initialLoad = true;
197
203
  private _lastTableContent: TemplateResult = html``;
@@ -226,6 +232,11 @@ export default class ZnDataTable extends ZincElement {
226
232
  return {rows: [], page: 1, perPage: this.itemsPerPage, total: 0};
227
233
  }
228
234
 
235
+ if (this.groupBy) {
236
+ // we want to load all the data possible so we can group and show multiple tables
237
+ this.itemsPerPage = 1000;
238
+ }
239
+
229
240
  const requestData: DataRequest = {
230
241
  page: this.page,
231
242
  perPage: this.itemsPerPage,
@@ -390,13 +401,58 @@ export default class ZnDataTable extends ZincElement {
390
401
  return this.emptyState();
391
402
  }
392
403
 
404
+ this._rows = this.getRows(data);
405
+
406
+ if (this.groupBy) {
407
+ // we need to group the rows by the group column
408
+ const groupedRows: Record<string, Row[]> = {};
409
+
410
+ // add groups to the current rows
411
+ const toAdd = this.groups.split(',').map(g => g.trim()).filter(g => g.length > 0);
412
+ toAdd.forEach((group) => {
413
+ if (!groupedRows[this.humanize(group)]) {
414
+ groupedRows[this.humanize(group)] = [];
415
+ }
416
+ });
417
+
418
+ this._rows.forEach((row: Row) => {
419
+ const groupCell = row.cells.find((cell: Cell) => cell.column === this.groupBy);
420
+ const groupValue = groupCell ? groupCell.text : 'Ungrouped';
421
+ if (!groupedRows[this.humanize(groupValue)] && (!this.groups || groupValue === 'Ungrouped')) {
422
+ groupedRows[this.humanize(groupValue)] = [];
423
+ }
424
+
425
+ if (groupedRows[this.humanize(groupValue)]) {
426
+ groupedRows[this.humanize(groupValue)].push(row);
427
+ }
428
+ });
429
+
430
+ // render a table for each group
431
+ return html`
432
+ <zn-sp flush>
433
+ ${Object.keys(groupedRows).map((groupKey) => html`
434
+ <div class="table-group">
435
+ <h3 class="table-group__title">${groupKey === "Ungrouped" ? "" : groupKey}</h3>
436
+ ${this.renderTableData(groupedRows[groupKey])}
437
+ </div>`
438
+ )}
439
+ </zn-sp>
440
+ `;
441
+ }
442
+
443
+
444
+ return this.renderTableData(this._rows);
445
+ }
446
+
447
+ public humanize(str: string) {
448
+ return str.charAt(0).toUpperCase() + str.slice(1);
449
+ }
450
+
451
+ public renderTableData(data: any) {
393
452
  const filteredHeaders = Object.values(this.headers).filter((header) => {
394
453
  return !Object.values(this.hiddenColumns).includes(header.key);
395
454
  });
396
455
 
397
- this._rows = this.getRows(data);
398
-
399
- const hasSelectedRows = this.selectedRows.length > 0;
400
456
  this.rowHasActions = this._rows.some((row: Row) => row.actions && row.actions.length > 0);
401
457
 
402
458
  // To make sure the cells are in the same order as the headers, we need to map them cell.col to heading.key
@@ -421,21 +477,17 @@ export default class ZnDataTable extends ZincElement {
421
477
  <table class="${classMap({
422
478
  'table': true,
423
479
  'table--standalone': this.standalone,
424
- 'with-hover': !this.unsortable,
480
+ 'with-hover': !this.unsortable && !this.hideCheckboxes,
425
481
  })}">
426
482
  <thead>
427
483
  <tr>
428
- ${this.hideCheckboxes || !hasSelectedRows ? html`` : html`
429
- <th>
430
- <div><input type="checkbox" @change="${this.selectAll}"></div>
431
- </th>`}
432
484
  ${filteredHeaders.map((header: HeaderConfig) => this.renderCellHeader(header))}
433
485
  ${this.rowHasActions ? html`
434
486
  <th></th>` : html``}
435
487
  </tr>
436
488
  </thead>
437
489
  <tbody>
438
- ${this._rows.map((row: Row) => html`
490
+ ${data.map((row: Row) => html`
439
491
  <tr class="${classMap({'table__row--selected': this.isRowSelected(row)})}">
440
492
  ${row.cells.map((value: Cell, index: number) => this.renderCellBody(index, value))}
441
493
  ${this.rowHasActions ? this.renderActions(row) : null}
@@ -552,10 +604,37 @@ export default class ZnDataTable extends ZincElement {
552
604
  getActions() {
553
605
  const actions = [];
554
606
 
607
+ const hasSlots = this.hasSlotController.test(ActionSlots.delete.valueOf())
608
+ || this.hasSlotController.test(ActionSlots.modify.valueOf())
609
+ || this.hasSlotController.test(ActionSlots.create.valueOf());
610
+
611
+ if(!hasSlots) {
612
+ return [];
613
+ }
614
+
615
+ if (!this.hideCheckboxes && this._rows.length > 0) {
616
+ actions.push(html`
617
+ <zn-button @click="${this.selectAll}"
618
+ id="select-all-rows"
619
+ color="transparent"
620
+ size="x-small"
621
+ icon="indeterminate_check_box"
622
+ icon-size="22"
623
+ icon-color="primary"
624
+ tooltip="Select All"
625
+ slot="trigger">
626
+ </zn-button>`);
627
+ }
628
+
555
629
  if (this.selectedRows.length > 0) {
556
630
  actions.push(html`
557
- <zn-button @click="${this.clearSelectedRows}" size="small" outline>
558
- Clear Selection
631
+ <zn-button @click="${this.clearSelectedRows}"
632
+ color="transparent"
633
+ size="x-small"
634
+ icon="disabled_by_default"
635
+ icon-size="22"
636
+ tooltip="Clear Selection"
637
+ slot="trigger">
559
638
  </zn-button>`);
560
639
 
561
640
  if (this.hasSlotController.test(ActionSlots.delete.valueOf())) {
@@ -613,21 +692,25 @@ export default class ZnDataTable extends ZincElement {
613
692
  }
614
693
 
615
694
  selectAll(event: Event) {
616
- const checkbox = event.target as HTMLInputElement;
617
- const checked = checkbox.checked;
695
+ const button = event.target as ZnButton;
696
+ if (button.disabled) return;
618
697
 
619
- // go through all the checkboxes and check them
620
- for (const row of this.renderRoot.querySelectorAll('tbody zn-checkbox')) {
621
- (row as HTMLInputElement).checked = checked;
698
+ if (this.numberOfRowsSelected === this._rows.length) {
699
+ this.clearSelectedRows(event);
700
+ return;
622
701
  }
623
702
 
624
- this.selectedRows = checked ? this._rows : [];
703
+ this.selectedRows = this._rows;
625
704
  this.numberOfRowsSelected = this.selectedRows.length;
626
705
  this.updateKeys();
627
706
  this.requestUpdate();
628
707
  }
629
708
 
630
709
  selectRow(e: Event) {
710
+ if (this.hideCheckboxes) {
711
+ return;
712
+ }
713
+
631
714
  if (!(e.target && (e.target instanceof Element))) {
632
715
  return;
633
716
  }
@@ -833,7 +916,7 @@ export default class ZnDataTable extends ZincElement {
833
916
 
834
917
  return html`
835
918
  <td
836
- @click="${this.selectRow}"
919
+ @click="${this.hideCheckboxes ? undefined : this.selectRow}"
837
920
  class="${classMap({
838
921
  'table__cell': true,
839
922
  'table__cell--wide': headerKey === this.wideColumn,
@@ -889,10 +972,19 @@ export default class ZnDataTable extends ZincElement {
889
972
  }
890
973
 
891
974
  private updateKeys() {
975
+ this.updateSelectAll();
892
976
  this.updateModifyKeys();
893
977
  this.updateDeleteKeys();
894
978
  }
895
979
 
980
+ private updateSelectAll() {
981
+ if (this.numberOfRowsSelected === this._rows.length) {
982
+ this.selectAllButton.icon = 'check_box';
983
+ } else {
984
+ this.selectAllButton.icon = 'indeterminate_check_box';
985
+ }
986
+ }
987
+
896
988
  private updateModifyKeys() {
897
989
  this.updateActionKeys('modify-action');
898
990
  }
@@ -364,11 +364,9 @@ table tbody tr.table__row--selected {
364
364
  border-bottom-right-radius: var(--zn-border-radius-large);
365
365
  }
366
366
 
367
- & + tr td {
367
+ & + tr.table__row--selected td {
368
368
  border-top: 0 !important;
369
- }
370
369
 
371
- & + tr.table__row--selected td {
372
370
  &:first-of-type {
373
371
  border-top-left-radius: 0;
374
372
  }
@@ -400,3 +398,16 @@ table tbody tr.table__row--selected {
400
398
  table:has(tbody tr:first-of-type.table__row--selected) thead tr th {
401
399
  border-bottom-color: var(--zn-color-purple-600) !important;
402
400
  }
401
+
402
+
403
+ .table-group__title {
404
+ font-weight: var(--zn-font-weight-semibold);
405
+ font-size: var(--zn-font-size-large);
406
+ color: rgba(var(--zn-text-panel-title), 100%);
407
+ line-height: var(--zn-line-height-looser);
408
+ padding-left: var(--zn-spacing-medium);
409
+ margin: 0;
410
+ overflow: hidden;
411
+ white-space: nowrap;
412
+ text-overflow: ellipsis;
413
+ }
@@ -66,6 +66,7 @@
66
66
 
67
67
  img {
68
68
  max-width: 40%;
69
+ height: auto;
69
70
  }
70
71
  }
71
72
 
@@ -231,7 +231,7 @@ export default class ZnExpandingAction extends ZincElement {
231
231
  this.color = color;
232
232
  }
233
233
  } catch {
234
- console.log('Unable to fetch context headers');
234
+ //console.log('Unable to fetch context headers');
235
235
  }
236
236
  }
237
237
 
@@ -39,6 +39,7 @@ export default class ZnHeader extends ZincElement {
39
39
  @property({type: Boolean}) transparent: boolean = false;
40
40
 
41
41
  @property() caption: string;
42
+ @property() icon: string;
42
43
 
43
44
  @property() description: string;
44
45
 
@@ -136,7 +137,8 @@ export default class ZnHeader extends ZincElement {
136
137
 
137
138
 
138
139
  <div class="content" part="content">
139
- ${hasBreadcrumb ? html`<slot name="breadcrumb" class="breadcrumb"></slot>` : null}
140
+ ${hasBreadcrumb ? html`
141
+ <slot name="breadcrumb" class="breadcrumb"></slot>` : null}
140
142
 
141
143
  ${hasPreviousPath ? html`
142
144
  <a href="${this.previousPath}" class="caption__back"
@@ -151,7 +153,11 @@ export default class ZnHeader extends ZincElement {
151
153
 
152
154
  <div class="caption">
153
155
  <div part="header-left" class="header__left">
154
- <span class="header__caption" part="header-caption">${this.caption}</span>
156
+ <span class="header__caption" part="header-caption">
157
+ ${this.icon ? html`
158
+ <zn-icon class="header__icon" src="${this.icon}"></zn-icon>` : null}
159
+ <slot name="caption">${this.caption}</slot>
160
+ </span>
155
161
  ${this.description ? html`
156
162
  <span class="header__description"
157
163
  part="header-description">${this.description}</span>`
@@ -24,6 +24,10 @@
24
24
  overflow: hidden;
25
25
  white-space: nowrap;
26
26
  text-overflow: ellipsis;
27
+ display: flex;
28
+ gap: var(--zn-spacing-x-small);
29
+ align-items: center;
30
+ text-wrap: auto;
27
31
  }
28
32
 
29
33
  &--has-breadcrumb {
@@ -195,7 +199,7 @@
195
199
  text-decoration: none;
196
200
  }
197
201
 
198
- zn-icon {
202
+ a zn-icon {
199
203
  cursor: pointer;
200
204
 
201
205
  &:hover {
@@ -123,6 +123,7 @@ export default class ZnLinkedSelect extends ZincElement implements ZincFormContr
123
123
  }
124
124
 
125
125
  public handleLinkedSelectChange = () => {
126
+ this.value = "";
126
127
  this.requestUpdate();
127
128
  this.formControlController.updateValidity();
128
129
  };
@@ -169,7 +169,6 @@ export default class ZnMenu extends ZincElement {
169
169
  // get the parent dropdown and close it
170
170
  (closestMenu?.closest('zn-dropdown') as ZnDropdown | null)?.hide();
171
171
 
172
- console.log('emitting select')
173
172
  this.emit('zn-select', {detail: {item}});
174
173
  }
175
174
 
@@ -182,7 +182,7 @@ export default class ZnNavbar extends ZincElement {
182
182
  }
183
183
 
184
184
  showMore() {
185
- console.log("Showing More")
185
+ //console.log("Showing More")
186
186
  }
187
187
 
188
188
  render() {
@@ -1,7 +1,7 @@
1
1
  import {classMap} from "lit/directives/class-map.js";
2
2
  import {type colors} from "../data-select/providers/color-data-provider";
3
3
  import {type CSSResultGroup, html, unsafeCSS} from 'lit';
4
- import {property} from "lit/decorators.js";
4
+ import {property, state} from "lit/decorators.js";
5
5
  import ZincElement from '../../internal/zinc-element';
6
6
 
7
7
  import styles from './note.scss';
@@ -32,6 +32,58 @@ export default class ZnNote extends ZincElement {
32
32
  @property({reflect: true}) date: string = '';
33
33
  @property({type: HTMLElement, reflect: true}) body: string = '';
34
34
 
35
+ // Number of lines at which the note body collapses; 0 disables collapsing
36
+ @property({type: Number, attribute: 'collapse-at-lines', reflect: true}) collapseAtLines: number = 3;
37
+
38
+ // Internal state: whether the body is currently expanded
39
+ @state() private expanded = false;
40
+
41
+ // Internal state: whether the content actually overflows the clamp
42
+ @state() private _isOverflowing = false;
43
+
44
+ private _toggleExpand = () => {
45
+ this.expanded = !this.expanded;
46
+ // re-evaluate overflow after toggle (in case of dynamic content)
47
+ this.updateComplete.then(() => this._measureOverflow());
48
+ }
49
+
50
+ private _measureOverflow() {
51
+ // Only relevant when collapseAtLines is set and not expanded
52
+ const container = this.shadowRoot?.querySelector('.note__body-container') as HTMLElement | null;
53
+ if (!container) {
54
+ this._isOverflowing = false;
55
+ return;
56
+ }
57
+
58
+ if (this.collapseAtLines <= 0) {
59
+ this._isOverflowing = false;
60
+ return;
61
+ }
62
+
63
+ // Temporarily ensure clamped styles are applied to measure
64
+ container.style.setProperty('--note-collapse-lines', String(this.collapseAtLines));
65
+ container.classList.toggle('note__body--clamped', !this.expanded);
66
+
67
+ // element starts hidden, cannot measure
68
+ if (container.clientHeight === 0) {
69
+ this._isOverflowing = false;
70
+ return;
71
+ }
72
+
73
+ // Measure overflow
74
+ this._isOverflowing = container.scrollHeight > container.clientHeight + 1; // +1 for rounding errors
75
+ }
76
+
77
+ protected firstUpdated() {
78
+ // Initial measurement of overflow
79
+ this._measureOverflow();
80
+ }
81
+
82
+ protected updated() {
83
+ // Re-measure on any update to catch slot/content changes
84
+ this._measureOverflow();
85
+ }
86
+
35
87
  protected render(): unknown {
36
88
  return html`
37
89
  <div class="${classMap({
@@ -52,7 +104,23 @@ export default class ZnNote extends ZincElement {
52
104
  <small>${this.date}</small>
53
105
  </slot>
54
106
  </div>
55
- <slot class="note__body"></slot>
107
+ <div class="${classMap({
108
+ 'note__body-container': true,
109
+ 'note__body--clamped': this.collapseAtLines > 0 && !this.expanded
110
+ })}" style="--note-collapse-lines: ${this.collapseAtLines}">
111
+ <slot class="note__body"></slot>
112
+ </div>
113
+ ${this.collapseAtLines > 0 && this._isOverflowing ? html`
114
+ <div class="note__toggle">
115
+ <zn-button color="transparent"
116
+ size="content"
117
+ class="note__toggle__btn"
118
+ @click="${this._toggleExpand}"
119
+ aria-expanded="${String(this.expanded)}">
120
+ ${this.expanded ? 'Show less' : 'Show more'}
121
+ </zn-button>
122
+ </div>
123
+ ` : ''}
56
124
  <slot name="footer" class="note__footer"></slot>
57
125
  </div>
58
126
  `;