@aceshooting/lyra-ui 1.0.1 → 1.2.0
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 +9 -10
- package/custom-elements.json +15118 -8342
- package/dist/components/chart/chart.d.ts +57 -1
- package/dist/components/chart/chart.js +202 -12
- package/dist/components/chart/chart.stories.d.ts +32 -0
- package/dist/components/chart/chart.stories.js +141 -0
- package/dist/components/chart/chart.styles.js +16 -0
- package/dist/components/chart/histogram.d.ts +2 -0
- package/dist/components/chart/histogram.js +18 -2
- package/dist/components/chart/line-chart.stories.d.ts +17 -0
- package/dist/components/chart/line-chart.stories.js +75 -0
- package/dist/components/chart/lite-chart.d.ts +83 -0
- package/dist/components/chart/lite-chart.js +424 -0
- package/dist/components/chart/lite-chart.stories.d.ts +14 -0
- package/dist/components/chart/lite-chart.stories.js +114 -0
- package/dist/components/chart/lite-chart.styles.d.ts +1 -0
- package/dist/components/chart/lite-chart.styles.js +83 -0
- package/dist/components/combobox/combobox.d.ts +9 -0
- package/dist/components/combobox/combobox.js +76 -13
- package/dist/components/combobox/combobox.stories.d.ts +18 -0
- package/dist/components/combobox/combobox.stories.js +75 -0
- package/dist/components/date-picker/calendar-core.js +13 -2
- package/dist/components/date-picker/date-input.d.ts +9 -1
- package/dist/components/date-picker/date-input.js +55 -7
- package/dist/components/date-picker/date-input.stories.d.ts +5 -0
- package/dist/components/date-picker/date-input.stories.js +36 -0
- package/dist/components/date-picker/date-picker.d.ts +11 -3
- package/dist/components/date-picker/date-picker.js +87 -37
- package/dist/components/date-picker/date-picker.stories.d.ts +4 -0
- package/dist/components/date-picker/date-picker.stories.js +12 -0
- package/dist/components/date-picker/date-picker.styles.js +1 -1
- package/dist/components/empty/empty.d.ts +12 -1
- package/dist/components/empty/empty.js +20 -3
- package/dist/components/empty/empty.stories.d.ts +2 -0
- package/dist/components/empty/empty.stories.js +20 -0
- package/dist/components/empty/empty.styles.js +11 -0
- package/dist/components/export-button/csv.js +6 -2
- package/dist/components/export-button/export-button.d.ts +15 -1
- package/dist/components/export-button/export-button.js +89 -12
- package/dist/components/export-button/export-button.stories.d.ts +1 -0
- package/dist/components/export-button/export-button.stories.js +5 -0
- package/dist/components/export-button/export-button.styles.js +11 -3
- package/dist/components/file-input/accept.d.ts +8 -2
- package/dist/components/file-input/accept.js +9 -3
- package/dist/components/file-input/file-input.d.ts +5 -2
- package/dist/components/file-input/file-input.js +23 -8
- package/dist/components/file-input/file-input.stories.d.ts +2 -0
- package/dist/components/file-input/file-input.stories.js +9 -0
- package/dist/components/flag/flag.d.ts +69 -3
- package/dist/components/flag/flag.js +107 -30
- package/dist/components/flag/flag.stories.d.ts +1 -0
- package/dist/components/flag/flag.stories.js +14 -0
- package/dist/components/flag/flag.styles.js +1 -1
- package/dist/components/flag/language-map.d.ts +11 -0
- package/dist/components/flag/language-map.js +12 -1
- package/dist/components/gauge/gauge.d.ts +2 -0
- package/dist/components/gauge/gauge.js +19 -5
- package/dist/components/gauge/gauge.stories.d.ts +2 -0
- package/dist/components/gauge/gauge.stories.js +6 -0
- package/dist/components/graph/graph-loader.d.ts +28 -0
- package/dist/components/graph/graph-loader.js +44 -0
- package/dist/components/graph/graph.d.ts +41 -15
- package/dist/components/graph/graph.js +146 -71
- package/dist/components/graph/graph.stories.d.ts +3 -0
- package/dist/components/graph/graph.stories.js +38 -0
- package/dist/components/graph/graph.styles.js +6 -1
- package/dist/components/heatmap/calendar-grid.d.ts +7 -0
- package/dist/components/heatmap/calendar-grid.js +27 -10
- package/dist/components/heatmap/heatmap.d.ts +149 -12
- package/dist/components/heatmap/heatmap.js +445 -28
- package/dist/components/heatmap/heatmap.stories.d.ts +22 -0
- package/dist/components/heatmap/heatmap.stories.js +155 -0
- package/dist/components/heatmap/heatmap.styles.js +60 -2
- package/dist/components/map/map.d.ts +17 -1
- package/dist/components/map/map.js +68 -21
- package/dist/components/map/map.stories.d.ts +10 -0
- package/dist/components/map/map.stories.js +107 -12
- package/dist/components/map/map.styles.js +2 -2
- package/dist/components/playback/playback.d.ts +6 -2
- package/dist/components/playback/playback.js +36 -12
- package/dist/components/playback/playback.stories.d.ts +2 -0
- package/dist/components/playback/playback.stories.js +6 -0
- package/dist/components/playback/playback.styles.js +10 -2
- package/dist/components/select/select.d.ts +125 -0
- package/dist/components/select/select.js +480 -0
- package/dist/components/select/select.stories.d.ts +21 -0
- package/dist/components/select/select.stories.js +91 -0
- package/dist/components/select/select.styles.d.ts +1 -0
- package/dist/components/select/select.styles.js +212 -0
- package/dist/components/skeleton/skeleton.js +4 -7
- package/dist/components/sparkline/sparkline.d.ts +1 -1
- package/dist/components/sparkline/sparkline.js +40 -13
- package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
- package/dist/components/sparkline/sparkline.stories.js +22 -0
- package/dist/components/split/split.d.ts +24 -3
- package/dist/components/split/split.js +155 -31
- package/dist/components/split/split.stories.d.ts +2 -0
- package/dist/components/split/split.stories.js +20 -0
- package/dist/components/stat/stat.d.ts +43 -1
- package/dist/components/stat/stat.js +97 -4
- package/dist/components/stat/stat.stories.d.ts +4 -0
- package/dist/components/stat/stat.stories.js +66 -1
- package/dist/components/stat/stat.styles.js +64 -0
- package/dist/components/table/table.d.ts +64 -2
- package/dist/components/table/table.js +213 -17
- package/dist/components/table/table.stories.d.ts +6 -0
- package/dist/components/table/table.stories.js +45 -0
- package/dist/components/table/table.styles.js +51 -4
- package/dist/components/time-range/time-range.d.ts +34 -5
- package/dist/components/time-range/time-range.js +217 -36
- package/dist/components/time-range/time-range.stories.d.ts +2 -0
- package/dist/components/time-range/time-range.stories.js +13 -0
- package/dist/components/time-range/time-range.styles.js +44 -2
- package/dist/components/toast/toast-item.d.ts +14 -2
- package/dist/components/toast/toast-item.js +134 -17
- package/dist/components/toast/toast-item.styles.js +28 -2
- package/dist/components/toast/toast.stories.d.ts +3 -0
- package/dist/components/toast/toast.stories.js +41 -1
- package/dist/components/toast/toast.styles.js +11 -11
- package/dist/components/toast/toaster.d.ts +1 -1
- package/dist/components/toast/toaster.js +13 -6
- package/dist/components/tree/tree-node.d.ts +13 -7
- package/dist/components/tree/tree-node.js +24 -10
- package/dist/components/tree/tree.d.ts +64 -15
- package/dist/components/tree/tree.js +113 -22
- package/dist/components/tree/tree.stories.d.ts +4 -0
- package/dist/components/tree/tree.stories.js +31 -1
- package/dist/components/tree/update-cascade.d.ts +13 -0
- package/dist/components/tree/update-cascade.js +13 -0
- package/dist/components/widget/widget.d.ts +25 -1
- package/dist/components/widget/widget.js +164 -6
- package/dist/components/widget/widget.stories.d.ts +3 -0
- package/dist/components/widget/widget.stories.js +43 -0
- package/dist/components/widget/widget.styles.js +20 -3
- package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
- package/dist/components/word-cloud/word-cloud-layout.js +145 -0
- package/dist/components/word-cloud/word-cloud.d.ts +65 -0
- package/dist/components/word-cloud/word-cloud.js +255 -0
- package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
- package/dist/components/word-cloud/word-cloud.stories.js +55 -0
- package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
- package/dist/components/word-cloud/word-cloud.styles.js +55 -0
- package/dist/internal/a11y.js +2 -1
- package/dist/internal/rtl.d.ts +7 -0
- package/dist/internal/rtl.js +9 -0
- package/dist/lyra.d.ts +9 -1
- package/dist/lyra.js +6 -0
- package/llms-full.txt +962 -276
- package/llms.txt +12 -9
- package/package.json +6 -4
|
@@ -29,8 +29,10 @@ import './date-picker.js';
|
|
|
29
29
|
* @event change - Fired on committed date transitions.
|
|
30
30
|
* @event lyra-show / lyra-hide - Calendar popover lifecycle.
|
|
31
31
|
* @event lyra-clear - The clear button was used.
|
|
32
|
-
* @csspart form-control, form-control-label, input-wrapper, input, clear-button, expand-button, popup, date-picker, hint, error
|
|
32
|
+
* @csspart form-control, form-control-label, input-wrapper, input, clear-button, expand-button, expand-icon, popup, date-picker, hint, error
|
|
33
|
+
* @slot label - Custom label content.
|
|
33
34
|
* @slot error - Custom error content.
|
|
35
|
+
* @slot hint - Custom hint content.
|
|
34
36
|
*/
|
|
35
37
|
export class LyraDateInput extends FormAssociated(LyraElement) {
|
|
36
38
|
constructor() {
|
|
@@ -49,7 +51,11 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
|
|
|
49
51
|
this.months = 1;
|
|
50
52
|
this.firstDayOfWeek = 'auto';
|
|
51
53
|
this.weekdayFormat = 'short';
|
|
54
|
+
this.disablePast = false;
|
|
55
|
+
this.disableFuture = false;
|
|
56
|
+
this.withOutsideDays = false;
|
|
52
57
|
this.inputId = nextId('date-input');
|
|
58
|
+
this.popupId = nextId('date-popup');
|
|
53
59
|
// Set on the date input's first `blur`; gates the `data-invalid`
|
|
54
60
|
// reflection below so validity styling never flashes on first render.
|
|
55
61
|
this.touched = false;
|
|
@@ -78,7 +84,13 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
|
|
|
78
84
|
this.emit('change');
|
|
79
85
|
return;
|
|
80
86
|
}
|
|
81
|
-
|
|
87
|
+
// A string that already looks like strict ISO (YYYY-MM-DD) must be judged
|
|
88
|
+
// by parseISO alone: it validates calendar correctness (rejects e.g.
|
|
89
|
+
// "2026-02-30"), but Date.parse() doesn't -- falling through to it for an
|
|
90
|
+
// ISO-shaped string would silently resurrect the same rollover parseISO
|
|
91
|
+
// guards against (Date.parse('2026-02-30') also normalizes to March 2).
|
|
92
|
+
const looksLikeISO = /^\d{4}-\d{2}-\d{2}$/.test(raw);
|
|
93
|
+
const parsed = looksLikeISO ? parseISO(raw) : isNaN(Date.parse(raw)) ? null : new Date(Date.parse(raw));
|
|
82
94
|
if (parsed) {
|
|
83
95
|
this.value = formatISO(parsed);
|
|
84
96
|
this.emit('input');
|
|
@@ -100,6 +112,17 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
|
|
|
100
112
|
this.show();
|
|
101
113
|
}
|
|
102
114
|
};
|
|
115
|
+
// Attached to the whole form-control, not just the text input, so Escape
|
|
116
|
+
// closes the popover from anywhere inside it -- including the nested
|
|
117
|
+
// picker's own day/nav buttons, which take real DOM focus (roving
|
|
118
|
+
// tabindex) rather than the input keeping focus throughout.
|
|
119
|
+
this.onFormControlKey = (e) => {
|
|
120
|
+
if (e.key === 'Escape' && this.open) {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
this.hide();
|
|
123
|
+
this.renderRoot.querySelector('[part="expand-button"]')?.focus();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
103
126
|
this.onInputBlur = () => {
|
|
104
127
|
this.touched = true;
|
|
105
128
|
};
|
|
@@ -112,14 +135,25 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
|
|
|
112
135
|
this.onLabelSlotChange = (e) => {
|
|
113
136
|
this.hasLabelSlot = e.target.assignedElements({ flatten: true }).length > 0;
|
|
114
137
|
};
|
|
115
|
-
|
|
138
|
+
// The nested picker's `input` event isn't wired anywhere else, so without
|
|
139
|
+
// this listener it bubbles+composes straight through this shadow boundary
|
|
140
|
+
// (LyraElement.emit always dispatches bubbles:true, composed:true) and
|
|
141
|
+
// fires on this host a *second* time, on top of the explicit emit below.
|
|
142
|
+
this.onPickerInput = (e) => {
|
|
116
143
|
e.stopPropagation();
|
|
117
144
|
const picker = e.target;
|
|
118
145
|
this.value = picker.value;
|
|
119
146
|
this.emit('input');
|
|
147
|
+
};
|
|
148
|
+
this.onPickerChange = (e) => {
|
|
149
|
+
e.stopPropagation();
|
|
150
|
+
const picker = e.target;
|
|
151
|
+
this.value = picker.value;
|
|
120
152
|
this.emit('change');
|
|
121
|
-
|
|
122
|
-
|
|
153
|
+
// The picker only fires `change` once a selection is finalized (a single
|
|
154
|
+
// pick, or the second click of a range), so this is always the right
|
|
155
|
+
// moment to close, in either mode.
|
|
156
|
+
this.hide();
|
|
123
157
|
};
|
|
124
158
|
}
|
|
125
159
|
static { this.styles = [LyraElement.styles, styles]; }
|
|
@@ -189,7 +223,7 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
|
|
|
189
223
|
const hasError = this.hasErrorSlot || this.errorText.length > 0;
|
|
190
224
|
const hasLabel = this.hasLabelSlot || this.label.length > 0;
|
|
191
225
|
return html `
|
|
192
|
-
<div part="form-control">
|
|
226
|
+
<div part="form-control" @keydown=${this.onFormControlKey}>
|
|
193
227
|
<label part="form-control-label" for=${this.inputId} ?hidden=${!hasLabel}>
|
|
194
228
|
${this.label}<slot name="label" @slotchange=${this.onLabelSlotChange}></slot>
|
|
195
229
|
</label>
|
|
@@ -223,13 +257,14 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
|
|
|
223
257
|
aria-label="Open calendar"
|
|
224
258
|
aria-haspopup="dialog"
|
|
225
259
|
aria-expanded=${this.open ? 'true' : 'false'}
|
|
260
|
+
aria-controls=${this.popupId}
|
|
226
261
|
?disabled=${this.disabled || this.readonly}
|
|
227
262
|
@click=${() => (this.open ? this.hide() : this.show())}
|
|
228
263
|
>
|
|
229
264
|
<span part="expand-icon" aria-hidden="true">${calendarIcon()}</span>
|
|
230
265
|
</button>
|
|
231
266
|
</div>
|
|
232
|
-
<div part="popup" role="dialog" aria-label="Choose date">
|
|
267
|
+
<div id=${this.popupId} part="popup" role="dialog" aria-label="Choose date">
|
|
233
268
|
<lyra-date-picker
|
|
234
269
|
part="date-picker"
|
|
235
270
|
.value=${this.value}
|
|
@@ -240,8 +275,12 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
|
|
|
240
275
|
.locale=${this.locale}
|
|
241
276
|
.disabled=${this.disabled}
|
|
242
277
|
.readonly=${this.readonly}
|
|
278
|
+
.disablePast=${this.disablePast}
|
|
279
|
+
.disableFuture=${this.disableFuture}
|
|
280
|
+
.withOutsideDays=${this.withOutsideDays}
|
|
243
281
|
first-day-of-week=${this.firstDayOfWeek}
|
|
244
282
|
weekday-format=${this.weekdayFormat}
|
|
283
|
+
@input=${this.onPickerInput}
|
|
245
284
|
@change=${this.onPickerChange}
|
|
246
285
|
></lyra-date-picker>
|
|
247
286
|
</div>
|
|
@@ -297,6 +336,15 @@ __decorate([
|
|
|
297
336
|
__decorate([
|
|
298
337
|
property({ attribute: 'weekday-format' })
|
|
299
338
|
], LyraDateInput.prototype, "weekdayFormat", void 0);
|
|
339
|
+
__decorate([
|
|
340
|
+
property({ type: Boolean, attribute: 'disable-past' })
|
|
341
|
+
], LyraDateInput.prototype, "disablePast", void 0);
|
|
342
|
+
__decorate([
|
|
343
|
+
property({ type: Boolean, attribute: 'disable-future' })
|
|
344
|
+
], LyraDateInput.prototype, "disableFuture", void 0);
|
|
345
|
+
__decorate([
|
|
346
|
+
property({ type: Boolean, attribute: 'with-outside-days' })
|
|
347
|
+
], LyraDateInput.prototype, "withOutsideDays", void 0);
|
|
300
348
|
__decorate([
|
|
301
349
|
state()
|
|
302
350
|
], LyraDateInput.prototype, "touched", void 0);
|
|
@@ -3,3 +3,8 @@ declare const meta: Meta;
|
|
|
3
3
|
export default meta;
|
|
4
4
|
type Story = StoryObj;
|
|
5
5
|
export declare const Default: Story;
|
|
6
|
+
export declare const MinMax: Story;
|
|
7
|
+
export declare const DisablePast: Story;
|
|
8
|
+
export declare const DisableFuture: Story;
|
|
9
|
+
export declare const Range: Story;
|
|
10
|
+
export declare const WithOutsideDays: Story;
|
|
@@ -10,3 +10,39 @@ export const Default = {
|
|
|
10
10
|
<lyra-date-input label="Start date" with-clear style="max-width: 16rem"></lyra-date-input>
|
|
11
11
|
`,
|
|
12
12
|
};
|
|
13
|
+
export const MinMax = {
|
|
14
|
+
render: () => html `
|
|
15
|
+
<lyra-date-input
|
|
16
|
+
label="Appointment date"
|
|
17
|
+
min="2026-07-10"
|
|
18
|
+
max="2026-07-20"
|
|
19
|
+
value="2026-07-15"
|
|
20
|
+
style="max-width: 16rem"
|
|
21
|
+
></lyra-date-input>
|
|
22
|
+
`,
|
|
23
|
+
};
|
|
24
|
+
export const DisablePast = {
|
|
25
|
+
render: () => html `
|
|
26
|
+
<lyra-date-input label="Upcoming date" disable-past style="max-width: 16rem"></lyra-date-input>
|
|
27
|
+
`,
|
|
28
|
+
};
|
|
29
|
+
export const DisableFuture = {
|
|
30
|
+
render: () => html `
|
|
31
|
+
<lyra-date-input label="Historical date" disable-future style="max-width: 16rem"></lyra-date-input>
|
|
32
|
+
`,
|
|
33
|
+
};
|
|
34
|
+
export const Range = {
|
|
35
|
+
render: () => html `
|
|
36
|
+
<lyra-date-input label="Trip dates" mode="range" months="2" style="max-width: 20rem"></lyra-date-input>
|
|
37
|
+
`,
|
|
38
|
+
};
|
|
39
|
+
export const WithOutsideDays = {
|
|
40
|
+
render: () => html `
|
|
41
|
+
<lyra-date-input
|
|
42
|
+
label="Meeting date"
|
|
43
|
+
with-outside-days
|
|
44
|
+
value="2026-07-15"
|
|
45
|
+
style="max-width: 16rem"
|
|
46
|
+
></lyra-date-input>
|
|
47
|
+
`,
|
|
48
|
+
};
|
|
@@ -14,8 +14,8 @@ export interface DateRange {
|
|
|
14
14
|
* @customElement lyra-date-picker
|
|
15
15
|
* @event change - The user committed a value.
|
|
16
16
|
* @event input - The value changed during interaction (range: after the first click).
|
|
17
|
-
* @csspart base, month, header, title, previous, next, weekdays, weekday, grid
|
|
18
|
-
* @csspart day, day-today, day-outside, day-selected, day-range-start, day-range-end, day-range-inner
|
|
17
|
+
* @csspart base, month, header, title, previous, next, weekdays, weekday, grid, week
|
|
18
|
+
* @csspart day, day-today, day-outside, day-selected, day-range-start, day-range-end, day-range-inner, day-placeholder
|
|
19
19
|
*/
|
|
20
20
|
export declare class LyraDatePicker extends LyraElement {
|
|
21
21
|
static styles: import("lit").CSSResultGroup[];
|
|
@@ -36,6 +36,7 @@ export declare class LyraDatePicker extends LyraElement {
|
|
|
36
36
|
private viewDate;
|
|
37
37
|
private focusedDate;
|
|
38
38
|
private focusPending;
|
|
39
|
+
private suppressViewSync;
|
|
39
40
|
get selection(): DateRange;
|
|
40
41
|
/** Read-only Date view of a single-mode value. */
|
|
41
42
|
get valueAsDate(): Date | null;
|
|
@@ -48,10 +49,17 @@ export declare class LyraDatePicker extends LyraElement {
|
|
|
48
49
|
clear(): void;
|
|
49
50
|
/** Navigate to today and focus it. */
|
|
50
51
|
goToToday(): void;
|
|
51
|
-
/** Navigate the view to a date and focus it
|
|
52
|
+
/** Navigate the view to a date and focus it, clamped to `min`/`max`. */
|
|
52
53
|
goToDate(date: string | Date): void;
|
|
53
54
|
private nav;
|
|
54
55
|
private onGridKey;
|
|
56
|
+
/**
|
|
57
|
+
* The anchor month for a newly-focused date, sliding the view by the
|
|
58
|
+
* minimum amount needed to bring it into view. With `months` > 1, a date
|
|
59
|
+
* that's already visible in a later grid must not discard an earlier grid
|
|
60
|
+
* that's already on-screen.
|
|
61
|
+
*/
|
|
62
|
+
private viewDateForFocus;
|
|
55
63
|
protected updated(): void;
|
|
56
64
|
private renderDay;
|
|
57
65
|
private renderMonth;
|
|
@@ -4,13 +4,13 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { html } from 'lit';
|
|
7
|
+
import { html, nothing } from 'lit';
|
|
8
8
|
import { property, state } from 'lit/decorators.js';
|
|
9
9
|
import { LyraElement } from '../../internal/lyra-element.js';
|
|
10
10
|
import { defineElement } from '../../internal/prefix.js';
|
|
11
11
|
import { chevronIcon } from '../../internal/icons.js';
|
|
12
12
|
import { styles } from './date-picker.styles.js';
|
|
13
|
-
import { monthMatrix, weekdayLabels, monthTitle, formatISO, parseISO, isSameDay, addMonths, resolveFirstDayOfWeek, } from './calendar-core.js';
|
|
13
|
+
import { monthMatrix, weekdayLabels, monthTitle, formatISO, parseISO, isSameDay, addMonths, clampDate, resolveFirstDayOfWeek, } from './calendar-core.js';
|
|
14
14
|
/**
|
|
15
15
|
* `<lyra-date-picker>` — an inline month-grid calendar for picking a single date
|
|
16
16
|
* or a date range. Mirrors the core `<wa-date-picker>` API under `lyra-`.
|
|
@@ -20,8 +20,8 @@ import { monthMatrix, weekdayLabels, monthTitle, formatISO, parseISO, isSameDay,
|
|
|
20
20
|
* @customElement lyra-date-picker
|
|
21
21
|
* @event change - The user committed a value.
|
|
22
22
|
* @event input - The value changed during interaction (range: after the first click).
|
|
23
|
-
* @csspart base, month, header, title, previous, next, weekdays, weekday, grid
|
|
24
|
-
* @csspart day, day-today, day-outside, day-selected, day-range-start, day-range-end, day-range-inner
|
|
23
|
+
* @csspart base, month, header, title, previous, next, weekdays, weekday, grid, week
|
|
24
|
+
* @csspart day, day-today, day-outside, day-selected, day-range-start, day-range-end, day-range-inner, day-placeholder
|
|
25
25
|
*/
|
|
26
26
|
export class LyraDatePicker extends LyraElement {
|
|
27
27
|
constructor() {
|
|
@@ -43,6 +43,12 @@ export class LyraDatePicker extends LyraElement {
|
|
|
43
43
|
this.viewDate = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
|
|
44
44
|
this.focusedDate = null;
|
|
45
45
|
this.focusPending = false;
|
|
46
|
+
// Set right before `commit()` assigns `value`, so `willUpdate` can tell its
|
|
47
|
+
// own write apart from an external assignment. Internal commits already
|
|
48
|
+
// know the right date is on-screen (it's the cell the user just clicked, or
|
|
49
|
+
// the view the user navigated to), so they must not force the view back to
|
|
50
|
+
// `selection.from`'s month -- only an externally-set `value` should do that.
|
|
51
|
+
this.suppressViewSync = false;
|
|
46
52
|
this.onGridKey = (e) => {
|
|
47
53
|
const current = this.focusedDate ?? this.selection.from ?? new Date();
|
|
48
54
|
let next = null;
|
|
@@ -81,7 +87,7 @@ export class LyraDatePicker extends LyraElement {
|
|
|
81
87
|
}
|
|
82
88
|
e.preventDefault();
|
|
83
89
|
this.focusedDate = next;
|
|
84
|
-
this.viewDate =
|
|
90
|
+
this.viewDate = this.viewDateForFocus(next);
|
|
85
91
|
this.focusPending = true;
|
|
86
92
|
};
|
|
87
93
|
}
|
|
@@ -95,26 +101,26 @@ export class LyraDatePicker extends LyraElement {
|
|
|
95
101
|
return this.mode === 'single' ? this.selection.from : null;
|
|
96
102
|
}
|
|
97
103
|
willUpdate(changed) {
|
|
98
|
-
if (changed.has('value')
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
if (changed.has('value')) {
|
|
105
|
+
const external = !this.suppressViewSync;
|
|
106
|
+
this.suppressViewSync = false;
|
|
107
|
+
if (external && this.value) {
|
|
108
|
+
const from = this.selection.from;
|
|
109
|
+
if (from)
|
|
110
|
+
this.viewDate = new Date(from.getFullYear(), from.getMonth(), 1);
|
|
111
|
+
}
|
|
102
112
|
}
|
|
103
113
|
}
|
|
104
114
|
get fdow() {
|
|
105
115
|
return resolveFirstDayOfWeek(this.firstDayOfWeek, this.locale);
|
|
106
116
|
}
|
|
107
|
-
isDisabled(d) {
|
|
117
|
+
isDisabled(d, min, max, today) {
|
|
108
118
|
if (this.disabled || this.readonly)
|
|
109
119
|
return true;
|
|
110
|
-
const min = parseISO(this.min);
|
|
111
|
-
const max = parseISO(this.max);
|
|
112
120
|
if (min && d < min)
|
|
113
121
|
return true;
|
|
114
122
|
if (max && d > max)
|
|
115
123
|
return true;
|
|
116
|
-
const today = new Date();
|
|
117
|
-
today.setHours(0, 0, 0, 0);
|
|
118
124
|
if (this.disablePast && d < today)
|
|
119
125
|
return true;
|
|
120
126
|
if (this.disableFuture && d > today)
|
|
@@ -122,22 +128,31 @@ export class LyraDatePicker extends LyraElement {
|
|
|
122
128
|
return false;
|
|
123
129
|
}
|
|
124
130
|
commit(from, to, fire) {
|
|
125
|
-
this.
|
|
126
|
-
|
|
127
|
-
? from
|
|
128
|
-
? `${formatISO(from)}/${formatISO(to)}`
|
|
129
|
-
: from
|
|
130
|
-
? formatISO(from)
|
|
131
|
-
: ''
|
|
131
|
+
const next = this.mode === 'range'
|
|
132
|
+
? from && to
|
|
133
|
+
? `${formatISO(from)}/${formatISO(to)}`
|
|
132
134
|
: from
|
|
133
135
|
? formatISO(from)
|
|
134
|
-
: ''
|
|
136
|
+
: ''
|
|
137
|
+
: from
|
|
138
|
+
? formatISO(from)
|
|
139
|
+
: '';
|
|
140
|
+
// Only arm the suppression when `value` is actually about to change --
|
|
141
|
+
// that's the only case `willUpdate` will see `changed.has('value')` and
|
|
142
|
+
// get a chance to consume (and clear) the flag.
|
|
143
|
+
if (next !== this.value)
|
|
144
|
+
this.suppressViewSync = true;
|
|
145
|
+
this.value = next;
|
|
135
146
|
this.emit('input');
|
|
136
147
|
if (fire)
|
|
137
148
|
this.emit('change');
|
|
138
149
|
}
|
|
139
150
|
selectDate(date) {
|
|
140
|
-
|
|
151
|
+
const min = parseISO(this.min);
|
|
152
|
+
const max = parseISO(this.max);
|
|
153
|
+
const today = new Date();
|
|
154
|
+
today.setHours(0, 0, 0, 0);
|
|
155
|
+
if (this.isDisabled(date, min, max, today))
|
|
141
156
|
return;
|
|
142
157
|
this.focusedDate = date;
|
|
143
158
|
if (this.mode === 'range') {
|
|
@@ -165,18 +180,35 @@ export class LyraDatePicker extends LyraElement {
|
|
|
165
180
|
goToToday() {
|
|
166
181
|
this.goToDate(new Date());
|
|
167
182
|
}
|
|
168
|
-
/** Navigate the view to a date and focus it
|
|
183
|
+
/** Navigate the view to a date and focus it, clamped to `min`/`max`. */
|
|
169
184
|
goToDate(date) {
|
|
170
185
|
const d = typeof date === 'string' ? parseISO(date) : date;
|
|
171
186
|
if (!d)
|
|
172
187
|
return;
|
|
173
|
-
|
|
174
|
-
this.
|
|
188
|
+
const clamped = clampDate(d, parseISO(this.min), parseISO(this.max));
|
|
189
|
+
this.viewDate = new Date(clamped.getFullYear(), clamped.getMonth(), 1);
|
|
190
|
+
this.focusedDate = clamped;
|
|
175
191
|
this.focusPending = true;
|
|
176
192
|
}
|
|
177
193
|
nav(delta) {
|
|
178
194
|
this.viewDate = addMonths(this.viewDate, delta);
|
|
179
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* The anchor month for a newly-focused date, sliding the view by the
|
|
198
|
+
* minimum amount needed to bring it into view. With `months` > 1, a date
|
|
199
|
+
* that's already visible in a later grid must not discard an earlier grid
|
|
200
|
+
* that's already on-screen.
|
|
201
|
+
*/
|
|
202
|
+
viewDateForFocus(next) {
|
|
203
|
+
const firstMonth = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth(), 1);
|
|
204
|
+
const nextMonth = new Date(next.getFullYear(), next.getMonth(), 1);
|
|
205
|
+
const lastMonth = addMonths(firstMonth, this.months - 1);
|
|
206
|
+
if (nextMonth < firstMonth)
|
|
207
|
+
return nextMonth;
|
|
208
|
+
if (nextMonth > lastMonth)
|
|
209
|
+
return addMonths(nextMonth, -(this.months - 1));
|
|
210
|
+
return firstMonth;
|
|
211
|
+
}
|
|
180
212
|
updated() {
|
|
181
213
|
if (this.focusPending && this.focusedDate) {
|
|
182
214
|
this.focusPending = false;
|
|
@@ -185,16 +217,24 @@ export class LyraDatePicker extends LyraElement {
|
|
|
185
217
|
cell?.focus();
|
|
186
218
|
}
|
|
187
219
|
}
|
|
188
|
-
renderDay(date, shownMonth) {
|
|
220
|
+
renderDay(date, shownMonth, selection, min, max, today, rowHasVisibleDay) {
|
|
189
221
|
const outside = date.getMonth() !== shownMonth;
|
|
190
222
|
// Outside-month days are empty placeholders by default (matches WA), keeping the
|
|
191
223
|
// 6-row grid aligned without low-contrast faded numbers.
|
|
192
224
|
if (outside && !this.withOutsideDays) {
|
|
193
|
-
|
|
225
|
+
// role="gridcell" stays either way. A trailing week can land entirely
|
|
226
|
+
// outside the shown month (monthMatrix always emits 6 rows of 7), and
|
|
227
|
+
// ARIA's row role requires at least one visible gridcell descendant --
|
|
228
|
+
// so only rows that already have a real, visible day cell may hide
|
|
229
|
+
// their placeholders from the accessibility tree.
|
|
230
|
+
return html `<span
|
|
231
|
+
part="day-placeholder"
|
|
232
|
+
role="gridcell"
|
|
233
|
+
aria-hidden=${rowHasVisibleDay ? 'true' : nothing}
|
|
234
|
+
></span>`;
|
|
194
235
|
}
|
|
195
|
-
const { from, to } =
|
|
196
|
-
const disabled = this.isDisabled(date);
|
|
197
|
-
const today = new Date();
|
|
236
|
+
const { from, to } = selection;
|
|
237
|
+
const disabled = this.isDisabled(date, min, max, today);
|
|
198
238
|
const isToday = isSameDay(date, today);
|
|
199
239
|
const isStart = from && isSameDay(date, from);
|
|
200
240
|
const isEnd = to && isSameDay(date, to);
|
|
@@ -232,12 +272,11 @@ export class LyraDatePicker extends LyraElement {
|
|
|
232
272
|
${date.getDate()}
|
|
233
273
|
</button>`;
|
|
234
274
|
}
|
|
235
|
-
renderMonth(offset) {
|
|
275
|
+
renderMonth(offset, selection, min, max, today, fdow, labels) {
|
|
236
276
|
const base = addMonths(this.viewDate, offset);
|
|
237
277
|
const year = base.getFullYear();
|
|
238
278
|
const month = base.getMonth();
|
|
239
|
-
const matrix = monthMatrix(year, month,
|
|
240
|
-
const labels = weekdayLabels(this.fdow, this.weekdayFormat, this.locale);
|
|
279
|
+
const matrix = monthMatrix(year, month, fdow);
|
|
241
280
|
const isFirst = offset === 0;
|
|
242
281
|
const isLast = offset === this.months - 1;
|
|
243
282
|
return html `<div part="month">
|
|
@@ -256,14 +295,25 @@ export class LyraDatePicker extends LyraElement {
|
|
|
256
295
|
</div>
|
|
257
296
|
<div part="weekdays">${labels.map((l) => html `<span part="weekday">${l}</span>`)}</div>
|
|
258
297
|
<div part="grid" role="grid" @keydown=${this.onGridKey}>
|
|
259
|
-
${matrix.map((week) =>
|
|
298
|
+
${matrix.map((week) => {
|
|
299
|
+
const rowHasVisibleDay = week.some((d) => d.getMonth() === month);
|
|
300
|
+
return html `<div part="week" role="row">${week.map((d) => this.renderDay(d, month, selection, min, max, today, rowHasVisibleDay))}</div>`;
|
|
301
|
+
})}
|
|
260
302
|
</div>
|
|
261
303
|
</div>`;
|
|
262
304
|
}
|
|
263
305
|
render() {
|
|
306
|
+
const selection = this.selection;
|
|
307
|
+
const min = parseISO(this.min);
|
|
308
|
+
const max = parseISO(this.max);
|
|
309
|
+
const today = new Date();
|
|
310
|
+
today.setHours(0, 0, 0, 0);
|
|
311
|
+
const fdow = this.fdow;
|
|
312
|
+
const labels = weekdayLabels(fdow, this.weekdayFormat, this.locale);
|
|
264
313
|
const monthEls = [];
|
|
265
|
-
for (let i = 0; i < this.months; i++)
|
|
266
|
-
monthEls.push(this.renderMonth(i));
|
|
314
|
+
for (let i = 0; i < this.months; i++) {
|
|
315
|
+
monthEls.push(this.renderMonth(i, selection, min, max, today, fdow, labels));
|
|
316
|
+
}
|
|
267
317
|
return html `<div part="base">${monthEls}</div>`;
|
|
268
318
|
}
|
|
269
319
|
}
|
|
@@ -4,3 +4,7 @@ export default meta;
|
|
|
4
4
|
type Story = StoryObj;
|
|
5
5
|
export declare const Single: Story;
|
|
6
6
|
export declare const Range: Story;
|
|
7
|
+
export declare const MinMax: Story;
|
|
8
|
+
export declare const DisablePast: Story;
|
|
9
|
+
export declare const DisableFuture: Story;
|
|
10
|
+
export declare const WithOutsideDays: Story;
|
|
@@ -11,3 +11,15 @@ export const Single = {
|
|
|
11
11
|
export const Range = {
|
|
12
12
|
render: () => html `<lyra-date-picker mode="range" months="2"></lyra-date-picker>`,
|
|
13
13
|
};
|
|
14
|
+
export const MinMax = {
|
|
15
|
+
render: () => html `<lyra-date-picker min="2026-07-10" max="2026-07-20" value="2026-07-15"></lyra-date-picker>`,
|
|
16
|
+
};
|
|
17
|
+
export const DisablePast = {
|
|
18
|
+
render: () => html `<lyra-date-picker disable-past></lyra-date-picker>`,
|
|
19
|
+
};
|
|
20
|
+
export const DisableFuture = {
|
|
21
|
+
render: () => html `<lyra-date-picker disable-future></lyra-date-picker>`,
|
|
22
|
+
};
|
|
23
|
+
export const WithOutsideDays = {
|
|
24
|
+
render: () => html `<lyra-date-picker with-outside-days value="2026-07-15"></lyra-date-picker>`,
|
|
25
|
+
};
|
|
@@ -7,7 +7,11 @@ import { LyraElement } from '../../internal/lyra-element.js';
|
|
|
7
7
|
* @customElement lyra-empty
|
|
8
8
|
* @slot - Custom icon or illustration (defaults to none).
|
|
9
9
|
* @slot actions - Buttons/links shown below the description.
|
|
10
|
-
* @csspart base
|
|
10
|
+
* @csspart base - The outer container.
|
|
11
|
+
* @csspart icon - The wrapper around the default-slotted icon/illustration.
|
|
12
|
+
* @csspart heading - The heading paragraph.
|
|
13
|
+
* @csspart description - The description paragraph.
|
|
14
|
+
* @csspart actions - The wrapper around the `actions`-slotted content.
|
|
11
15
|
*/
|
|
12
16
|
export declare class LyraEmpty extends LyraElement {
|
|
13
17
|
static styles: import("lit").CSSResultGroup[];
|
|
@@ -15,6 +19,13 @@ export declare class LyraEmpty extends LyraElement {
|
|
|
15
19
|
heading: string;
|
|
16
20
|
/** Supporting copy, e.g. "Try a different search." */
|
|
17
21
|
description: string;
|
|
22
|
+
/**
|
|
23
|
+
* Compact rendering for use inside a constrained space (e.g. a widget body
|
|
24
|
+
* or table cell) rather than as a full-page state: left-aligned, tighter
|
|
25
|
+
* padding, and a lighter heading weight instead of the centered/spacious
|
|
26
|
+
* default.
|
|
27
|
+
*/
|
|
28
|
+
compact: boolean;
|
|
18
29
|
private hasIcon;
|
|
19
30
|
private hasActions;
|
|
20
31
|
protected willUpdate(): void;
|
|
@@ -16,7 +16,11 @@ import { styles } from './empty.styles.js';
|
|
|
16
16
|
* @customElement lyra-empty
|
|
17
17
|
* @slot - Custom icon or illustration (defaults to none).
|
|
18
18
|
* @slot actions - Buttons/links shown below the description.
|
|
19
|
-
* @csspart base
|
|
19
|
+
* @csspart base - The outer container.
|
|
20
|
+
* @csspart icon - The wrapper around the default-slotted icon/illustration.
|
|
21
|
+
* @csspart heading - The heading paragraph.
|
|
22
|
+
* @csspart description - The description paragraph.
|
|
23
|
+
* @csspart actions - The wrapper around the `actions`-slotted content.
|
|
20
24
|
*/
|
|
21
25
|
export class LyraEmpty extends LyraElement {
|
|
22
26
|
constructor() {
|
|
@@ -25,6 +29,13 @@ export class LyraEmpty extends LyraElement {
|
|
|
25
29
|
this.heading = '';
|
|
26
30
|
/** Supporting copy, e.g. "Try a different search." */
|
|
27
31
|
this.description = '';
|
|
32
|
+
/**
|
|
33
|
+
* Compact rendering for use inside a constrained space (e.g. a widget body
|
|
34
|
+
* or table cell) rather than as a full-page state: left-aligned, tighter
|
|
35
|
+
* padding, and a lighter heading weight instead of the centered/spacious
|
|
36
|
+
* default.
|
|
37
|
+
*/
|
|
38
|
+
this.compact = false;
|
|
28
39
|
// `[part='icon']:empty` never matches because the part always contains a
|
|
29
40
|
// `<slot>` element (CSS `:empty` only ignores text/comment nodes). Track
|
|
30
41
|
// real slot assignment in JS instead and key the CSS off these instead.
|
|
@@ -44,7 +55,10 @@ export class LyraEmpty extends LyraElement {
|
|
|
44
55
|
// `firstUpdated` (after the update completes) would schedule a second,
|
|
45
56
|
// wasted update (Lit's dev-mode "change-in-update" warning).
|
|
46
57
|
if (!this.hasUpdated) {
|
|
47
|
-
|
|
58
|
+
// An explicit `slot=""` still assigns to the default slot per the HTML
|
|
59
|
+
// slot algorithm, so check the attribute's value rather than its mere
|
|
60
|
+
// presence.
|
|
61
|
+
this.hasIcon = Array.from(this.children).some((el) => !el.getAttribute('slot'));
|
|
48
62
|
this.hasActions = Array.from(this.children).some((el) => el.getAttribute('slot') === 'actions');
|
|
49
63
|
}
|
|
50
64
|
}
|
|
@@ -64,7 +78,7 @@ export class LyraEmpty extends LyraElement {
|
|
|
64
78
|
}
|
|
65
79
|
render() {
|
|
66
80
|
return html `
|
|
67
|
-
<div part="base">
|
|
81
|
+
<div part="base" role="status" aria-live="polite">
|
|
68
82
|
<div part="icon" ?hidden=${!this.hasIcon}><slot @slotchange=${this.onIconSlotChange}></slot></div>
|
|
69
83
|
<p part="heading">${this.heading}</p>
|
|
70
84
|
<p part="description">${this.description}</p>
|
|
@@ -81,6 +95,9 @@ __decorate([
|
|
|
81
95
|
__decorate([
|
|
82
96
|
property()
|
|
83
97
|
], LyraEmpty.prototype, "description", void 0);
|
|
98
|
+
__decorate([
|
|
99
|
+
property({ type: Boolean, reflect: true })
|
|
100
|
+
], LyraEmpty.prototype, "compact", void 0);
|
|
84
101
|
__decorate([
|
|
85
102
|
state()
|
|
86
103
|
], LyraEmpty.prototype, "hasIcon", void 0);
|
|
@@ -12,3 +12,23 @@ export const Default = {
|
|
|
12
12
|
</lyra-empty>
|
|
13
13
|
`,
|
|
14
14
|
};
|
|
15
|
+
export const WithIcon = {
|
|
16
|
+
render: () => html `
|
|
17
|
+
<lyra-empty heading="No results" description="Try a different search.">
|
|
18
|
+
<svg slot="" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
|
19
|
+
<circle cx="11" cy="11" r="7" stroke-width="2"></circle>
|
|
20
|
+
<path d="M21 21l-4.35-4.35" stroke-width="2" stroke-linecap="round"></path>
|
|
21
|
+
</svg>
|
|
22
|
+
<span slot="actions"><button>Reset</button></span>
|
|
23
|
+
</lyra-empty>
|
|
24
|
+
`,
|
|
25
|
+
};
|
|
26
|
+
export const Compact = {
|
|
27
|
+
render: () => html `
|
|
28
|
+
<div style="max-width: 16rem; border: 1px solid var(--lyra-color-border);">
|
|
29
|
+
<lyra-empty compact heading="No results" description="Try a different search.">
|
|
30
|
+
<span slot="actions"><button>Reset</button></span>
|
|
31
|
+
</lyra-empty>
|
|
32
|
+
</div>
|
|
33
|
+
`,
|
|
34
|
+
};
|
|
@@ -13,6 +13,11 @@ export const styles = css `
|
|
|
13
13
|
color: var(--lyra-color-text-quiet);
|
|
14
14
|
inline-size: 100%;
|
|
15
15
|
}
|
|
16
|
+
:host([compact]) [part='base'] {
|
|
17
|
+
align-items: flex-start;
|
|
18
|
+
text-align: start;
|
|
19
|
+
padding: var(--lyra-space-xs);
|
|
20
|
+
}
|
|
16
21
|
[part='icon'] {
|
|
17
22
|
font-size: 2rem;
|
|
18
23
|
line-height: 1;
|
|
@@ -26,6 +31,12 @@ export const styles = css `
|
|
|
26
31
|
color: var(--lyra-color-text);
|
|
27
32
|
margin: 0;
|
|
28
33
|
}
|
|
34
|
+
[part='heading']:empty {
|
|
35
|
+
display: none;
|
|
36
|
+
}
|
|
37
|
+
:host([compact]) [part='heading'] {
|
|
38
|
+
font-weight: 400;
|
|
39
|
+
}
|
|
29
40
|
[part='description'] {
|
|
30
41
|
font-size: 0.875rem;
|
|
31
42
|
margin: 0;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// A bare leading '-' is not itself formula syntax (only '=', and context-dependent '+'/'@',
|
|
2
|
+
// reliably trigger spreadsheet formula evaluation) — guarding it forces text rendering on
|
|
3
|
+
// ordinary negative numbers/currency. Per OWASP CSV-injection guidance, '-' is intentionally
|
|
4
|
+
// excluded here.
|
|
5
|
+
const UNSAFE_LEADING = /^[=+@\t\r]/;
|
|
6
|
+
const NEEDS_QUOTING = /[",\r\n]/;
|
|
3
7
|
/** Escapes a CSV field: quotes as needed, guards against formula injection. */
|
|
4
8
|
export function escapeCsvField(value) {
|
|
5
9
|
let s = value == null ? '' : String(value);
|