@kubex/zinc 1.1.67 → 1.1.68

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.67",
3
+ "version": "1.1.68",
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",
@@ -36,6 +36,7 @@ const DEFAULT_PER_PAGE = 10;
36
36
 
37
37
  interface Cell {
38
38
  text: string;
39
+ subText?: string;
39
40
  column: string;
40
41
  color?: string;
41
42
  style?: string;
@@ -51,6 +52,7 @@ interface Cell {
51
52
  uri?: string;
52
53
  target?: string;
53
54
  copyable?: boolean;
55
+ title?: string;
54
56
  }
55
57
 
56
58
  interface Row {
@@ -123,7 +125,7 @@ const defaultTemplates = {
123
125
  dateTime: ((cell) => {
124
126
  const t = cell.text || '';
125
127
  return html`
126
- <div class="table__collum--datetime">
128
+ <div class="table__collum--datetime" title="${ifDefined(cell.title)}">
127
129
  <span><strong>${t.slice(0, 10)}</strong></span>
128
130
  <zn-style size="s" muted>${t.slice(11)}</zn-style>
129
131
  </div>`
@@ -1037,7 +1039,7 @@ export default class ZnDataTable extends ZincElement {
1037
1039
  if (header?.cellTemplate !== undefined) {
1038
1040
  data = {...header.cellTemplate, ...data}
1039
1041
  }
1040
-
1042
+
1041
1043
  let content: TemplateResult | ZincElement = html`${data.text}`;
1042
1044
 
1043
1045
  if (data.style || data.color) {
@@ -1063,55 +1065,25 @@ export default class ZnDataTable extends ZincElement {
1063
1065
  border="${isBorder || nothing}"
1064
1066
  center="${isCenter || nothing}"
1065
1067
  muted="${isMuted || nothing}"
1066
- color="${ifDefined(data.color)}">${content}
1068
+ color="${ifDefined(data.color)}"
1069
+ title="${ifDefined(data.title)}"
1070
+ >${content}
1067
1071
  </zn-style>`;
1068
1072
  }
1069
1073
 
1070
1074
  if (data.uri) {
1071
1075
  content = html`
1072
1076
  <a href="${data.uri}"
1077
+ title="${ifDefined(data.title)}"
1073
1078
  data-target="${ifDefined(data.target || nothing)}"
1074
1079
  gaid="${ifDefined(data.gaid || nothing)}">${content}</a>`;
1075
1080
  }
1076
1081
 
1077
1082
  if (data.chipColor) {
1078
- return html`
1079
- <zn-chip type="${data.chipColor}">${content}</zn-chip>`;
1083
+ content = html`
1084
+ <zn-chip type="${data.chipColor}" title="${ifDefined(data.title)}">${content}</zn-chip>`;
1080
1085
  }
1081
1086
 
1082
- if (data.hoverContent) {
1083
- const placement = data.hoverPlacement ?? 'top';
1084
-
1085
- if (data.iconSrc) {
1086
- const src = data.iconSrc;
1087
- const color = data.iconColor;
1088
- const size = data.iconSize;
1089
- const tokens = (data.iconStyle ?? '').split(',').map(s => s.trim()).filter(Boolean);
1090
-
1091
- return html`
1092
- ${content}
1093
- <zn-hover-container placement="${placement}" flip>
1094
- <zn-icon
1095
- src="${src}"
1096
- size="${ifDefined(size)}"
1097
- color="${ifDefined(color)}"
1098
- ${ref(el => {
1099
- if (!el) return;
1100
- tokens.forEach(t => el.setAttribute(t, ''));
1101
- })}
1102
- ></zn-icon>
1103
- <div slot="content">
1104
- ${unsafeHTML(data.hoverContent)}
1105
- </div>
1106
- </zn-hover-container>`;
1107
- }
1108
-
1109
- return html`
1110
- <zn-hover-container placement="${placement}" flip>
1111
- <div slot="anchor">${content}</div>
1112
- ${unsafeHTML(data.hoverContent)}
1113
- </zn-hover-container>`;
1114
- }
1115
1087
 
1116
1088
  if (data.iconSrc) {
1117
1089
  const src = data.iconSrc;
@@ -1119,24 +1091,57 @@ export default class ZnDataTable extends ZincElement {
1119
1091
  const size = data.iconSize;
1120
1092
  const tokens = (data.iconStyle ?? '').split(',').map(s => s.trim()).filter(Boolean);
1121
1093
 
1122
- return html`
1094
+ content = html`
1123
1095
  <zn-icon
1124
1096
  src="${src}"
1125
1097
  size="${ifDefined(size)}"
1126
1098
  color="${ifDefined(color)}"
1127
1099
  ${ref(el => {
1128
- if (!el) return;
1129
- tokens.forEach(t => el.setAttribute(t, ''));
1130
- })}
1100
+ if (!el) return;
1101
+ tokens.forEach(t => el.setAttribute(t, ''));
1102
+ })}
1103
+ title="${ifDefined(data.title)}"
1131
1104
  ></zn-icon> ${content}`;
1132
1105
  }
1133
1106
 
1107
+ if (data.hoverContent) {
1108
+ const placement = data.hoverPlacement ?? 'top';
1109
+ if (data.iconSrc) {
1110
+ content = html`
1111
+ <zn-hover-container placement="${placement}" flip>
1112
+ ${content}
1113
+ <div slot="content">
1114
+ ${unsafeHTML(data.hoverContent)}
1115
+ </div>
1116
+ </zn-hover-container>`;
1117
+ } else {
1118
+ content = html`
1119
+ <zn-hover-container placement="${placement}" flip>
1120
+ ${content}
1121
+ <div slot="content">
1122
+ ${unsafeHTML(data.hoverContent)}
1123
+ </div>
1124
+ </zn-hover-container>`;
1125
+ }
1126
+ }
1127
+
1134
1128
  if (data.copyable) {
1135
- return html`
1129
+ content = html`
1136
1130
  ${content}
1137
1131
  <zn-copy-button value="${data.text}"></zn-copy-button>`;
1138
1132
  }
1139
1133
 
1134
+
1135
+ if (data.subText) {
1136
+ content = html`
1137
+ <div>
1138
+ <div>${content}</div>
1139
+ <span class="table__cell--subtext">${data.subText}</span>
1140
+ </div>
1141
+ `
1142
+ }
1143
+
1144
+
1140
1145
  return content;
1141
1146
 
1142
1147
  }
@@ -1217,7 +1222,7 @@ export default class ZnDataTable extends ZincElement {
1217
1222
  return !header.secondary;
1218
1223
  });
1219
1224
  const header: HeaderConfig | undefined = filteredHeaders[index];
1220
- const headerKey: string = header?.key;
1225
+ const headerKey: string | undefined = header?.key;
1221
1226
 
1222
1227
  return html`
1223
1228
  <td
@@ -444,3 +444,7 @@ tbody tr.table__row--error td {
444
444
  align-items: flex-end;
445
445
  gap:2px;
446
446
  }
447
+
448
+ .table__cell--subtext{
449
+ @include wc.text-style(subheading-small);
450
+ }
@@ -1,5 +1,6 @@
1
1
  import '../../../dist/zn.min.js';
2
2
  import { expect, fixture, html, waitUntil } from '@open-wc/testing';
3
+ import { render } from 'lit';
3
4
  import type ZnDataTable from './data-table.component';
4
5
 
5
6
  describe('<zn-data-table>', () => {
@@ -124,4 +125,45 @@ describe('<zn-data-table>', () => {
124
125
  const templates = (el as unknown as {displayTemplates: Record<string, unknown>}).displayTemplates;
125
126
  expect(templates).to.be.an('object');
126
127
  });
128
+
129
+ it('renders subText as muted secondary text', async () => {
130
+ const el = await fixture<ZnDataTable>(html` <zn-data-table></zn-data-table> `);
131
+ const container = document.createElement('div');
132
+ render(el.renderCell({text: 'Main', column: 'name', subText: 'Secondary'}), container);
133
+
134
+ const subtext = container.querySelector('.table__cell--subtext');
135
+ expect(subtext).to.exist;
136
+ expect(subtext?.textContent).to.contain('Secondary');
137
+ expect(container.textContent).to.contain('Main');
138
+ });
139
+
140
+ it('applies title as a tooltip on link cells', async () => {
141
+ const el = await fixture<ZnDataTable>(html` <zn-data-table></zn-data-table> `);
142
+ const container = document.createElement('div');
143
+ render(el.renderCell({text: 'Go', column: 'name', uri: 'https://example.com', title: 'Tip'}), container);
144
+
145
+ const link = container.querySelector('a');
146
+ expect(link).to.exist;
147
+ expect(link?.getAttribute('title')).to.equal('Tip');
148
+ });
149
+
150
+ it('applies title as a tooltip on styled cells', async () => {
151
+ const el = await fixture<ZnDataTable>(html` <zn-data-table></zn-data-table> `);
152
+ const container = document.createElement('div');
153
+ render(el.renderCell({text: 'Val', column: 'name', style: 'bold', title: 'Info'}), container);
154
+
155
+ const styled = container.querySelector('zn-style');
156
+ expect(styled).to.exist;
157
+ expect(styled?.getAttribute('title')).to.equal('Info');
158
+ });
159
+
160
+ it('omits the title attribute when no title is provided', async () => {
161
+ const el = await fixture<ZnDataTable>(html` <zn-data-table></zn-data-table> `);
162
+ const container = document.createElement('div');
163
+ render(el.renderCell({text: 'Plain', column: 'name', uri: 'https://example.com'}), container);
164
+
165
+ const link = container.querySelector('a');
166
+ expect(link).to.exist;
167
+ expect(link?.hasAttribute('title')).to.be.false;
168
+ });
127
169
  });
@@ -1,15 +1,23 @@
1
- import {type CSSResultGroup, html, type PropertyValues, type TemplateResult, unsafeCSS} from 'lit';
1
+ import {type CSSResultGroup, html, nothing, type PropertyValues, type TemplateResult, unsafeCSS} from 'lit';
2
2
  import {FormControlController} from '../../internal/form';
3
3
  import {ifDefined} from 'lit/directives/if-defined.js';
4
4
  import {property, query} from 'lit/decorators.js';
5
5
  import {watch} from '../../internal/watch';
6
6
  import ZincElement from '../../internal/zinc-element';
7
+ import ZnButton from '../button';
8
+ import ZnDropdown from '../dropdown';
9
+ import ZnInput from '../input';
10
+ import ZnOption from '../option';
11
+ import ZnSelect from '../select';
7
12
  import type {ZincFormControl} from '../../internal/zinc-element';
8
- import type ZnDropdown from '../dropdown';
9
- import type ZnInput from '../input';
10
13
 
11
14
  import styles from './defined-label.scss';
12
15
 
16
+ interface PredefinedLabel {
17
+ name: string;
18
+ options?: string[];
19
+ }
20
+
13
21
  /**
14
22
  * @summary This component provides a labeled input with support for predefined and custom labels,
15
23
  * allowing users to select or enter label-value pairs within a dropdown interface.
@@ -21,31 +29,51 @@ import styles from './defined-label.scss';
21
29
  * @dependency zn-dropdown
22
30
  * @dependency zn-input
23
31
  * @dependency zn-option
24
- * @dependency zn-panel
25
32
  * @dependency zn-select
26
- * @dependency zn-sp
27
33
  *
28
34
  * @csspart input - The component's main input.
29
35
  * @csspart input-value - The label's value inputs.
30
36
  */
31
37
  export default class ZnDefinedLabel extends ZincElement implements ZincFormControl {
32
38
  static styles: CSSResultGroup = unsafeCSS(styles);
39
+ static dependencies = {
40
+ 'zn-button': ZnButton,
41
+ 'zn-dropdown': ZnDropdown,
42
+ 'zn-input': ZnInput,
43
+ 'zn-option': ZnOption,
44
+ 'zn-select': ZnSelect
45
+ };
33
46
 
34
47
  private readonly formControlController = new FormControlController(this, {
35
48
  value: (control: this) => control.value + (control.inputValue ? `:${control.inputValue}` : ''),
36
49
  });
37
50
 
38
- @query('.input__control') input: ZnInput;
51
+ @query('.defined-label__input') input: ZnInput;
39
52
  @query('.defined-label__dropdown') dropdown: ZnDropdown;
40
53
 
54
+ /** The selected label key. Also acts as the filter while typing. */
41
55
  @property() value: string = '';
56
+
57
+ /** The value entered for the selected label. Submitted as `label:value` when present. */
42
58
  @property() inputValue: string = '';
59
+
60
+ /** The size of the main input. */
43
61
  @property({attribute: 'input-size', reflect: true}) inputSize: 'x-small' | 'small' | 'medium' | 'large' = 'medium';
62
+
63
+ /** The name of the control. Used for form submission. */
44
64
  @property() name: string = 'label';
65
+
66
+ /** The title of the main input. */
45
67
  @property() title: string;
68
+
69
+ /** Disables the control. */
46
70
  @property({type: Boolean}) disabled: boolean = false;
71
+
72
+ /** Allows labels that aren't in the predefined list. */
47
73
  @property({attribute: 'allow-custom', type: Boolean}) allowCustom: boolean = false;
48
- @property({type: Array, attribute: 'predefined-labels'}) predefinedLabels = [];
74
+
75
+ /** The predefined labels. Entries are either a string or `{name, options}`. */
76
+ @property({type: Array, attribute: 'predefined-labels'}) predefinedLabels: (PredefinedLabel | string)[] = [];
49
77
 
50
78
  get validationMessage() {
51
79
  return this.input.validationMessage;
@@ -83,6 +111,14 @@ export default class ZnDefinedLabel extends ZincElement implements ZincFormContr
83
111
  this.formControlController.updateValidity();
84
112
  }
85
113
 
114
+ private getFilteredLabels(): PredefinedLabel[] {
115
+ const filter = this.value.toLowerCase();
116
+ return this.predefinedLabels
117
+ .map(label => typeof label === 'string' ? {name: label} : label)
118
+ .filter((label): label is PredefinedLabel => Boolean(label?.name))
119
+ .filter(label => !filter || label.name.toLowerCase().includes(filter));
120
+ }
121
+
86
122
  private handleChange() {
87
123
  if (!this.dropdown.open) {
88
124
  this.dropdown.show().then(r => r);
@@ -117,14 +153,18 @@ export default class ZnDefinedLabel extends ZincElement implements ZincFormContr
117
153
  if (target.hasAttribute('data-label')) this.value = target.getAttribute('data-label') ?? "";
118
154
  }
119
155
 
120
- private handleInputValueInput(e: Event) {
121
- const target = e.target as HTMLInputElement | HTMLSelectElement;
122
- this.inputValue = target.value.toLowerCase();
123
-
124
- if (target.hasAttribute('data-label')) this.value = target.getAttribute('data-label') ?? "";
125
- }
156
+ private handleFormSubmit(e: Event, label?: string) {
157
+ // Sync from the submitted row so a partially typed filter never wins over
158
+ // the row's actual label key, and stale values from other rows are discarded
159
+ const row = (e.currentTarget as HTMLElement).closest('.defined-label__row');
160
+ const control = row?.querySelector<ZnInput | ZnSelect>('.defined-label__value');
161
+ if (label) {
162
+ this.value = label;
163
+ }
164
+ if (control) {
165
+ this.inputValue = String(control.value ?? '').toLowerCase();
166
+ }
126
167
 
127
- private handleFormSubmit() {
128
168
  const form = this.formControlController.getForm();
129
169
 
130
170
  if (form && form.reportValidity()) {
@@ -135,106 +175,89 @@ export default class ZnDefinedLabel extends ZincElement implements ZincFormContr
135
175
  }
136
176
  }
137
177
 
138
- render() {
139
- let predefinedLabels = html``;
140
-
141
- if (this.predefinedLabels.length > 0) {
142
- this.predefinedLabels.forEach((label: { [key: string]: any } | string | null) => {
143
- // label = ['name' => 'label', 'options'=>['one', 'two', 'three']]
144
- let options: string[] | undefined;
145
- if (label && typeof label !== 'string') {
146
- options = label.options as string[];
147
- label = label.name as string;
148
- }
149
-
150
- if (this.value && !label?.toLowerCase().includes(this.value.toLowerCase())) {
151
- return;
152
- }
153
-
154
- let selector: TemplateResult<1>;
155
- if (options && options.length > 0) {
156
- selector = html`
157
- <zn-select
158
- part="input-value"
159
- id="input-value input-value-${label}"
160
- class="input__control-value input__control-value--${label}"
161
- data-label="${label}"
162
- @zn-change="${this.handleInputValueChange}"
163
- @zn-input="${this.handleInputValueInput}"
164
- size="small">
165
- <zn-option value="">Select ${label}</zn-option>
166
- ${options.map((option: string) => html`
167
- <zn-option value="${option}">${option}</zn-option>
168
- `)}
169
- </zn-select>`;
170
- } else {
171
- selector = html`
172
- <zn-input
173
- part="input-value"
174
- id="input-value input-value-${label}"
175
- class="input__control-value input__control-value--${label}"
176
- type="text"
177
- data-label="${label}"
178
- @zn-change="${this.handleInputValueChange}"
179
- @zn-input="${this.handleInputValueInput}"
180
- size="small"></zn-input>`;
181
- }
182
-
183
- predefinedLabels = html`
184
- <div class="defined-label__input">
185
- <small>${label}</small>
186
- <div class="defined-label__input-wrap">
187
- ${selector}
188
- <zn-button type="submit" icon="add" @click="${this.handleFormSubmit}"></zn-button>
189
- </div>
190
- </div>`;
191
- });
178
+ private renderValueControl(label: PredefinedLabel): TemplateResult {
179
+ if (label.options && label.options.length > 0) {
180
+ return html`
181
+ <zn-select
182
+ part="input-value"
183
+ class="defined-label__value"
184
+ data-label="${label.name}"
185
+ size="small"
186
+ @zn-change="${this.handleInputValueChange}"
187
+ @zn-input="${this.handleInputValueChange}">
188
+ <zn-option value="">Select ${label.name}</zn-option>
189
+ ${label.options.map(option => html`
190
+ <zn-option value="${option}">${option}</zn-option>`)}
191
+ </zn-select>`;
192
192
  }
193
193
 
194
194
  return html`
195
- <zn-dropdown class="defined-label__dropdown">
195
+ <zn-input
196
+ part="input-value"
197
+ class="defined-label__value"
198
+ type="text"
199
+ placeholder="Label Value"
200
+ data-label="${label.name}"
201
+ size="small"
202
+ @zn-change="${this.handleInputValueChange}"
203
+ @zn-input="${this.handleInputValueChange}"></zn-input>`;
204
+ }
205
+
206
+ private renderRow(name: string, control: TemplateResult, label?: string): TemplateResult {
207
+ return html`
208
+ <div class="defined-label__row">
209
+ <small class="defined-label__row-label">${name}</small>
210
+ <div class="defined-label__row-controls">
211
+ ${control}
212
+ <zn-button
213
+ type="submit"
214
+ icon="add"
215
+ @click="${(e: Event) => this.handleFormSubmit(e, label)}"></zn-button>
216
+ </div>
217
+ </div>`;
218
+ }
219
+
220
+ render() {
221
+ const labels = this.getFilteredLabels();
222
+
223
+ return html`
224
+ <zn-dropdown class="defined-label__dropdown" sync="width">
196
225
  <zn-input
197
226
  part="input"
198
227
  id="input"
199
- class="input__control"
228
+ class="defined-label__input"
200
229
  type="text"
201
- title="${this.title}"
230
+ title="${ifDefined(this.title)}"
202
231
  value="${this.value}"
203
232
  name="${ifDefined(this.name)}"
204
233
  placeholder="Add a Label"
205
234
  maxlength="60"
206
235
  autocomplete="off"
207
236
  size="${this.inputSize}"
237
+ ?disabled="${this.disabled}"
208
238
  @zn-change="${this.handleChange}"
209
239
  @zn-input="${this.handleInput}"
210
240
  @click="${this.handleClick}"
211
241
  slot="trigger"
212
242
  ></zn-input>
213
243
 
214
- <zn-panel class="defined-label__container">
215
- <zn-sp flush divide>
216
- ${predefinedLabels.values.length > 0 ? predefinedLabels : html`
217
- <div class="defined-label__input">
218
- <small>Cannot find any predefined labels</small>
219
- </div>`}
220
-
221
- ${this.allowCustom && this.value !== '' ? html`
222
- <div class="defined-label__input">
223
- <small>${this.value}</small>
224
- <div class="defined-label__input-wrap">
225
- <zn-input part="input-value"
226
- placeholder="Label Value"
227
- type="text"
228
- size="small"
229
- maxlength="60"
230
- @zn-change="${this.handleInputValueChange}"
231
- @zn-input="${this.handleInputValueInput}"></zn-input>
232
- <zn-button type="submit" icon="add" @click="${this.handleFormSubmit}"></zn-button>
233
- </div>
234
- </div>` : ''}
235
-
236
- </zn-sp>
237
- </zn-panel>
244
+ <div class="defined-label__panel">
245
+ ${labels.length > 0
246
+ ? labels.map(label => this.renderRow(label.name, this.renderValueControl(label), label.name))
247
+ : html`
248
+ <div class="defined-label__empty">Cannot find any predefined labels</div>`}
249
+
250
+ ${this.allowCustom && this.value !== '' ? this.renderRow(this.value, html`
251
+ <zn-input
252
+ part="input-value"
253
+ class="defined-label__value"
254
+ placeholder="Label Value"
255
+ type="text"
256
+ size="small"
257
+ maxlength="60"
258
+ @zn-change="${this.handleInputValueChange}"
259
+ @zn-input="${this.handleInputValueChange}"></zn-input>`) : nothing}
260
+ </div>
238
261
  </zn-dropdown>
239
262
  `;
240
263
  }
@@ -4,40 +4,52 @@
4
4
  display: contents;
5
5
  }
6
6
 
7
- .defined-label {
8
- .input__control, .input__control-value {
9
- background-color: rgb(var(--zn-panel));
10
- }
7
+ .defined-label__dropdown {
8
+ width: 100%;
9
+ }
11
10
 
12
- &__dropdown {
13
- width: 100%;
14
- }
11
+ .defined-label__panel {
12
+ display: flex;
13
+ flex-direction: column;
14
+ min-width: 260px;
15
+ max-height: 275px;
16
+ overflow-y: auto;
17
+ overscroll-behavior: none;
18
+ background-color: rgb(var(--zn-panel));
19
+ border: 1px solid rgb(var(--zn-border-color));
20
+ border-radius: var(--zn-border-radius);
21
+ box-shadow: 0 4px 8px -4px rgba(33, 33, 33, 0.1);
22
+ }
15
23
 
16
- &__container {
17
- display: flex;
18
- flex-direction: row;
19
- align-items: center;
20
- gap: var(--zn-spacing-small);
21
- width: 100%;
24
+ .defined-label__row {
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: var(--zn-spacing-x-small);
28
+ padding: var(--zn-spacing-small);
22
29
 
23
- zn-sp {
24
- max-height: 275px;
25
- }
30
+ & + & {
31
+ border-top: 1px solid rgb(var(--zn-border-color));
26
32
  }
33
+ }
27
34
 
28
- &__input {
29
- display: flex;
30
- flex-direction: column;
31
- gap: 8px;
35
+ .defined-label__row-label {
36
+ font-size: var(--zn-font-size-small);
37
+ color: rgb(var(--zn-text-muted));
38
+ }
32
39
 
33
- &-wrap {
34
- display: flex;
35
- align-items: center;
36
- gap: 8px;
37
- }
40
+ .defined-label__row-controls {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: var(--zn-spacing-x-small);
38
44
 
39
- zn-input, zn-select {
40
- flex-grow: 1;
41
- }
45
+ .defined-label__value {
46
+ flex-grow: 1;
42
47
  }
43
48
  }
49
+
50
+ .defined-label__empty {
51
+ padding: var(--zn-spacing-small);
52
+ font-size: var(--zn-font-size-small);
53
+ color: rgb(var(--zn-text-muted));
54
+ text-align: center;
55
+ }