@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
package/README.md CHANGED
@@ -53,6 +53,11 @@ picker.source = async ({ start, end, signal }) => {
53
53
  };
54
54
  ```
55
55
 
56
+ `start` and `end` are the inclusive civil envelope of the visible window. With
57
+ `hiddenDays` set it can be wider than `dayCount`, so load the whole envelope and
58
+ let the component pick the columns; days outside it are ignored. No adaptive
59
+ fetching is ever required: the envelope is known before the request.
60
+
56
61
  A source may also be an object that additionally resolves the next known
57
62
  availability beyond the visible range:
58
63
 
@@ -89,10 +94,16 @@ metadata stay outside core.
89
94
  ></slot-picker>
90
95
  ```
91
96
 
92
- - `day-count` is the maximum intention; `visibleDayCount` is the capacity actually resolved from `min`/`max` and the component's own width (opt-in via `responsive`, overridable with `responsiveBreakpoints`, observed with `ResizeObserver`).
97
+ - `day-count` is the maximum intention, counted in **columns**; `visibleDayCount` is the capacity actually resolved from `min`/`max` and the component's own width (opt-in via `responsive`, overridable with `responsiveBreakpoints`, observed with `ResizeObserver`).
93
98
  - `min`/`max` are hard bounds. `min > max` is an invalid configuration: `range` becomes `{ start: "", end: "", dayCount: 0 }`, `data-invalid-range` is set, and the source is never called.
99
+ - `hiddenDays` removes weekdays from the calendar structure; it never costs a column.
94
100
  - `home-date` is a reference date for `goHome()`, never confused with `min`.
95
101
 
102
+ At equal width and bounds the picker keeps its column capacity: only `min`/`max`
103
+ can really reduce it, and only when the interval itself holds too few
104
+ projectable days. `previous()`/`next()` move by projected days, so the count of
105
+ columns never changes as you navigate.
106
+
96
107
  ```js
97
108
  picker.goTo("2026-12-08"); // bring a date into view and consult it
98
109
  picker.goHome(); // reference window
@@ -242,17 +253,60 @@ setter rejects an impossible civil date such as `2026-02-31T10:00`; a malformed
242
253
  `value` attribute is ignored and reads as empty, so imperfect markup never
243
254
  breaks the element upgrade.
244
255
 
245
- ### Open, empty and closed days
256
+ ### Hidden days, closed days, empty days
257
+
258
+ Three separate ideas, deliberately not merged:
259
+
260
+ | | what it is | column? |
261
+ | --- | --- | --- |
262
+ | `hiddenDays` | calendar structure, known before loading | never rendered |
263
+ | `closed: true` | business state of a real date | rendered, says `Closed` |
264
+ | `slots: []` | open date with nothing bookable | rendered, says `No availability` |
246
265
 
247
- Three cases stay distinct:
266
+ #### hiddenDays
248
267
 
249
- - slots present → normal availability;
250
- - `slots: []`, open `messages.empty` ("No availability");
251
- - `slots: []`, `closed: true` → `messages.closed` ("Closed").
268
+ ```js
269
+ picker.hiddenDays = [0, 6]; // Sunday and Saturday are never a column
270
+ ```
252
271
 
253
- `closed` is a normal day state set by the application (weekend, weekly
254
- closure); the component computes no opening hours. Closed days stay in the
255
- civil window and are stylable through `[data-closed]`:
272
+ Weekday indexes match `Date#getDay` (0 = Sunday). Because the rule is known
273
+ before any request, `dayCount` keeps meaning "columns": a hidden weekday widens
274
+ the civil envelope instead of eating a column.
275
+
276
+ ```js
277
+ picker.configure({ start: "2026-11-19", dayCount: 5, hiddenDays: [0, 6] });
278
+ picker.range; // { start: "2026-11-19", end: "2026-11-25", dayCount: 5 }
279
+ ```
280
+
281
+ Five columns (Thu, Fri, Mon, Tue, Wed) over a seven-day envelope; `source.load()`
282
+ receives `2026-11-19` to `2026-11-25`. `previous()`/`next()` step by five
283
+ projected days, so the grid never changes shape. A `start` landing on a hidden
284
+ weekday resolves forward to the first projected day, and `activeDate` resolves
285
+ to the next projected column. It is a property, like `days` and `source`, with no
286
+ matching attribute; `configure({ hiddenDays })` applies it inside a transaction.
287
+ Anything but integers `0`–`6` throws, and hiding all seven weekdays is rejected.
288
+
289
+ This is what makes a sparse weekly schedule usable. A practitioner working only
290
+ Tuesdays and Thursdays is calendar structure, not availability:
291
+
292
+ ```js
293
+ picker.configure({ dayCount: 5, hiddenDays: [0, 1, 3, 5, 6] });
294
+ picker.range.dayCount; // 5 columns: Tue, Thu, Tue, Thu, Tue
295
+ picker.range; // spanning about three weeks of civil time
296
+ ```
297
+
298
+ Five columns are five real opening days, instead of a grid mostly made of
299
+ closed ones, and `next()` moves to the five following opening days. Without
300
+ `hiddenDays` the same consumer would have to page through weeks to find two
301
+ bookable columns.
302
+
303
+ Combined with `responsive`, this is what makes a narrow screen usable.
304
+ `day-count` is a ceiling, and the width resolves it: a phone may only fit two
305
+ columns, but they are two real opening days rather than two arbitrary civil
306
+ days. Width, bounds and `hiddenDays` are all resolved before the request, so the
307
+ capacity is known before any data arrives and a response can never change it.
308
+
309
+ #### closed
256
310
 
257
311
  ```js
258
312
  picker.days = [{ date: "2026-11-22", slots: [], closed: true }];
@@ -264,32 +318,18 @@ slot-picker .sp-day[data-closed] {
264
318
  }
265
319
  ```
266
320
 
321
+ `closed` is set by the application (a holiday, an exceptional closure); the
322
+ component computes no opening hours and never derives a recurrence from the
323
+ data. It is a rendered state, exactly like `notice`: loaded data can never add
324
+ or remove a column, so a closed day keeps its place and explains why there is
325
+ nothing that day. A window made only of closed days still renders its columns.
326
+
267
327
  `notice` remains reserved for exceptional information and may coexist with
268
328
  `closed`. A closed day carrying slots is invalid input and is rejected by
269
329
  normalization.
270
330
 
271
- `closed-days="hide"` drops closed days from the projection without changing the
272
- civil range:
273
-
274
- ```html
275
- <slot-picker closed-days="hide" start="2026-11-21" day-count="5"></slot-picker>
276
- ```
277
-
278
- ```js
279
- picker.range; // still { start: "2026-11-21", end: "2026-11-25", dayCount: 5 }
280
- ```
281
-
282
- Hiding a closed day does not extend the civil range to compensate for the
283
- hidden column: `day-count="5"` with two closed days renders three columns.
284
- `range`, `visibleDayCount`, `previous()`/`next()`, `goTo()` and `source.load()`
285
- stay civil, and toggling `closed-days` never reloads the source. In
286
- `layout="day"`, a closed day that is the consulted `activeDate` stays visible
287
- until another day is activated, so data arrival never moves `activeDate`. A
288
- window that is closed in its entirety renders the range empty state with
289
- `messages.closedRange`.
290
-
291
331
  To jump between windows by real availability, let the source expose `next()`
292
- and call `goToNextAvailability()`; the civil shape of a window never changes.
332
+ and call `goToNextAvailability()`; the shape of a window never changes.
293
333
 
294
334
  ## Per-slot presentation
295
335
 
@@ -302,7 +342,7 @@ picker.days = [{ date: "2026-11-17", slots: [{ start: "13:35", tone: "video" }]
302
342
 
303
343
  ```html
304
344
  <!-- rendered -->
305
- <button type="button" role="option" class="sp-slot" data-tone="video" ...>13:35</button>
345
+ <button type="button" role="option" class="sp-slot" data-tone="video" ...><span class="sp-slot-time">13:35</span></button>
306
346
  ```
307
347
 
308
348
  Map tones to color with the dedicated slot tokens, so a tone never touches the
@@ -318,17 +358,16 @@ slot-picker .sp-slot[data-tone="video"] {
318
358
  The slot surface is `--sp-slot-bg`, `--sp-slot-fg`, `--sp-slot-border`,
319
359
  `--sp-slot-hover-bg`, `--sp-slot-selected-bg`, `--sp-slot-selected-fg`.
320
360
 
321
- An icon next to the time is theme-owned too. `::after` needs
322
- `grid-auto-flow: column`, because `.sp-slot` centers its content with a grid:
361
+ An icon next to the time is theme-owned too. The time lives in a
362
+ `.sp-slot-time` inline-flex wrapper, so append the glyph to that wrapper with a
363
+ `::after` and a logical margin: it stays in flow, thus really adjacent to the
364
+ time, and readjusts in RTL. The slot is width-clamped (`min-inline-size: 0;
365
+ max-inline-size: 100%`), so the icon never widens the day column.
323
366
 
324
367
  ```css
325
- slot-picker .sp-slot[data-tone="instant"] {
326
- grid-auto-flow: column;
327
- justify-content: center;
328
- gap: 0.35rem;
329
- }
330
- slot-picker .sp-slot[data-tone="instant"]::after {
368
+ slot-picker .sp-slot[data-tone="instant"] .sp-slot-time::after {
331
369
  content: "⚡";
370
+ margin-inline-start: 0.35rem;
332
371
  font-size: 0.85em;
333
372
  line-height: 1;
334
373
  }
@@ -368,3 +407,10 @@ Then open `demo/index.html` directly, or serve it with `bun run dev`.
368
407
  It shows both `columns` and `day` projections, wide and in narrow
369
408
  phone-like containers, plus bounded/responsive navigation and the range empty
370
409
  state.
410
+
411
+ The same page is published from `master` at
412
+ <https://lekoala.github.io/slot-picker/> (GitHub Pages serves the repository
413
+ root and redirects to `demo/`). Pages publishes the committed `dist/` bundles,
414
+ so run `bun run build:bundle` and commit `dist/slot-picker.js` /
415
+ `dist/slot-picker.css` whenever `src/` changes; CI rejects a stale bundle.
416
+
@@ -29,11 +29,6 @@
29
29
  "type": { "text": "\"inline\" | \"action\" | \"none\"" },
30
30
  "default": "action"
31
31
  },
32
- {
33
- "name": "closed-days",
34
- "type": { "text": "\"show\" | \"hide\"" },
35
- "default": "show"
36
- },
37
32
  { "name": "locale", "type": { "text": "string" } }
38
33
  ],
39
34
  "events": [
@@ -56,6 +56,11 @@
56
56
  font: inherit;
57
57
  }
58
58
 
59
+ /* The component renders in light DOM, so a page stylesheet can reach these
60
+ declarations. `!important` is what keeps assistive-technology-only text (the
61
+ loading status, the day strip's spoken labels) out of sight: losing it is an
62
+ accessibility regression, not a cosmetic one. */
63
+ /* biome-ignore-start lint/complexity/noImportantStyles: visually-hidden must win over page styles */
59
64
  .sp-visually-hidden {
60
65
  position: absolute !important;
61
66
  inline-size: 1px !important;
@@ -67,6 +72,7 @@
67
72
  white-space: nowrap !important;
68
73
  border: 0 !important;
69
74
  }
75
+ /* biome-ignore-end lint/complexity/noImportantStyles: visually-hidden must win over page styles */
70
76
 
71
77
  .sp-shell {
72
78
  display: block;
@@ -214,6 +220,9 @@ slot-picker[home-date] .sp-shortcuts {
214
220
 
215
221
  .sp-slots {
216
222
  display: grid;
223
+ /* Bound the single column to the day column: an `auto` track follows the
224
+ widest slot's min-content and would push every slot past the day card. */
225
+ grid-template-columns: minmax(0, 1fr);
217
226
  gap: var(--sp-gap);
218
227
  }
219
228
 
@@ -225,6 +234,19 @@ slot-picker[home-date] .sp-shortcuts {
225
234
  box-sizing: border-box;
226
235
  border-radius: var(--sp-radius);
227
236
  font: inherit;
237
+ /* A grid item defaults to `min-width: auto`, i.e. its min-content; that would
238
+ let a wide slot (a theme icon next to the time) push the bounded day column.
239
+ Clamp the button to its track instead; the content stays centered. */
240
+ min-inline-size: 0;
241
+ max-inline-size: 100%;
242
+ }
243
+
244
+ /* Time wrapper: inline-flex so a theme can append an in-flow icon (`::after`)
245
+ that stays adjacent to the time and vertically centered. */
246
+ .sp-slot-time {
247
+ display: inline-flex;
248
+ align-items: center;
249
+ justify-content: center;
228
250
  }
229
251
 
230
252
  .sp-slot {