@lekoala/slot-picker 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +370 -0
- package/custom-elements.json +53 -0
- package/dist/slot-picker.css +597 -0
- package/dist/slot-picker.js +1382 -0
- package/dist/slot-picker.min.css +1 -0
- package/dist/slot-picker.min.js +36 -0
- package/dist/types/date.d.ts +23 -0
- package/dist/types/date.d.ts.map +1 -0
- package/dist/types/define.d.ts +2 -0
- package/dist/types/define.d.ts.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/locales/ar.d.ts +20 -0
- package/dist/types/locales/ar.d.ts.map +1 -0
- package/dist/types/locales/de.d.ts +20 -0
- package/dist/types/locales/de.d.ts.map +1 -0
- package/dist/types/locales/en.d.ts +20 -0
- package/dist/types/locales/en.d.ts.map +1 -0
- package/dist/types/locales/es.d.ts +20 -0
- package/dist/types/locales/es.d.ts.map +1 -0
- package/dist/types/locales/fr.d.ts +20 -0
- package/dist/types/locales/fr.d.ts.map +1 -0
- package/dist/types/locales/hi.d.ts +20 -0
- package/dist/types/locales/hi.d.ts.map +1 -0
- package/dist/types/locales/id.d.ts +20 -0
- package/dist/types/locales/id.d.ts.map +1 -0
- package/dist/types/locales/it.d.ts +20 -0
- package/dist/types/locales/it.d.ts.map +1 -0
- package/dist/types/locales/ja.d.ts +20 -0
- package/dist/types/locales/ja.d.ts.map +1 -0
- package/dist/types/locales/ko.d.ts +20 -0
- package/dist/types/locales/ko.d.ts.map +1 -0
- package/dist/types/locales/nl.d.ts +20 -0
- package/dist/types/locales/nl.d.ts.map +1 -0
- package/dist/types/locales/pl.d.ts +20 -0
- package/dist/types/locales/pl.d.ts.map +1 -0
- package/dist/types/locales/pt-BR.d.ts +20 -0
- package/dist/types/locales/pt-BR.d.ts.map +1 -0
- package/dist/types/locales/pt-PT.d.ts +20 -0
- package/dist/types/locales/pt-PT.d.ts.map +1 -0
- package/dist/types/locales/ru.d.ts +20 -0
- package/dist/types/locales/ru.d.ts.map +1 -0
- package/dist/types/locales/tr.d.ts +20 -0
- package/dist/types/locales/tr.d.ts.map +1 -0
- package/dist/types/locales/zh-CN.d.ts +20 -0
- package/dist/types/locales/zh-CN.d.ts.map +1 -0
- package/dist/types/messages.d.ts +67 -0
- package/dist/types/messages.d.ts.map +1 -0
- package/dist/types/model.d.ts +213 -0
- package/dist/types/model.d.ts.map +1 -0
- package/dist/types/slot-picker.d.ts +182 -0
- package/dist/types/slot-picker.d.ts.map +1 -0
- package/dist/types/source.d.ts +45 -0
- package/dist/types/source.d.ts.map +1 -0
- package/dist/types/views/columns.d.ts +28 -0
- package/dist/types/views/columns.d.ts.map +1 -0
- package/dist/types/views/day.d.ts +27 -0
- package/dist/types/views/day.d.ts.map +1 -0
- package/dist/types/views/shared.d.ts +99 -0
- package/dist/types/views/shared.d.ts.map +1 -0
- package/docs/USE_CASES.md +197 -0
- package/package.json +148 -0
- package/src/date.js +66 -0
- package/src/define.js +5 -0
- package/src/index.js +25 -0
- package/src/locales/ar.js +18 -0
- package/src/locales/de.js +18 -0
- package/src/locales/en.js +18 -0
- package/src/locales/es.js +18 -0
- package/src/locales/fr.js +18 -0
- package/src/locales/hi.js +18 -0
- package/src/locales/id.js +18 -0
- package/src/locales/it.js +18 -0
- package/src/locales/ja.js +18 -0
- package/src/locales/ko.js +18 -0
- package/src/locales/nl.js +18 -0
- package/src/locales/pl.js +18 -0
- package/src/locales/pt-BR.js +18 -0
- package/src/locales/pt-PT.js +18 -0
- package/src/locales/ru.js +18 -0
- package/src/locales/tr.js +18 -0
- package/src/locales/zh-CN.js +18 -0
- package/src/messages.js +44 -0
- package/src/model.js +357 -0
- package/src/slot-picker.css +597 -0
- package/src/slot-picker.js +1094 -0
- package/src/source.js +87 -0
- package/src/views/columns.js +108 -0
- package/src/views/day.js +122 -0
- package/src/views/shared.js +157 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lekoala
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# @lekoala/slot-picker
|
|
2
|
+
|
|
3
|
+
A small, native-first Web Component for choosing appointment slots across a short civil date range.
|
|
4
|
+
|
|
5
|
+
It deliberately sits between `@lekoala/date-picker` and `@lekoala/calendar`:
|
|
6
|
+
|
|
7
|
+
- more structured than a date picker because it displays times;
|
|
8
|
+
- much smaller than a scheduler because it does not render events/resources;
|
|
9
|
+
- application-neutral: booking workflows stay outside.
|
|
10
|
+
|
|
11
|
+
## Basic usage
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<link rel="stylesheet" href="./dist/slot-picker.css">
|
|
15
|
+
<script src="./dist/slot-picker.js"></script>
|
|
16
|
+
|
|
17
|
+
<slot-picker id="slots" start="2026-11-17" day-count="5"></slot-picker>
|
|
18
|
+
<slot-picker id="day" start="2026-11-17" day-count="5" layout="day"></slot-picker>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
const picker = document.querySelector("#slots");
|
|
23
|
+
|
|
24
|
+
picker.days = [
|
|
25
|
+
{
|
|
26
|
+
date: "2026-11-17",
|
|
27
|
+
slots: [
|
|
28
|
+
{ start: "13:35", end: "14:05" },
|
|
29
|
+
{ start: "19:00", end: "19:30" }
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
date: "2026-11-21",
|
|
34
|
+
slots: [],
|
|
35
|
+
notice: {
|
|
36
|
+
label: "Exceptionally unavailable",
|
|
37
|
+
description: "The practitioner is exceptionally unavailable that day."
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
picker.addEventListener("slotactivate", ({ detail }) => {
|
|
43
|
+
console.log(detail.value, detail.slot);
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Remote source
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
picker.source = async ({ start, end, signal }) => {
|
|
51
|
+
const response = await fetch(`/availability?start=${start}&end=${end}`, { signal });
|
|
52
|
+
return response.json();
|
|
53
|
+
};
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
A source may also be an object that additionally resolves the next known
|
|
57
|
+
availability beyond the visible range:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
picker.source = {
|
|
61
|
+
load({ start, end, signal }) {
|
|
62
|
+
// availability for the visible window
|
|
63
|
+
},
|
|
64
|
+
async next({ after, signal }) {
|
|
65
|
+
// a date after `after`, or null
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
await picker.goToNextAvailability(); // -> source.next({ after: picker.range.end })
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Without `source.next`, `goToNextAvailability()` dispatches `nextrequest`
|
|
73
|
+
(`{ after }`) and returns `null`; the consumer then calls `picker.goTo(date)`.
|
|
74
|
+
When the source does answer, it resolves to the destination actually reached
|
|
75
|
+
(clamped to `min`/`max`), never the raw source proposal.
|
|
76
|
+
The source returns normalized day data. Backend query conventions and business
|
|
77
|
+
metadata stay outside core.
|
|
78
|
+
|
|
79
|
+
## Navigation
|
|
80
|
+
|
|
81
|
+
```html
|
|
82
|
+
<slot-picker
|
|
83
|
+
start="2026-11-17"
|
|
84
|
+
day-count="7"
|
|
85
|
+
responsive
|
|
86
|
+
min="2026-11-17"
|
|
87
|
+
max="2027-03-31"
|
|
88
|
+
home-date="2026-11-17"
|
|
89
|
+
></slot-picker>
|
|
90
|
+
```
|
|
91
|
+
|
|
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`).
|
|
93
|
+
- `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.
|
|
94
|
+
- `home-date` is a reference date for `goHome()`, never confused with `min`.
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
picker.goTo("2026-12-08"); // bring a date into view and consult it
|
|
98
|
+
picker.goHome(); // reference window
|
|
99
|
+
picker.goToNextAvailability(); // next known availability (may skip windows)
|
|
100
|
+
picker.previous(); // adjacent window
|
|
101
|
+
picker.next();
|
|
102
|
+
picker.configure({ min: "2026-11-17", max: "2027-03-31", dayCount: 7, responsive: true, homeDate: "2026-11-17" });
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`configure()` is transactional: it validates every option before applying, so an
|
|
106
|
+
invalid option throws and leaves the component unchanged. A valid batch applies
|
|
107
|
+
as one pass: one reload and one final event pair, never one per setting. Event
|
|
108
|
+
order is stable: `rangechange`, then `daychange`, then any reload. Resizing and
|
|
109
|
+
navigating change the range but never `value`.
|
|
110
|
+
|
|
111
|
+
Navigation chrome is a symmetric `previous`/`next` pair framing the projection.
|
|
112
|
+
`home` is a persistent capability but not their mirror: it lives in an
|
|
113
|
+
auxiliary, start-aligned shortcut and only appears while `homeDate` is outside
|
|
114
|
+
the visible range, so it never clutters the initial window. There is no "go to
|
|
115
|
+
end". `goToNextAvailability()` is contextual and appears in the range empty
|
|
116
|
+
state when `next-availability` is set, because that is exactly where a real
|
|
117
|
+
business search makes sense. It is never merged with `next()`. The responsive
|
|
118
|
+
capacity is resolved from the projection's own width, inside the rail.
|
|
119
|
+
|
|
120
|
+
A window with no slot and no notice renders a range empty state
|
|
121
|
+
(`.sp-range-empty`) with a `next()` action, plus the availability action when
|
|
122
|
+
`next-availability` is set. A window with a notice but no slots keeps its
|
|
123
|
+
normal projection, so "exceptionally unavailable" stays distinct from
|
|
124
|
+
"no availability".
|
|
125
|
+
|
|
126
|
+
A day notice is never a fake slot. `notice-display` chooses how it projects:
|
|
127
|
+
|
|
128
|
+
- `action` (default) renders only a real control (the dot button) with an
|
|
129
|
+
accessible name; no inline text. It emits `noticeactivate` so the
|
|
130
|
+
application opens a modal, a popover or anything else.
|
|
131
|
+
- `inline` additionally renders the readable notice content: a compact label in
|
|
132
|
+
`columns` (description kept in the DOM, revealed by themes) and the full
|
|
133
|
+
label + description in `day`. The text itself is never interactive, and it
|
|
134
|
+
never overflows its column.
|
|
135
|
+
- `none` renders nothing; the application owns the notice elsewhere.
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
picker.addEventListener("noticeactivate", ({ detail }) => {
|
|
139
|
+
// detail = { day, notice, anchor }
|
|
140
|
+
openModal({ title: detail.notice.label, body: detail.notice.description });
|
|
141
|
+
// or: showPopover({ anchor: detail.anchor, ...detail.notice });
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`noticeactivate` bubbles, has no default behavior, and is only emitted from an
|
|
146
|
+
explicit control. The component never embeds a tooltip, popover or floating
|
|
147
|
+
engine.
|
|
148
|
+
|
|
149
|
+
## Stable footprint and loading
|
|
150
|
+
|
|
151
|
+
Collapsed `columns` projection keeps a stable footprint derived from
|
|
152
|
+
`max-visible-rows`: a full, sparse or wholly empty range reserves the same
|
|
153
|
+
height, so `previous()`/`next()` never jerk the page. `expanded` content grows
|
|
154
|
+
past that floor. In `layout="day"`, only the empty state gets a baseline.
|
|
155
|
+
|
|
156
|
+
- `--sp-day-header-block-size` is a minimum, never a fixed height.
|
|
157
|
+
- `--sp-collapsed-rows` defaults to `max-visible-rows`.
|
|
158
|
+
- `--sp-collapsed-body-block-size`, `--sp-footer-block-size` and
|
|
159
|
+
`--sp-collapsed-block-size` describe the reserved area.
|
|
160
|
+
- `--sp-panel-min-block-size` is the day baseline.
|
|
161
|
+
|
|
162
|
+
When `home-date` is configured, the `.sp-shortcuts` wrapper stays in the DOM
|
|
163
|
+
even while `homeDate` is already visible, so the shortcut appearing never shifts
|
|
164
|
+
the page. Its reserved height is `--sp-shortcuts-block-size` (default `2.5rem`).
|
|
165
|
+
|
|
166
|
+
While collapsed, the "show more" control floats inside the reserved footer band
|
|
167
|
+
instead of adding a row: a gradient (`--sp-more-fade-size`, `--sp-more-bg`)
|
|
168
|
+
rises above it to suggest the availability that is not rendered. Hidden slots
|
|
169
|
+
stay out of the DOM. Expanding returns the control to normal flow, with no
|
|
170
|
+
overlay and no gradient.
|
|
171
|
+
|
|
172
|
+
Loading is signalled by `aria-busy="true"` on the host plus a CSS fade that only
|
|
173
|
+
starts after `--sp-loading-fade-delay` (default `400ms`), so fast responses
|
|
174
|
+
never flicker. `messages.loading` stays announced through a visually hidden
|
|
175
|
+
`role="status"`, so there is no layout shift. The fade honours
|
|
176
|
+
`prefers-reduced-motion` (delay kept, animation removed).
|
|
177
|
+
|
|
178
|
+
## Locales
|
|
179
|
+
|
|
180
|
+
`Intl` already localizes weekday and month names from the component's `locale`
|
|
181
|
+
(or `lang`). Message packs only provide the UI strings:
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
import fr from "@lekoala/slot-picker/locales/fr";
|
|
185
|
+
|
|
186
|
+
picker.messages = fr;
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Or set a global default once:
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
import { setDefaultMessages } from "@lekoala/slot-picker";
|
|
193
|
+
import fr from "@lekoala/slot-picker/locales/fr";
|
|
194
|
+
|
|
195
|
+
setDefaultMessages(fr);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Resolution order: `DEFAULT_MESSAGES`, then global defaults, then the instance
|
|
199
|
+
`messages`. Global defaults are resolved at render time, so
|
|
200
|
+
`setDefaultMessages()` also reaches instances that already exist on their next
|
|
201
|
+
render. Packs are never auto-imported, which keeps them tree-shakable.
|
|
202
|
+
|
|
203
|
+
Included packs: `ar`, `de`, `en`, `es`, `fr`, `hi`, `id`, `it`, `ja`, `ko`,
|
|
204
|
+
`nl`, `pl`, `pt-BR`, `pt-PT`, `ru`, `tr`, `zh-CN`.
|
|
205
|
+
|
|
206
|
+
These are provided message packs to review, not fully localized copy:
|
|
207
|
+
pluralization is intentionally simple (`oneSlot` / `manySlots` with `{n}`) and
|
|
208
|
+
does not use `Intl.PluralRules`.
|
|
209
|
+
|
|
210
|
+
Locale priority for formatting: `locale` attribute, then `lang`, then
|
|
211
|
+
`document.documentElement.lang`, then `navigator.language`. Only `locale`/`lang`
|
|
212
|
+
set on the component is observed; changing the document language does not
|
|
213
|
+
re-render existing pickers, and no global `MutationObserver` is used.
|
|
214
|
+
|
|
215
|
+
## Data shape
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
{
|
|
219
|
+
date: "2026-11-17",
|
|
220
|
+
slots: [
|
|
221
|
+
{
|
|
222
|
+
start: "13:35",
|
|
223
|
+
end: "14:05",
|
|
224
|
+
disabled: false,
|
|
225
|
+
description: "Optional accessible detail",
|
|
226
|
+
tone: "video",
|
|
227
|
+
meta: {}
|
|
228
|
+
}
|
|
229
|
+
],
|
|
230
|
+
closed: false,
|
|
231
|
+
notice: {
|
|
232
|
+
label: "Unavailable",
|
|
233
|
+
description: "Optional longer explanation",
|
|
234
|
+
meta: {}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Dates are civil `YYYY-MM-DD`, times are `HH:mm`, and the selected value is
|
|
240
|
+
`YYYY-MM-DDTHH:mm`. No timezone conversion is performed. The `value` property
|
|
241
|
+
setter rejects an impossible civil date such as `2026-02-31T10:00`; a malformed
|
|
242
|
+
`value` attribute is ignored and reads as empty, so imperfect markup never
|
|
243
|
+
breaks the element upgrade.
|
|
244
|
+
|
|
245
|
+
### Open, empty and closed days
|
|
246
|
+
|
|
247
|
+
Three cases stay distinct:
|
|
248
|
+
|
|
249
|
+
- slots present → normal availability;
|
|
250
|
+
- `slots: []`, open → `messages.empty` ("No availability");
|
|
251
|
+
- `slots: []`, `closed: true` → `messages.closed` ("Closed").
|
|
252
|
+
|
|
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]`:
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
picker.days = [{ date: "2026-11-22", slots: [], closed: true }];
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
```css
|
|
262
|
+
slot-picker .sp-day[data-closed] {
|
|
263
|
+
--sp-closed-opacity: 0.4;
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`notice` remains reserved for exceptional information and may coexist with
|
|
268
|
+
`closed`. A closed day carrying slots is invalid input and is rejected by
|
|
269
|
+
normalization.
|
|
270
|
+
|
|
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
|
+
To jump between windows by real availability, let the source expose `next()`
|
|
292
|
+
and call `goToNextAvailability()`; the civil shape of a window never changes.
|
|
293
|
+
|
|
294
|
+
## Per-slot presentation
|
|
295
|
+
|
|
296
|
+
`slot.tone` is a neutral presentation token. The component never interprets it:
|
|
297
|
+
it is only surfaced as `data-tone`, and the core ships no palette.
|
|
298
|
+
|
|
299
|
+
```js
|
|
300
|
+
picker.days = [{ date: "2026-11-17", slots: [{ start: "13:35", tone: "video" }] }];
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
```html
|
|
304
|
+
<!-- rendered -->
|
|
305
|
+
<button type="button" role="option" class="sp-slot" data-tone="video" ...>13:35</button>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Map tones to color with the dedicated slot tokens, so a tone never touches the
|
|
309
|
+
focus ring (`--sp-focus`) or the rest of the picker:
|
|
310
|
+
|
|
311
|
+
```css
|
|
312
|
+
slot-picker .sp-slot[data-tone="video"] {
|
|
313
|
+
--sp-slot-bg: #e0f2fe;
|
|
314
|
+
--sp-slot-fg: #075985;
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
The slot surface is `--sp-slot-bg`, `--sp-slot-fg`, `--sp-slot-border`,
|
|
319
|
+
`--sp-slot-hover-bg`, `--sp-slot-selected-bg`, `--sp-slot-selected-fg`.
|
|
320
|
+
|
|
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:
|
|
323
|
+
|
|
324
|
+
```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 {
|
|
331
|
+
content: "⚡";
|
|
332
|
+
font-size: 0.85em;
|
|
333
|
+
line-height: 1;
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
A generated glyph can enter the accessible name ("13:35 ⚡"): keep the meaning
|
|
338
|
+
in `slot.description`, or use `content: ""` plus a background/mask when the
|
|
339
|
+
name must stay purely the time.
|
|
340
|
+
|
|
341
|
+
Accessible detail stays on `slot.description` (`aria-description`). The
|
|
342
|
+
component deliberately ships no tooltip or popover engine (see U4 in
|
|
343
|
+
`docs/USE_CASES.md`): a hover/focus tooltip is application-owned. Because the
|
|
344
|
+
DOM is light, an application can delegate on `.sp-slot` and read
|
|
345
|
+
`data-value`/`data-tone` for advanced enrichments, but the primary workflow
|
|
346
|
+
only needs `slotactivate`.
|
|
347
|
+
|
|
348
|
+
## Events
|
|
349
|
+
|
|
350
|
+
- `rangechange` — the visible civil range changed.
|
|
351
|
+
- `daychange` — the consulted day (`activeDate`) changed; never selects a slot.
|
|
352
|
+
- `slotactivate` — the user explicitly chose a slot (sets `activeDate` first); bubbles.
|
|
353
|
+
- `noticeactivate` — the user activated a day notice control (bubbles, detail `{ day, notice, anchor }`, no default behavior).
|
|
354
|
+
- `nextrequest` — no `source.next` was available; detail is `{ after }`.
|
|
355
|
+
- `loadstart` / `loadend` / `loaderror` — optional remote-source lifecycle.
|
|
356
|
+
|
|
357
|
+
See `docs/USE_CASES.md` for the product contract.
|
|
358
|
+
|
|
359
|
+
## Demo
|
|
360
|
+
|
|
361
|
+
The demo page points at the built artifacts, so no web server is required:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
bun run build
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Then open `demo/index.html` directly, or serve it with `bun run dev`.
|
|
368
|
+
It shows both `columns` and `day` projections, wide and in narrow
|
|
369
|
+
phone-like containers, plus bounded/responsive navigation and the range empty
|
|
370
|
+
state.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0.0",
|
|
3
|
+
"readme": "README.md",
|
|
4
|
+
"modules": [
|
|
5
|
+
{
|
|
6
|
+
"kind": "javascript-module",
|
|
7
|
+
"path": "src/slot-picker.js",
|
|
8
|
+
"declarations": [
|
|
9
|
+
{
|
|
10
|
+
"kind": "class",
|
|
11
|
+
"name": "SlotPickerElement",
|
|
12
|
+
"tagName": "slot-picker",
|
|
13
|
+
"description": "Inline appointment slot chooser.",
|
|
14
|
+
"attributes": [
|
|
15
|
+
{ "name": "start", "type": { "text": "string" } },
|
|
16
|
+
{ "name": "day-count", "type": { "text": "number" } },
|
|
17
|
+
{ "name": "min", "type": { "text": "string" } },
|
|
18
|
+
{ "name": "max", "type": { "text": "string" } },
|
|
19
|
+
{ "name": "value", "type": { "text": "string" } },
|
|
20
|
+
{ "name": "active-date", "type": { "text": "string" } },
|
|
21
|
+
{ "name": "layout", "type": { "text": "string" } },
|
|
22
|
+
{ "name": "max-visible-rows", "type": { "text": "number" } },
|
|
23
|
+
{ "name": "expanded", "type": { "text": "boolean" } },
|
|
24
|
+
{ "name": "responsive", "type": { "text": "boolean" } },
|
|
25
|
+
{ "name": "home-date", "type": { "text": "string" } },
|
|
26
|
+
{ "name": "next-availability", "type": { "text": "boolean" } },
|
|
27
|
+
{
|
|
28
|
+
"name": "notice-display",
|
|
29
|
+
"type": { "text": "\"inline\" | \"action\" | \"none\"" },
|
|
30
|
+
"default": "action"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "closed-days",
|
|
34
|
+
"type": { "text": "\"show\" | \"hide\"" },
|
|
35
|
+
"default": "show"
|
|
36
|
+
},
|
|
37
|
+
{ "name": "locale", "type": { "text": "string" } }
|
|
38
|
+
],
|
|
39
|
+
"events": [
|
|
40
|
+
{ "name": "rangechange" },
|
|
41
|
+
{ "name": "daychange" },
|
|
42
|
+
{ "name": "slotactivate" },
|
|
43
|
+
{ "name": "noticeactivate" },
|
|
44
|
+
{ "name": "nextrequest" },
|
|
45
|
+
{ "name": "loadstart" },
|
|
46
|
+
{ "name": "loadend" },
|
|
47
|
+
{ "name": "loaderror" }
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|