@aceshooting/lyra-ui 0.1.5 → 1.0.1
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/README.md +17 -8
- package/custom-elements.json +5655 -4175
- package/dist/components/chart/bar-chart.stories.d.ts +5 -0
- package/dist/components/chart/bar-chart.stories.js +20 -0
- package/dist/components/chart/box-plot.stories.d.ts +5 -0
- package/dist/components/chart/box-plot.stories.js +29 -0
- package/dist/components/chart/bubble-chart.stories.d.ts +5 -0
- package/dist/components/chart/bubble-chart.stories.js +26 -0
- package/dist/components/chart/doughnut-chart.stories.d.ts +5 -0
- package/dist/components/chart/doughnut-chart.stories.js +26 -0
- package/dist/components/chart/histogram.stories.d.ts +5 -0
- package/dist/components/chart/histogram.stories.js +17 -0
- package/dist/components/chart/line-chart.stories.d.ts +5 -0
- package/dist/components/chart/line-chart.stories.js +24 -0
- package/dist/components/chart/pie-chart.stories.d.ts +5 -0
- package/dist/components/chart/pie-chart.stories.js +26 -0
- package/dist/components/chart/polar-area-chart.stories.d.ts +5 -0
- package/dist/components/chart/polar-area-chart.stories.js +20 -0
- package/dist/components/chart/radar-chart.stories.d.ts +5 -0
- package/dist/components/chart/radar-chart.stories.js +20 -0
- package/dist/components/chart/scatter-chart.stories.d.ts +5 -0
- package/dist/components/chart/scatter-chart.stories.js +31 -0
- package/dist/components/combobox/combobox.d.ts +26 -1
- package/dist/components/combobox/combobox.js +147 -31
- package/dist/components/combobox/combobox.styles.js +21 -0
- package/dist/components/combobox/option.d.ts +4 -0
- package/dist/components/combobox/option.js +10 -0
- package/dist/components/date-picker/calendar-core.d.ts +6 -2
- package/dist/components/date-picker/calendar-core.js +23 -4
- package/dist/components/date-picker/date-input.d.ts +1 -0
- package/dist/components/date-picker/date-input.js +19 -2
- package/dist/components/date-picker/date-picker.js +4 -2
- package/dist/components/date-picker/date-picker.styles.js +5 -1
- package/dist/components/empty/empty.stories.d.ts +5 -0
- package/dist/components/empty/empty.stories.js +14 -0
- package/dist/components/export-button/export-button.js +7 -2
- package/dist/components/export-button/export-button.stories.d.ts +6 -0
- package/dist/components/export-button/export-button.stories.js +32 -0
- package/dist/components/file-input/file-input.stories.d.ts +5 -0
- package/dist/components/file-input/file-input.stories.js +10 -0
- package/dist/components/flag/flag.js +12 -2
- package/dist/components/gauge/gauge.js +8 -4
- package/dist/components/gauge/gauge.stories.d.ts +6 -0
- package/dist/components/gauge/gauge.stories.js +13 -0
- package/dist/components/graph/graph.stories.d.ts +5 -0
- package/dist/components/graph/graph.stories.js +30 -0
- package/dist/components/heatmap/heatmap.stories.d.ts +5 -0
- package/dist/components/heatmap/heatmap.stories.js +24 -0
- package/dist/components/map/map.d.ts +6 -0
- package/dist/components/map/map.js +69 -9
- package/dist/components/map/map.stories.d.ts +5 -0
- package/dist/components/map/map.stories.js +40 -0
- package/dist/components/playback/playback.stories.d.ts +5 -0
- package/dist/components/playback/playback.stories.js +10 -0
- package/dist/components/skeleton/skeleton.stories.d.ts +5 -0
- package/dist/components/skeleton/skeleton.stories.js +16 -0
- package/dist/components/split/split.d.ts +1 -0
- package/dist/components/split/split.js +25 -4
- package/dist/components/split/split.stories.d.ts +5 -0
- package/dist/components/split/split.stories.js +16 -0
- package/dist/components/table/table.stories.d.ts +6 -0
- package/dist/components/table/table.stories.js +22 -0
- package/dist/components/time-range/time-range.stories.d.ts +6 -0
- package/dist/components/time-range/time-range.stories.js +13 -0
- package/dist/components/toast/toast-item.d.ts +1 -0
- package/dist/components/toast/toast-item.js +16 -1
- package/dist/components/tree/tree.stories.d.ts +5 -0
- package/dist/components/tree/tree.stories.js +22 -0
- package/dist/components/widget/widget.d.ts +40 -0
- package/dist/components/widget/widget.js +159 -0
- package/dist/components/widget/widget.stories.d.ts +6 -0
- package/dist/components/widget/widget.stories.js +31 -0
- package/dist/components/widget/widget.styles.d.ts +1 -0
- package/dist/components/widget/widget.styles.js +90 -0
- package/dist/internal/icons.d.ts +2 -0
- package/dist/internal/icons.js +9 -0
- package/dist/internal/scroll-lock.d.ts +7 -0
- package/dist/internal/scroll-lock.js +25 -0
- package/dist/internal/tokens.styles.js +5 -0
- package/dist/lyra.d.ts +5 -0
- package/dist/lyra.js +2 -0
- package/llms.txt +3 -0
- package/package.json +3 -3
|
@@ -62,6 +62,8 @@ export class LyraCombobox extends LyraElement {
|
|
|
62
62
|
this.maxOptionsVisible = 3;
|
|
63
63
|
this.emptyText = 'No results';
|
|
64
64
|
this.filter = null;
|
|
65
|
+
this.source = null;
|
|
66
|
+
this.maxRender = 200;
|
|
65
67
|
this.query = '';
|
|
66
68
|
this.activeIndex = -1;
|
|
67
69
|
this.options = [];
|
|
@@ -78,7 +80,17 @@ export class LyraCombobox extends LyraElement {
|
|
|
78
80
|
this.hasHintSlot = false;
|
|
79
81
|
this.hasErrorSlot = false;
|
|
80
82
|
this.hasLabelSlot = false;
|
|
83
|
+
this.loading = false;
|
|
84
|
+
this.asyncRows = [];
|
|
85
|
+
this.sourceToken = 0;
|
|
86
|
+
this._selectedLabelCache = new Map();
|
|
87
|
+
// Rebuilt once per render (in render(), before renderRows()) from the
|
|
88
|
+
// currently-visible row set -- backs the delegated listbox click/mousedown
|
|
89
|
+
// handlers' data-value lookup below, instead of each option row closing
|
|
90
|
+
// over its own row object.
|
|
91
|
+
this._rowsByValue = new Map();
|
|
81
92
|
this.listId = nextId('combobox-list');
|
|
93
|
+
this.inputId = nextId('combobox-input');
|
|
82
94
|
this._selected = [];
|
|
83
95
|
// What `form.reset()` restores to. Captured exactly once, from whatever
|
|
84
96
|
// `<lyra-option selected>` markup was present the first time slotted
|
|
@@ -121,6 +133,8 @@ export class LyraCombobox extends LyraElement {
|
|
|
121
133
|
this.query = e.target.value;
|
|
122
134
|
this.activeIndex = -1;
|
|
123
135
|
this.show();
|
|
136
|
+
if (this.source)
|
|
137
|
+
this.runSource(this.query);
|
|
124
138
|
this.emit('input');
|
|
125
139
|
};
|
|
126
140
|
this.onInputBlur = () => {
|
|
@@ -136,22 +150,24 @@ export class LyraCombobox extends LyraElement {
|
|
|
136
150
|
this.hasLabelSlot = e.target.assignedElements({ flatten: true }).length > 0;
|
|
137
151
|
};
|
|
138
152
|
this.onKeyDown = (e) => {
|
|
139
|
-
const
|
|
153
|
+
const navigable = this.renderedRows.rows.filter((r) => !r.disabled);
|
|
140
154
|
switch (e.key) {
|
|
141
155
|
case 'ArrowDown':
|
|
142
156
|
e.preventDefault();
|
|
143
157
|
if (!this.open)
|
|
144
158
|
return this.show();
|
|
145
|
-
this.activeIndex = Math.min(
|
|
159
|
+
this.activeIndex = Math.min(navigable.length - 1, this.activeIndex + 1);
|
|
146
160
|
break;
|
|
147
161
|
case 'ArrowUp':
|
|
148
162
|
e.preventDefault();
|
|
163
|
+
if (!this.open)
|
|
164
|
+
return this.show();
|
|
149
165
|
this.activeIndex = Math.max(0, this.activeIndex - 1);
|
|
150
166
|
break;
|
|
151
167
|
case 'Enter':
|
|
152
|
-
if (this.open && this.activeIndex >= 0 &&
|
|
168
|
+
if (this.open && this.activeIndex >= 0 && navigable[this.activeIndex]) {
|
|
153
169
|
e.preventDefault();
|
|
154
|
-
this.
|
|
170
|
+
this.pickRow(navigable[this.activeIndex]);
|
|
155
171
|
}
|
|
156
172
|
break;
|
|
157
173
|
case 'Escape':
|
|
@@ -169,7 +185,7 @@ export class LyraCombobox extends LyraElement {
|
|
|
169
185
|
case 'End':
|
|
170
186
|
if (this.open) {
|
|
171
187
|
e.preventDefault();
|
|
172
|
-
this.activeIndex =
|
|
188
|
+
this.activeIndex = navigable.length - 1;
|
|
173
189
|
}
|
|
174
190
|
break;
|
|
175
191
|
case 'Backspace':
|
|
@@ -188,6 +204,22 @@ export class LyraCombobox extends LyraElement {
|
|
|
188
204
|
this.renderRoot.querySelector('[part="combobox-input"]')?.focus();
|
|
189
205
|
this.show();
|
|
190
206
|
};
|
|
207
|
+
// Delegated onto [part="listbox"] (see render()) rather than one closure
|
|
208
|
+
// pair allocated per option per render -- resolves the target row via
|
|
209
|
+
// closest('[part="option"]') + a data-value lookup into `_rowsByValue`.
|
|
210
|
+
this.onListboxMouseDown = (e) => {
|
|
211
|
+
if (e.target.closest('[part="option"]'))
|
|
212
|
+
e.preventDefault();
|
|
213
|
+
};
|
|
214
|
+
this.onListboxClick = (e) => {
|
|
215
|
+
const optionEl = e.target.closest('[part="option"]');
|
|
216
|
+
const value = optionEl?.dataset.value;
|
|
217
|
+
if (value === undefined)
|
|
218
|
+
return;
|
|
219
|
+
const row = this._rowsByValue.get(value);
|
|
220
|
+
if (row)
|
|
221
|
+
this.pickRow(row);
|
|
222
|
+
};
|
|
191
223
|
this.internals = this.attachInternals();
|
|
192
224
|
}
|
|
193
225
|
connectedCallback() {
|
|
@@ -257,7 +289,36 @@ export class LyraCombobox extends LyraElement {
|
|
|
257
289
|
o.selected = sel.has(o.value);
|
|
258
290
|
}
|
|
259
291
|
labelFor(value) {
|
|
260
|
-
return this.options.find((o) => o.value === value)?.label ?? value;
|
|
292
|
+
return this._selectedLabelCache.get(value) ?? this.options.find((o) => o.value === value)?.label ?? value;
|
|
293
|
+
}
|
|
294
|
+
/** The current row set in a source-agnostic shape, before capping. */
|
|
295
|
+
get effectiveRows() {
|
|
296
|
+
if (this.source)
|
|
297
|
+
return this.asyncRows;
|
|
298
|
+
return this.filtered.map((o) => ({
|
|
299
|
+
value: o.value,
|
|
300
|
+
label: o.label,
|
|
301
|
+
sub: o.sub || undefined,
|
|
302
|
+
dotColor: o.dotColor || undefined,
|
|
303
|
+
group: o.group || undefined,
|
|
304
|
+
disabled: o.disabled,
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
/** `effectiveRows` capped to `maxRender`, always keeping the current selection visible. */
|
|
308
|
+
get renderedRows() {
|
|
309
|
+
const all = this.effectiveRows;
|
|
310
|
+
if (all.length <= this.maxRender)
|
|
311
|
+
return { rows: all, overflow: 0 };
|
|
312
|
+
const capped = all.slice(0, this.maxRender);
|
|
313
|
+
const cappedValues = new Set(capped.map((r) => r.value));
|
|
314
|
+
for (const v of this._selected) {
|
|
315
|
+
if (!cappedValues.has(v)) {
|
|
316
|
+
const selectedRow = all.find((r) => r.value === v);
|
|
317
|
+
if (selectedRow)
|
|
318
|
+
capped.push(selectedRow);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return { rows: capped, overflow: all.length - capped.length };
|
|
261
322
|
}
|
|
262
323
|
get filtered() {
|
|
263
324
|
const q = this.query.trim().toLowerCase();
|
|
@@ -298,6 +359,12 @@ export class LyraCombobox extends LyraElement {
|
|
|
298
359
|
const listbox = this.renderRoot.querySelector('[part="listbox"]');
|
|
299
360
|
if (anchor && listbox)
|
|
300
361
|
this.cleanup = place(anchor, listbox);
|
|
362
|
+
// Reacting to the `open` property itself (not just inside show())
|
|
363
|
+
// means this fires however `open` became true — via show()'s own
|
|
364
|
+
// user-interaction paths, or a consumer/test setting `el.open = true`
|
|
365
|
+
// directly, which bypasses show() entirely.
|
|
366
|
+
if (this.source && this.asyncRows.length === 0)
|
|
367
|
+
this.runSource(this.query);
|
|
301
368
|
}
|
|
302
369
|
}
|
|
303
370
|
if (changed.has('required'))
|
|
@@ -306,17 +373,18 @@ export class LyraCombobox extends LyraElement {
|
|
|
306
373
|
this.toggleAttribute('data-invalid', this.touched && !this.internals.validity.valid);
|
|
307
374
|
}
|
|
308
375
|
}
|
|
309
|
-
|
|
310
|
-
if (
|
|
376
|
+
pickRow(row) {
|
|
377
|
+
if (row.disabled)
|
|
311
378
|
return;
|
|
379
|
+
this._selectedLabelCache.set(row.value, row.label);
|
|
312
380
|
if (this.multiple) {
|
|
313
381
|
const set = new Set(this._selected);
|
|
314
|
-
set.has(
|
|
382
|
+
set.has(row.value) ? set.delete(row.value) : set.add(row.value);
|
|
315
383
|
this.value = [...set];
|
|
316
384
|
this.query = '';
|
|
317
385
|
}
|
|
318
386
|
else {
|
|
319
|
-
this.value =
|
|
387
|
+
this.value = row.value;
|
|
320
388
|
this.query = '';
|
|
321
389
|
this.hide();
|
|
322
390
|
}
|
|
@@ -333,38 +401,61 @@ export class LyraCombobox extends LyraElement {
|
|
|
333
401
|
this.emit('lyra-clear');
|
|
334
402
|
this.emit('change');
|
|
335
403
|
}
|
|
336
|
-
|
|
337
|
-
|
|
404
|
+
runSource(query) {
|
|
405
|
+
if (!this.source)
|
|
406
|
+
return;
|
|
407
|
+
clearTimeout(this.sourceTimer);
|
|
408
|
+
this.sourceTimer = setTimeout(() => {
|
|
409
|
+
const token = ++this.sourceToken;
|
|
410
|
+
this.loading = true;
|
|
411
|
+
this.source(query)
|
|
412
|
+
.then((rows) => {
|
|
413
|
+
if (token !== this.sourceToken || !this.isConnected)
|
|
414
|
+
return;
|
|
415
|
+
this.asyncRows = rows;
|
|
416
|
+
})
|
|
417
|
+
.finally(() => {
|
|
418
|
+
if (token === this.sourceToken)
|
|
419
|
+
this.loading = false;
|
|
420
|
+
});
|
|
421
|
+
}, 200);
|
|
422
|
+
}
|
|
423
|
+
renderRows(rows, activeId) {
|
|
424
|
+
const out = [];
|
|
338
425
|
let currentGroup;
|
|
339
|
-
|
|
340
|
-
|
|
426
|
+
const selectedSet = new Set(this._selected);
|
|
427
|
+
rows.forEach((o, i) => {
|
|
341
428
|
if (o.group !== currentGroup) {
|
|
342
429
|
currentGroup = o.group;
|
|
343
430
|
if (currentGroup)
|
|
344
|
-
|
|
431
|
+
out.push(html `<div class="group-label">${currentGroup}</div>`);
|
|
345
432
|
}
|
|
346
|
-
const id = `${this.listId}-opt-${
|
|
347
|
-
const selected =
|
|
348
|
-
|
|
433
|
+
const id = `${this.listId}-opt-${i}`;
|
|
434
|
+
const selected = selectedSet.has(o.value);
|
|
435
|
+
out.push(html `<div
|
|
349
436
|
part="option"
|
|
350
437
|
id=${id}
|
|
351
438
|
role="option"
|
|
439
|
+
data-value=${o.value}
|
|
352
440
|
aria-selected=${selected ? 'true' : 'false'}
|
|
353
441
|
aria-disabled=${o.disabled ? 'true' : 'false'}
|
|
354
442
|
?data-active=${id === activeId}
|
|
355
|
-
@mousedown=${(e) => e.preventDefault()}
|
|
356
|
-
@click=${() => this.pick(o)}
|
|
357
443
|
>
|
|
358
|
-
|
|
444
|
+
${o.dotColor ? html `<span part="option-dot" style=${`background:${o.dotColor}`}></span>` : ''}
|
|
445
|
+
<span part="option-label">
|
|
446
|
+
<span>${o.label}</span>
|
|
447
|
+
${o.sub ? html `<span part="option-sub">${o.sub}</span>` : ''}
|
|
448
|
+
</span>
|
|
359
449
|
</div>`);
|
|
360
450
|
});
|
|
361
|
-
return
|
|
451
|
+
return out;
|
|
362
452
|
}
|
|
363
453
|
render() {
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
const
|
|
454
|
+
const { rows, overflow } = this.renderedRows;
|
|
455
|
+
this._rowsByValue = new Map(rows.map((r) => [r.value, r]));
|
|
456
|
+
const navigable = rows.filter((r) => !r.disabled);
|
|
457
|
+
const active = this.activeIndex >= 0 ? navigable[this.activeIndex] : undefined;
|
|
458
|
+
const activeId = active ? `${this.listId}-opt-${rows.indexOf(active)}` : '';
|
|
368
459
|
const shownTags = this.multiple ? this._selected.slice(0, this.maxOptionsVisible) : [];
|
|
369
460
|
const extra = this.multiple ? this._selected.length - shownTags.length : 0;
|
|
370
461
|
const hasValue = this._selected.length > 0;
|
|
@@ -373,7 +464,7 @@ export class LyraCombobox extends LyraElement {
|
|
|
373
464
|
const hasLabel = this.hasLabelSlot || this.label.length > 0;
|
|
374
465
|
return html `
|
|
375
466
|
<div part="form-control">
|
|
376
|
-
<label part="form-control-label" ?hidden=${!hasLabel}>
|
|
467
|
+
<label part="form-control-label" for=${this.inputId} ?hidden=${!hasLabel}>
|
|
377
468
|
${this.label}<slot name="label" @slotchange=${this.onLabelSlotChange}></slot>
|
|
378
469
|
</label>
|
|
379
470
|
<div part="combobox" @mousedown=${this.onComboMouseDown}>
|
|
@@ -394,6 +485,7 @@ export class LyraCombobox extends LyraElement {
|
|
|
394
485
|
${extra > 0 ? html `<span part="tag">+${extra}</span>` : ''}
|
|
395
486
|
</div>
|
|
396
487
|
<input
|
|
488
|
+
id=${this.inputId}
|
|
397
489
|
part="combobox-input"
|
|
398
490
|
role="combobox"
|
|
399
491
|
aria-label=${this.label || this.placeholder || 'Combobox'}
|
|
@@ -425,10 +517,22 @@ export class LyraCombobox extends LyraElement {
|
|
|
425
517
|
: ''}
|
|
426
518
|
<span part="expand-icon" aria-hidden="true">${chevronIcon()}</span>
|
|
427
519
|
</div>
|
|
428
|
-
<div
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
520
|
+
<div
|
|
521
|
+
part="listbox"
|
|
522
|
+
id=${this.listId}
|
|
523
|
+
role="listbox"
|
|
524
|
+
aria-multiselectable=${this.multiple ? 'true' : 'false'}
|
|
525
|
+
@mousedown=${this.onListboxMouseDown}
|
|
526
|
+
@click=${this.onListboxClick}
|
|
527
|
+
>
|
|
528
|
+
${this.loading
|
|
529
|
+
? html `<div class="loading">Loading…</div>`
|
|
530
|
+
: rows.length === 0
|
|
531
|
+
? html `<div class="empty">${this.emptyText}</div>`
|
|
532
|
+
: html `${this.renderRows(rows, activeId)}
|
|
533
|
+
${overflow > 0
|
|
534
|
+
? html `<div part="option-overflow">+${overflow} more — refine your search</div>`
|
|
535
|
+
: ''}`}
|
|
432
536
|
</div>
|
|
433
537
|
<div part="error" ?hidden=${!hasError}>
|
|
434
538
|
${this.errorText}<slot name="error" @slotchange=${this.onErrorSlotChange}></slot>
|
|
@@ -480,6 +584,12 @@ __decorate([
|
|
|
480
584
|
__decorate([
|
|
481
585
|
property({ attribute: false })
|
|
482
586
|
], LyraCombobox.prototype, "filter", void 0);
|
|
587
|
+
__decorate([
|
|
588
|
+
property({ attribute: false })
|
|
589
|
+
], LyraCombobox.prototype, "source", void 0);
|
|
590
|
+
__decorate([
|
|
591
|
+
property({ attribute: 'max-render', type: Number })
|
|
592
|
+
], LyraCombobox.prototype, "maxRender", void 0);
|
|
483
593
|
__decorate([
|
|
484
594
|
state()
|
|
485
595
|
], LyraCombobox.prototype, "query", void 0);
|
|
@@ -501,4 +611,10 @@ __decorate([
|
|
|
501
611
|
__decorate([
|
|
502
612
|
state()
|
|
503
613
|
], LyraCombobox.prototype, "hasLabelSlot", void 0);
|
|
614
|
+
__decorate([
|
|
615
|
+
state()
|
|
616
|
+
], LyraCombobox.prototype, "loading", void 0);
|
|
617
|
+
__decorate([
|
|
618
|
+
state()
|
|
619
|
+
], LyraCombobox.prototype, "asyncRows", void 0);
|
|
504
620
|
defineElement('combobox', LyraCombobox);
|
|
@@ -172,6 +172,27 @@ export const styles = css `
|
|
|
172
172
|
opacity: var(--lyra-opacity-disabled);
|
|
173
173
|
cursor: not-allowed;
|
|
174
174
|
}
|
|
175
|
+
[part='option-dot'] {
|
|
176
|
+
inline-size: 0.5rem;
|
|
177
|
+
block-size: 0.5rem;
|
|
178
|
+
border-radius: 50%;
|
|
179
|
+
flex: 0 0 auto;
|
|
180
|
+
}
|
|
181
|
+
[part='option-label'] {
|
|
182
|
+
display: flex;
|
|
183
|
+
flex-direction: column;
|
|
184
|
+
min-inline-size: 0;
|
|
185
|
+
}
|
|
186
|
+
[part='option-sub'] {
|
|
187
|
+
font-size: 0.75rem;
|
|
188
|
+
color: var(--lyra-color-text-quiet);
|
|
189
|
+
}
|
|
190
|
+
[part='option-overflow'],
|
|
191
|
+
.loading {
|
|
192
|
+
padding: var(--lyra-space-s) var(--lyra-space-m);
|
|
193
|
+
font-size: 0.8125rem;
|
|
194
|
+
color: var(--lyra-color-text-quiet);
|
|
195
|
+
}
|
|
175
196
|
|
|
176
197
|
.group-label {
|
|
177
198
|
padding: var(--lyra-space-xs) var(--lyra-space-s) 0;
|
|
@@ -21,6 +21,10 @@ export declare class LyraOption extends LyraElement {
|
|
|
21
21
|
group: string;
|
|
22
22
|
/** Extra text the filter should match beyond the label. */
|
|
23
23
|
searchText: string;
|
|
24
|
+
/** Optional secondary line rendered under the label (e.g. a status/date summary). */
|
|
25
|
+
sub: string;
|
|
26
|
+
/** Optional color for a small leading status dot (any valid CSS color). */
|
|
27
|
+
dotColor: string;
|
|
24
28
|
/** Resolved label: explicit non-empty `label` attribute wins, else trimmed text content. */
|
|
25
29
|
get label(): string;
|
|
26
30
|
render(): TemplateResult;
|
|
@@ -30,6 +30,10 @@ export class LyraOption extends LyraElement {
|
|
|
30
30
|
this.group = '';
|
|
31
31
|
/** Extra text the filter should match beyond the label. */
|
|
32
32
|
this.searchText = '';
|
|
33
|
+
/** Optional secondary line rendered under the label (e.g. a status/date summary). */
|
|
34
|
+
this.sub = '';
|
|
35
|
+
/** Optional color for a small leading status dot (any valid CSS color). */
|
|
36
|
+
this.dotColor = '';
|
|
33
37
|
}
|
|
34
38
|
static { this.styles = [
|
|
35
39
|
LyraElement.styles,
|
|
@@ -63,4 +67,10 @@ __decorate([
|
|
|
63
67
|
__decorate([
|
|
64
68
|
property({ attribute: 'search-text' })
|
|
65
69
|
], LyraOption.prototype, "searchText", void 0);
|
|
70
|
+
__decorate([
|
|
71
|
+
property()
|
|
72
|
+
], LyraOption.prototype, "sub", void 0);
|
|
73
|
+
__decorate([
|
|
74
|
+
property({ attribute: 'dot-color' })
|
|
75
|
+
], LyraOption.prototype, "dotColor", void 0);
|
|
66
76
|
defineElement('option', LyraOption);
|
|
@@ -16,5 +16,9 @@ export declare function monthMatrix(year: number, month: number, firstDayOfWeek?
|
|
|
16
16
|
export declare function weekdayLabels(firstDayOfWeek?: number, format?: WeekdayFormat, locale?: string): string[];
|
|
17
17
|
/** Localized "Month YYYY" title. */
|
|
18
18
|
export declare function monthTitle(year: number, month: number, locale?: string): string;
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a `first-day-of-week` attribute (name or 'auto') to a 0–6 index.
|
|
21
|
+
* `'auto'` derives the answer from `locale` via `Intl.Locale`'s week-info
|
|
22
|
+
* when the runtime supports it, falling back to Sunday otherwise.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveFirstDayOfWeek(value: string, locale?: string): number;
|
|
@@ -63,9 +63,28 @@ export function monthTitle(year, month, locale) {
|
|
|
63
63
|
return new Intl.DateTimeFormat(locale || undefined, { month: 'long', year: 'numeric' }).format(new Date(year, month, 1));
|
|
64
64
|
}
|
|
65
65
|
const FDOW = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
/** Locale-derived first day of week (0=Sunday … 6=Saturday), or null if unsupported. */
|
|
67
|
+
function localeFirstDayOfWeek(locale) {
|
|
68
|
+
try {
|
|
69
|
+
const l = new Intl.Locale(locale);
|
|
70
|
+
const firstDay = l.weekInfo?.firstDay ?? l.getWeekInfo?.().firstDay;
|
|
71
|
+
if (!firstDay)
|
|
72
|
+
return null;
|
|
73
|
+
return firstDay % 7; // ISO 1-7 (Mon-Sun) -> 0-6 (Sun-Sat)
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Resolve a `first-day-of-week` attribute (name or 'auto') to a 0–6 index.
|
|
81
|
+
* `'auto'` derives the answer from `locale` via `Intl.Locale`'s week-info
|
|
82
|
+
* when the runtime supports it, falling back to Sunday otherwise.
|
|
83
|
+
*/
|
|
84
|
+
export function resolveFirstDayOfWeek(value, locale) {
|
|
85
|
+
if (!value || value === 'auto') {
|
|
86
|
+
const derived = locale ? localeFirstDayOfWeek(locale) : null;
|
|
87
|
+
return derived ?? 0;
|
|
88
|
+
}
|
|
70
89
|
return FDOW[value.toLowerCase()] ?? 0;
|
|
71
90
|
}
|
|
@@ -10,6 +10,7 @@ import { LyraElement } from '../../internal/lyra-element.js';
|
|
|
10
10
|
import { FormAssociated } from '../../internal/form-associated.js';
|
|
11
11
|
import { defineElement } from '../../internal/prefix.js';
|
|
12
12
|
import { place } from '../../internal/positioner.js';
|
|
13
|
+
import { nextId } from '../../internal/a11y.js';
|
|
13
14
|
import { closeIcon, calendarIcon } from '../../internal/icons.js';
|
|
14
15
|
import { parseISO, formatISO } from './calendar-core.js';
|
|
15
16
|
import { styles } from './date-input.styles.js';
|
|
@@ -48,6 +49,7 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
|
|
|
48
49
|
this.months = 1;
|
|
49
50
|
this.firstDayOfWeek = 'auto';
|
|
50
51
|
this.weekdayFormat = 'short';
|
|
52
|
+
this.inputId = nextId('date-input');
|
|
51
53
|
// Set on the date input's first `blur`; gates the `data-invalid`
|
|
52
54
|
// reflection below so validity styling never flashes on first render.
|
|
53
55
|
this.touched = false;
|
|
@@ -66,8 +68,11 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
|
|
|
66
68
|
this.hide();
|
|
67
69
|
};
|
|
68
70
|
this.onInputChange = (e) => {
|
|
69
|
-
const
|
|
71
|
+
const target = e.target;
|
|
72
|
+
const raw = target.value.trim();
|
|
70
73
|
if (!raw) {
|
|
74
|
+
// The mixin's value setter recomputes validity (updateValidity()),
|
|
75
|
+
// which clears any stale badInput state along the way.
|
|
71
76
|
this.value = '';
|
|
72
77
|
this.emit('input');
|
|
73
78
|
this.emit('change');
|
|
@@ -79,6 +84,15 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
|
|
|
79
84
|
this.emit('input');
|
|
80
85
|
this.emit('change');
|
|
81
86
|
}
|
|
87
|
+
else {
|
|
88
|
+
// Unparseable text: don't silently keep the committed value while the
|
|
89
|
+
// field still shows garbage -- revert the displayed text back to the
|
|
90
|
+
// last valid commit and flag the constraint-validation state (mirrors
|
|
91
|
+
// how `required` is already wired via internals.setValidity(), see
|
|
92
|
+
// form-associated.ts).
|
|
93
|
+
target.value = this.displayText;
|
|
94
|
+
this.internals.setValidity({ badInput: true }, 'Enter a valid date');
|
|
95
|
+
}
|
|
82
96
|
};
|
|
83
97
|
this.onInputKey = (e) => {
|
|
84
98
|
if (e.altKey && e.key === 'ArrowDown') {
|
|
@@ -176,11 +190,12 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
|
|
|
176
190
|
const hasLabel = this.hasLabelSlot || this.label.length > 0;
|
|
177
191
|
return html `
|
|
178
192
|
<div part="form-control">
|
|
179
|
-
<label part="form-control-label" ?hidden=${!hasLabel}>
|
|
193
|
+
<label part="form-control-label" for=${this.inputId} ?hidden=${!hasLabel}>
|
|
180
194
|
${this.label}<slot name="label" @slotchange=${this.onLabelSlotChange}></slot>
|
|
181
195
|
</label>
|
|
182
196
|
<div part="input-wrapper">
|
|
183
197
|
<input
|
|
198
|
+
id=${this.inputId}
|
|
184
199
|
part="input"
|
|
185
200
|
type="text"
|
|
186
201
|
aria-label=${this.label || this.placeholder || 'Date'}
|
|
@@ -223,6 +238,8 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
|
|
|
223
238
|
.max=${this.max}
|
|
224
239
|
.months=${this.months}
|
|
225
240
|
.locale=${this.locale}
|
|
241
|
+
.disabled=${this.disabled}
|
|
242
|
+
.readonly=${this.readonly}
|
|
226
243
|
first-day-of-week=${this.firstDayOfWeek}
|
|
227
244
|
weekday-format=${this.weekdayFormat}
|
|
228
245
|
@change=${this.onPickerChange}
|
|
@@ -102,9 +102,11 @@ export class LyraDatePicker extends LyraElement {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
get fdow() {
|
|
105
|
-
return resolveFirstDayOfWeek(this.firstDayOfWeek);
|
|
105
|
+
return resolveFirstDayOfWeek(this.firstDayOfWeek, this.locale);
|
|
106
106
|
}
|
|
107
107
|
isDisabled(d) {
|
|
108
|
+
if (this.disabled || this.readonly)
|
|
109
|
+
return true;
|
|
108
110
|
const min = parseISO(this.min);
|
|
109
111
|
const max = parseISO(this.max);
|
|
110
112
|
if (min && d < min)
|
|
@@ -135,7 +137,7 @@ export class LyraDatePicker extends LyraElement {
|
|
|
135
137
|
this.emit('change');
|
|
136
138
|
}
|
|
137
139
|
selectDate(date) {
|
|
138
|
-
if (this.
|
|
140
|
+
if (this.isDisabled(date))
|
|
139
141
|
return;
|
|
140
142
|
this.focusedDate = date;
|
|
141
143
|
if (this.mode === 'range') {
|
|
@@ -4,6 +4,10 @@ export const styles = css `
|
|
|
4
4
|
display: inline-block;
|
|
5
5
|
--lyra-cell-size: 2.25rem;
|
|
6
6
|
}
|
|
7
|
+
:host([disabled]) {
|
|
8
|
+
opacity: var(--lyra-opacity-disabled);
|
|
9
|
+
pointer-events: none;
|
|
10
|
+
}
|
|
7
11
|
[part='base'] {
|
|
8
12
|
display: flex;
|
|
9
13
|
gap: var(--lyra-space-l);
|
|
@@ -94,7 +98,7 @@ export const styles = css `
|
|
|
94
98
|
[part~='day-range-start'],
|
|
95
99
|
[part~='day-range-end'] {
|
|
96
100
|
background: var(--lyra-color-brand);
|
|
97
|
-
color:
|
|
101
|
+
color: var(--lyra-color-on-brand);
|
|
98
102
|
}
|
|
99
103
|
[part~='day']:disabled {
|
|
100
104
|
color: var(--lyra-color-text-quiet);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
const meta = {
|
|
3
|
+
title: 'Empty',
|
|
4
|
+
component: 'lyra-empty',
|
|
5
|
+
tags: ['autodocs'],
|
|
6
|
+
};
|
|
7
|
+
export default meta;
|
|
8
|
+
export const Default = {
|
|
9
|
+
render: () => html `
|
|
10
|
+
<lyra-empty heading="No results" description="Try a different search.">
|
|
11
|
+
<span slot="actions"><button>Reset</button></span>
|
|
12
|
+
</lyra-empty>
|
|
13
|
+
`,
|
|
14
|
+
};
|
|
@@ -54,13 +54,11 @@ export class LyraExportButton extends LyraElement {
|
|
|
54
54
|
if (this.open)
|
|
55
55
|
return;
|
|
56
56
|
this.open = true;
|
|
57
|
-
document.addEventListener('pointerdown', this.onDocPointer);
|
|
58
57
|
}
|
|
59
58
|
closeMenu() {
|
|
60
59
|
if (!this.open)
|
|
61
60
|
return;
|
|
62
61
|
this.open = false;
|
|
63
|
-
document.removeEventListener('pointerdown', this.onDocPointer);
|
|
64
62
|
}
|
|
65
63
|
firstUpdated() {
|
|
66
64
|
// Single delegated listener catches Escape from the trigger button or
|
|
@@ -71,11 +69,18 @@ export class LyraExportButton extends LyraElement {
|
|
|
71
69
|
if (changed.has('open')) {
|
|
72
70
|
this.cleanup?.();
|
|
73
71
|
this.cleanup = undefined;
|
|
72
|
+
// Reacting to the `open` property itself (not just inside
|
|
73
|
+
// openMenu()) means this fires however `open` became true -- via
|
|
74
|
+
// openMenu()'s own click path, or a consumer/test setting
|
|
75
|
+
// `el.open = true` directly (valid API on a `reflect: true`
|
|
76
|
+
// property), which bypasses openMenu() entirely.
|
|
77
|
+
document.removeEventListener('pointerdown', this.onDocPointer);
|
|
74
78
|
if (this.open) {
|
|
75
79
|
const anchor = this.renderRoot.querySelector('[part="trigger"]');
|
|
76
80
|
const menu = this.renderRoot.querySelector('[part="menu"]');
|
|
77
81
|
if (anchor && menu)
|
|
78
82
|
this.cleanup = place(anchor, menu);
|
|
83
|
+
document.addEventListener('pointerdown', this.onDocPointer);
|
|
79
84
|
}
|
|
80
85
|
}
|
|
81
86
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
const rows = [
|
|
3
|
+
{ id: 'a', name: 'Alpha', score: 92 },
|
|
4
|
+
{ id: 'b', name: 'Beta', score: 81 },
|
|
5
|
+
{ id: 'c', name: 'Gamma', score: 76 },
|
|
6
|
+
];
|
|
7
|
+
const columns = [
|
|
8
|
+
{ key: 'id', label: 'ID' },
|
|
9
|
+
{ key: 'name', label: 'Name' },
|
|
10
|
+
{ key: 'score', label: 'Score' },
|
|
11
|
+
];
|
|
12
|
+
const meta = {
|
|
13
|
+
title: 'ExportButton',
|
|
14
|
+
component: 'lyra-export-button',
|
|
15
|
+
tags: ['autodocs'],
|
|
16
|
+
};
|
|
17
|
+
export default meta;
|
|
18
|
+
export const SingleFormat = {
|
|
19
|
+
render: () => html `
|
|
20
|
+
<lyra-export-button filename="demo" .rows=${rows} .columns=${columns}></lyra-export-button>
|
|
21
|
+
`,
|
|
22
|
+
};
|
|
23
|
+
export const MultiFormatMenu = {
|
|
24
|
+
render: () => html `
|
|
25
|
+
<lyra-export-button
|
|
26
|
+
filename="demo"
|
|
27
|
+
.rows=${rows}
|
|
28
|
+
.columns=${columns}
|
|
29
|
+
.formats=${['csv', 'json']}
|
|
30
|
+
></lyra-export-button>
|
|
31
|
+
`,
|
|
32
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
const meta = {
|
|
3
|
+
title: 'FileInput',
|
|
4
|
+
component: 'lyra-file-input',
|
|
5
|
+
tags: ['autodocs'],
|
|
6
|
+
};
|
|
7
|
+
export default meta;
|
|
8
|
+
export const Default = {
|
|
9
|
+
render: () => html `<lyra-file-input multiple accept=".csv,.xlsx"></lyra-file-input>`,
|
|
10
|
+
};
|