@kubex/zinc 1.1.47 → 1.1.49

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.
@@ -0,0 +1,34 @@
1
+ ---
2
+ meta:
3
+ title: Color Select
4
+ description: A simple color picker. The dropdown lists color swatches with names; the closed control shows only the selected color's swatch.
5
+ layout: component
6
+ ---
7
+
8
+ ```html:preview
9
+ <zn-color-select label="Color" name="color"></zn-color-select>
10
+ ```
11
+
12
+ ## Examples
13
+
14
+ ### With a Default Value
15
+
16
+ Set the selected color with the `value` attribute (lowercase color name).
17
+
18
+ ```html:preview
19
+ <zn-color-select label="Color" name="color" value="blue"></zn-color-select>
20
+ ```
21
+
22
+ ### Clearable
23
+
24
+ Add the `clearable` attribute to show a clear button when a color is selected.
25
+
26
+ ```html:preview
27
+ <zn-color-select label="Color" name="color" value="green" clearable></zn-color-select>
28
+ ```
29
+
30
+ ### Help Text
31
+
32
+ ```html:preview
33
+ <zn-color-select label="Color" name="color" help-text="Pick a color for the label."></zn-color-select>
34
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.47",
3
+ "version": "1.1.49",
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,16 @@ 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
+ ? this.title
228
+ ? html`
229
+ <h3 class="channel-tile__title"><slot name="title">${this.title}</slot></h3>
230
+ <p class="channel-tile__subtitle">${this.subtitle} (${remaining}s)</p>`
231
+ : html`
232
+ <h3 class="channel-tile__title">${this.subtitle} (${remaining}s)</h3>`
233
+ : html`
234
+ <h3 class="channel-tile__title"><slot name="title">${title}</slot></h3>
235
+ <p class="channel-tile__subtitle"><slot name="subtitle">${this.subtitle}</slot></p>`}
220
236
  </div>
221
237
  <slot name="footer" class="channel-tile__footer"></slot>
222
238
  </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;
@@ -0,0 +1,123 @@
1
+ import {colors} from "../data-select/providers/color-data-provider";
2
+ import {type CSSResultGroup, html, nothing, unsafeCSS} from 'lit';
3
+ import {FormControlController} from "../../internal/form";
4
+ import {property, query} from 'lit/decorators.js';
5
+ import {watch} from "../../internal/watch";
6
+ import ZincElement from '../../internal/zinc-element';
7
+ import ZnOption from "../option";
8
+ import ZnSelect from "../select";
9
+ import type {ZincFormControl} from '../../internal/zinc-element';
10
+
11
+ import styles from './color-select.scss';
12
+
13
+ /**
14
+ * @summary A simple color picker. The dropdown lists color swatches with names; the closed control shows only the selected color's swatch.
15
+ * @documentation https://zinc.style/components/color-select
16
+ * @status experimental
17
+ * @since 1.0
18
+ *
19
+ * @dependency zn-select
20
+ * @dependency zn-option
21
+ *
22
+ * @event zn-input - Emitted when the selected color changes.
23
+ * @event zn-clear - Emitted when the clear button is activated.
24
+ *
25
+ * @csspart swatch - The color swatch shown in the closed control.
26
+ * @csspart combobox - The container that wraps the swatch and expand button (forwarded from zn-select).
27
+ * @csspart form-control-help-text - The help text's wrapper (forwarded from zn-select).
28
+ * @csspart form-control-input - The select's wrapper (forwarded from zn-select).
29
+ */
30
+ export default class ZnColorSelect extends ZincElement implements ZincFormControl {
31
+ static styles: CSSResultGroup = unsafeCSS(styles);
32
+ static dependencies = {
33
+ 'zn-select': ZnSelect,
34
+ 'zn-option': ZnOption,
35
+ };
36
+
37
+ @query('#select') select: ZnSelect;
38
+
39
+ /** The name of the select. Used for form submission. */
40
+ @property() name: string;
41
+
42
+ /** The selected color (lowercase name, e.g. "red"). Used for form submission. */
43
+ @property() value = '';
44
+
45
+ /** The select's label. */
46
+ @property() label = '';
47
+
48
+ /** The select's help text. */
49
+ @property({attribute: 'help-text'}) helpText = '';
50
+
51
+ /** Disables the select. */
52
+ @property({type: Boolean, reflect: true}) disabled = false;
53
+
54
+ /** Makes the select a required field. */
55
+ @property({type: Boolean, reflect: true}) required = false;
56
+
57
+ /** Shows a clear button when a color is selected. */
58
+ @property({type: Boolean}) clearable = false;
59
+
60
+ private readonly formControlController = new FormControlController(this);
61
+
62
+ get validity(): ValidityState {
63
+ return this.select.validity;
64
+ }
65
+
66
+ get validationMessage(): string {
67
+ return this.select.validationMessage;
68
+ }
69
+
70
+ checkValidity(): boolean {
71
+ return this.select.checkValidity();
72
+ }
73
+
74
+ getForm(): HTMLFormElement | null {
75
+ return this.formControlController.getForm();
76
+ }
77
+
78
+ reportValidity(): boolean {
79
+ return this.select.reportValidity();
80
+ }
81
+
82
+ setCustomValidity(message: string): void {
83
+ this.select.setCustomValidity(message);
84
+ }
85
+
86
+ @watch('value', {waitUntilFirstUpdate: true})
87
+ handleValueChange() {
88
+ this.formControlController.updateValidity();
89
+ }
90
+
91
+ private handleInput = (e: Event) => {
92
+ const target = e.target as ZnSelect;
93
+ this.value = (target.value as string) ?? '';
94
+ };
95
+
96
+ private handleClear = () => {
97
+ this.value = '';
98
+ };
99
+
100
+ protected render() {
101
+ return html`
102
+ <zn-select id="select"
103
+ placement="bottom-start"
104
+ label="${this.label}"
105
+ help-text="${this.helpText}"
106
+ name="${this.name}"
107
+ clearable=${this.clearable || nothing}
108
+ required=${this.required || nothing}
109
+ ?disabled="${this.disabled}"
110
+ .value="${this.value}"
111
+ @zn-input="${this.handleInput}"
112
+ @zn-clear="${this.handleClear}"
113
+ exportparts="combobox,form-control-help-text,form-control-input">
114
+ <div slot="prefix" part="swatch"
115
+ class="color-swatch ${this.value ? `color-swatch--${this.value}` : 'color-swatch--empty'}"></div>
116
+ ${colors.map(color => html`
117
+ <zn-option value="${color.toLowerCase()}">
118
+ <div slot="prefix" class="color-swatch color-swatch--${color.toLowerCase()}"></div>
119
+ ${color}
120
+ </zn-option>`)}
121
+ </zn-select>`;
122
+ }
123
+ }
@@ -0,0 +1,78 @@
1
+ @use "../../wc";
2
+
3
+ // Same palette as data-select's color provider
4
+ $colors: (
5
+ red: var(--zn-color-red-500),
6
+ blue: var(--zn-color-blue-500),
7
+ orange: var(--zn-color-orange-500),
8
+ yellow: var(--zn-color-yellow-400),
9
+ indigo: var(--zn-color-indigo-500),
10
+ violet: var(--zn-color-violet-500),
11
+ green: var(--zn-color-green-500),
12
+ pink: var(--zn-color-pink-500),
13
+ gray: var(--zn-color-gray-500)
14
+ );
15
+
16
+ // Only wide enough for the swatch and the expand icon
17
+ :host {
18
+ display: inline-block;
19
+ }
20
+
21
+ .color-swatch {
22
+ width: var(--zn-spacing-small);
23
+ height: var(--zn-spacing-small);
24
+ display: block;
25
+ border-radius: var(--zn-border-radius-circle);
26
+ flex-shrink: 0;
27
+
28
+ @each $name, $color in $colors {
29
+ &--#{"" + $name} {
30
+ background-color: #{$color};
31
+ }
32
+ }
33
+
34
+ &--empty {
35
+ border: 1px dashed var(--zn-color-neutral-400);
36
+ }
37
+ }
38
+
39
+ // Never stretch to label/help-text width — only fit the swatch and controls
40
+ zn-select::part(combobox) {
41
+ width: fit-content;
42
+ padding-inline-end: var(--zn-spacing-2x-small);
43
+ }
44
+
45
+ // Preview shows only the swatch — collapse the text input entirely
46
+ zn-select::part(display-input) {
47
+ width: 0;
48
+ min-width: 0;
49
+ color: transparent;
50
+ caret-color: transparent;
51
+ }
52
+
53
+ // Let the dropdown grow to fit swatch + name instead of matching the control's width
54
+ zn-select::part(listbox) {
55
+ min-width: max-content;
56
+ max-height: 300px;
57
+ padding-block: 0;
58
+ }
59
+
60
+ // Tighten the combobox: small, even gaps between swatch, clear button and chevron
61
+ .color-swatch[part="swatch"] {
62
+ margin-inline-start: var(--zn-spacing-x-small);
63
+ margin-inline-end: 0;
64
+ }
65
+
66
+ // Gap between swatch and name in the dropdown list
67
+ zn-option .color-swatch {
68
+ margin-inline-end: var(--zn-spacing-x-small);
69
+ }
70
+
71
+ zn-select::part(clear-button),
72
+ zn-select::part(expand-icon) {
73
+ margin-inline-start: var(--zn-spacing-2x-small);
74
+ }
75
+
76
+ zn-select::part(clear-button) {
77
+ margin-inline-start: var(--zn-spacing-x-small);
78
+ }
@@ -0,0 +1,28 @@
1
+ import '../../../dist/zn.min.js';
2
+ import {expect, fixture, html} from '@open-wc/testing';
3
+
4
+ describe('<zn-color-select>', () => {
5
+ it('should render a component', async () => {
6
+ const el = await fixture(html`<zn-color-select></zn-color-select>`);
7
+ expect(el).to.exist;
8
+ expect(el.shadowRoot).to.exist;
9
+ });
10
+
11
+ it('should list the nine built-in colors', async () => {
12
+ const el = await fixture(html`<zn-color-select></zn-color-select>`);
13
+ const options = el.shadowRoot!.querySelectorAll('zn-option');
14
+ expect(options.length).to.equal(9);
15
+ });
16
+
17
+ it('should show the selected color swatch in the preview', async () => {
18
+ const el = await fixture(html`<zn-color-select value="red"></zn-color-select>`);
19
+ const swatch = el.shadowRoot!.querySelector('[slot="prefix"]')!;
20
+ expect(swatch.classList.contains('color-swatch--red')).to.be.true;
21
+ });
22
+
23
+ it('should show an empty swatch when no color is selected', async () => {
24
+ const el = await fixture(html`<zn-color-select></zn-color-select>`);
25
+ const swatch = el.shadowRoot!.querySelector('[slot="prefix"]')!;
26
+ expect(swatch.classList.contains('color-swatch--empty')).to.be.true;
27
+ });
28
+ });
@@ -0,0 +1,12 @@
1
+ import ZnColorSelect from './color-select.component';
2
+
3
+ export * from './color-select.component';
4
+ export default ZnColorSelect;
5
+
6
+ ZnColorSelect.define('zn-color-select');
7
+
8
+ declare global {
9
+ interface HTMLElementTagNameMap {
10
+ 'zn-color-select': ZnColorSelect;
11
+ }
12
+ }
@@ -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
  }
package/src/zinc.ts CHANGED
@@ -73,6 +73,7 @@ export { default as File } from './components/file';
73
73
  export { default as CheckboxGroup } from './components/checkbox-group';
74
74
  export { default as Item } from './components/item';
75
75
  export { default as DataSelect } from './components/data-select';
76
+ export { default as ColorSelect } from './components/color-select';
76
77
  export { default as ButtonMenu } from './components/button-menu';
77
78
  export { default as HoverContainer } from './components/hover-container';
78
79
  export { default as Slideout } from './components/slideout';