@kubex/zinc 1.1.39 → 1.1.41

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.
@@ -14,4 +14,19 @@ describe('<zn-data-table>', () => {
14
14
 
15
15
  expect(() => (el as unknown as { updateKeys: () => void }).updateKeys()).not.to.throw();
16
16
  });
17
+
18
+ it('accepts a single Row object on the data property', async () => {
19
+ const el = await fixture<ZnDataTable>(html` <zn-data-table></zn-data-table> `);
20
+ (el as unknown as {data: unknown}).data = {
21
+ id: '1',
22
+ cells: [{text: 'Solo', column: 'name'}],
23
+ };
24
+ expect(() => (el as unknown as {requestUpdate: () => void}).requestUpdate()).not.to.throw();
25
+ });
26
+
27
+ it('exposes displayTemplates as an object property', async () => {
28
+ const el = await fixture<ZnDataTable>(html` <zn-data-table></zn-data-table> `);
29
+ const templates = (el as unknown as {displayTemplates: Record<string, unknown>}).displayTemplates;
30
+ expect(templates).to.be.an('object');
31
+ });
17
32
  });
@@ -98,7 +98,19 @@ export default class ZnDataTableSearch extends ZincElement implements ZincFormCo
98
98
  if (!slot) return params;
99
99
 
100
100
  const elements = slot.assignedElements({flatten: true});
101
- const allowedInputs = ['zn-input', 'zn-select', 'zn-query-builder', 'zn-multiselect', 'zn-params-select', 'zn-datepicker', 'input', 'select', 'textarea'];
101
+ const allowedInputs = [
102
+ 'zn-input',
103
+ 'zn-select',
104
+ 'zn-query-builder',
105
+ 'zn-multiselect',
106
+ 'zn-params-select',
107
+ 'zn-datepicker',
108
+ 'input',
109
+ 'select',
110
+ 'textarea',
111
+ 'zn-cols',
112
+ 'zn-input-group',
113
+ ];
102
114
 
103
115
  elements.forEach((element) => {
104
116
  if (allowedInputs.includes(element.tagName.toLowerCase())) {
@@ -153,9 +153,39 @@ export default class ZnDatepicker extends ZincElement implements ZincFormControl
153
153
  */
154
154
  @property() format: string = 'MM/dd/yyyy';
155
155
 
156
+ /** Display time selector. **/
157
+ @property({attribute: 'time-picker',type: Boolean, reflect: true}) timePicker?: boolean = false;
158
+
159
+ /** Display only time selector, without date. **/
160
+ @property({attribute: 'only-time'}) onlyTimepicker?: boolean = false;
161
+
162
+ /**
163
+ * Time format for display and input selector. Uses AirDatepicker format tokens.
164
+ * Default : hh:mm AA
165
+ *
166
+ * Possible symbols:
167
+ * h — hours in 12-hour mode
168
+ * hh — hours in 12-hour mode with leading zero
169
+ * H — hours in 24-hour mode
170
+ * HH — hours in 24-hour mode with leading zero
171
+ * m — minutes
172
+ * mm — minutes with leading zero
173
+ * aa — day period lower case
174
+ * AA — day period upper case
175
+ */
176
+ @property({attribute: 'format-time'}) timeFormat?: string = "hh:mm AA";
177
+ @property({attribute: 'container'}) container?: string | HTMLElement;
178
+
179
+
180
+
156
181
  private _instance: AirDatepicker<HTMLInputElement>;
157
182
 
158
183
 
184
+ get timestamp(): number {
185
+ const timestamp = (this.input.parentElement!.querySelector("#date-picker-timestamp")! as HTMLInputElement).value ?? '';
186
+ return Number(timestamp);
187
+ }
188
+
159
189
  /** Gets the validity state object */
160
190
  get validity() {
161
191
  return this.input?.validity;
@@ -194,6 +224,8 @@ export default class ZnDatepicker extends ZincElement implements ZincFormControl
194
224
  @watch('minDate', {waitUntilFirstUpdate: true})
195
225
  @watch('maxDate', {waitUntilFirstUpdate: true})
196
226
  @watch('disablePastDates', {waitUntilFirstUpdate: true})
227
+ @watch('timePicker', {waitUntilFirstUpdate: true})
228
+ @watch('timeFormat', {waitUntilFirstUpdate: true})
197
229
  handleDatepickerOptionsChange() {
198
230
  this.init();
199
231
  }
@@ -241,24 +273,44 @@ export default class ZnDatepicker extends ZincElement implements ZincFormControl
241
273
  }
242
274
  }
243
275
 
244
- const inputElement = this.shadowRoot?.querySelector('input') as HTMLInputElement;
276
+ const inputElement = this.shadowRoot!.querySelector('input')! as HTMLInputElement;
245
277
  if (inputElement) {
278
+ const timestampField = this.shadowRoot!.querySelector("#date-picker-timestamp")! as HTMLElement;
279
+
246
280
  const options: AirDatepickerOptions = {
247
281
  locale: enLocale,
248
282
  dateFormat: this.format,
249
283
  range: this.range,
250
284
  toggleSelected: false,
285
+ timepicker: this.timePicker,
286
+ onlyTimepicker: this.onlyTimepicker,
287
+ timeFormat: this.timeFormat,
288
+ container: this.container,
289
+ altField: timestampField,
251
290
  onSelect: ({date}) => {
291
+ // Blur the input after selection to prevent invisible keyboard navigation
292
+ if (this.timePicker || this.onlyTimepicker){
293
+ return
294
+ }
295
+ if (date && !this.range) {
296
+ // For single date selection, blur immediately
297
+ this.input.blur();
252
298
  this.handleChange();
253
- // Blur the input after selection to prevent invisible keyboard navigation
254
- if (date && !this.range) {
255
- // For single date selection, blur immediately
256
- this.input.blur();
257
- } else if (date && this.range && Array.isArray(date) && date.length === 2) {
258
- // For range selection, blur only after both dates are selected
259
- this.input.blur();
260
- }
299
+ } else if (date && this.range && Array.isArray(date) && date.length === 2) {
300
+ // For range selection, blur only after both dates are selected
301
+ this.input.blur();
302
+ this.handleChange();
261
303
  }
304
+ },
305
+ onHide:(isAnimationComplete) => {
306
+ if (!isAnimationComplete) {
307
+ return
308
+ }
309
+
310
+ if (this.timePicker || this.onlyTimepicker){
311
+ this.handleChange();
312
+ }
313
+ },
262
314
  };
263
315
 
264
316
  if (this.minDate) {
@@ -532,8 +584,11 @@ export default class ZnDatepicker extends ZincElement implements ZincFormControl
532
584
  return formatted;
533
585
  }
534
586
 
535
- protected updated(_changedProperties: PropertyValues) {
587
+ updated(_changedProperties: PropertyValues) {
536
588
  super.updated(_changedProperties);
589
+ }
590
+
591
+ firstUpdated(){
537
592
  this.init();
538
593
  }
539
594
 
@@ -617,13 +672,12 @@ export default class ZnDatepicker extends ZincElement implements ZincFormControl
617
672
  ?autofocus=${this.autofocus}
618
673
  spellcheck=${this.spellcheck}
619
674
  aria-describedby="help-text"
620
- @change=${this.handleChange}
621
675
  @input=${this.handleInput}
622
676
  @invalid=${this.handleInvalid}
623
677
  @keydown=${this.handleKeyDown}
624
678
  @paste=${this.handlePaste}
625
679
  @blur=${this.handleBlur}>
626
-
680
+ <input id="date-picker-timestamp" type="hidden">
627
681
  <span part="suffix" class="input__suffix">
628
682
  <slot name="suffix"></slot>
629
683
  </span>
@@ -1,6 +1,7 @@
1
1
  @use "../../wc";
2
2
  @use "../../form-control";
3
3
 
4
+
4
5
  :host {
5
6
  display: block;
6
7
  }
@@ -3,6 +3,7 @@ import {deepQuerySelectorAll} from "../../utilities/query";
3
3
  import {FormControlController} from '../../internal/form';
4
4
  import {on} from "../../utilities/on";
5
5
  import {property, query} from 'lit/decorators.js';
6
+ import {Store} from "../../internal/storage";
6
7
  import Attachment from "./modules/attachment/attachment";
7
8
  import ContextMenu from "./modules/context-menu/context-menu";
8
9
  import DatePicker from "./modules/date-picker/date-picker";
@@ -90,6 +91,21 @@ export default class ZnEditor extends ZincElement implements ZincFormControl {
90
91
  @property({attribute: 'ai', type: Boolean}) aiEnabled: boolean = false;
91
92
  @property({attribute: 'ai-path'}) aiPath: string = '';
92
93
 
94
+ /**
95
+ * Caches unsent content while typing and restores it when the editor next loads,
96
+ * so agent replies in tickets/chats survive reloads and navigation. Use a key
97
+ * unique to the conversation (e.g. the ticket ID). The cache is cleared on submit.
98
+ */
99
+ @property({attribute: 'store-key', reflect: true}) storeKey: string = "";
100
+
101
+ /** Cached-content expiry in seconds. Defaults to 1 day. */
102
+ @property({attribute: 'store-ttl', type: Number, reflect: true}) storeTtl = 86400;
103
+
104
+ /** Cache to localStorage instead of sessionStorage, persisting across tabs and browser restarts. */
105
+ @property({attribute: 'local-storage', type: Boolean, reflect: true}) localStorage: boolean;
106
+
107
+ protected _store: Store;
108
+
93
109
  private quillElement: Quill;
94
110
  private _content: string = '';
95
111
  private _selectionRange: Range = {index: 0, length: 0};
@@ -119,6 +135,11 @@ export default class ZnEditor extends ZincElement implements ZincFormControl {
119
135
  this.formControlController.updateValidity();
120
136
  }
121
137
 
138
+ connectedCallback() {
139
+ super.connectedCallback();
140
+ this._store = new Store(this.localStorage ? window.localStorage : window.sessionStorage, "zned:", this.storeTtl);
141
+ }
142
+
122
143
  protected firstUpdated(_changedProperties: PropertyValues) {
123
144
  this.formControlController.updateValidity();
124
145
 
@@ -261,6 +282,7 @@ export default class ZnEditor extends ZincElement implements ZincFormControl {
261
282
  this.quillElement = quill;
262
283
 
263
284
  this.getForm()?.addEventListener('submit', () => {
285
+ this._clearCachedContent();
264
286
  setTimeout(() => {
265
287
  const attachmentModule = this.quillElement.getModule('attachment') as Attachment;
266
288
  attachmentModule?.reset();
@@ -372,6 +394,14 @@ export default class ZnEditor extends ZincElement implements ZincFormControl {
372
394
  this._selectionRange = range ?? oldRange;
373
395
  });
374
396
 
397
+ if (this.storeKey && this._isEmpty(this.value ?? '')) {
398
+ const cached = this._store.get(this.storeKey);
399
+ if (cached) {
400
+ this.value = cached;
401
+ this.editorHtml.value = cached;
402
+ }
403
+ }
404
+
375
405
  const delta = quill.clipboard.convert({html: this.value});
376
406
  quill.setContents(delta, Quill.sources.SILENT);
377
407
 
@@ -382,15 +412,30 @@ export default class ZnEditor extends ZincElement implements ZincFormControl {
382
412
  private _handleTextChange() {
383
413
  this.value = this.quillElement.getSemanticHTML();
384
414
  this.editorHtml.value = this.value;
415
+ this._cacheContent();
385
416
  this.emit('zn-change');
386
417
  }
387
418
 
388
- private _getQuillKeyboardBindings() {
389
- const empty = (value: string) => {
390
- const match = value.match(/[^<pbr\s>/]/);
391
- return match === null;
392
- };
419
+ private _isEmpty(value: string) {
420
+ return value.match(/[^<pbr\s>/]/) === null;
421
+ }
422
+
423
+ private _cacheContent() {
424
+ if (!this.storeKey || !this._store) return;
425
+ if (this._isEmpty(this.value)) {
426
+ this._store.remove(this.storeKey);
427
+ } else {
428
+ this._store.set(this.storeKey, this.value);
429
+ }
430
+ }
393
431
 
432
+ private _clearCachedContent() {
433
+ if (this.storeKey && this._store) {
434
+ this._store.remove(this.storeKey);
435
+ }
436
+ }
437
+
438
+ private _getQuillKeyboardBindings() {
394
439
  // Always add an Enter binding to support emoji selection in all interaction types
395
440
  return {
396
441
  'enter': {
@@ -406,7 +451,7 @@ export default class ZnEditor extends ZincElement implements ZincFormControl {
406
451
 
407
452
  if (this.interactionType === 'chat') {
408
453
  const form = this.closest('form');
409
- const hasText = !!this.value && this.value.trim().length > 0 && !empty(this.value);
454
+ const hasText = !!this.value && this.value.trim().length > 0 && !this._isEmpty(this.value);
410
455
  const attachmentInput = form?.querySelector('input[name="attachments"]') as HTMLInputElement | null;
411
456
  const hasAttachments = !!attachmentInput?.value && attachmentInput.value !== '[]';
412
457
  if (form && (hasText || hasAttachments)) {
@@ -1,5 +1,6 @@
1
1
  import '../../../dist/zn.min.js';
2
- import { expect, fixture, html } from '@open-wc/testing';
2
+ import {expect, fixture, html} from '@open-wc/testing';
3
+ import type ZnEditor from './editor.component';
3
4
 
4
5
  describe('<zn-editor>', () => {
5
6
  it('should render a component', async () => {
@@ -7,4 +8,64 @@ describe('<zn-editor>', () => {
7
8
 
8
9
  expect(el).to.exist;
9
10
  });
11
+
12
+ describe('content caching', () => {
13
+ const KEY = 'zned:test-draft';
14
+
15
+ afterEach(() => {
16
+ sessionStorage.removeItem(KEY);
17
+ localStorage.removeItem(KEY);
18
+ });
19
+
20
+ it('should restore cached content into an empty editor', async () => {
21
+ sessionStorage.setItem(KEY, '0,<p>Hello draft</p>');
22
+ const el = await fixture<ZnEditor>(html`
23
+ <zn-editor store-key="test-draft"></zn-editor>`);
24
+
25
+ expect(el.value).to.contain('Hello draft');
26
+ });
27
+
28
+ it('should not overwrite an initial value with cached content', async () => {
29
+ sessionStorage.setItem(KEY, '0,<p>Hello draft</p>');
30
+ const el = await fixture<ZnEditor>(html`
31
+ <zn-editor store-key="test-draft" .value=${'<p>Existing</p>'}></zn-editor>`);
32
+
33
+ expect(el.value).to.contain('Existing');
34
+ expect(el.value).to.not.contain('Hello draft');
35
+ });
36
+
37
+ it('should read from localStorage when local-storage is set', async () => {
38
+ localStorage.setItem(KEY, '0,<p>Local draft</p>');
39
+ const el = await fixture<ZnEditor>(html`
40
+ <zn-editor store-key="test-draft" local-storage></zn-editor>`);
41
+
42
+ expect(el.value).to.contain('Local draft');
43
+ });
44
+
45
+ it('should cache content when the editor changes', async () => {
46
+ const el = await fixture<ZnEditor>(html`
47
+ <zn-editor store-key="test-draft" .value=${'<p>Typed reply</p>'}></zn-editor>`);
48
+
49
+ document.dispatchEvent(new Event('zn-editor-update'));
50
+ await el.updateComplete;
51
+
52
+ // Quill's getSemanticHTML encodes spaces as &nbsp;
53
+ expect(sessionStorage.getItem(KEY)).to.contain('Typed&nbsp;reply');
54
+ });
55
+
56
+ it('should clear cached content on form submit', async () => {
57
+ const form = await fixture<HTMLFormElement>(html`
58
+ <form @submit=${(e: Event) => e.preventDefault()}>
59
+ <zn-editor store-key="test-draft" .value=${'<p>Typed reply</p>'}></zn-editor>
60
+ </form>`);
61
+ const el = form.querySelector<ZnEditor>('zn-editor')!;
62
+
63
+ document.dispatchEvent(new Event('zn-editor-update'));
64
+ await el.updateComplete;
65
+ expect(sessionStorage.getItem(KEY)).to.contain('Typed&nbsp;reply');
66
+
67
+ form.requestSubmit();
68
+ expect(sessionStorage.getItem(KEY)).to.equal(null);
69
+ });
70
+ });
10
71
  });
@@ -13,6 +13,7 @@ import type {ZnChangeEvent} from "../../events/zn-change";
13
13
  import type {ZnInputEvent} from "../../events/zn-input";
14
14
 
15
15
  import styles from './query-builder.scss';
16
+ import type ZnDatepicker from "../datepicker";
16
17
 
17
18
  export type QueryBuilderData = QueryBuilderItem[];
18
19
 
@@ -22,10 +23,24 @@ export interface QueryBuilderItem {
22
23
  type?: QueryBuilderType;
23
24
  options?: QueryBuilderOptions;
24
25
  operators: QueryBuilderOperators[];
26
+ dateSubmitFormat?: QueryBuilderDateSubmitFormat;
25
27
  maxOptionsVisible?: string;
26
28
  }
27
29
 
28
- export type QueryBuilderType = 'bool' | 'boolean' | 'date' | 'number';
30
+ /**
31
+ * Controls how `date` and `dateTime` filter values are serialized when the
32
+ * query is submitted.
33
+ *
34
+ * - `'iso'` — RFC 3339 / ISO 8601 (e.g. `2026-06-09T16:05:00Z`).
35
+ * - `'timestamp'` — Unix timestamp in seconds since epoch.
36
+ * - `'legacy'` — whatever format the current system emits. Kept so existing
37
+ * backends keep working while consumers migrate to one of the
38
+ * formats above. - DEFAULT
39
+ */
40
+ export type QueryBuilderDateSubmitFormat = 'iso' | 'timestamp' | 'legacy';
41
+
42
+ export type QueryBuilderType = 'bool' | 'boolean' | 'date' | 'dateTime' | 'number';
43
+
29
44
 
30
45
  export interface QueryBuilderOptions {
31
46
  [key: string | number]: string | number;
@@ -187,6 +202,7 @@ export default class ZnQueryBuilder extends ZincElement implements ZincFormContr
187
202
  </zn-option>`)}
188
203
  </zn-select>
189
204
  <input id="main-input" name="${this.name}" value="${this.value}" hidden>
205
+ <div id="air-datepicker-query-builder-container" class="air-datepicker-global-container"></div>
190
206
  </div>
191
207
  `;
192
208
  }
@@ -214,6 +230,7 @@ export default class ZnQueryBuilder extends ZincElement implements ZincFormContr
214
230
  if (filter === undefined) return;
215
231
 
216
232
  const uniqueId = Math.random().toString(36).substring(7);
233
+
217
234
  this._selectedRules.set(uniqueId, {
218
235
  id: filter.id,
219
236
  name: filter.name,
@@ -306,7 +323,7 @@ export default class ZnQueryBuilder extends ZincElement implements ZincFormContr
306
323
  }
307
324
 
308
325
  private _createInput(filter: QueryBuilderItem, uniqueId: string, selectedComparator: QueryBuilderOperators) {
309
- let input: ZnSelect | ZnInput | null;
326
+ let input: ZnSelect | ZnInput | ZnDatepicker | null;
310
327
  switch (filter.type) {
311
328
  case 'bool':
312
329
  case 'boolean': {
@@ -318,7 +335,11 @@ export default class ZnQueryBuilder extends ZincElement implements ZincFormContr
318
335
  break;
319
336
  }
320
337
  case 'date': {
321
- input = this._createDateInput(uniqueId);
338
+ input = this._createDateInput(uniqueId, false, filter.dateSubmitFormat);
339
+ break;
340
+ }
341
+ case 'dateTime': {
342
+ input = this._createDateInput(uniqueId, true , filter.dateSubmitFormat);
322
343
  break;
323
344
  }
324
345
  default: {
@@ -373,13 +394,19 @@ export default class ZnQueryBuilder extends ZincElement implements ZincFormContr
373
394
  return litToHTML<ZnInput>(input);
374
395
  }
375
396
 
376
- private _createDateInput(uniqueId: string): ZnInput | null {
397
+ private _createDateInput(uniqueId: string, hasTime: boolean, submitFormat: QueryBuilderDateSubmitFormat = 'legacy'): ZnDatepicker | null {
377
398
  const input = html`
378
- <zn-input type="date"
379
- class="query-builder__value"
380
- @zn-input="${(e: ZnInputEvent) => this._updateDateValue(uniqueId, e)}">
381
- </zn-input>`;
382
- return litToHTML<ZnInput>(input);
399
+ <zn-datepicker
400
+ name="${uniqueId}"
401
+ format-time="HH:mm"
402
+ .container=${this.container.querySelector('#air-datepicker-query-builder-container')}
403
+ time-picker="${hasTime}"
404
+ class="query-builder__value"
405
+ @zn-change="${(e: ZnChangeEvent) => this._updateDateValue(uniqueId, e, submitFormat)}"
406
+ >
407
+ </zn-datepicker>
408
+ `;
409
+ return litToHTML<ZnDatepicker>(input);
383
410
  }
384
411
 
385
412
  private _createSelectInput(uniqueId: string, filter: QueryBuilderItem, selectedComparator: QueryBuilderOperators): ZnSelect | null {
@@ -426,23 +453,36 @@ export default class ZnQueryBuilder extends ZincElement implements ZincFormContr
426
453
  this._handleChange();
427
454
  }
428
455
 
429
- private _updateDateValue(id: string, event: Event | { target: ZnSelect | ZnInput | HTMLDivElement }) {
456
+ private _updateDateValue(id: string, event: Event | { target: ZnDatepicker | HTMLDivElement }, submitFormat: QueryBuilderDateSubmitFormat = 'legacy') {
430
457
  const filter = this._selectedRules.get(id);
431
458
  if (!filter) return;
432
- const input = event.target as ZnSelect | ZnInput;
433
- const operator = filter.operator as QueryBuilderOperators;
434
- let timestamp: string;
435
459
 
436
- if (operator === QueryBuilderOperators.Eq || operator === QueryBuilderOperators.Neq) {
437
- timestamp = (Date.parse(input.value as string) / 1000).toString();
438
- } else {
439
- // Dodgy logic to offset backend filter comparator values
440
- // Ref: backend/src/Infrastructure/Helpers/AdvancedFilterHelper.php:106
441
- const multiplier = operator === QueryBuilderOperators.Before ? -1 : 1;
442
- timestamp = (Math.floor((Date.now() - Date.parse(input.value as string)) / 1000 / 60) * multiplier).toString();
460
+ const input = event.target as ZnDatepicker;
461
+ const operator = filter.operator as QueryBuilderOperators;
462
+ let d: Date;
463
+ let value: string;
464
+
465
+ switch (submitFormat) {
466
+ case "legacy":
467
+ if (operator === QueryBuilderOperators.Eq || operator === QueryBuilderOperators.Neq) {
468
+ value = (input.timestamp / 1000).toString();
469
+ } else {
470
+ // Dodgy logic to offset backend filter comparator values
471
+ // Ref: backend/src/Infrastructure/Helpers/AdvancedFilterHelper.php:106
472
+ const multiplier = operator === QueryBuilderOperators.Before ? -1 : 1;
473
+ value = (Math.floor((Date.now() - input.timestamp) / 1000 / 60) * multiplier).toString();
474
+ }
475
+ break;
476
+ case "timestamp":
477
+ value = input.timestamp.toString();
478
+ break;
479
+ case "iso":
480
+ d = new Date(input.timestamp);
481
+ value = new Date(d.getTime() - d.getTimezoneOffset() * 60_000).toISOString();
482
+ break;
443
483
  }
444
484
 
445
- filter.value = timestamp as string;
485
+ filter.value = value;
446
486
 
447
487
  this._selectedRules.set(id, filter);
448
488
  this._handleChange();
@@ -459,6 +499,7 @@ export default class ZnQueryBuilder extends ZincElement implements ZincFormContr
459
499
  this._handleChange();
460
500
  }
461
501
 
502
+
462
503
  private updateInValue(id: string, event: Event) {
463
504
  const filter = this._selectedRules.get(id);
464
505
  if (!filter) return;
@@ -1,6 +1,10 @@
1
1
  @use "../../wc";
2
2
  @use "../../../scss/shared/form-elements";
3
3
 
4
+
5
+ @use "../../../scss/air-datapicker";
6
+
7
+
4
8
  :host {
5
9
  display: contents;
6
10
 
@@ -1,10 +1,11 @@
1
1
  import {type CSSResultGroup, unsafeCSS} from 'lit';
2
+ import {customElement, property} from 'lit/decorators.js';
2
3
  import {LocalizeController} from '../../utilities/localize';
3
- import {property} from 'lit/decorators.js';
4
4
  import ZincElement from '../../internal/zinc-element';
5
5
 
6
6
  import styles from './style.scss';
7
7
 
8
+ @customElement('zn-style')
8
9
  export default class ZnStyle extends ZincElement {
9
10
  static styles: CSSResultGroup = unsafeCSS(styles);
10
11
 
@@ -12,7 +13,8 @@ export default class ZnStyle extends ZincElement {
12
13
  private readonly localize = new LocalizeController(this);
13
14
 
14
15
  @property() color = '';
15
- @property({type: Boolean}) border = false;
16
+ @property() border = '';
17
+ @property() size = '';
16
18
  @property({type: Boolean}) error = false;
17
19
  @property({type: Boolean}) success = false;
18
20
  @property({type: Boolean}) info = false;
@@ -26,6 +28,7 @@ export default class ZnStyle extends ZincElement {
26
28
  @property() height = '';
27
29
  @property() pad = '';
28
30
  @property() margin = '';
31
+ @property({type: Boolean}) muted = false;
29
32
  @property({type: Boolean}) gutter = false;
30
33
  @property({attribute: 'a-margin'}) autoMargin = '';
31
34
 
@@ -81,9 +84,34 @@ export default class ZnStyle extends ZincElement {
81
84
  this.classList.toggle('h-full', true);
82
85
  }
83
86
 
84
- if (this.border) {
87
+ // border accepts any combination of t/b/l/r, plus 'a' (or 'tblr') as a
88
+ // shortcut for all four sides. Existing `<zn-style border>` (boolean
89
+ // attribute → empty string value with attribute present) keeps rendering
90
+ // all four sides; pure absence renders no border.
91
+ const borderSides = this.hasAttribute('border')
92
+ ? (this.border || 'a')
93
+ : '';
94
+ if (borderSides) {
85
95
  display = 'inline-block'
86
- this.classList.toggle('zn-border', true);
96
+ for (const c of borderSides) {
97
+ switch (c) {
98
+ case 'a':
99
+ this.classList.toggle('zn-border', true);
100
+ break;
101
+ case 't':
102
+ this.classList.toggle('zn-bt', true);
103
+ break;
104
+ case 'b':
105
+ this.classList.toggle('zn-bb', true);
106
+ break;
107
+ case 'l':
108
+ this.classList.toggle('zn-bl', true);
109
+ break;
110
+ case 'r':
111
+ this.classList.toggle('zn-br', true);
112
+ break;
113
+ }
114
+ }
87
115
  }
88
116
 
89
117
  if (this.pad) {
@@ -174,6 +202,17 @@ export default class ZnStyle extends ZincElement {
174
202
  }
175
203
  }
176
204
 
205
+ if (this.muted) {
206
+ display = 'inline-block';
207
+ this.classList.toggle('zn-muted', true);
208
+ }
209
+
210
+ // size: xs/s/l/xl each toggle a sizing class. 'm' (and empty) are the
211
+ // default and apply no class.
212
+ if (this.size && this.size !== 'm') {
213
+ this.classList.toggle('zn-size-' + this.size, true);
214
+ }
215
+
177
216
  if (this.display) {
178
217
  // Force attribute display
179
218
  display = this.display