@lekoala/slot-picker 0.1.0 → 0.1.2

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 (78) hide show
  1. package/README.md +85 -39
  2. package/custom-elements.json +0 -5
  3. package/dist/slot-picker.css +22 -0
  4. package/dist/slot-picker.js +160 -134
  5. package/dist/slot-picker.min.css +1 -1
  6. package/dist/slot-picker.min.js +24 -24
  7. package/dist/types/date.d.ts +8 -1
  8. package/dist/types/date.d.ts.map +1 -1
  9. package/dist/types/index.d.ts +2 -2
  10. package/dist/types/index.d.ts.map +1 -1
  11. package/dist/types/locales/ar.d.ts +0 -1
  12. package/dist/types/locales/ar.d.ts.map +1 -1
  13. package/dist/types/locales/de.d.ts +0 -1
  14. package/dist/types/locales/de.d.ts.map +1 -1
  15. package/dist/types/locales/en.d.ts +0 -1
  16. package/dist/types/locales/en.d.ts.map +1 -1
  17. package/dist/types/locales/es.d.ts +0 -1
  18. package/dist/types/locales/es.d.ts.map +1 -1
  19. package/dist/types/locales/fr.d.ts +0 -1
  20. package/dist/types/locales/fr.d.ts.map +1 -1
  21. package/dist/types/locales/hi.d.ts +0 -1
  22. package/dist/types/locales/hi.d.ts.map +1 -1
  23. package/dist/types/locales/id.d.ts +0 -1
  24. package/dist/types/locales/id.d.ts.map +1 -1
  25. package/dist/types/locales/it.d.ts +0 -1
  26. package/dist/types/locales/it.d.ts.map +1 -1
  27. package/dist/types/locales/ja.d.ts +0 -1
  28. package/dist/types/locales/ja.d.ts.map +1 -1
  29. package/dist/types/locales/ko.d.ts +0 -1
  30. package/dist/types/locales/ko.d.ts.map +1 -1
  31. package/dist/types/locales/nl.d.ts +0 -1
  32. package/dist/types/locales/nl.d.ts.map +1 -1
  33. package/dist/types/locales/pl.d.ts +0 -1
  34. package/dist/types/locales/pl.d.ts.map +1 -1
  35. package/dist/types/locales/pt-BR.d.ts +0 -1
  36. package/dist/types/locales/pt-BR.d.ts.map +1 -1
  37. package/dist/types/locales/pt-PT.d.ts +0 -1
  38. package/dist/types/locales/pt-PT.d.ts.map +1 -1
  39. package/dist/types/locales/ru.d.ts +0 -1
  40. package/dist/types/locales/ru.d.ts.map +1 -1
  41. package/dist/types/locales/tr.d.ts +0 -1
  42. package/dist/types/locales/tr.d.ts.map +1 -1
  43. package/dist/types/locales/zh-CN.d.ts +0 -1
  44. package/dist/types/locales/zh-CN.d.ts.map +1 -1
  45. package/dist/types/messages.d.ts +0 -3
  46. package/dist/types/messages.d.ts.map +1 -1
  47. package/dist/types/model.d.ts +58 -23
  48. package/dist/types/model.d.ts.map +1 -1
  49. package/dist/types/slot-picker.d.ts +25 -15
  50. package/dist/types/slot-picker.d.ts.map +1 -1
  51. package/dist/types/views/shared.d.ts +5 -4
  52. package/dist/types/views/shared.d.ts.map +1 -1
  53. package/docs/USE_CASES.md +24 -18
  54. package/package.json +1 -1
  55. package/src/date.js +11 -1
  56. package/src/index.js +13 -2
  57. package/src/locales/ar.js +0 -1
  58. package/src/locales/de.js +0 -1
  59. package/src/locales/en.js +0 -1
  60. package/src/locales/es.js +0 -1
  61. package/src/locales/fr.js +0 -1
  62. package/src/locales/hi.js +0 -1
  63. package/src/locales/id.js +0 -1
  64. package/src/locales/it.js +0 -1
  65. package/src/locales/ja.js +0 -1
  66. package/src/locales/ko.js +0 -1
  67. package/src/locales/nl.js +0 -1
  68. package/src/locales/pl.js +0 -1
  69. package/src/locales/pt-BR.js +0 -1
  70. package/src/locales/pt-PT.js +0 -1
  71. package/src/locales/ru.js +0 -1
  72. package/src/locales/tr.js +0 -1
  73. package/src/locales/zh-CN.js +0 -1
  74. package/src/messages.js +0 -1
  75. package/src/model.js +149 -55
  76. package/src/slot-picker.css +22 -0
  77. package/src/slot-picker.js +123 -100
  78. package/src/views/shared.js +10 -7
@@ -1,21 +1,23 @@
1
- import { addDays, compareDates, isDateValue, rangeEnd, todayValue } from "./date.js";
1
+ import { compareDates, isDateValue, todayValue } from "./date.js";
2
2
  import { resolveMessages } from "./messages.js";
3
3
  import {
4
- boundedDayCount,
4
+ clampDate,
5
5
  clampStart,
6
6
  collapsedDays,
7
7
  ensureVisible,
8
8
  hasDayContent,
9
9
  isSlotValue,
10
- isValidRange,
11
10
  moveFocus,
12
11
  normalizeBreakpoints,
13
12
  normalizeDays,
13
+ normalizeHiddenDays,
14
+ projectDates,
14
15
  RESPONSIVE_BREAKPOINTS,
15
16
  rangeDetail,
16
17
  resolveActiveDate,
17
18
  resolveVisibleDayCount,
18
19
  slotValue,
20
+ stepStart,
19
21
  visibleDays,
20
22
  } from "./model.js";
21
23
  import { SlotSourceController } from "./source.js";
@@ -46,7 +48,7 @@ const RANGE_ATTRIBUTES = new Set(["start", "min", "max", "day-count", "responsiv
46
48
  * responsive: boolean,
47
49
  * homeDate: string|null,
48
50
  * responsiveBreakpoints: ResponsiveBreakpoint[]|null,
49
- * closedDays: "show"|"hide",
51
+ * hiddenDays: number[]|null,
50
52
  * }>} ConfigureOptions
51
53
  */
52
54
 
@@ -59,9 +61,11 @@ const RANGE_ATTRIBUTES = new Set(["start", "min", "max", "day-count", "responsiv
59
61
  * - value: YYYY-MM-DDTHH:mm
60
62
  *
61
63
  * State:
62
- * - dayCount: consumer's maximum intention
64
+ * - dayCount: consumer's maximum intention, counted in columns
65
+ * - hiddenDays: weekdays that are never a column (calendar structure)
63
66
  * - visibleDayCount: capacity actually resolved from bounds + width
64
- * - start/range: visible civil range
67
+ * - start/range: first and last projected day; the civil envelope between
68
+ * them may be wider than `dayCount` when weekdays are hidden
65
69
  * - activeDate: consulted day
66
70
  * - focusedValue: keyboard target, never a selection
67
71
  * - value: selected local slot value
@@ -89,13 +93,14 @@ export class SlotPickerElement extends HTMLElement {
89
93
  "home-date",
90
94
  "next-availability",
91
95
  "notice-display",
92
- "closed-days",
93
96
  "locale",
94
97
  "lang",
95
98
  ];
96
99
 
97
100
  /** @type {SlotDay[]} */
98
101
  #days = [];
102
+ /** @type {number[]} */
103
+ #hiddenDays = [];
99
104
  #sourceController = new SlotSourceController();
100
105
  /** @type {Partial<typeof import("./messages.js").DEFAULT_MESSAGES>|null} */
101
106
  #messages = null;
@@ -176,7 +181,10 @@ export class SlotPickerElement extends HTMLElement {
176
181
  set start(value) {
177
182
  if (!isDateValue(value)) throw new TypeError("start must be YYYY-MM-DD");
178
183
  const count = this.visibleDayCount;
179
- this.setAttribute("start", count > 0 ? clampStart(value, this.min, this.max, count) : value);
184
+ this.setAttribute(
185
+ "start",
186
+ count > 0 ? clampStart(value, this.min, this.max, count, this.#hiddenDays) : value,
187
+ );
180
188
  }
181
189
 
182
190
  get dayCount() {
@@ -191,14 +199,54 @@ export class SlotPickerElement extends HTMLElement {
191
199
  }
192
200
 
193
201
  /**
194
- * Consumer's maximum intention.
195
- * @see visibleDayCount for the resolved value.
202
+ * Weekdays that are never rendered as a column, as `Date#getDay` indexes
203
+ * (`[0, 6]` hides the weekend). Calendar structure, resolved before any
204
+ * data is loaded, so `dayCount` keeps meaning "columns": hidden weekdays
205
+ * widen the civil envelope handed to the source instead of eating a column.
206
+ * Never confused with `closed`, which is the state of a real date.
207
+ * @returns {number[]}
208
+ */
209
+ get hiddenDays() {
210
+ return [...this.#hiddenDays];
211
+ }
212
+
213
+ /** @param {readonly number[]|null} value */
214
+ set hiddenDays(value) {
215
+ this.#hiddenDays = normalizeHiddenDays(value);
216
+ if (!this.isConnected || this.#batching) return;
217
+ this.#reconcile({ pinActive: true });
218
+ }
219
+
220
+ /**
221
+ * Capacity requested before the bounds: the consumer's intention narrowed
222
+ * by the responsive ladder. Step 1 of the pipeline.
223
+ */
224
+ #requestedDayCount() {
225
+ if (!this.responsive || this.#measuredWidth === null) return this.dayCount;
226
+ return resolveVisibleDayCount(
227
+ this.dayCount,
228
+ this.#measuredWidth,
229
+ this.#breakpoints ?? RESPONSIVE_BREAKPOINTS,
230
+ );
231
+ }
232
+
233
+ /**
234
+ * Step 2, and the single source of truth for everything downstream: the
235
+ * civil dates actually projected as columns. `range`, `activeDate`,
236
+ * navigation, rendering and loading all read this, so they can never
237
+ * disagree on what a "day" is.
238
+ * @returns {string[]}
239
+ */
240
+ #projection() {
241
+ return projectDates(this.start, this.#requestedDayCount(), this.#hiddenDays, this.min, this.max);
242
+ }
243
+
244
+ /**
245
+ * Capacity actually resolved from `dayCount`, the width and the bounds.
246
+ * @see dayCount for the consumer's intention.
196
247
  */
197
248
  get visibleDayCount() {
198
- if (!isValidRange(this.min, this.max)) return 0;
199
- const bounded = boundedDayCount(this.dayCount, this.min, this.max);
200
- if (!this.responsive || this.#measuredWidth === null) return bounded;
201
- return resolveVisibleDayCount(bounded, this.#measuredWidth, this.#breakpoints ?? RESPONSIVE_BREAKPOINTS);
249
+ return this.#projection().length;
202
250
  }
203
251
 
204
252
  get responsive() {
@@ -263,11 +311,9 @@ export class SlotPickerElement extends HTMLElement {
263
311
  }
264
312
 
265
313
  get activeDate() {
266
- const count = this.visibleDayCount;
267
- if (!count) return "";
268
- const raw = this.getAttribute("active-date") || "";
269
- const base = isDateValue(raw) ? raw : this.start;
270
- return resolveActiveDate(this.start, count, base);
314
+ const dates = this.#projection();
315
+ if (!dates.length) return "";
316
+ return resolveActiveDate(dates, this.getAttribute("active-date") || "");
271
317
  }
272
318
 
273
319
  set activeDate(value) {
@@ -319,23 +365,6 @@ export class SlotPickerElement extends HTMLElement {
319
365
  this.setAttribute("notice-display", value);
320
366
  }
321
367
 
322
- /**
323
- * Projection policy for `closed` days:
324
- * - `show` (default) renders every civil day;
325
- * - `hide` omits closed days from the projection without changing the civil
326
- * range, `previous()`/`next()` or `source.load()`.
327
- */
328
- get closedDays() {
329
- return this.getAttribute("closed-days") === "hide" ? "hide" : "show";
330
- }
331
-
332
- set closedDays(value) {
333
- if (value !== "show" && value !== "hide") {
334
- throw new TypeError('closed-days must be "show" or "hide"');
335
- }
336
- this.setAttribute("closed-days", value);
337
- }
338
-
339
368
  get expanded() {
340
369
  return this.hasAttribute("expanded");
341
370
  }
@@ -399,19 +428,22 @@ export class SlotPickerElement extends HTMLElement {
399
428
  else this.setAttribute("locale", value);
400
429
  }
401
430
 
402
- /** Resolved visible civil range. Empty and invalid when count is 0. */
431
+ /**
432
+ * Resolved visible range: first projected day, last projected day, and the
433
+ * number of columns. Empty and invalid when the count is 0. The civil
434
+ * envelope `start`..`end` is what the source must load and may be wider
435
+ * than `dayCount`.
436
+ */
403
437
  get range() {
404
- const count = this.visibleDayCount;
405
- if (!count) return { start: "", end: "", dayCount: 0 };
406
- return rangeDetail(this.start, count);
438
+ return rangeDetail(this.#projection());
407
439
  }
408
440
 
409
441
  previous() {
410
- this.#navigate(-this.visibleDayCount);
442
+ this.#navigate(-1);
411
443
  }
412
444
 
413
445
  next() {
414
- this.#navigate(this.visibleDayCount);
446
+ this.#navigate(1);
415
447
  }
416
448
 
417
449
  /**
@@ -421,10 +453,9 @@ export class SlotPickerElement extends HTMLElement {
421
453
  * @returns {string} the resolved active date, or "" when the range is invalid
422
454
  */
423
455
  goTo(date) {
424
- const count = this.visibleDayCount;
425
- if (!count) return "";
426
- const target = isDateValue(date) ? this.#clampToBounds(date) : this.start;
427
- this.#mutate(() => this.#moveTo(target, count));
456
+ if (!this.visibleDayCount) return "";
457
+ const target = isDateValue(date) ? clampDate(date, this.min, this.max) : this.start;
458
+ this.#mutate(() => this.#moveTo(target));
428
459
  this.#commit();
429
460
  return this.activeDate;
430
461
  }
@@ -476,7 +507,7 @@ export class SlotPickerElement extends HTMLElement {
476
507
  if (options.homeDate !== undefined) this.homeDate = options.homeDate ?? "";
477
508
  if (options.responsiveBreakpoints !== undefined)
478
509
  this.responsiveBreakpoints = options.responsiveBreakpoints;
479
- if (options.closedDays !== undefined) this.closedDays = options.closedDays;
510
+ if (options.hiddenDays !== undefined) this.hiddenDays = options.hiddenDays;
480
511
  });
481
512
  // An explicit start wins over the previous active-day anchor; only the
482
513
  // other options preserve the consulted day.
@@ -507,21 +538,23 @@ export class SlotPickerElement extends HTMLElement {
507
538
  if (options.responsiveBreakpoints != null && !Array.isArray(options.responsiveBreakpoints)) {
508
539
  throw new TypeError("responsiveBreakpoints must be an array or null");
509
540
  }
510
- if (options.closedDays !== undefined && options.closedDays !== "show" && options.closedDays !== "hide") {
511
- throw new TypeError('closedDays must be "show" or "hide"');
512
- }
541
+ if (options.hiddenDays !== undefined) normalizeHiddenDays(options.hiddenDays);
513
542
  }
514
543
 
515
544
  async reload() {
516
545
  await this.#load();
517
546
  }
518
547
 
519
- /** @param {number} delta */
520
- #navigate(delta) {
521
- const count = this.visibleDayCount;
522
- if (!count) return;
523
- const next = clampStart(addDays(this.start, delta), this.min, this.max, count);
524
- if (next === this.start) return;
548
+ /**
549
+ * Move one window, counted in projected days: the adjacent window keeps the
550
+ * same number of columns.
551
+ * @param {1|-1} direction
552
+ */
553
+ #navigate(direction) {
554
+ const dates = this.#projection();
555
+ if (!dates.length) return;
556
+ const next = stepStart(dates, direction, this.#hiddenDays, this.min, this.max);
557
+ if (!next || next === this.start) return;
525
558
  this.#mutate(() => this.setAttribute("start", next));
526
559
  this.#commit();
527
560
  }
@@ -529,23 +562,16 @@ export class SlotPickerElement extends HTMLElement {
529
562
  /**
530
563
  * Move the window just enough to make `target` visible.
531
564
  * @param {string} target
532
- * @param {number} count
533
565
  */
534
- #moveTo(target, count) {
535
- const nextStart = ensureVisible(this.start, count, target, this.min, this.max);
566
+ #moveTo(target) {
567
+ const count = this.#requestedDayCount();
568
+ const nextStart = ensureVisible(this.start, count, target, this.min, this.max, this.#hiddenDays);
536
569
  if (nextStart !== this.getAttribute("start")) this.setAttribute("start", nextStart);
537
- const nextActive = resolveActiveDate(nextStart, count, target);
570
+ const dates = projectDates(nextStart, count, this.#hiddenDays, this.min, this.max);
571
+ const nextActive = resolveActiveDate(dates, target);
538
572
  if (this.getAttribute("active-date") !== nextActive) this.setAttribute("active-date", nextActive);
539
573
  }
540
574
 
541
- /** @param {string} date */
542
- #clampToBounds(date) {
543
- let next = date;
544
- if (this.min && compareDates(next, this.min) < 0) next = this.min;
545
- if (this.max && compareDates(next, this.max) > 0) next = this.max;
546
- return next;
547
- }
548
-
549
575
  /**
550
576
  * @param {() => void} fn
551
577
  */
@@ -570,8 +596,8 @@ export class SlotPickerElement extends HTMLElement {
570
596
  const wasBatching = this.#batching;
571
597
  this.#batching = true;
572
598
  try {
573
- const count = this.visibleDayCount;
574
- const invalid = count === 0;
599
+ const count = this.#requestedDayCount();
600
+ const invalid = this.visibleDayCount === 0;
575
601
  this.toggleAttribute("data-invalid-range", invalid);
576
602
  if (!invalid) {
577
603
  const rawActive = this.getAttribute("active-date") || "";
@@ -581,17 +607,18 @@ export class SlotPickerElement extends HTMLElement {
581
607
  let anchor = this.start;
582
608
  if (pinActive) {
583
609
  if (previous?.dayCount && isDateValue(rawActive)) {
584
- anchor = resolveActiveDate(previous.start, previous.dayCount, rawActive);
610
+ anchor = clampDate(rawActive, previous.start, previous.end);
585
611
  } else if (isDateValue(rawActive)) {
586
612
  anchor = rawActive;
587
613
  } else if (previous?.start) {
588
614
  anchor = previous.start;
589
615
  }
590
616
  }
591
- const nextStart = ensureVisible(this.start, count, anchor, this.min, this.max);
617
+ const nextStart = ensureVisible(this.start, count, anchor, this.min, this.max, this.#hiddenDays);
592
618
  if (nextStart !== this.getAttribute("start")) this.setAttribute("start", nextStart);
593
619
  if (pinActive && isDateValue(anchor)) {
594
- const nextActive = resolveActiveDate(nextStart, count, anchor);
620
+ const dates = projectDates(nextStart, count, this.#hiddenDays, this.min, this.max);
621
+ const nextActive = resolveActiveDate(dates, anchor);
595
622
  if (this.getAttribute("active-date") !== nextActive) this.setAttribute("active-date", nextActive);
596
623
  }
597
624
  }
@@ -665,9 +692,9 @@ export class SlotPickerElement extends HTMLElement {
665
692
 
666
693
  /** @param {string} date @param {boolean} emit */
667
694
  #setActiveDate(date, emit = true) {
668
- const count = this.visibleDayCount;
669
- if (!count) return;
670
- const resolved = resolveActiveDate(this.start, count, date);
695
+ const dates = this.#projection();
696
+ if (!dates.length) return;
697
+ const resolved = resolveActiveDate(dates, date);
671
698
  const before = this.activeDate;
672
699
  if (resolved === before && this.getAttribute("active-date") === resolved) return;
673
700
  this.setAttribute("active-date", resolved);
@@ -763,19 +790,13 @@ export class SlotPickerElement extends HTMLElement {
763
790
  }
764
791
 
765
792
  /**
766
- * Days actually handed to a projection. The civil range never changes; only
767
- * `closed-days="hide"` removes closed days, and in `layout="day"` the
768
- * consulted day is kept even when closed so data arrival can never move
769
- * `activeDate` on its own.
793
+ * Days actually handed to a projection: one per projected date, filled in
794
+ * when the source knows nothing about that day. Loaded data never adds or
795
+ * removes a column, so `closed: true` cannot change the grid.
770
796
  * @returns {SlotDay[]}
771
797
  */
772
798
  #projectedDays() {
773
- const count = this.visibleDayCount;
774
- if (!count) return [];
775
- const days = visibleDays(this.#days, this.start, count);
776
- if (this.closedDays !== "hide") return days;
777
- const active = this.activeDate;
778
- return days.filter((day) => !day.closed || (this.layout === "day" && day.date === active));
799
+ return visibleDays(this.#days, this.#projection());
779
800
  }
780
801
 
781
802
  #render() {
@@ -785,18 +806,19 @@ export class SlotPickerElement extends HTMLElement {
785
806
  const pendingSelector = this.#pendingFocus;
786
807
  this.#pendingFocus = "";
787
808
 
788
- const count = this.visibleDayCount;
789
- const invalid = count === 0;
790
- // The civil range stays the source of truth; the projection may hide closed
791
- // days without changing `range`, `previous()/next()` or `source.load()`.
792
- const civilDays = count ? visibleDays(this.#days, this.start, count) : [];
793
- const days = this.#projectedDays();
794
- const allClosed = civilDays.length > 0 && civilDays.every((day) => day.closed);
809
+ const dates = this.#projection();
810
+ const range = rangeDetail(dates);
811
+ const invalid = range.dayCount === 0;
812
+ const days = visibleDays(this.#days, dates);
795
813
  // Resolve at render time so `setDefaultMessages()` reaches live instances.
796
814
  const messages = resolveMessages(this.#messages);
797
815
  const locale = this.locale;
798
- const canPrevious = !invalid && (!this.min || compareDates(this.start, this.min) > 0);
799
- const canNext = !invalid && (!this.max || compareDates(rangeEnd(this.start, count), this.max) < 0);
816
+ // Navigation is offered when stepping actually moves the window: with
817
+ // hidden weekdays a bound can sit on a day that is never projected, so a
818
+ // civil comparison would enable a button that does nothing.
819
+ const canPrevious =
820
+ !invalid && stepStart(dates, -1, this.#hiddenDays, this.min, this.max) !== range.start;
821
+ const canNext = !invalid && stepStart(dates, 1, this.#hiddenDays, this.min, this.max) !== range.start;
800
822
 
801
823
  // A range is "empty" only when the projected days have no meaningful
802
824
  // content. A notice or a closed day without slots keeps the projection.
@@ -806,12 +828,11 @@ export class SlotPickerElement extends HTMLElement {
806
828
  let hasOverflow = false;
807
829
  if (empty) {
808
830
  content = rangeEmpty({
809
- start: this.range.start,
810
- end: this.range.end,
831
+ start: range.start,
832
+ end: range.end,
811
833
  invalid,
812
834
  canNext,
813
835
  nextAvailability: this.hasAttribute("next-availability"),
814
- closed: allClosed,
815
836
  locale,
816
837
  messages,
817
838
  });
@@ -857,9 +878,11 @@ export class SlotPickerElement extends HTMLElement {
857
878
  const homeAvailable = this.hasAttribute("home-date") && !invalid;
858
879
  const homeInRange =
859
880
  homeAvailable &&
860
- compareDates(this.homeDate, this.range.start) >= 0 &&
861
- compareDates(this.homeDate, this.range.end) <= 0;
881
+ compareDates(this.homeDate, range.start) >= 0 &&
882
+ compareDates(this.homeDate, range.end) <= 0;
862
883
 
884
+ // Column capacity is resolved before the data: at equal width and bounds
885
+ // it never changes, whatever the source says about those days.
863
886
  this.style.setProperty("--_sp-day-count", String(Math.max(1, days.length)));
864
887
  this.style.setProperty("--_sp-max-visible-rows", String(this.maxVisibleRows));
865
888
  // `aria-busy` is the single source of loading state; the fade is pure CSS.
@@ -3,7 +3,7 @@ import { slotValue } from "../model.js";
3
3
 
4
4
  /** @typedef {import("../model.js").Slot} Slot */
5
5
  /** @typedef {{oneSlot:string,manySlots:string}} SlotCountMessages */
6
- /** @typedef {{rangeEmptyTitle:string,rangeEmptyDescription:string,rangeEmptyNext:string,nextAvailability:string,closedRange:string}} RangeEmptyMessages */
6
+ /** @typedef {{rangeEmptyTitle:string,rangeEmptyDescription:string,rangeEmptyNext:string,nextAvailability:string}} RangeEmptyMessages */
7
7
 
8
8
  /** @type {Record<string,string>} */
9
9
  const ENTITIES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
@@ -60,7 +60,9 @@ export function slotButton({ date, slot, dayIndex, slotIndex, selected, tabbed }
60
60
  const disabled = slot.disabled ? ' disabled aria-disabled="true"' : "";
61
61
  // Neutral presentation hook: a theme maps tones to color, the core does not.
62
62
  const tone = slot.tone ? ` data-tone="${escapeAttr(slot.tone)}"` : "";
63
- return `<button type="button" role="option" class="sp-slot" data-day-index="${dayIndex}" data-slot-index="${slotIndex}" data-value="${escapeAttr(value)}"${tone} aria-selected="${selected ? "true" : "false"}" tabindex="${tabbed ? 0 : -1}"${disabled}${description}>${escapeHtml(slot.start)}</button>`;
63
+ // The time sits in its own inline-flex wrapper so a theme can append an icon
64
+ // that stays in flow next to it without leaking width into the day column.
65
+ return `<button type="button" role="option" class="sp-slot" data-day-index="${dayIndex}" data-slot-index="${slotIndex}" data-value="${escapeAttr(value)}"${tone} aria-selected="${selected ? "true" : "false"}" tabindex="${tabbed ? 0 : -1}"${disabled}${description}><span class="sp-slot-time">${escapeHtml(slot.start)}</span></button>`;
64
66
  }
65
67
 
66
68
  /**
@@ -119,9 +121,12 @@ export function slotCountText(count, messages) {
119
121
  * the immediately following window, while `next availability` (opt-in via
120
122
  * `next-availability`) is the contextual business search that may skip
121
123
  * several windows through the source. They are never merged.
122
- * @param {{start:string,end:string,invalid:boolean,canNext:boolean,nextAvailability:boolean,closed:boolean,locale:string,messages:RangeEmptyMessages}} options
124
+ *
125
+ * A closed day is day content, so a window holding one never reaches this
126
+ * state: it keeps its columns and says "Closed" in each of them.
127
+ * @param {{start:string,end:string,invalid:boolean,canNext:boolean,nextAvailability:boolean,locale:string,messages:RangeEmptyMessages}} options
123
128
  */
124
- export function rangeEmpty({ start, end, invalid, canNext, nextAvailability, closed, locale, messages }) {
129
+ export function rangeEmpty({ start, end, invalid, canNext, nextAvailability, locale, messages }) {
125
130
  const showDescription = !invalid && Boolean(start);
126
131
  let description = "";
127
132
  if (showDescription) {
@@ -147,10 +152,8 @@ export function rangeEmpty({ start, end, invalid, canNext, nextAvailability, clo
147
152
  );
148
153
  }
149
154
  const action = actions.length ? `<div class="sp-range-empty-actions">${actions.join("")}</div>` : "";
150
- // A wholly closed civil range is not the same statement as "no availability".
151
- const title = closed ? messages.closedRange : messages.rangeEmptyTitle;
152
155
  return `<div class="sp-range-empty">
153
- <strong class="sp-range-empty-title">${escapeHtml(title)}</strong>
156
+ <strong class="sp-range-empty-title">${escapeHtml(messages.rangeEmptyTitle)}</strong>
154
157
  ${description}
155
158
  ${action}
156
159
  </div>`;