@0001-labs/design-system-1 0.3.0-alpha.23 → 0.3.0-alpha.24

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 (50) hide show
  1. package/DS1/0-face/translations/da.json +5 -0
  2. package/DS1/0-face/translations/de.json +5 -0
  3. package/DS1/0-face/translations/en.json +5 -0
  4. package/DS1/0-face/translations/es.json +5 -0
  5. package/DS1/0-face/translations/fi.json +5 -0
  6. package/DS1/0-face/translations/fr.json +5 -0
  7. package/DS1/0-face/translations/is.json +5 -0
  8. package/DS1/0-face/translations/it.json +5 -0
  9. package/DS1/0-face/translations/ja.json +5 -0
  10. package/DS1/0-face/translations/ko.json +5 -0
  11. package/DS1/0-face/translations/nb.json +5 -0
  12. package/DS1/0-face/translations/pt-BR.json +5 -0
  13. package/DS1/0-face/translations/pt.json +5 -0
  14. package/DS1/0-face/translations/sv.json +5 -0
  15. package/DS1/0-face/translations/zh.json +5 -0
  16. package/DS1/1-root/cjk.css +1 -1
  17. package/DS1/1-root/fonts.css +1 -1
  18. package/DS1/1-root/one.css +1 -1
  19. package/DS1/1-root/one.semantic.css +1 -1
  20. package/DS1/3-unit/ds-calendar.ts +230 -14
  21. package/DS1/3-unit/styles/ds-calendar.css +111 -6
  22. package/dist/0-face/translations/da.json +5 -0
  23. package/dist/0-face/translations/de.json +5 -0
  24. package/dist/0-face/translations/en.json +5 -0
  25. package/dist/0-face/translations/es.json +5 -0
  26. package/dist/0-face/translations/fi.json +5 -0
  27. package/dist/0-face/translations/fr.json +5 -0
  28. package/dist/0-face/translations/is.json +5 -0
  29. package/dist/0-face/translations/it.json +5 -0
  30. package/dist/0-face/translations/ja.json +5 -0
  31. package/dist/0-face/translations/ko.json +5 -0
  32. package/dist/0-face/translations/nb.json +5 -0
  33. package/dist/0-face/translations/pt-BR.json +5 -0
  34. package/dist/0-face/translations/pt.json +5 -0
  35. package/dist/0-face/translations/sv.json +5 -0
  36. package/dist/0-face/translations/zh.json +5 -0
  37. package/dist/3-unit/ds-calendar.d.ts +47 -2
  38. package/dist/3-unit/ds-calendar.d.ts.map +1 -1
  39. package/dist/3-unit/ds-calendar.js +225 -15
  40. package/dist/design-system-1.bundle.js +225 -15
  41. package/dist/design-system-1.bundle.js.map +1 -1
  42. package/dist/design-system-1.bundle.min.js +1183 -1048
  43. package/dist/design-system-1.bundle.min.js.map +1 -1
  44. package/dist/{shared-words-DAVSET4Z.js → shared-words-BDyeT1zO.js} +89 -14
  45. package/dist/shared-words-BDyeT1zO.js.map +1 -0
  46. package/dist/{shared-words-CyZYc_De.js → shared-words-LuszeoCp.js} +76 -1
  47. package/dist/shared-words-LuszeoCp.js.map +1 -0
  48. package/package.json +1 -1
  49. package/dist/shared-words-CyZYc_De.js.map +0 -1
  50. package/dist/shared-words-DAVSET4Z.js.map +0 -1
@@ -111,6 +111,11 @@
111
111
  "key.pagedown": "Page Down",
112
112
  "Previous month": "Forrige måned",
113
113
  "Next month": "Næste måned",
114
+ "Previous year": "Forrige år",
115
+ "Next year": "Næste år",
116
+ "Earlier years": "Tidligere år",
117
+ "Later years": "Senere år",
118
+ "Choose a month and year": "Vælg måned og år",
114
119
  "Choose a date": "Vælg en dato",
115
120
  "Date": "Dato",
116
121
  "W{week}": "U{week}",
@@ -111,6 +111,11 @@
111
111
  "key.pagedown": "Bild ab",
112
112
  "Previous month": "Vorheriger Monat",
113
113
  "Next month": "Nächster Monat",
114
+ "Previous year": "Vorheriges Jahr",
115
+ "Next year": "Nächstes Jahr",
116
+ "Earlier years": "Frühere Jahre",
117
+ "Later years": "Spätere Jahre",
118
+ "Choose a month and year": "Monat und Jahr wählen",
114
119
  "Choose a date": "Datum wählen",
115
120
  "Date": "Datum",
116
121
  "W{week}": "KW{week}",
@@ -111,6 +111,11 @@
111
111
  "key.pagedown": "PgDn",
112
112
  "Previous month": "Previous month",
113
113
  "Next month": "Next month",
114
+ "Previous year": "Previous year",
115
+ "Next year": "Next year",
116
+ "Earlier years": "Earlier years",
117
+ "Later years": "Later years",
118
+ "Choose a month and year": "Choose a month and year",
114
119
  "Choose a date": "Choose a date",
115
120
  "Date": "Date",
116
121
  "W{week}": "W{week}",
@@ -111,6 +111,11 @@
111
111
  "key.pagedown": "Av Pág",
112
112
  "Previous month": "Mes anterior",
113
113
  "Next month": "Mes siguiente",
114
+ "Previous year": "Año anterior",
115
+ "Next year": "Año siguiente",
116
+ "Earlier years": "Años anteriores",
117
+ "Later years": "Años siguientes",
118
+ "Choose a month and year": "Elegir mes y año",
114
119
  "Choose a date": "Elegir una fecha",
115
120
  "Date": "Fecha",
116
121
  "W{week}": "S{week}",
@@ -111,6 +111,11 @@
111
111
  "key.pagedown": "Page Down",
112
112
  "Previous month": "Edellinen kuukausi",
113
113
  "Next month": "Seuraava kuukausi",
114
+ "Previous year": "Edellinen vuosi",
115
+ "Next year": "Seuraava vuosi",
116
+ "Earlier years": "Aiemmat vuodet",
117
+ "Later years": "Myöhemmät vuodet",
118
+ "Choose a month and year": "Valitse kuukausi ja vuosi",
114
119
  "Choose a date": "Valitse päivämäärä",
115
120
  "Date": "Päivämäärä",
116
121
  "W{week}": "Vk{week}",
@@ -111,6 +111,11 @@
111
111
  "key.pagedown": "Page suiv.",
112
112
  "Previous month": "Mois précédent",
113
113
  "Next month": "Mois suivant",
114
+ "Previous year": "Année précédente",
115
+ "Next year": "Année suivante",
116
+ "Earlier years": "Années précédentes",
117
+ "Later years": "Années suivantes",
118
+ "Choose a month and year": "Choisir le mois et l’année",
114
119
  "Choose a date": "Choisir une date",
115
120
  "Date": "Date",
116
121
  "W{week}": "S{week}",
@@ -104,6 +104,11 @@
104
104
  "key.pagedown": "Page Down",
105
105
  "Previous month": "Fyrri mánuður",
106
106
  "Next month": "Næsti mánuður",
107
+ "Previous year": "Fyrra ár",
108
+ "Next year": "Næsta ár",
109
+ "Earlier years": "Fyrri ár",
110
+ "Later years": "Síðari ár",
111
+ "Choose a month and year": "Velja mánuð og ár",
107
112
  "Choose a date": "Veldu dagsetningu",
108
113
  "Date": "Dagsetning",
109
114
  "W{week}": "V{week}",
@@ -111,6 +111,11 @@
111
111
  "key.pagedown": "Pag giù",
112
112
  "Previous month": "Mese precedente",
113
113
  "Next month": "Mese successivo",
114
+ "Previous year": "Anno precedente",
115
+ "Next year": "Anno successivo",
116
+ "Earlier years": "Anni precedenti",
117
+ "Later years": "Anni successivi",
118
+ "Choose a month and year": "Scegli mese e anno",
114
119
  "Choose a date": "Scegli una data",
115
120
  "Date": "Data",
116
121
  "W{week}": "S{week}",
@@ -111,6 +111,11 @@
111
111
  "key.pagedown": "Page Down",
112
112
  "Previous month": "前の月",
113
113
  "Next month": "次の月",
114
+ "Previous year": "前の年",
115
+ "Next year": "次の年",
116
+ "Earlier years": "前の12年",
117
+ "Later years": "次の12年",
118
+ "Choose a month and year": "年と月を選択",
114
119
  "Choose a date": "日付を選択",
115
120
  "Date": "日付",
116
121
  "W{week}": "第{week}週",
@@ -111,6 +111,11 @@
111
111
  "key.pagedown": "Page Down",
112
112
  "Previous month": "이전 달",
113
113
  "Next month": "다음 달",
114
+ "Previous year": "이전 연도",
115
+ "Next year": "다음 연도",
116
+ "Earlier years": "이전 연도들",
117
+ "Later years": "다음 연도들",
118
+ "Choose a month and year": "연도와 월 선택",
114
119
  "Choose a date": "날짜 선택",
115
120
  "Date": "날짜",
116
121
  "W{week}": "{week}주",
@@ -111,6 +111,11 @@
111
111
  "key.pagedown": "Page Down",
112
112
  "Previous month": "Forrige måned",
113
113
  "Next month": "Neste måned",
114
+ "Previous year": "Forrige år",
115
+ "Next year": "Neste år",
116
+ "Earlier years": "Tidligere år",
117
+ "Later years": "Senere år",
118
+ "Choose a month and year": "Velg måned og år",
114
119
  "Choose a date": "Velg en dato",
115
120
  "Date": "Dato",
116
121
  "W{week}": "U{week}",
@@ -111,6 +111,11 @@
111
111
  "key.pagedown": "Page Down",
112
112
  "Previous month": "Mês anterior",
113
113
  "Next month": "Próximo mês",
114
+ "Previous year": "Ano anterior",
115
+ "Next year": "Próximo ano",
116
+ "Earlier years": "Anos anteriores",
117
+ "Later years": "Anos seguintes",
118
+ "Choose a month and year": "Escolher mês e ano",
114
119
  "Choose a date": "Escolher uma data",
115
120
  "Date": "Data",
116
121
  "W{week}": "S{week}",
@@ -111,6 +111,11 @@
111
111
  "key.pagedown": "Page Down",
112
112
  "Previous month": "Mês anterior",
113
113
  "Next month": "Mês seguinte",
114
+ "Previous year": "Ano anterior",
115
+ "Next year": "Ano seguinte",
116
+ "Earlier years": "Anos anteriores",
117
+ "Later years": "Anos seguintes",
118
+ "Choose a month and year": "Escolher mês e ano",
114
119
  "Choose a date": "Escolher uma data",
115
120
  "Date": "Data",
116
121
  "W{week}": "S{week}",
@@ -111,6 +111,11 @@
111
111
  "key.pagedown": "Page Down",
112
112
  "Previous month": "Föregående månad",
113
113
  "Next month": "Nästa månad",
114
+ "Previous year": "Föregående år",
115
+ "Next year": "Nästa år",
116
+ "Earlier years": "Tidigare år",
117
+ "Later years": "Senare år",
118
+ "Choose a month and year": "Välj månad och år",
114
119
  "Choose a date": "Välj ett datum",
115
120
  "Date": "Datum",
116
121
  "W{week}": "V{week}",
@@ -111,6 +111,11 @@
111
111
  "key.pagedown": "Page Down",
112
112
  "Previous month": "上个月",
113
113
  "Next month": "下个月",
114
+ "Previous year": "上一年",
115
+ "Next year": "下一年",
116
+ "Earlier years": "更早的年份",
117
+ "Later years": "更晚的年份",
118
+ "Choose a month and year": "选择月份和年份",
114
119
  "Choose a date": "选择日期",
115
120
  "Date": "日期",
116
121
  "W{week}": "第{week}周",
@@ -1,4 +1,4 @@
1
- /* version 0.3.0-alpha.23 */
1
+ /* version 0.3.0-alpha.24 */
2
2
 
3
3
  /* Noto Sans for Japanese and Chinese, for a product that shows them. Opt in:
4
4
  link it beside one.css or fonts.css, or ask Sync 1 for "cjk": true. Without
@@ -1,4 +1,4 @@
1
- /* version 0.3.0-alpha.23 */
1
+ /* version 0.3.0-alpha.24 */
2
2
 
3
3
  /* Design system 1's faces and the typeface tokens that name them, on their
4
4
  own: a page that does not load one.css links this sheet (one.css imports
@@ -1,4 +1,4 @@
1
- /* version 0.3.0-alpha.23 */
1
+ /* version 0.3.0-alpha.24 */
2
2
 
3
3
  @import "./grid.css";
4
4
  @import "./component-tokens.css";
@@ -1,4 +1,4 @@
1
- /* DS1 — version 0.3.0-alpha.23 */
1
+ /* DS1 — version 0.3.0-alpha.24 */
2
2
  /* A classless stylesheet for native HTML. No JavaScript is required. */
3
3
 
4
4
  /* It draws on Design system 1's measures: the 20px square, 14px type on
@@ -24,6 +24,7 @@ import {
24
24
  heatLevels,
25
25
  isDay,
26
26
  isoWeek,
27
+ monthName,
27
28
  monthTitle,
28
29
  monthWeeks,
29
30
  sameMonth,
@@ -114,6 +115,8 @@ const openPickers = new Set<Calendar>();
114
115
  * for a scale that stays put from month to month; the busiest day shown
115
116
  * by default.
116
117
  * @prop {boolean} noHeader - Leave out the title and arrows (`no-header`).
118
+ * With a header, pressing the title travels by months and years: the
119
+ * twelve months of the year, then twelve years, and back down.
117
120
  * @prop {boolean} noToday - Leave out the Today button (`no-today`).
118
121
  * @prop {string} min - The first day a person may choose.
119
122
  * @prop {string} max - The last day a person may choose.
@@ -128,7 +131,7 @@ const openPickers = new Set<Calendar>();
128
131
  * @prop {string} length - How a picker writes a day: `short`, `medium` (the
129
132
  * default) or `long`.
130
133
  * @prop {boolean} time - A picker chooses a time with each day (Include time).
131
- * @prop {boolean} required - A form needs a value; a picker leaves Clear out.
134
+ * @prop {boolean} required - A form needs a value; Clear is left out.
132
135
  * @prop {boolean} disabled - A picker cannot be opened.
133
136
  * @prop {boolean} open - A picker's month is open.
134
137
  * @prop {CalendarEvent[]} events - What happens on the days (property only).
@@ -142,6 +145,9 @@ const openPickers = new Set<Calendar>();
142
145
  * @fires navigate - A person moved to another month; `{ month }`.
143
146
  * @fires ds-calendar-toggle - A picker's month opened or closed; `{ open }`.
144
147
  *
148
+ * A selectable calendar has Clear one square below its month, and `clear()`
149
+ * empties the value from code; both fire `change` with an empty value.
150
+ *
145
151
  * @example
146
152
  * <ds-calendar month="2026-10" week-numbers></ds-calendar>
147
153
  * <ds-calendar compact selectable value="2026-10-18/2026-10-24"></ds-calendar>
@@ -172,6 +178,10 @@ export class Calendar extends CalendarBase {
172
178
  disabled: { type: Boolean, reflect: true },
173
179
  open: { type: Boolean, reflect: true },
174
180
  unfolded: { state: true },
181
+ embedded: { attribute: false },
182
+ level: { state: true },
183
+ browseYear: { state: true },
184
+ pickFocus: { state: true },
175
185
  };
176
186
 
177
187
  static styles = [unsafeCSS(metrics), unsafeCSS(controls), unsafeCSS(styles)];
@@ -197,6 +207,14 @@ export class Calendar extends CalendarBase {
197
207
  declare open: boolean;
198
208
  /** The day whose hidden events are unfolded. */
199
209
  declare unfolded: Day | undefined;
210
+ /** The small month inside a picker, whose Clear is the picker's own. */
211
+ declare embedded: boolean;
212
+ /** What the grid shows: the month's days, the twelve months of a year, or twelve years. */
213
+ declare level: "days" | "months" | "years";
214
+ /** The year the months and years are drawn around. */
215
+ declare browseYear: number;
216
+ /** The month (0–11) or year holding the Tab stop among the months or years. */
217
+ declare pickFocus: number | undefined;
200
218
 
201
219
  protected readonly weeksRun = "rows" as const;
202
220
 
@@ -223,6 +241,9 @@ export class Calendar extends CalendarBase {
223
241
  this.required = false;
224
242
  this.disabled = false;
225
243
  this.open = false;
244
+ this.embedded = false;
245
+ this.level = "days";
246
+ this.browseYear = 0;
226
247
  }
227
248
 
228
249
  disconnectedCallback() {
@@ -310,10 +331,184 @@ export class Calendar extends CalendarBase {
310
331
 
311
332
  /** Today's month, as the Today button does. */
312
333
  showToday() {
334
+ this.level = "days";
313
335
  this.showMonth(this.todayDay, true);
314
336
  this.focusDay = this.todayDay;
315
337
  }
316
338
 
339
+ /** Empties the value, as Clear does, and says so with `change`. */
340
+ clear() {
341
+ if (this.picker) {
342
+ this.commit("");
343
+ this.open = false;
344
+ return;
345
+ }
346
+ const previous = this.value || "";
347
+ this.endPending = false;
348
+ this.hoverDay = undefined;
349
+ if (!previous) return;
350
+ this.value = "";
351
+ this.dispatchEvent(new CustomEvent<CalendarChangeDetail>("change", { bubbles: true, composed: true, detail: { value: "", previous, start: "", end: "" } }));
352
+ }
353
+
354
+ // Travelling by months and years: the title goes up a level, a choice comes back down.
355
+
356
+ /** A person has moved between levels, so a level's grid fades in when it comes. */
357
+ private travelled = false;
358
+
359
+ /** The twelve years drawn around `browseYear`, as their first and last. */
360
+ private get yearSpan(): [number, number] {
361
+ const first = Math.floor(this.browseYear / 12) * 12;
362
+ return [first, first + 11];
363
+ }
364
+
365
+ private yearName(year: number) {
366
+ return dateFormatter({ year: "numeric", timeZone: "UTC" }).format(Date.UTC(year, 6, 1));
367
+ }
368
+
369
+ /** The title: the days go up to the months of their year, the months to twelve years. */
370
+ private levelUp = () => {
371
+ this.travelled = true;
372
+ if (this.level === "days") {
373
+ this.browseYear = Number(this.shownMonth.slice(0, 4));
374
+ this.pickFocus = Number(this.shownMonth.slice(5, 7)) - 1;
375
+ this.level = "months";
376
+ } else if (this.level === "months") {
377
+ this.pickFocus = this.browseYear;
378
+ this.level = "years";
379
+ }
380
+ };
381
+
382
+ /** Back down a level: from twelve years to the months, from the months to the days. */
383
+ private levelDown(focus = false) {
384
+ this.travelled = true;
385
+ if (this.level === "years") {
386
+ this.pickFocus = Number(this.shownMonth.slice(5, 7)) - 1;
387
+ this.level = "months";
388
+ if (focus) void this.focusPick();
389
+ } else if (this.level === "months") {
390
+ this.level = "days";
391
+ if (focus) void this.focusCell(this.tabDay);
392
+ }
393
+ }
394
+
395
+ /** The arrows: a month among the days, a year among the months, twelve years among the years. */
396
+ private step(direction: -1 | 1) {
397
+ if (this.level === "days") return direction < 0 ? this.previousMonth() : this.nextMonth();
398
+ this.browseYear += direction * (this.level === "months" ? 1 : 12);
399
+ if (this.level === "years") this.pickFocus = (this.pickFocus ?? this.browseYear) + direction * 12;
400
+ }
401
+
402
+ /** A month or a year chosen: a year shows its months, a month its days. */
403
+ private choose(value: number) {
404
+ this.travelled = true;
405
+ if (this.level === "years") {
406
+ this.browseYear = value;
407
+ this.pickFocus = Number(this.shownMonth.slice(5, 7)) - 1;
408
+ this.level = "months";
409
+ void this.focusPick();
410
+ return;
411
+ }
412
+ const month = `${String(this.browseYear).padStart(4, "0")}-${String(value + 1).padStart(2, "0")}-01`;
413
+ this.level = "days";
414
+ this.showMonth(month, true);
415
+ const day = [this.focusDay, this.chosen.start, this.todayDay].find(day => day && sameMonth(day, month)) ?? month;
416
+ void this.focusCell(day);
417
+ }
418
+
419
+ private async focusPick() {
420
+ await this.updateComplete;
421
+ this.renderRoot.querySelector<HTMLElement>('.pick[tabindex="0"]')?.focus({ preventScroll: true });
422
+ }
423
+
424
+ private onPickKey = (event: KeyboardEvent) => {
425
+ const cell = (event.target as HTMLElement).closest<HTMLElement>("[data-pick]");
426
+ if (!cell) return;
427
+ const value = Number(cell.dataset.pick);
428
+ const columns = this.pickColumns;
429
+ const rtl = getComputedStyle(this).direction === "rtl";
430
+ const moves: Record<string, number> = { ArrowRight: rtl ? -1 : 1, ArrowLeft: rtl ? 1 : -1, ArrowDown: columns, ArrowUp: -columns };
431
+ if (event.key === "Enter" || event.key === " ") {
432
+ event.preventDefault();
433
+ this.choose(value);
434
+ } else if (event.key === "Escape") {
435
+ event.preventDefault();
436
+ event.stopPropagation();
437
+ this.levelDown(true);
438
+ } else if (event.key in moves) {
439
+ event.preventDefault();
440
+ let next = value + moves[event.key];
441
+ if (this.level === "months") {
442
+ // Past December is the next year's January.
443
+ if (next < 0 || next > 11) { this.browseYear += next < 0 ? -1 : 1; next = (next + 12) % 12; }
444
+ } else {
445
+ const [first, last] = this.yearSpan;
446
+ if (next < first || next > last) this.browseYear = next;
447
+ }
448
+ this.pickFocus = next;
449
+ void this.focusPick();
450
+ }
451
+ };
452
+
453
+ /** Months and years stand in rows of one square, one square apart: three across a narrow month, four across a wider one. */
454
+ private get pickColumns() {
455
+ return 7 * this.squaresPerDay >= 12 ? 4 : 3;
456
+ }
457
+
458
+ private renderPicks(weeks: number) {
459
+ const level = this.level;
460
+ const year = this.browseYear;
461
+ const today = this.todayDay;
462
+ const chosen = this.chosen.start;
463
+ const columns = this.pickColumns;
464
+ const cell = Math.floor((7 * this.squaresPerDay) / columns);
465
+ const values = level === "months" ? Array.from({ length: 12 }, (_, month) => month) : Array.from({ length: 12 }, (_, index) => this.yearSpan[0] + index);
466
+ const focus = this.pickFocus !== undefined && values.includes(this.pickFocus)
467
+ ? this.pickFocus
468
+ : level === "months" ? (chosen && Number(chosen.slice(0, 4)) === year ? Number(chosen.slice(5, 7)) - 1 : 0) : year;
469
+ const rows = Array.from({ length: Math.ceil(values.length / columns) }, (_, row) => values.slice(row * columns, row * columns + columns));
470
+ const long = cell >= 4;
471
+ const name = (value: number) => level === "months" ? monthName(`${String(year).padStart(4, "0")}-${String(value + 1).padStart(2, "0")}-01`, long ? "long" : "short") : this.yearName(value);
472
+ const isToday = (value: number) => level === "months" ? today.slice(0, 7) === `${String(year).padStart(4, "0")}-${String(value + 1).padStart(2, "0")}` : Number(today.slice(0, 4)) === value;
473
+ const isChosen = (value: number) => Boolean(chosen) && (level === "months" ? chosen.slice(0, 7) === `${String(year).padStart(4, "0")}-${String(value + 1).padStart(2, "0")}` : Number(chosen.slice(0, 4)) === value);
474
+ const allowed = (value: number) => {
475
+ const min = this.minDay, max = this.maxDay;
476
+ if (level === "years") return !(min && value < Number(min.slice(0, 4))) && !(max && value > Number(max.slice(0, 4)));
477
+ const month = `${String(year).padStart(4, "0")}-${String(value + 1).padStart(2, "0")}`;
478
+ return !(min && month < min.slice(0, 7)) && !(max && month > max.slice(0, 7));
479
+ };
480
+ return html`<div
481
+ class="picks"
482
+ part="picks"
483
+ role="grid"
484
+ data-level=${level}
485
+ ?data-travel=${this.travelled}
486
+ aria-label=${level === "months" ? this.yearName(year) : `${this.yearName(this.yearSpan[0])} – ${this.yearName(this.yearSpan[1])}`}
487
+ style=${`--ds-calendar-pick-columns: ${columns}; --ds-calendar-pick-cell: ${cell}; --ds-calendar-weeks: ${weeks}`}
488
+ @keydown=${this.onPickKey}
489
+ >${rows.map(row => html`<div role="row" class="pick-row">${row.map(value => html`<button
490
+ type="button"
491
+ role="gridcell"
492
+ class="pick"
493
+ part=${["pick", isChosen(value) ? "selected" : "", isToday(value) ? "today" : ""].filter(Boolean).join(" ")}
494
+ data-pick=${value}
495
+ tabindex=${value === focus ? 0 : -1}
496
+ aria-selected=${String(isChosen(value))}
497
+ aria-current=${isToday(value) ? "date" : nothing}
498
+ ?disabled=${!allowed(value)}
499
+ ?data-selected=${isChosen(value)}
500
+ ?data-today=${isToday(value)}
501
+ @click=${() => this.choose(value)}
502
+ ><span class="pick-mark">${name(value)}</span></button>`)}</div>`)}</div>`;
503
+ }
504
+
505
+ private renderClear() {
506
+ if (!this.selectable || this.embedded || this.required) return nothing;
507
+ return html`<div class="foot" part="foot">
508
+ <button class="ds-button ds-button--secondary clear-value" part="clear" type="button" ?disabled=${!this.value} @click=${() => this.clear()}>${this.words("Clear")}</button>
509
+ </div>`;
510
+ }
511
+
317
512
  /** The day holding the Tab stop: the one moved to, the selected day, today, or the first. */
318
513
  private get tabDay(): Day {
319
514
  const month = this.shownMonth;
@@ -383,17 +578,38 @@ export class Calendar extends CalendarBase {
383
578
  const min = this.minDay, max = this.maxDay;
384
579
  const before = Boolean(min && addMonths(month, -1).slice(0, 7) < min.slice(0, 7));
385
580
  const after = Boolean(max && addMonths(month, 1).slice(0, 7) > max.slice(0, 7));
581
+ const level = this.level;
582
+ const year = this.browseYear;
583
+ const span = this.yearSpan;
584
+ const labels = { days: ["Previous month", "Next month"], months: ["Previous year", "Next year"], years: ["Earlier years", "Later years"] }[level];
585
+ const blocked = level === "days"
586
+ ? [before, after]
587
+ : level === "months"
588
+ ? [Boolean(min && year - 1 < Number(min.slice(0, 4))), Boolean(max && year + 1 > Number(max.slice(0, 4)))]
589
+ : [Boolean(min && span[0] - 1 < Number(min.slice(0, 4))), Boolean(max && span[0] + 12 > Number(max.slice(0, 4)))];
386
590
  const arrow = (which: "previous" | "next") => html`<button
387
591
  class="ds-button ds-button--icon nav"
388
592
  part=${which}
389
593
  type="button"
390
- aria-label=${this.words(which === "previous" ? "Previous month" : "Next month")}
391
- ?disabled=${which === "previous" ? before : after}
392
- @click=${() => (which === "previous" ? this.previousMonth() : this.nextMonth())}
594
+ aria-label=${this.words(labels[which === "previous" ? 0 : 1])}
595
+ ?disabled=${blocked[which === "previous" ? 0 : 1]}
596
+ @click=${() => this.step(which === "previous" ? -1 : 1)}
393
597
  ><ds-icon type=${which === "previous" ? "chevron-left" : "chevron-right"} size="large" aria-hidden="true"></ds-icon></button>`;
394
- const title = monthTitle(month, this.compact ? "short" : "long");
598
+ const title = level === "days"
599
+ ? monthTitle(month, this.compact ? "short" : "long")
600
+ : level === "months"
601
+ ? this.yearName(year)
602
+ : `${this.yearName(span[0])} – ${this.yearName(span[0] + 11)}`;
395
603
  return html`<div class="header" part="header">
396
- <span class="title" id="title" aria-live="polite">${title}</span>
604
+ ${level === "years"
605
+ ? html`<span class="title" id="title" aria-live="polite">${title}</span>`
606
+ : html`<button
607
+ class="title title-button"
608
+ part="title"
609
+ type="button"
610
+ aria-label=${`${title}, ${this.words("Choose a month and year")}`}
611
+ @click=${this.levelUp}
612
+ ><span class="title-text" id="title" aria-live="polite">${title}</span><ds-icon type="chevron-down" size="large" aria-hidden="true"></ds-icon></button>`}
397
613
  <span class="controls">
398
614
  ${this.noToday || this.squaresPerDay < 2 ? nothing : html`<button class="ds-button today-button" part="today" type="button" @click=${() => this.showToday()}>${this.words("Today")}</button>`}
399
615
  ${arrow("previous")}
@@ -471,10 +687,11 @@ export class Calendar extends CalendarBase {
471
687
  const long = weekdayNames(first, "long");
472
688
  return html`<div class="calendar" part="calendar" @focusout=${this.onFocusOut} @pointerover=${this.onPointerOver} @pointerleave=${() => (this.hoverDay = undefined)}>
473
689
  ${this.renderHeader()}
474
- <div
690
+ ${this.level !== "days" && !this.noHeader ? this.renderPicks(weeks.length) : html`<div
475
691
  class="grid"
476
692
  part="grid"
477
693
  role="grid"
694
+ ?data-travel=${this.travelled}
478
695
  aria-label=${this.nameOr(monthTitle(month))}
479
696
  aria-multiselectable=${this.selectable ? "true" : nothing}
480
697
  @keydown=${this.onKeyDown}
@@ -484,7 +701,8 @@ export class Calendar extends CalendarBase {
484
701
  ${short.map((name, index) => html`<span role="columnheader" class="weekday" aria-label=${long[index]}>${name}</span>`)}
485
702
  </div>
486
703
  ${weeks.map(week => html`<div role="row" class="week" part="week">${week.map((day, column) => this.renderDay(day, column))}</div>`)}
487
- </div>
704
+ </div>`}
705
+ ${this.renderClear()}
488
706
  </div>`;
489
707
  }
490
708
 
@@ -524,8 +742,9 @@ export class Calendar extends CalendarBase {
524
742
  const month = this.smallMonth;
525
743
  if (month) {
526
744
  month.month = (this.ends.start?.day || this.today || this.todayDay).slice(0, 7);
527
- // Each opening starts a new pick: the first press is a first day.
745
+ // Each opening starts a new pick on the month's days: the first press is a first day.
528
746
  month.endPending = false;
747
+ month.level = "days";
529
748
  }
530
749
  panel.showPopover?.();
531
750
  this.place();
@@ -689,10 +908,6 @@ export class Calendar extends CalendarBase {
689
908
  if (control && !event.composedPath().includes(control)) control.click();
690
909
  };
691
910
 
692
- private clear = () => {
693
- this.commit("");
694
- this.open = false;
695
- };
696
911
 
697
912
  private moment(side: CalendarEnd) {
698
913
  const [year, month, date] = side.day.split("-").map(Number);
@@ -785,6 +1000,7 @@ export class Calendar extends CalendarBase {
785
1000
  selectable
786
1001
  fixed
787
1002
  outside-days
1003
+ .embedded=${true}
788
1004
  .value=${days}
789
1005
  .min=${this.min}
790
1006
  .max=${this.max}
@@ -801,7 +1017,7 @@ export class Calendar extends CalendarBase {
801
1017
  ${this.required
802
1018
  ? nothing
803
1019
  : html`<div class="separator" aria-hidden="true"></div>
804
- <button class="option clear" part="clear" type="button" ?disabled=${!start} @click=${this.clear}>${this.pickerWords(undefined, "Clear")}</button>`}
1020
+ <button class="option clear" part="clear" type="button" ?disabled=${!start} @click=${() => this.clear()}>${this.pickerWords(undefined, "Clear")}</button>`}
805
1021
  </div>
806
1022
  </div>`;
807
1023
  }