@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
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export type Slot = import("../model.js").Slot;
|
|
2
|
+
export type SlotCountMessages = {
|
|
3
|
+
oneSlot: string;
|
|
4
|
+
manySlots: string;
|
|
5
|
+
};
|
|
6
|
+
export type RangeEmptyMessages = {
|
|
7
|
+
rangeEmptyTitle: string;
|
|
8
|
+
rangeEmptyDescription: string;
|
|
9
|
+
rangeEmptyNext: string;
|
|
10
|
+
nextAvailability: string;
|
|
11
|
+
closedRange: string;
|
|
12
|
+
};
|
|
13
|
+
/** @param {string} value */
|
|
14
|
+
export declare function escapeHtml(value: string): string;
|
|
15
|
+
/** @param {string} value */
|
|
16
|
+
export declare function escapeAttr(value: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Ordered values of enabled slots only. Disabled slots stay discoverable
|
|
19
|
+
* in the DOM but never take the roving tabindex.
|
|
20
|
+
* @param {{date:string,slots:Slot[]}[]} days
|
|
21
|
+
*/
|
|
22
|
+
export declare function enabledValues(days: {
|
|
23
|
+
date: string;
|
|
24
|
+
slots: Slot[];
|
|
25
|
+
}[]): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Pick the single roving value: first preferred value that is enabled,
|
|
28
|
+
* otherwise the first enabled value, otherwise "".
|
|
29
|
+
* @param {string[]} enabled
|
|
30
|
+
* @param {...string} preferred
|
|
31
|
+
*/
|
|
32
|
+
export declare function rovingValue(enabled: string[], ...preferred: string[]): string;
|
|
33
|
+
/**
|
|
34
|
+
* Single slot button. Semantics must stay identical across projections.
|
|
35
|
+
* @param {{date:string,slot:Slot,dayIndex:number,slotIndex:number,selected:boolean,tabbed:boolean}} options
|
|
36
|
+
*/
|
|
37
|
+
export declare function slotButton({ date, slot, dayIndex, slotIndex, selected, tabbed }: {
|
|
38
|
+
date: string;
|
|
39
|
+
slot: Slot;
|
|
40
|
+
dayIndex: number;
|
|
41
|
+
slotIndex: number;
|
|
42
|
+
selected: boolean;
|
|
43
|
+
tabbed: boolean;
|
|
44
|
+
}): string;
|
|
45
|
+
/**
|
|
46
|
+
* Per-day notice indicator. Without options it is purely decorative. With
|
|
47
|
+
* `interactive: true` it is the only control that emits `noticeactivate`:
|
|
48
|
+
* the accessible name carries the description, the native `title` is a
|
|
49
|
+
* desktop-only bonus.
|
|
50
|
+
* @param {{interactive?:boolean,dayIndex?:number,label?:string,description?:string}} [options]
|
|
51
|
+
*/
|
|
52
|
+
export declare function noticeIndicator(options?: {
|
|
53
|
+
interactive?: boolean;
|
|
54
|
+
dayIndex?: number;
|
|
55
|
+
label?: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
}): string;
|
|
58
|
+
/**
|
|
59
|
+
* Readable day notice. Deliberately non-interactive: `noticeactivate` belongs
|
|
60
|
+
* to the explicit control, never to displayed text, so there is no mouse-only
|
|
61
|
+
* activation.
|
|
62
|
+
* @param {{label:string,description?:string}} notice
|
|
63
|
+
*/
|
|
64
|
+
export declare function noticeBlock({ label, description }: {
|
|
65
|
+
label: string;
|
|
66
|
+
description?: string;
|
|
67
|
+
}): string;
|
|
68
|
+
/**
|
|
69
|
+
* Real DOM empty state. Text comes from messages, never from CSS.
|
|
70
|
+
* @param {string} message
|
|
71
|
+
*/
|
|
72
|
+
export declare function dayEmpty(message: string): string;
|
|
73
|
+
/**
|
|
74
|
+
* Localized slot count for the day strip. Zero renders as an en dash,
|
|
75
|
+
* keeping the indicator distinct from a real sentence.
|
|
76
|
+
* @param {number} count
|
|
77
|
+
* @param {SlotCountMessages} messages
|
|
78
|
+
*/
|
|
79
|
+
export declare function slotCountText(count: number, messages: SlotCountMessages): string;
|
|
80
|
+
/**
|
|
81
|
+
* Range-level empty state, distinct from per-day empty days.
|
|
82
|
+
* Rendered only when the whole window has no slot and no notice; the theme
|
|
83
|
+
* decides how prominent it is. Two actions stay distinct: `next()` moves to
|
|
84
|
+
* the immediately following window, while `next availability` (opt-in via
|
|
85
|
+
* `next-availability`) is the contextual business search that may skip
|
|
86
|
+
* several windows through the source. They are never merged.
|
|
87
|
+
* @param {{start:string,end:string,invalid:boolean,canNext:boolean,nextAvailability:boolean,closed:boolean,locale:string,messages:RangeEmptyMessages}} options
|
|
88
|
+
*/
|
|
89
|
+
export declare function rangeEmpty({ start, end, invalid, canNext, nextAvailability, closed, locale, messages }: {
|
|
90
|
+
start: string;
|
|
91
|
+
end: string;
|
|
92
|
+
invalid: boolean;
|
|
93
|
+
canNext: boolean;
|
|
94
|
+
nextAvailability: boolean;
|
|
95
|
+
closed: boolean;
|
|
96
|
+
locale: string;
|
|
97
|
+
messages: RangeEmptyMessages;
|
|
98
|
+
}): string;
|
|
99
|
+
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../src/views/shared.js"],"names":[],"mappings":"AAGI,YAAsC,IAAI,GAAhC,OAAO,aAAa,EAAE,IAAI,CAAM;AAC1C,YAA6C,iBAAiB,GAApD;IAAC,OAAO,EAAC,MAAM,CAAC;IAAA,SAAS,EAAC,MAAM,CAAA;CAAC,CAAmB;AAC9D,YAAkI,kBAAkB,GAA1I;IAAC,eAAe,EAAC,MAAM,CAAC;IAAA,qBAAqB,EAAC,MAAM,CAAC;IAAA,cAAc,EAAC,MAAM,CAAC;IAAA,gBAAgB,EAAC,MAAM,CAAC;IAAA,WAAW,EAAC,MAAM,CAAA;CAAC,CAAoB;AAKxJ,4BAA4B;AAC5B,wBAAgB,UAAU,CAAC,KAAK,EADpB,MACoB,UAE/B;AAED,4BAA4B;AAC5B,wBAAgB,UAAU,CAAC,KAAK,EADpB,MACoB,UAE/B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAFvB;IAAC,IAAI,EAAC,MAAM,CAAC;IAAA,KAAK,EAAC,IAAI,EAAE,CAAA;CAAC,EAEH,YASjC;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAHxB,MAAM,EAGkB,EAAE,GAAG,SAAS,EAFnC,MAAM,EAE6B,UAKhD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAFrE;IAAC,IAAI,EAAC,MAAM,CAAC;IAAA,IAAI,EAAC,IAAI,CAAC;IAAA,QAAQ,EAAC,MAAM,CAAC;IAAA,SAAS,EAAC,MAAM,CAAC;IAAA,QAAQ,EAAC,OAAO,CAAC;IAAA,MAAM,EAAC,OAAO,CAAA;CAElB,UAU/E;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,AAFpC,CAA4F,EAApF;IAAC,WAAW,CAAC,EAAC,OAAO,CAAC;IAAA,QAAQ,CAAC,EAAC,MAAM,CAAC;IAAA,KAAK,CAAC,EAAC,MAAM,CAAC;IAAA,WAAW,CAAC,EAAC,MAAM,CAAA;CAEvC,UAO3C;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,EAFvC;IAAC,KAAK,EAAC,MAAM,CAAC;IAAA,WAAW,CAAC,EAAC,MAAM,CAAA;CAEM,UAKjD;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAFrB,MAEqB,UAE/B;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAHxB,MAGwB,EAAE,QAAQ,EAFlC,iBAEkC,UAI5C;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,EAF5F;IAAC,KAAK,EAAC,MAAM,CAAC;IAAA,GAAG,EAAC,MAAM,CAAC;IAAA,OAAO,EAAC,OAAO,CAAC;IAAA,OAAO,EAAC,OAAO,CAAC;IAAA,gBAAgB,EAAC,OAAO,CAAC;IAAA,MAAM,EAAC,OAAO,CAAC;IAAA,MAAM,EAAC,MAAM,CAAC;IAAA,QAAQ,EAAC,kBAAkB,CAAA;CAE9C,UAiCtG"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Use cases
|
|
2
|
+
|
|
3
|
+
These cases define the product more strongly than a feature checklist.
|
|
4
|
+
|
|
5
|
+
## U1 — Inline appointment availability
|
|
6
|
+
|
|
7
|
+
A consumer provides five civil days containing zero or more appointment slots.
|
|
8
|
+
|
|
9
|
+
Acceptance:
|
|
10
|
+
|
|
11
|
+
- each day is clearly labelled;
|
|
12
|
+
- slots render chronologically;
|
|
13
|
+
- activating a slot dispatches `slotactivate`;
|
|
14
|
+
- the selected value is a local civil datetime (`YYYY-MM-DDTHH:mm`);
|
|
15
|
+
- empty days remain visible;
|
|
16
|
+
- `slot.tone` is a neutral presentation token surfaced as `data-tone`; the core defines no palette and never interprets the token;
|
|
17
|
+
- `slot.description` carries accessible detail; a tooltip or popover remains application-owned.
|
|
18
|
+
|
|
19
|
+
## U2 — Controlled range navigation
|
|
20
|
+
|
|
21
|
+
The consumer can show three or five days depending on its layout.
|
|
22
|
+
|
|
23
|
+
Acceptance:
|
|
24
|
+
|
|
25
|
+
- `dayCount` means exactly N consecutive civil days;
|
|
26
|
+
- previous/next navigation changes `start`, not `value`;
|
|
27
|
+
- `rangechange` exposes the requested start/end range;
|
|
28
|
+
- min/max bounds disable impossible navigation;
|
|
29
|
+
- responsive policy remains consumer-owned.
|
|
30
|
+
|
|
31
|
+
## U3 — Remote availability
|
|
32
|
+
|
|
33
|
+
A source function loads availability for the visible range.
|
|
34
|
+
|
|
35
|
+
Acceptance:
|
|
36
|
+
|
|
37
|
+
- the source receives the exact visible range;
|
|
38
|
+
- superseded requests are aborted;
|
|
39
|
+
- stale responses never repaint the current range;
|
|
40
|
+
- source/backend URL conventions are application-owned;
|
|
41
|
+
- loading and error states do not mutate selection;
|
|
42
|
+
- loading is signalled without layout shift: `aria-busy="true"` on the host, the loading text announced through a visually hidden `role="status"`, and only a delayed CSS fade once the response is actually slow;
|
|
43
|
+
|
|
44
|
+
## U4 — Day notice / exceptional unavailability
|
|
45
|
+
|
|
46
|
+
A day can carry a notice such as "exceptionally unavailable".
|
|
47
|
+
|
|
48
|
+
Acceptance:
|
|
49
|
+
|
|
50
|
+
- the notice is distinct from an appointment slot;
|
|
51
|
+
- it does not become a fake slot;
|
|
52
|
+
- `notice-display` selects the projection strategy: `action` (default), `inline`, or `none`;
|
|
53
|
+
- `action` renders only a real control (the dot button) with an accessible name; no inline text is shown;
|
|
54
|
+
- `inline` additionally renders the readable notice content, by layout: compact label in `columns` (description kept in the DOM, revealed by themes) and full label + description in `day`;
|
|
55
|
+
- `inline` never overflows its column, even with an unbroken long label;
|
|
56
|
+
- displayed notice text is never interactive: `noticeactivate` belongs to an explicit control only, so there is no mouse-only activation;
|
|
57
|
+
- `notice-display="action"` renders the control in the day header (`columns`) or the panel header (`day`); `inline` keeps it in `columns` and relies on the text in `day`;
|
|
58
|
+
- `none` renders nothing; the application owns the notice elsewhere;
|
|
59
|
+
- activating the control dispatches `noticeactivate` with `{ day, notice, anchor }`, bubbling, without a default behavior and without selecting a slot;
|
|
60
|
+
- the component never owns a tooltip, popover or floating engine; the application decides (modal, popover, drawer or nothing).
|
|
61
|
+
|
|
62
|
+
## U5 — Collapsed availability
|
|
63
|
+
|
|
64
|
+
A dense range can initially show only the first N slot rows.
|
|
65
|
+
|
|
66
|
+
Acceptance:
|
|
67
|
+
|
|
68
|
+
- collapsing is presentation only;
|
|
69
|
+
- hidden slots remain part of the loaded model;
|
|
70
|
+
- "show more" expands without fetching again;
|
|
71
|
+
- selection remains stable across expand/collapse;
|
|
72
|
+
- while collapsed, "show more" floats inside the reserved footer band and a gradient hints at the availability that is not rendered; expanding returns it to normal flow with no overlay;
|
|
73
|
+
- hidden slots are not rendered in the DOM while collapsed, so no invisible button can be focused.
|
|
74
|
+
|
|
75
|
+
## U6 — Keyboard-only slot choice
|
|
76
|
+
|
|
77
|
+
A user can navigate and select without a pointing device.
|
|
78
|
+
|
|
79
|
+
Acceptance:
|
|
80
|
+
|
|
81
|
+
- one slot is in the Tab sequence;
|
|
82
|
+
- ArrowUp/ArrowDown move within a day;
|
|
83
|
+
- ArrowLeft/ArrowRight move to the nearest row in the adjacent day;
|
|
84
|
+
- Home/End move to the first/last slot of the current day;
|
|
85
|
+
- Enter/Space activates;
|
|
86
|
+
- focus movement never changes `value`;
|
|
87
|
+
- `slotactivate` bubbles, so one ancestor listener can delegate several pickers.
|
|
88
|
+
|
|
89
|
+
## U7 — Controlled selection
|
|
90
|
+
|
|
91
|
+
An application can set or clear `value`.
|
|
92
|
+
|
|
93
|
+
Acceptance:
|
|
94
|
+
|
|
95
|
+
- `value` is `YYYY-MM-DDTHH:mm` or empty;
|
|
96
|
+
- an impossible civil date (for example `2026-02-31T10:00`) is rejected by the property setter;
|
|
97
|
+
- a malformed `value` attribute is ignored and reads as empty, so imperfect markup never breaks the element upgrade;
|
|
98
|
+
- setting `value` only changes visual selection;
|
|
99
|
+
- programmatic assignment does not emit `slotactivate`;
|
|
100
|
+
- selecting a disabled slot is impossible.
|
|
101
|
+
|
|
102
|
+
## U8 — Legacy application adapter
|
|
103
|
+
|
|
104
|
+
A legacy application converts its existing event payload to normalized days/slots.
|
|
105
|
+
|
|
106
|
+
Acceptance:
|
|
107
|
+
|
|
108
|
+
- FullCalendar class names never enter the component;
|
|
109
|
+
- booking URLs and domain metadata can be retained as opaque `meta`;
|
|
110
|
+
- the adapter maps its domain status to a neutral `slot.tone`; the core never interprets it;
|
|
111
|
+
- the application owns post-selection booking behavior;
|
|
112
|
+
- replacing the legacy backend does not require changing the picker.
|
|
113
|
+
|
|
114
|
+
## U9 — Day projection of the same model
|
|
115
|
+
|
|
116
|
+
The same selection model can be projected as a day strip plus the slots of one consulted day.
|
|
117
|
+
|
|
118
|
+
Acceptance:
|
|
119
|
+
|
|
120
|
+
- `layout` is `columns` (default) or `day`; the consumer chooses it explicitly;
|
|
121
|
+
- there is no automatic `columns` to `day` breakpoint on viewport size;
|
|
122
|
+
- `activeDate` is the last day explicitly targeted by the user (day click or slot activation);
|
|
123
|
+
- `activeDate` resolution is civil-only: absent maps to `start`, before-range maps to `start`, after-range maps to `end`; loaded slots never influence it;
|
|
124
|
+
- activating a slot sets `activeDate` to that slot's day before dispatching `slotactivate`;
|
|
125
|
+
- changing `layout` preserves `activeDate` and `value`;
|
|
126
|
+
- the active day uses `aria-pressed`, never `aria-current="date"`;
|
|
127
|
+
- today remains independently discoverable from the consulted day;
|
|
128
|
+
- empty days render a real DOM empty state driven by `messages.empty`, never CSS-generated text;
|
|
129
|
+
- keyboard offers two comprehensible tab stops (day strip, then day slots) with roving tabindex in each zone;
|
|
130
|
+
- Left/Right/Home/End on the strip change `activeDate` without selecting a slot.
|
|
131
|
+
|
|
132
|
+
## U10 — Bounded and responsive navigation
|
|
133
|
+
|
|
134
|
+
A consumer can bound the visible range, adapt the number of days to the
|
|
135
|
+
component's own width, and navigate by window, by reference date, or to the
|
|
136
|
+
next known availability. A wholly empty window is a distinct state.
|
|
137
|
+
|
|
138
|
+
Acceptance:
|
|
139
|
+
|
|
140
|
+
- `min`/`max` are hard bounds: `min <= start` and `end <= max`;
|
|
141
|
+
- when the requested `dayCount` does not fit the bounds, the resolved `visibleDayCount` shrinks instead of showing out-of-bounds days;
|
|
142
|
+
- `min > max` is an explicit invalid configuration: `range` is `{ start: "", end: "", dayCount: 0 }`, `data-invalid-range` is set, navigation is disabled, and the source is never called;
|
|
143
|
+
- `dayCount` is the consumer's maximum intention; `visibleDayCount` is the capacity resolved from bounds and the width available to the projection;
|
|
144
|
+
- responsive resolution is opt-in via `responsive`, uses `ResizeObserver`, and never reads `window.innerWidth`; it measures the projection width (inside the navigation rail), not the host width; `responsiveBreakpoints` is overridable;
|
|
145
|
+
- changing `visibleDayCount` keeps `activeDate` visible by shifting `start` just enough, within the bounds;
|
|
146
|
+
- `previous()`/`next()` move to the adjacent window of `visibleDayCount` days and never select;
|
|
147
|
+
- `goTo(date)` brings `date` into the window and consults it, respecting the bounds;
|
|
148
|
+
- `goHome()` returns to `homeDate`, a reference date that is never confused with `min`;
|
|
149
|
+
- `goToNextAvailability()` asks the source (`source.next`) or falls back to a `nextrequest` event; it may skip several windows and is never merged with `next()`; it returns the destination actually reached (clamped to `min`/`max`), never the raw source proposal;
|
|
150
|
+
- `configure()` validates all options before applying any: an invalid option throws and leaves the component unchanged;
|
|
151
|
+
- navigation controls are a symmetric `previous`/`next` pair framing the projection; they are the only range-navigation chrome;
|
|
152
|
+
- `goHome()` (opt-in via `home-date`) is a persistent capability but an auxiliary shortcut, kept outside the prev/next rail; it is offered only while `homeDate` is outside the visible range; its wrapper reserves its height whenever `home-date` is set, so the shortcut appearing later never shifts the content;
|
|
153
|
+
- there is no "go to end" action;
|
|
154
|
+
- `goToNextAvailability()` is a contextual action, never permanent chrome: it is surfaced in the range empty state when `next-availability` is set;
|
|
155
|
+
- a window with no slot, no notice and no closed day renders a range empty state (`.sp-range-empty` replacing the projection, with a `next()` action, plus the contextual availability action when `next-availability` is set);
|
|
156
|
+
- a window with a notice or a closed day but no slots keeps the normal projection, so the exception or the closure stays visible;
|
|
157
|
+
- resize changes the range, never `value`; event order is stable (`rangechange`, then `daychange`, then any reload);
|
|
158
|
+
- collapsed `columns` projection keeps a stable footprint derived from `max-visible-rows`: full, sparse and wholly empty ranges reserve the same height, so navigating between equivalent ranges never causes avoidable vertical layout shift;
|
|
159
|
+
- `expanded` content may grow naturally past that floor; in `layout="day"`, only the empty state gets a baseline.
|
|
160
|
+
|
|
161
|
+
## U11 — Closed days / sparse availability
|
|
162
|
+
|
|
163
|
+
The source can distinguish a closed day from an open day with no availability,
|
|
164
|
+
without the component knowing any opening-hours rule.
|
|
165
|
+
|
|
166
|
+
Acceptance:
|
|
167
|
+
|
|
168
|
+
- a day carries `closed: true` (for example a weekend or a weekly closure); the component never computes opening hours, recurrence or "Monday only";
|
|
169
|
+
- the window still contains `visibleDayCount` consecutive civil days: closed days remain visible and are never compressed or hidden;
|
|
170
|
+
- three cases stay distinct: slots present; `slots: []` and open → `messages.empty`; `slots: []` and `closed` → `messages.closed`;
|
|
171
|
+
- a closed day is stylable through `[data-closed]` on the day, the panel and the day strip; `--sp-closed-opacity` is the default theming hook;
|
|
172
|
+
- `notice` stays reserved for exceptional information and may coexist with `closed`;
|
|
173
|
+
- a window whose only content is closed days keeps the normal projection instead of collapsing into the range empty state;
|
|
174
|
+
- `source.next()` / `goToNextAvailability()` let the application jump to the next real availability between windows; they never change the civil shape of a window;
|
|
175
|
+
- `closed-days="show"` (default) renders every civil day; `closed-days="hide"` removes closed days from the projection only;
|
|
176
|
+
- hiding a closed day never extends the civil range to compensate: `day-count` stays the civil count, and the rendered column count follows the projected days;
|
|
177
|
+
- hiding never alters `range`, `visibleDayCount`, `previous()`/`next()`, `goTo()` or `source.load()`, and toggling `closed-days` never reloads the source;
|
|
178
|
+
- in `layout="day"`, a closed day that is the consulted `activeDate` stays visible until another day is activated; data arrival never moves `activeDate`;
|
|
179
|
+
- a window whose civil days are all closed renders the range empty state with `messages.closedRange`, distinct from `messages.empty`;
|
|
180
|
+
- `closed: true` with bookable slots is invalid input and is rejected by normalization;
|
|
181
|
+
- no recurrence or opening-hours rule enters the core.
|
|
182
|
+
|
|
183
|
+
## Not a use case for this package
|
|
184
|
+
|
|
185
|
+
Keep these outside the component:
|
|
186
|
+
|
|
187
|
+
- booking confirmation flows;
|
|
188
|
+
- login/signup;
|
|
189
|
+
- doctor/reason/security identifiers;
|
|
190
|
+
- "anti swiss cheese" booking rules;
|
|
191
|
+
- payment;
|
|
192
|
+
- resource scheduling;
|
|
193
|
+
- drag/drop;
|
|
194
|
+
- event rendering;
|
|
195
|
+
- recurrence;
|
|
196
|
+
- timezone conversion;
|
|
197
|
+
- tooltip/flyout positioning frameworks.
|
package/package.json
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lekoala/slot-picker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Native-first day and appointment slot picker primitive",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": [
|
|
7
|
+
"./src/define.js",
|
|
8
|
+
"./src/slot-picker.css",
|
|
9
|
+
"./dist/slot-picker.js",
|
|
10
|
+
"./dist/slot-picker.min.js",
|
|
11
|
+
"./dist/slot-picker.css",
|
|
12
|
+
"./dist/slot-picker.min.css"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"customElements": "custom-elements.json",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/lekoala/slot-picker.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/lekoala/slot-picker",
|
|
21
|
+
"bugs": "https://github.com/lekoala/slot-picker/issues",
|
|
22
|
+
"packageManager": "bun@1.4.2",
|
|
23
|
+
"keywords": [
|
|
24
|
+
"slot-picker",
|
|
25
|
+
"appointment",
|
|
26
|
+
"availability",
|
|
27
|
+
"web-component",
|
|
28
|
+
"accessibility"
|
|
29
|
+
],
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"src",
|
|
33
|
+
"docs",
|
|
34
|
+
"custom-elements.json",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./dist/types/index.d.ts",
|
|
41
|
+
"import": "./src/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./define": {
|
|
44
|
+
"types": "./dist/types/define.d.ts",
|
|
45
|
+
"import": "./src/define.js"
|
|
46
|
+
},
|
|
47
|
+
"./locales/ar": {
|
|
48
|
+
"types": "./dist/types/locales/ar.d.ts",
|
|
49
|
+
"import": "./src/locales/ar.js"
|
|
50
|
+
},
|
|
51
|
+
"./locales/de": {
|
|
52
|
+
"types": "./dist/types/locales/de.d.ts",
|
|
53
|
+
"import": "./src/locales/de.js"
|
|
54
|
+
},
|
|
55
|
+
"./locales/en": {
|
|
56
|
+
"types": "./dist/types/locales/en.d.ts",
|
|
57
|
+
"import": "./src/locales/en.js"
|
|
58
|
+
},
|
|
59
|
+
"./locales/es": {
|
|
60
|
+
"types": "./dist/types/locales/es.d.ts",
|
|
61
|
+
"import": "./src/locales/es.js"
|
|
62
|
+
},
|
|
63
|
+
"./locales/fr": {
|
|
64
|
+
"types": "./dist/types/locales/fr.d.ts",
|
|
65
|
+
"import": "./src/locales/fr.js"
|
|
66
|
+
},
|
|
67
|
+
"./locales/hi": {
|
|
68
|
+
"types": "./dist/types/locales/hi.d.ts",
|
|
69
|
+
"import": "./src/locales/hi.js"
|
|
70
|
+
},
|
|
71
|
+
"./locales/id": {
|
|
72
|
+
"types": "./dist/types/locales/id.d.ts",
|
|
73
|
+
"import": "./src/locales/id.js"
|
|
74
|
+
},
|
|
75
|
+
"./locales/it": {
|
|
76
|
+
"types": "./dist/types/locales/it.d.ts",
|
|
77
|
+
"import": "./src/locales/it.js"
|
|
78
|
+
},
|
|
79
|
+
"./locales/ja": {
|
|
80
|
+
"types": "./dist/types/locales/ja.d.ts",
|
|
81
|
+
"import": "./src/locales/ja.js"
|
|
82
|
+
},
|
|
83
|
+
"./locales/ko": {
|
|
84
|
+
"types": "./dist/types/locales/ko.d.ts",
|
|
85
|
+
"import": "./src/locales/ko.js"
|
|
86
|
+
},
|
|
87
|
+
"./locales/nl": {
|
|
88
|
+
"types": "./dist/types/locales/nl.d.ts",
|
|
89
|
+
"import": "./src/locales/nl.js"
|
|
90
|
+
},
|
|
91
|
+
"./locales/pl": {
|
|
92
|
+
"types": "./dist/types/locales/pl.d.ts",
|
|
93
|
+
"import": "./src/locales/pl.js"
|
|
94
|
+
},
|
|
95
|
+
"./locales/pt-BR": {
|
|
96
|
+
"types": "./dist/types/locales/pt-BR.d.ts",
|
|
97
|
+
"import": "./src/locales/pt-BR.js"
|
|
98
|
+
},
|
|
99
|
+
"./locales/pt-PT": {
|
|
100
|
+
"types": "./dist/types/locales/pt-PT.d.ts",
|
|
101
|
+
"import": "./src/locales/pt-PT.js"
|
|
102
|
+
},
|
|
103
|
+
"./locales/ru": {
|
|
104
|
+
"types": "./dist/types/locales/ru.d.ts",
|
|
105
|
+
"import": "./src/locales/ru.js"
|
|
106
|
+
},
|
|
107
|
+
"./locales/tr": {
|
|
108
|
+
"types": "./dist/types/locales/tr.d.ts",
|
|
109
|
+
"import": "./src/locales/tr.js"
|
|
110
|
+
},
|
|
111
|
+
"./locales/zh-CN": {
|
|
112
|
+
"types": "./dist/types/locales/zh-CN.d.ts",
|
|
113
|
+
"import": "./src/locales/zh-CN.js"
|
|
114
|
+
},
|
|
115
|
+
"./slot-picker.css": "./src/slot-picker.css",
|
|
116
|
+
"./dist/slot-picker.js": "./dist/slot-picker.js",
|
|
117
|
+
"./dist/slot-picker.min.js": "./dist/slot-picker.min.js",
|
|
118
|
+
"./dist/slot-picker.css": "./dist/slot-picker.css",
|
|
119
|
+
"./dist/slot-picker.min.css": "./dist/slot-picker.min.css"
|
|
120
|
+
},
|
|
121
|
+
"publishConfig": {
|
|
122
|
+
"access": "public"
|
|
123
|
+
},
|
|
124
|
+
"scripts": {
|
|
125
|
+
"dev": "node test/server.js",
|
|
126
|
+
"build:bundle": "bun scripts/build.js",
|
|
127
|
+
"build:types": "bun run types",
|
|
128
|
+
"build": "bun run build:bundle && bun run build:types",
|
|
129
|
+
"sync": "bun run build",
|
|
130
|
+
"lint": "biome check .",
|
|
131
|
+
"format": "biome format --write .",
|
|
132
|
+
"typecheck": "tsc -p jsconfig.json",
|
|
133
|
+
"types": "tsc -p tsconfig.types.json",
|
|
134
|
+
"test": "bun test test/unit",
|
|
135
|
+
"test:syntax": "node --check src/date.js && node --check src/model.js && node --check src/source.js && node --check src/messages.js && node --check src/slot-picker.js && node --check src/index.js && node --check src/define.js && node --check src/views/shared.js && node --check src/views/columns.js && node --check src/views/day.js",
|
|
136
|
+
"test:browser": "bun run build:bundle && playwright test browser --project chromium",
|
|
137
|
+
"test:browser:all": "bun run build:bundle && playwright test browser",
|
|
138
|
+
"check": "bun run test:syntax && bun run lint && bun run typecheck && bun run test",
|
|
139
|
+
"verify": "bun run check && bun run sync",
|
|
140
|
+
"prepack": "bun run sync",
|
|
141
|
+
"prepublishOnly": "bun run verify"
|
|
142
|
+
},
|
|
143
|
+
"devDependencies": {
|
|
144
|
+
"@biomejs/biome": "2.5.13",
|
|
145
|
+
"@playwright/test": "1.63.0",
|
|
146
|
+
"typescript": "7.0.2"
|
|
147
|
+
}
|
|
148
|
+
}
|
package/src/date.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
2
|
+
|
|
3
|
+
/** @param {string} value */
|
|
4
|
+
export function isDateValue(value) {
|
|
5
|
+
if (!DATE_RE.test(value)) return false;
|
|
6
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
7
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
8
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** @param {string} value */
|
|
12
|
+
function parts(value) {
|
|
13
|
+
if (!isDateValue(value)) throw new TypeError(`Invalid civil date: ${value}`);
|
|
14
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
15
|
+
return { year, month, day };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @param {string} value @param {number} amount */
|
|
19
|
+
export function addDays(value, amount) {
|
|
20
|
+
const { year, month, day } = parts(value);
|
|
21
|
+
const date = new Date(Date.UTC(year, month - 1, day + amount));
|
|
22
|
+
return [
|
|
23
|
+
String(date.getUTCFullYear()).padStart(4, "0"),
|
|
24
|
+
String(date.getUTCMonth() + 1).padStart(2, "0"),
|
|
25
|
+
String(date.getUTCDate()).padStart(2, "0"),
|
|
26
|
+
].join("-");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @param {string} a @param {string} b */
|
|
30
|
+
export function compareDates(a, b) {
|
|
31
|
+
return a.localeCompare(b);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Signed civil day difference (b - a).
|
|
36
|
+
* @param {string} a
|
|
37
|
+
* @param {string} b
|
|
38
|
+
*/
|
|
39
|
+
export function daysBetween(a, b) {
|
|
40
|
+
return Math.round((toUtcDate(b).getTime() - toUtcDate(a).getTime()) / 86_400_000);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Inclusive visible civil range.
|
|
45
|
+
* @param {string} start
|
|
46
|
+
* @param {number} dayCount
|
|
47
|
+
*/
|
|
48
|
+
export function rangeEnd(start, dayCount) {
|
|
49
|
+
return addDays(start, Math.max(1, dayCount) - 1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @param {string} value */
|
|
53
|
+
export function toUtcDate(value) {
|
|
54
|
+
const { year, month, day } = parts(value);
|
|
55
|
+
return new Date(Date.UTC(year, month - 1, day));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Local civil "today" without timezone conversion of public values. */
|
|
59
|
+
export function todayValue() {
|
|
60
|
+
const now = new Date();
|
|
61
|
+
return [
|
|
62
|
+
String(now.getFullYear()).padStart(4, "0"),
|
|
63
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
64
|
+
String(now.getDate()).padStart(2, "0"),
|
|
65
|
+
].join("-");
|
|
66
|
+
}
|
package/src/define.js
ADDED
package/src/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export { addDays, compareDates, daysBetween, isDateValue, rangeEnd, todayValue } from "./date.js";
|
|
2
|
+
export { DEFAULT_MESSAGES, getDefaultMessages, resolveMessages, setDefaultMessages } from "./messages.js";
|
|
3
|
+
export {
|
|
4
|
+
boundedDayCount,
|
|
5
|
+
clampStart,
|
|
6
|
+
collapsedDays,
|
|
7
|
+
ensureVisible,
|
|
8
|
+
focusableSlots,
|
|
9
|
+
hasDayContent,
|
|
10
|
+
hasSlots,
|
|
11
|
+
isSlotValue,
|
|
12
|
+
isTimeValue,
|
|
13
|
+
isValidRange,
|
|
14
|
+
moveFocus,
|
|
15
|
+
normalizeBreakpoints,
|
|
16
|
+
normalizeDays,
|
|
17
|
+
RESPONSIVE_BREAKPOINTS,
|
|
18
|
+
rangeDetail,
|
|
19
|
+
resolveActiveDate,
|
|
20
|
+
resolveVisibleDayCount,
|
|
21
|
+
slotValue,
|
|
22
|
+
visibleDays,
|
|
23
|
+
} from "./model.js";
|
|
24
|
+
export { SlotPickerElement } from "./slot-picker.js";
|
|
25
|
+
export { SlotSourceController } from "./source.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
previous: "الأيام السابقة",
|
|
3
|
+
next: "الأيام التالية",
|
|
4
|
+
home: "العودة إلى البداية",
|
|
5
|
+
nextAvailability: "التوفر التالي",
|
|
6
|
+
showMore: "إظهار مزيد من التوفر",
|
|
7
|
+
showLess: "إظهار أقل",
|
|
8
|
+
loading: "جارٍ تحميل التوفر",
|
|
9
|
+
empty: "لا يوجد توفر",
|
|
10
|
+
closed: "مغلق",
|
|
11
|
+
closedRange: "مغلق خلال هذه الفترة",
|
|
12
|
+
rangeEmptyTitle: "لا يوجد أي موعد متاح في هذه الفترة",
|
|
13
|
+
rangeEmptyDescription: "من {start} إلى {end}",
|
|
14
|
+
rangeEmptyNext: "عرض الفترة التالية",
|
|
15
|
+
oneSlot: "موعد واحد",
|
|
16
|
+
manySlots: "{n} مواعيد",
|
|
17
|
+
days: "الأيام",
|
|
18
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
previous: "Vorherige Tage",
|
|
3
|
+
next: "Nächste Tage",
|
|
4
|
+
home: "Zurück zum Anfang",
|
|
5
|
+
nextAvailability: "Nächste Verfügbarkeit",
|
|
6
|
+
showMore: "Mehr Verfügbarkeiten anzeigen",
|
|
7
|
+
showLess: "Weniger anzeigen",
|
|
8
|
+
loading: "Verfügbarkeiten werden geladen",
|
|
9
|
+
empty: "Keine Verfügbarkeit",
|
|
10
|
+
closed: "Geschlossen",
|
|
11
|
+
closedRange: "In diesem Zeitraum geschlossen",
|
|
12
|
+
rangeEmptyTitle: "Keine Verfügbarkeit in diesem Zeitraum",
|
|
13
|
+
rangeEmptyDescription: "Vom {start} bis {end}",
|
|
14
|
+
rangeEmptyNext: "Nächsten Zeitraum anzeigen",
|
|
15
|
+
oneSlot: "1 Termin",
|
|
16
|
+
manySlots: "{n} Termine",
|
|
17
|
+
days: "Tage",
|
|
18
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
previous: "Previous days",
|
|
3
|
+
next: "Next days",
|
|
4
|
+
home: "Back to start",
|
|
5
|
+
nextAvailability: "Next availability",
|
|
6
|
+
showMore: "Show more availability",
|
|
7
|
+
showLess: "Show less",
|
|
8
|
+
loading: "Loading availability",
|
|
9
|
+
empty: "No availability",
|
|
10
|
+
closed: "Closed",
|
|
11
|
+
closedRange: "Closed during this period",
|
|
12
|
+
rangeEmptyTitle: "No availability in this period",
|
|
13
|
+
rangeEmptyDescription: "From {start} to {end}",
|
|
14
|
+
rangeEmptyNext: "See the next period",
|
|
15
|
+
oneSlot: "1 slot",
|
|
16
|
+
manySlots: "{n} slots",
|
|
17
|
+
days: "Days",
|
|
18
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
previous: "Días anteriores",
|
|
3
|
+
next: "Días siguientes",
|
|
4
|
+
home: "Volver al inicio",
|
|
5
|
+
nextAvailability: "Próxima disponibilidad",
|
|
6
|
+
showMore: "Mostrar más disponibilidad",
|
|
7
|
+
showLess: "Mostrar menos",
|
|
8
|
+
loading: "Cargando disponibilidad",
|
|
9
|
+
empty: "Sin disponibilidad",
|
|
10
|
+
closed: "Cerrado",
|
|
11
|
+
closedRange: "Cerrado durante este período",
|
|
12
|
+
rangeEmptyTitle: "Sin disponibilidad en este período",
|
|
13
|
+
rangeEmptyDescription: "Del {start} al {end}",
|
|
14
|
+
rangeEmptyNext: "Ver el siguiente período",
|
|
15
|
+
oneSlot: "1 hueco",
|
|
16
|
+
manySlots: "{n} huecos",
|
|
17
|
+
days: "Días",
|
|
18
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
previous: "Jours précédents",
|
|
3
|
+
next: "Jours suivants",
|
|
4
|
+
home: "Retour au début",
|
|
5
|
+
nextAvailability: "Prochain créneau disponible",
|
|
6
|
+
showMore: "Voir plus de disponibilités",
|
|
7
|
+
showLess: "Voir moins",
|
|
8
|
+
loading: "Chargement des disponibilités",
|
|
9
|
+
empty: "Aucune disponibilité",
|
|
10
|
+
closed: "Fermé",
|
|
11
|
+
closedRange: "Fermé sur cette période",
|
|
12
|
+
rangeEmptyTitle: "Aucun créneau disponible sur cette période",
|
|
13
|
+
rangeEmptyDescription: "Du {start} au {end}",
|
|
14
|
+
rangeEmptyNext: "Voir la période suivante",
|
|
15
|
+
oneSlot: "1 créneau",
|
|
16
|
+
manySlots: "{n} créneaux",
|
|
17
|
+
days: "Jours",
|
|
18
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
previous: "पिछले दिन",
|
|
3
|
+
next: "अगले दिन",
|
|
4
|
+
home: "शुरुआत पर वापस जाएँ",
|
|
5
|
+
nextAvailability: "अगली उपलब्धता",
|
|
6
|
+
showMore: "और उपलब्धताएँ दिखाएँ",
|
|
7
|
+
showLess: "कम दिखाएँ",
|
|
8
|
+
loading: "उपलब्धता लोड हो रही है",
|
|
9
|
+
empty: "कोई उपलब्धता नहीं",
|
|
10
|
+
closed: "बंद",
|
|
11
|
+
closedRange: "इस अवधि के दौरान बंद",
|
|
12
|
+
rangeEmptyTitle: "इस अवधि में कोई स्लॉट उपलब्ध नहीं",
|
|
13
|
+
rangeEmptyDescription: "{start} से {end} तक",
|
|
14
|
+
rangeEmptyNext: "अगली अवधि देखें",
|
|
15
|
+
oneSlot: "1 स्लॉट",
|
|
16
|
+
manySlots: "{n} स्लॉट",
|
|
17
|
+
days: "दिन",
|
|
18
|
+
};
|