@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.
- package/README.md +85 -39
- package/custom-elements.json +0 -5
- package/dist/slot-picker.css +22 -0
- package/dist/slot-picker.js +160 -134
- package/dist/slot-picker.min.css +1 -1
- package/dist/slot-picker.min.js +24 -24
- package/dist/types/date.d.ts +8 -1
- package/dist/types/date.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/locales/ar.d.ts +0 -1
- package/dist/types/locales/ar.d.ts.map +1 -1
- package/dist/types/locales/de.d.ts +0 -1
- package/dist/types/locales/de.d.ts.map +1 -1
- package/dist/types/locales/en.d.ts +0 -1
- package/dist/types/locales/en.d.ts.map +1 -1
- package/dist/types/locales/es.d.ts +0 -1
- package/dist/types/locales/es.d.ts.map +1 -1
- package/dist/types/locales/fr.d.ts +0 -1
- package/dist/types/locales/fr.d.ts.map +1 -1
- package/dist/types/locales/hi.d.ts +0 -1
- package/dist/types/locales/hi.d.ts.map +1 -1
- package/dist/types/locales/id.d.ts +0 -1
- package/dist/types/locales/id.d.ts.map +1 -1
- package/dist/types/locales/it.d.ts +0 -1
- package/dist/types/locales/it.d.ts.map +1 -1
- package/dist/types/locales/ja.d.ts +0 -1
- package/dist/types/locales/ja.d.ts.map +1 -1
- package/dist/types/locales/ko.d.ts +0 -1
- package/dist/types/locales/ko.d.ts.map +1 -1
- package/dist/types/locales/nl.d.ts +0 -1
- package/dist/types/locales/nl.d.ts.map +1 -1
- package/dist/types/locales/pl.d.ts +0 -1
- package/dist/types/locales/pl.d.ts.map +1 -1
- package/dist/types/locales/pt-BR.d.ts +0 -1
- package/dist/types/locales/pt-BR.d.ts.map +1 -1
- package/dist/types/locales/pt-PT.d.ts +0 -1
- package/dist/types/locales/pt-PT.d.ts.map +1 -1
- package/dist/types/locales/ru.d.ts +0 -1
- package/dist/types/locales/ru.d.ts.map +1 -1
- package/dist/types/locales/tr.d.ts +0 -1
- package/dist/types/locales/tr.d.ts.map +1 -1
- package/dist/types/locales/zh-CN.d.ts +0 -1
- package/dist/types/locales/zh-CN.d.ts.map +1 -1
- package/dist/types/messages.d.ts +0 -3
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/model.d.ts +58 -23
- package/dist/types/model.d.ts.map +1 -1
- package/dist/types/slot-picker.d.ts +25 -15
- package/dist/types/slot-picker.d.ts.map +1 -1
- package/dist/types/views/shared.d.ts +5 -4
- package/dist/types/views/shared.d.ts.map +1 -1
- package/docs/USE_CASES.md +24 -18
- package/package.json +1 -1
- package/src/date.js +11 -1
- package/src/index.js +13 -2
- package/src/locales/ar.js +0 -1
- package/src/locales/de.js +0 -1
- package/src/locales/en.js +0 -1
- package/src/locales/es.js +0 -1
- package/src/locales/fr.js +0 -1
- package/src/locales/hi.js +0 -1
- package/src/locales/id.js +0 -1
- package/src/locales/it.js +0 -1
- package/src/locales/ja.js +0 -1
- package/src/locales/ko.js +0 -1
- package/src/locales/nl.js +0 -1
- package/src/locales/pl.js +0 -1
- package/src/locales/pt-BR.js +0 -1
- package/src/locales/pt-PT.js +0 -1
- package/src/locales/ru.js +0 -1
- package/src/locales/tr.js +0 -1
- package/src/locales/zh-CN.js +0 -1
- package/src/messages.js +0 -1
- package/src/model.js +149 -55
- package/src/slot-picker.css +22 -0
- package/src/slot-picker.js +123 -100
- package/src/views/shared.js +10 -7
package/src/model.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { addDays, compareDates,
|
|
1
|
+
import { addDays, compareDates, isDateValue, weekdayIndex } from "./date.js";
|
|
2
2
|
|
|
3
3
|
const TIME_RE = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
|
|
4
4
|
|
|
@@ -24,8 +24,10 @@ export const RESPONSIVE_BREAKPOINTS = Object.freeze([
|
|
|
24
24
|
*/
|
|
25
25
|
/** @typedef {{label:string,description?:string,meta?:unknown}} DayNotice */
|
|
26
26
|
/**
|
|
27
|
-
* `closed` is
|
|
28
|
-
*
|
|
27
|
+
* `closed` is the business state of one date: it exists in the projection,
|
|
28
|
+
* keeps its column and simply is not open. It is never calendar structure —
|
|
29
|
+
* a weekday that should not be a column at all belongs to `hiddenDays` — and
|
|
30
|
+
* stays distinct from `notice` and from an open day with no availability.
|
|
29
31
|
* @typedef {{date:string,slots:Slot[],closed?:boolean,notice?:DayNotice}} SlotDay
|
|
30
32
|
*/
|
|
31
33
|
|
|
@@ -108,18 +110,15 @@ export function normalizeDays(input) {
|
|
|
108
110
|
|
|
109
111
|
/**
|
|
110
112
|
* Fill missing days so an empty day remains visible.
|
|
113
|
+
* The projected dates decide which columns exist; loaded data never does.
|
|
111
114
|
* @param {SlotDay[]} days
|
|
112
|
-
* @param {string}
|
|
113
|
-
* @param {number} dayCount
|
|
115
|
+
* @param {string[]} dates
|
|
114
116
|
*/
|
|
115
|
-
export function visibleDays(days,
|
|
117
|
+
export function visibleDays(days, dates) {
|
|
116
118
|
const normalized = normalizeDays(days);
|
|
117
119
|
const byDate = new Map(normalized.map((day) => [day.date, day]));
|
|
118
120
|
|
|
119
|
-
return
|
|
120
|
-
const date = addDays(start, index);
|
|
121
|
-
return byDate.get(date) ?? { date, slots: [] };
|
|
122
|
-
});
|
|
121
|
+
return dates.map((date) => byDate.get(date) ?? { date, slots: [] });
|
|
123
122
|
}
|
|
124
123
|
|
|
125
124
|
/**
|
|
@@ -151,18 +150,108 @@ export function isValidRange(min, max) {
|
|
|
151
150
|
}
|
|
152
151
|
|
|
153
152
|
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* Invalid bounds (`min > max`) resolve to 0, never a magic window.
|
|
157
|
-
* @param {number} dayCount
|
|
153
|
+
* Keep a civil date inside an inclusive interval. Empty bounds are open.
|
|
154
|
+
* @param {string} date
|
|
158
155
|
* @param {string} min
|
|
159
156
|
* @param {string} max
|
|
160
157
|
*/
|
|
161
|
-
export function
|
|
162
|
-
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
158
|
+
export function clampDate(date, min, max) {
|
|
159
|
+
if (min && compareDates(date, min) < 0) return min;
|
|
160
|
+
if (max && compareDates(date, max) > 0) return max;
|
|
161
|
+
return date;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Weekdays that are never projected as a column, as `Date#getDay` indexes
|
|
166
|
+
* (0 = Sunday). This is calendar structure, known before any data is loaded:
|
|
167
|
+
* it is not an availability rule and never inspects a `SlotDay`.
|
|
168
|
+
* @param {readonly number[]|null|undefined} input
|
|
169
|
+
* @returns {number[]}
|
|
170
|
+
*/
|
|
171
|
+
export function normalizeHiddenDays(input) {
|
|
172
|
+
if (input === null || input === undefined) return [];
|
|
173
|
+
if (!Array.isArray(input)) throw new TypeError("hiddenDays must be an array of weekday indexes");
|
|
174
|
+
const indexes = input.map((value) => {
|
|
175
|
+
const index = Number(value);
|
|
176
|
+
if (!Number.isInteger(index) || index < 0 || index > 6) {
|
|
177
|
+
throw new TypeError("hiddenDays entries must be integers from 0 (Sunday) to 6 (Saturday)");
|
|
178
|
+
}
|
|
179
|
+
return index;
|
|
180
|
+
});
|
|
181
|
+
const hidden = [...new Set(indexes)].sort((a, b) => a - b);
|
|
182
|
+
// Hiding every weekday would leave no day to project at all.
|
|
183
|
+
if (hidden.length === 7) throw new TypeError("hiddenDays cannot hide every weekday");
|
|
184
|
+
return hidden;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Collect up to `count` projected days walking one civil day at a time.
|
|
189
|
+
* `step` is 1 or -1; `bound` is the civil date the walk must not cross.
|
|
190
|
+
* @param {string} from
|
|
191
|
+
* @param {number} count
|
|
192
|
+
* @param {number[]} hidden
|
|
193
|
+
* @param {1|-1} step
|
|
194
|
+
* @param {string} bound
|
|
195
|
+
*/
|
|
196
|
+
function walk(from, count, hidden, step, bound) {
|
|
197
|
+
/** @type {string[]} */
|
|
198
|
+
const dates = [];
|
|
199
|
+
let date = from;
|
|
200
|
+
// `hidden` can never hold all seven weekdays, so the walk always advances.
|
|
201
|
+
while (dates.length < count) {
|
|
202
|
+
if (bound && (step > 0 ? compareDates(date, bound) > 0 : compareDates(date, bound) < 0)) break;
|
|
203
|
+
if (!hidden.includes(weekdayIndex(date))) dates.push(date);
|
|
204
|
+
date = addDays(date, step);
|
|
205
|
+
}
|
|
206
|
+
return dates;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The projection: the civil dates actually rendered as columns.
|
|
211
|
+
*
|
|
212
|
+
* `dayCount` counts columns, never civil days. A hidden weekday widens the
|
|
213
|
+
* civil envelope instead of eating a column, so at equal width and bounds a
|
|
214
|
+
* window always keeps its capacity. Only `min`/`max` can return fewer dates
|
|
215
|
+
* than requested, because the interval itself holds too few projectable days.
|
|
216
|
+
* @param {string} start
|
|
217
|
+
* @param {number} dayCount
|
|
218
|
+
* @param {readonly number[]} [hiddenDays]
|
|
219
|
+
* @param {string} [min]
|
|
220
|
+
* @param {string} [max]
|
|
221
|
+
* @returns {string[]}
|
|
222
|
+
*/
|
|
223
|
+
export function projectDates(start, dayCount, hiddenDays = [], min = "", max = "") {
|
|
224
|
+
const count = Math.max(0, Math.trunc(Number(dayCount) || 0));
|
|
225
|
+
if (!count || !isDateValue(start) || !isValidRange(min, max)) return [];
|
|
226
|
+
const hidden = normalizeHiddenDays(hiddenDays);
|
|
227
|
+
const from = clampDate(start, min, max);
|
|
228
|
+
const forward = walk(from, count, hidden, 1, max);
|
|
229
|
+
if (forward.length === count) return forward;
|
|
230
|
+
// The upper bound truncated the window: recover the missing columns before
|
|
231
|
+
// `from`, so bounds reduce the capacity only when the interval really is
|
|
232
|
+
// too short, never because the window happens to sit at the end.
|
|
233
|
+
const backward = walk(addDays(forward[0] ?? from, -1), count - forward.length, hidden, -1, min);
|
|
234
|
+
return [...backward.reverse(), ...forward];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Adjacent window, stepping by projected days so navigation preserves the
|
|
239
|
+
* column capacity. `direction` is 1 or -1.
|
|
240
|
+
* @param {string[]} dates
|
|
241
|
+
* @param {1|-1} direction
|
|
242
|
+
* @param {readonly number[]} [hiddenDays]
|
|
243
|
+
* @param {string} [min]
|
|
244
|
+
* @param {string} [max]
|
|
245
|
+
*/
|
|
246
|
+
export function stepStart(dates, direction, hiddenDays = [], min = "", max = "") {
|
|
247
|
+
if (!dates.length) return "";
|
|
248
|
+
const hidden = normalizeHiddenDays(hiddenDays);
|
|
249
|
+
const count = dates.length;
|
|
250
|
+
if (direction > 0) {
|
|
251
|
+
return projectDates(addDays(dates[count - 1], 1), count, hidden, min, max)[0] ?? dates[0];
|
|
252
|
+
}
|
|
253
|
+
const backward = walk(addDays(dates[0], -1), count, hidden, -1, min);
|
|
254
|
+
return projectDates(backward[backward.length - 1] ?? dates[0], count, hidden, min, max)[0] ?? dates[0];
|
|
166
255
|
}
|
|
167
256
|
|
|
168
257
|
/**
|
|
@@ -196,43 +285,43 @@ export function resolveVisibleDayCount(dayCount, width, breakpoints = RESPONSIVE
|
|
|
196
285
|
}
|
|
197
286
|
|
|
198
287
|
/**
|
|
199
|
-
*
|
|
200
|
-
* count unless the interval itself is shorter.
|
|
288
|
+
* First projected day of the window `start` belongs to, inside the bounds.
|
|
201
289
|
* @param {string} start
|
|
202
290
|
* @param {string} min
|
|
203
291
|
* @param {string} max
|
|
204
292
|
* @param {number} dayCount
|
|
293
|
+
* @param {readonly number[]} [hiddenDays]
|
|
205
294
|
*/
|
|
206
|
-
export function clampStart(start, min, max, dayCount) {
|
|
207
|
-
|
|
208
|
-
if (min && compareDates(next, min) < 0) next = min;
|
|
209
|
-
const count = Math.max(1, dayCount);
|
|
210
|
-
if (max) {
|
|
211
|
-
const latest = addDays(max, -(count - 1));
|
|
212
|
-
if (compareDates(latest, min || next) < 0) next = min || latest;
|
|
213
|
-
else if (compareDates(next, latest) > 0) next = latest;
|
|
214
|
-
}
|
|
215
|
-
return next;
|
|
295
|
+
export function clampStart(start, min, max, dayCount, hiddenDays = []) {
|
|
296
|
+
return projectDates(start, dayCount, hiddenDays, min, max)[0] ?? clampDate(start, min, max);
|
|
216
297
|
}
|
|
217
298
|
|
|
218
299
|
/**
|
|
219
|
-
* Stable range adjustment: keep `start` when `date` is already
|
|
300
|
+
* Stable range adjustment: keep `start` when `date` is already projected,
|
|
220
301
|
* otherwise shift just enough to bring `date` back into the window.
|
|
221
|
-
* Shared by resize, `goTo`, and min/max/day-count changes.
|
|
302
|
+
* Shared by resize, `goTo`, and min/max/day-count/hidden-day changes.
|
|
222
303
|
* @param {string} start
|
|
223
304
|
* @param {number} dayCount
|
|
224
305
|
* @param {string} date
|
|
225
306
|
* @param {string} min
|
|
226
307
|
* @param {string} max
|
|
308
|
+
* @param {readonly number[]} [hiddenDays]
|
|
227
309
|
*/
|
|
228
|
-
export function ensureVisible(start, dayCount, date, min, max) {
|
|
310
|
+
export function ensureVisible(start, dayCount, date, min, max, hiddenDays = []) {
|
|
311
|
+
const hidden = normalizeHiddenDays(hiddenDays);
|
|
229
312
|
const count = Math.max(1, dayCount);
|
|
230
|
-
const
|
|
231
|
-
if (!
|
|
232
|
-
const
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
313
|
+
const dates = projectDates(start, count, hidden, min, max);
|
|
314
|
+
if (!dates.length) return clampDate(start, min, max);
|
|
315
|
+
const first = dates[0];
|
|
316
|
+
if (!isDateValue(date)) return first;
|
|
317
|
+
if (compareDates(date, first) < 0) return projectDates(date, count, hidden, min, max)[0] ?? first;
|
|
318
|
+
if (compareDates(date, dates[dates.length - 1]) <= 0) return first;
|
|
319
|
+
// After the window: align it on the first projected day at or after `date`,
|
|
320
|
+
// without moving further than necessary.
|
|
321
|
+
const target = walk(date, 1, hidden, 1, max)[0];
|
|
322
|
+
if (!target) return first;
|
|
323
|
+
const backward = walk(target, count, hidden, -1, min);
|
|
324
|
+
return projectDates(backward[backward.length - 1] ?? target, count, hidden, min, max)[0] ?? first;
|
|
236
325
|
}
|
|
237
326
|
|
|
238
327
|
/**
|
|
@@ -257,27 +346,32 @@ export function hasDayContent(days) {
|
|
|
257
346
|
}
|
|
258
347
|
|
|
259
348
|
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
349
|
+
* Public range of a projection: first and last rendered day plus the number
|
|
350
|
+
* of columns. `end` is the last projected day, so the civil envelope
|
|
351
|
+
* (`start` to `end`) may span more days than `dayCount` when weekdays are
|
|
352
|
+
* hidden. That envelope is exactly what a source has to load.
|
|
353
|
+
* @param {string[]} dates
|
|
262
354
|
*/
|
|
263
|
-
export function rangeDetail(
|
|
264
|
-
return { start, end:
|
|
355
|
+
export function rangeDetail(dates) {
|
|
356
|
+
if (!dates.length) return { start: "", end: "", dayCount: 0 };
|
|
357
|
+
return { start: dates[0], end: dates[dates.length - 1], dayCount: dates.length };
|
|
265
358
|
}
|
|
266
359
|
|
|
267
360
|
/**
|
|
268
|
-
* Civil-only active day resolution.
|
|
269
|
-
* Loaded slots never influence the consulted day:
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* @param {
|
|
361
|
+
* Civil-only active day resolution against the projected dates.
|
|
362
|
+
* Loaded slots never influence the consulted day: absent/invalid maps to the
|
|
363
|
+
* first column, out-of-range clamps to the window, and a date falling on a
|
|
364
|
+
* hidden weekday resolves to the next projected column.
|
|
365
|
+
* @param {string[]} dates
|
|
273
366
|
* @param {string} activeDate
|
|
274
367
|
*/
|
|
275
|
-
export function resolveActiveDate(
|
|
276
|
-
if (!
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (compareDates(activeDate,
|
|
280
|
-
return
|
|
368
|
+
export function resolveActiveDate(dates, activeDate) {
|
|
369
|
+
if (!dates.length) return "";
|
|
370
|
+
const last = dates[dates.length - 1];
|
|
371
|
+
if (!isDateValue(activeDate)) return dates[0];
|
|
372
|
+
if (compareDates(activeDate, dates[0]) <= 0) return dates[0];
|
|
373
|
+
if (compareDates(activeDate, last) >= 0) return last;
|
|
374
|
+
return dates.find((date) => compareDates(date, activeDate) >= 0) ?? last;
|
|
281
375
|
}
|
|
282
376
|
|
|
283
377
|
/**
|
package/src/slot-picker.css
CHANGED
|
@@ -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 {
|