@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.
Files changed (150) hide show
  1. package/README.md +9 -10
  2. package/custom-elements.json +15118 -8342
  3. package/dist/components/chart/chart.d.ts +57 -1
  4. package/dist/components/chart/chart.js +202 -12
  5. package/dist/components/chart/chart.stories.d.ts +32 -0
  6. package/dist/components/chart/chart.stories.js +141 -0
  7. package/dist/components/chart/chart.styles.js +16 -0
  8. package/dist/components/chart/histogram.d.ts +2 -0
  9. package/dist/components/chart/histogram.js +18 -2
  10. package/dist/components/chart/line-chart.stories.d.ts +17 -0
  11. package/dist/components/chart/line-chart.stories.js +75 -0
  12. package/dist/components/chart/lite-chart.d.ts +83 -0
  13. package/dist/components/chart/lite-chart.js +424 -0
  14. package/dist/components/chart/lite-chart.stories.d.ts +14 -0
  15. package/dist/components/chart/lite-chart.stories.js +114 -0
  16. package/dist/components/chart/lite-chart.styles.d.ts +1 -0
  17. package/dist/components/chart/lite-chart.styles.js +83 -0
  18. package/dist/components/combobox/combobox.d.ts +9 -0
  19. package/dist/components/combobox/combobox.js +76 -13
  20. package/dist/components/combobox/combobox.stories.d.ts +18 -0
  21. package/dist/components/combobox/combobox.stories.js +75 -0
  22. package/dist/components/date-picker/calendar-core.js +13 -2
  23. package/dist/components/date-picker/date-input.d.ts +9 -1
  24. package/dist/components/date-picker/date-input.js +55 -7
  25. package/dist/components/date-picker/date-input.stories.d.ts +5 -0
  26. package/dist/components/date-picker/date-input.stories.js +36 -0
  27. package/dist/components/date-picker/date-picker.d.ts +11 -3
  28. package/dist/components/date-picker/date-picker.js +87 -37
  29. package/dist/components/date-picker/date-picker.stories.d.ts +4 -0
  30. package/dist/components/date-picker/date-picker.stories.js +12 -0
  31. package/dist/components/date-picker/date-picker.styles.js +1 -1
  32. package/dist/components/empty/empty.d.ts +12 -1
  33. package/dist/components/empty/empty.js +20 -3
  34. package/dist/components/empty/empty.stories.d.ts +2 -0
  35. package/dist/components/empty/empty.stories.js +20 -0
  36. package/dist/components/empty/empty.styles.js +11 -0
  37. package/dist/components/export-button/csv.js +6 -2
  38. package/dist/components/export-button/export-button.d.ts +15 -1
  39. package/dist/components/export-button/export-button.js +89 -12
  40. package/dist/components/export-button/export-button.stories.d.ts +1 -0
  41. package/dist/components/export-button/export-button.stories.js +5 -0
  42. package/dist/components/export-button/export-button.styles.js +11 -3
  43. package/dist/components/file-input/accept.d.ts +8 -2
  44. package/dist/components/file-input/accept.js +9 -3
  45. package/dist/components/file-input/file-input.d.ts +5 -2
  46. package/dist/components/file-input/file-input.js +23 -8
  47. package/dist/components/file-input/file-input.stories.d.ts +2 -0
  48. package/dist/components/file-input/file-input.stories.js +9 -0
  49. package/dist/components/flag/flag.d.ts +69 -3
  50. package/dist/components/flag/flag.js +107 -30
  51. package/dist/components/flag/flag.stories.d.ts +1 -0
  52. package/dist/components/flag/flag.stories.js +14 -0
  53. package/dist/components/flag/flag.styles.js +1 -1
  54. package/dist/components/flag/language-map.d.ts +11 -0
  55. package/dist/components/flag/language-map.js +12 -1
  56. package/dist/components/gauge/gauge.d.ts +2 -0
  57. package/dist/components/gauge/gauge.js +19 -5
  58. package/dist/components/gauge/gauge.stories.d.ts +2 -0
  59. package/dist/components/gauge/gauge.stories.js +6 -0
  60. package/dist/components/graph/graph-loader.d.ts +28 -0
  61. package/dist/components/graph/graph-loader.js +44 -0
  62. package/dist/components/graph/graph.d.ts +41 -15
  63. package/dist/components/graph/graph.js +146 -71
  64. package/dist/components/graph/graph.stories.d.ts +3 -0
  65. package/dist/components/graph/graph.stories.js +38 -0
  66. package/dist/components/graph/graph.styles.js +6 -1
  67. package/dist/components/heatmap/calendar-grid.d.ts +7 -0
  68. package/dist/components/heatmap/calendar-grid.js +27 -10
  69. package/dist/components/heatmap/heatmap.d.ts +149 -12
  70. package/dist/components/heatmap/heatmap.js +445 -28
  71. package/dist/components/heatmap/heatmap.stories.d.ts +22 -0
  72. package/dist/components/heatmap/heatmap.stories.js +155 -0
  73. package/dist/components/heatmap/heatmap.styles.js +60 -2
  74. package/dist/components/map/map.d.ts +17 -1
  75. package/dist/components/map/map.js +68 -21
  76. package/dist/components/map/map.stories.d.ts +10 -0
  77. package/dist/components/map/map.stories.js +107 -12
  78. package/dist/components/map/map.styles.js +2 -2
  79. package/dist/components/playback/playback.d.ts +6 -2
  80. package/dist/components/playback/playback.js +36 -12
  81. package/dist/components/playback/playback.stories.d.ts +2 -0
  82. package/dist/components/playback/playback.stories.js +6 -0
  83. package/dist/components/playback/playback.styles.js +10 -2
  84. package/dist/components/select/select.d.ts +125 -0
  85. package/dist/components/select/select.js +480 -0
  86. package/dist/components/select/select.stories.d.ts +21 -0
  87. package/dist/components/select/select.stories.js +91 -0
  88. package/dist/components/select/select.styles.d.ts +1 -0
  89. package/dist/components/select/select.styles.js +212 -0
  90. package/dist/components/skeleton/skeleton.js +4 -7
  91. package/dist/components/sparkline/sparkline.d.ts +1 -1
  92. package/dist/components/sparkline/sparkline.js +40 -13
  93. package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
  94. package/dist/components/sparkline/sparkline.stories.js +22 -0
  95. package/dist/components/split/split.d.ts +24 -3
  96. package/dist/components/split/split.js +155 -31
  97. package/dist/components/split/split.stories.d.ts +2 -0
  98. package/dist/components/split/split.stories.js +20 -0
  99. package/dist/components/stat/stat.d.ts +43 -1
  100. package/dist/components/stat/stat.js +97 -4
  101. package/dist/components/stat/stat.stories.d.ts +4 -0
  102. package/dist/components/stat/stat.stories.js +66 -1
  103. package/dist/components/stat/stat.styles.js +64 -0
  104. package/dist/components/table/table.d.ts +64 -2
  105. package/dist/components/table/table.js +213 -17
  106. package/dist/components/table/table.stories.d.ts +6 -0
  107. package/dist/components/table/table.stories.js +45 -0
  108. package/dist/components/table/table.styles.js +51 -4
  109. package/dist/components/time-range/time-range.d.ts +34 -5
  110. package/dist/components/time-range/time-range.js +217 -36
  111. package/dist/components/time-range/time-range.stories.d.ts +2 -0
  112. package/dist/components/time-range/time-range.stories.js +13 -0
  113. package/dist/components/time-range/time-range.styles.js +44 -2
  114. package/dist/components/toast/toast-item.d.ts +14 -2
  115. package/dist/components/toast/toast-item.js +134 -17
  116. package/dist/components/toast/toast-item.styles.js +28 -2
  117. package/dist/components/toast/toast.stories.d.ts +3 -0
  118. package/dist/components/toast/toast.stories.js +41 -1
  119. package/dist/components/toast/toast.styles.js +11 -11
  120. package/dist/components/toast/toaster.d.ts +1 -1
  121. package/dist/components/toast/toaster.js +13 -6
  122. package/dist/components/tree/tree-node.d.ts +13 -7
  123. package/dist/components/tree/tree-node.js +24 -10
  124. package/dist/components/tree/tree.d.ts +64 -15
  125. package/dist/components/tree/tree.js +113 -22
  126. package/dist/components/tree/tree.stories.d.ts +4 -0
  127. package/dist/components/tree/tree.stories.js +31 -1
  128. package/dist/components/tree/update-cascade.d.ts +13 -0
  129. package/dist/components/tree/update-cascade.js +13 -0
  130. package/dist/components/widget/widget.d.ts +25 -1
  131. package/dist/components/widget/widget.js +164 -6
  132. package/dist/components/widget/widget.stories.d.ts +3 -0
  133. package/dist/components/widget/widget.stories.js +43 -0
  134. package/dist/components/widget/widget.styles.js +20 -3
  135. package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
  136. package/dist/components/word-cloud/word-cloud-layout.js +145 -0
  137. package/dist/components/word-cloud/word-cloud.d.ts +65 -0
  138. package/dist/components/word-cloud/word-cloud.js +255 -0
  139. package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
  140. package/dist/components/word-cloud/word-cloud.stories.js +55 -0
  141. package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
  142. package/dist/components/word-cloud/word-cloud.styles.js +55 -0
  143. package/dist/internal/a11y.js +2 -1
  144. package/dist/internal/rtl.d.ts +7 -0
  145. package/dist/internal/rtl.js +9 -0
  146. package/dist/lyra.d.ts +9 -1
  147. package/dist/lyra.js +6 -0
  148. package/llms-full.txt +962 -276
  149. package/llms.txt +12 -9
  150. 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
- const parsed = parseISO(raw) ?? (isNaN(Date.parse(raw)) ? null : new Date(Date.parse(raw)));
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
- this.onPickerChange = (e) => {
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
- if (this.mode === 'single')
122
- this.hide();
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 = new Date(next.getFullYear(), next.getMonth(), 1);
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') && this.value) {
99
- const from = this.selection.from;
100
- if (from)
101
- this.viewDate = new Date(from.getFullYear(), from.getMonth(), 1);
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.value =
126
- this.mode === 'range'
127
- ? from && to
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
- if (this.isDisabled(date))
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
- this.viewDate = new Date(d.getFullYear(), d.getMonth(), 1);
174
- this.focusedDate = d;
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
- return html `<span part="day-placeholder" role="gridcell"></span>`;
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 } = this.selection;
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, this.fdow);
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) => html `<div part="week" role="row">${week.map((d) => this.renderDay(d, month))}</div>`)}
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
+ };
@@ -102,7 +102,7 @@ export const styles = css `
102
102
  }
103
103
  [part~='day']:disabled {
104
104
  color: var(--lyra-color-text-quiet);
105
- opacity: 0.35;
105
+ opacity: var(--lyra-opacity-disabled);
106
106
  cursor: not-allowed;
107
107
  }
108
108
  [part~='day']:focus-visible {
@@ -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, icon, heading, description, actions
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, icon, heading, description, actions
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
- this.hasIcon = Array.from(this.children).some((el) => !el.hasAttribute('slot'));
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);
@@ -3,3 +3,5 @@ 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 WithIcon: Story;
7
+ export declare const Compact: Story;
@@ -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
- const UNSAFE_LEADING = /^[=+\-@\t\r]/;
2
- const NEEDS_QUOTING = /[",\n]/;
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);