@forgebuild/hotal-ui 0.1.0 → 0.2.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/README.md +44 -35
- package/dist/hotal-ui.js +3254 -0
- package/dist/index.d.ts +22 -1
- package/package.json +7 -13
- package/dist/ReservationBar.svelte +0 -489
- package/dist/ReservationBar.svelte.d.ts +0 -20
- package/dist/index.js +0 -1
- package/dist/lib/calendar.d.ts +0 -20
- package/dist/lib/calendar.js +0 -72
package/dist/index.d.ts
CHANGED
|
@@ -1 +1,22 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface ReservationBarElement extends HTMLElement {
|
|
2
|
+
minGuests?: number;
|
|
3
|
+
maxGuests?: number;
|
|
4
|
+
defaultGuests?: number;
|
|
5
|
+
timeSlots?: string[];
|
|
6
|
+
defaultTime?: string;
|
|
7
|
+
showReserveButton?: boolean;
|
|
8
|
+
reserveHref?: string;
|
|
9
|
+
syncWithUrl?: boolean;
|
|
10
|
+
closedWeekdays?: number[];
|
|
11
|
+
fetchAvailableDates?: (partySize: number) => Promise<string[]>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare global {
|
|
15
|
+
interface HTMLElementTagNameMap {
|
|
16
|
+
'reservation-bar': ReservationBarElement;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface HTMLElementEventMap {
|
|
20
|
+
reservationchange: CustomEvent<{ date: string; guests: number; time: string }>;
|
|
21
|
+
}
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,45 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgebuild/hotal-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Reusable Svelte UI components for booking and reservation experiences.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist"
|
|
9
9
|
],
|
|
10
|
-
"main": "./dist/
|
|
11
|
-
"module": "./dist/
|
|
12
|
-
"svelte": "./dist/index.js",
|
|
10
|
+
"main": "./dist/hotal-ui.js",
|
|
11
|
+
"module": "./dist/hotal-ui.js",
|
|
13
12
|
"types": "./dist/index.d.ts",
|
|
14
13
|
"exports": {
|
|
15
14
|
".": {
|
|
16
15
|
"types": "./dist/index.d.ts",
|
|
17
|
-
"
|
|
18
|
-
"default": "./dist/index.js"
|
|
16
|
+
"import": "./dist/hotal-ui.js"
|
|
19
17
|
}
|
|
20
18
|
},
|
|
21
19
|
"publishConfig": {
|
|
22
20
|
"access": "public"
|
|
23
21
|
},
|
|
24
|
-
"peerDependencies": {
|
|
25
|
-
"svelte": "^5.0.0"
|
|
26
|
-
},
|
|
27
22
|
"dependencies": {
|
|
28
|
-
"@floating-ui/dom": "^1.8.0"
|
|
23
|
+
"@floating-ui/dom": "^1.8.0",
|
|
24
|
+
"svelte": "^5.56.8"
|
|
29
25
|
},
|
|
30
26
|
"devDependencies": {
|
|
31
|
-
"@sveltejs/package": "^2.5.8",
|
|
32
27
|
"@sveltejs/vite-plugin-svelte": "^7.2.0",
|
|
33
28
|
"@tsconfig/svelte": "^5.0.8",
|
|
34
29
|
"@types/node": "^24.13.3",
|
|
35
|
-
"svelte": "^5.56.8",
|
|
36
30
|
"svelte-check": "^4.7.3",
|
|
37
31
|
"typescript": "~6.0.2",
|
|
38
32
|
"vite": "^8.2.0"
|
|
39
33
|
},
|
|
40
34
|
"scripts": {
|
|
41
35
|
"dev": "vite",
|
|
42
|
-
"build": "
|
|
36
|
+
"build": "vite build && node scripts/copy-types.mjs",
|
|
43
37
|
"prepublishOnly": "npm run build",
|
|
44
38
|
"preview": "vite preview",
|
|
45
39
|
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
|
|
@@ -1,489 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { computePosition, autoUpdate, flip, shift, offset } from '@floating-ui/dom';
|
|
3
|
-
import {
|
|
4
|
-
buildCalendarGrid,
|
|
5
|
-
resolveInitialDate,
|
|
6
|
-
formatDateLabel,
|
|
7
|
-
toISODate,
|
|
8
|
-
startOfDay,
|
|
9
|
-
MONTH_NAMES,
|
|
10
|
-
WEEKDAY_LABELS,
|
|
11
|
-
type CalendarCell,
|
|
12
|
-
} from './lib/calendar';
|
|
13
|
-
|
|
14
|
-
interface Props {
|
|
15
|
-
minGuests?: number;
|
|
16
|
-
maxGuests?: number;
|
|
17
|
-
defaultGuests?: number;
|
|
18
|
-
timeSlots?: string[];
|
|
19
|
-
defaultTime?: string;
|
|
20
|
-
showReserveButton?: boolean;
|
|
21
|
-
reserveHref?: string;
|
|
22
|
-
syncWithUrl?: boolean;
|
|
23
|
-
closedWeekdays?: number[];
|
|
24
|
-
fetchAvailableDates?: (partySize: number) => Promise<string[]>;
|
|
25
|
-
onchange?: (detail: { date: string; guests: number; time: string }) => void;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
let {
|
|
29
|
-
minGuests = 1,
|
|
30
|
-
maxGuests = 20,
|
|
31
|
-
defaultGuests = 2,
|
|
32
|
-
timeSlots = [
|
|
33
|
-
'7:00 PM', '7:30 PM', '8:00 PM', '8:30 PM', '9:00 PM', '9:30 PM',
|
|
34
|
-
'10:00 PM', '10:30 PM', '11:00 PM',
|
|
35
|
-
],
|
|
36
|
-
defaultTime = '7:00 PM',
|
|
37
|
-
showReserveButton = true,
|
|
38
|
-
reserveHref = '/reservations',
|
|
39
|
-
syncWithUrl = false,
|
|
40
|
-
closedWeekdays = [],
|
|
41
|
-
fetchAvailableDates,
|
|
42
|
-
onchange,
|
|
43
|
-
}: Props = $props();
|
|
44
|
-
|
|
45
|
-
const today = startOfDay(new Date());
|
|
46
|
-
const urlParams = syncWithUrl && typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : null;
|
|
47
|
-
const initialDate = resolveInitialDate(today, closedWeekdays, urlParams?.get('date') ?? null);
|
|
48
|
-
|
|
49
|
-
let viewYear = $state(initialDate.getFullYear());
|
|
50
|
-
let viewMonth = $state(initialDate.getMonth());
|
|
51
|
-
let selectedDate = $state<Date>(initialDate);
|
|
52
|
-
|
|
53
|
-
const urlGuests = urlParams ? Number(urlParams.get('guests')) : NaN;
|
|
54
|
-
let guests = $state(!Number.isNaN(urlGuests) && urlGuests > 0 ? urlGuests : defaultGuests);
|
|
55
|
-
|
|
56
|
-
const urlTime = urlParams?.get('time');
|
|
57
|
-
let time = $state(urlTime || defaultTime);
|
|
58
|
-
|
|
59
|
-
let openPanel = $state<'date' | 'guests' | 'time' | null>(null);
|
|
60
|
-
|
|
61
|
-
let availableDates = $state<Set<string> | null>(null);
|
|
62
|
-
|
|
63
|
-
async function refreshAvailability(partySize: number) {
|
|
64
|
-
if (!fetchAvailableDates) return;
|
|
65
|
-
try {
|
|
66
|
-
const dates = await fetchAvailableDates(partySize);
|
|
67
|
-
availableDates = new Set(dates);
|
|
68
|
-
} catch {
|
|
69
|
-
availableDates = new Set();
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
$effect(() => {
|
|
74
|
-
refreshAvailability(guests);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
let dateTrigger = $state<HTMLButtonElement>();
|
|
78
|
-
let guestsTrigger = $state<HTMLButtonElement>();
|
|
79
|
-
let timeTrigger = $state<HTMLButtonElement>();
|
|
80
|
-
let datePanel = $state<HTMLElement>();
|
|
81
|
-
let guestsPanel = $state<HTMLElement>();
|
|
82
|
-
let timePanel = $state<HTMLElement>();
|
|
83
|
-
|
|
84
|
-
const dateLabel = $derived(formatDateLabel(selectedDate));
|
|
85
|
-
const dateIso = $derived(toISODate(selectedDate));
|
|
86
|
-
const calendarCells = $derived(buildCalendarGrid(viewYear, viewMonth, today, closedWeekdays));
|
|
87
|
-
const canGoPrevMonth = $derived(!(viewYear === today.getFullYear() && viewMonth === today.getMonth()));
|
|
88
|
-
const guestOptions = $derived(Array.from({ length: maxGuests - minGuests + 1 }, (_, i) => minGuests + i));
|
|
89
|
-
|
|
90
|
-
const reserveUrl = $derived.by(() => {
|
|
91
|
-
const params = new URLSearchParams();
|
|
92
|
-
params.set('date', dateIso);
|
|
93
|
-
params.set('guests', String(guests));
|
|
94
|
-
params.set('time', time);
|
|
95
|
-
return `${reserveHref}?${params.toString()}`;
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
$effect(() => {
|
|
99
|
-
onchange?.({ date: dateIso, guests, time });
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
function isCellDisabled(cell: CalendarCell): boolean {
|
|
103
|
-
if (!cell.inMonth || cell.isPast || cell.isClosedWeekday) return true;
|
|
104
|
-
if (fetchAvailableDates && availableDates !== null && !availableDates.has(cell.iso)) return true;
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function applyIfAuto() {
|
|
109
|
-
if (!showReserveButton && typeof window !== 'undefined') {
|
|
110
|
-
window.location.href = reserveUrl;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function selectDate(cell: CalendarCell) {
|
|
115
|
-
if (isCellDisabled(cell)) return;
|
|
116
|
-
selectedDate = cell.date;
|
|
117
|
-
closePanel();
|
|
118
|
-
applyIfAuto();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function prevMonth() {
|
|
122
|
-
if (!canGoPrevMonth) return;
|
|
123
|
-
viewMonth -= 1;
|
|
124
|
-
if (viewMonth < 0) {
|
|
125
|
-
viewMonth = 11;
|
|
126
|
-
viewYear -= 1;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function nextMonth() {
|
|
131
|
-
viewMonth += 1;
|
|
132
|
-
if (viewMonth > 11) {
|
|
133
|
-
viewMonth = 0;
|
|
134
|
-
viewYear += 1;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function selectGuests(value: number) {
|
|
139
|
-
guests = value;
|
|
140
|
-
closePanel();
|
|
141
|
-
applyIfAuto();
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function selectTime(value: string) {
|
|
145
|
-
time = value;
|
|
146
|
-
closePanel();
|
|
147
|
-
applyIfAuto();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function closePanel() {
|
|
151
|
-
openPanel = null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function togglePanel(panel: 'date' | 'guests' | 'time') {
|
|
155
|
-
openPanel = openPanel === panel ? null : panel;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function triggerFor(panel: 'date' | 'guests' | 'time') {
|
|
159
|
-
return panel === 'date' ? dateTrigger : panel === 'guests' ? guestsTrigger : timeTrigger;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function panelElFor(panel: 'date' | 'guests' | 'time') {
|
|
163
|
-
return panel === 'date' ? datePanel : panel === 'guests' ? guestsPanel : timePanel;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Panels use the native Popover API (top-layer rendering): they escape
|
|
167
|
-
// any transformed/overflow-hidden ancestor without being moved in the
|
|
168
|
-
// DOM, and outside-click / Escape dismissal come from the browser for
|
|
169
|
-
// free. Floating UI keeps the open panel positioned next to its trigger
|
|
170
|
-
// through scroll/resize.
|
|
171
|
-
$effect(() => {
|
|
172
|
-
const panel = openPanel;
|
|
173
|
-
if (!panel) return;
|
|
174
|
-
const triggerEl = triggerFor(panel);
|
|
175
|
-
const panelEl = panelElFor(panel);
|
|
176
|
-
if (!triggerEl || !panelEl) return;
|
|
177
|
-
|
|
178
|
-
if (panelEl.hasAttribute('data-match-trigger-width')) {
|
|
179
|
-
panelEl.style.width = `${triggerEl.getBoundingClientRect().width}px`;
|
|
180
|
-
}
|
|
181
|
-
panelEl.showPopover();
|
|
182
|
-
|
|
183
|
-
const stopAutoUpdate = autoUpdate(triggerEl, panelEl, async () => {
|
|
184
|
-
const { x, y } = await computePosition(triggerEl, panelEl, {
|
|
185
|
-
strategy: 'fixed',
|
|
186
|
-
placement: 'bottom-start',
|
|
187
|
-
middleware: [offset(8), flip(), shift({ padding: 12 })],
|
|
188
|
-
});
|
|
189
|
-
panelEl.style.left = `${x}px`;
|
|
190
|
-
panelEl.style.top = `${y}px`;
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
function onToggle(e: Event) {
|
|
194
|
-
if ((e as ToggleEvent).newState === 'closed' && openPanel === panel) {
|
|
195
|
-
openPanel = null;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
panelEl.addEventListener('toggle', onToggle);
|
|
199
|
-
|
|
200
|
-
return () => {
|
|
201
|
-
stopAutoUpdate();
|
|
202
|
-
panelEl.removeEventListener('toggle', onToggle);
|
|
203
|
-
if (panelEl.matches(':popover-open')) panelEl.hidePopover();
|
|
204
|
-
};
|
|
205
|
-
});
|
|
206
|
-
</script>
|
|
207
|
-
|
|
208
|
-
<div class="rb-bar">
|
|
209
|
-
<button
|
|
210
|
-
bind:this={dateTrigger}
|
|
211
|
-
type="button"
|
|
212
|
-
class="rb-trigger"
|
|
213
|
-
aria-haspopup="true"
|
|
214
|
-
aria-expanded={openPanel === 'date'}
|
|
215
|
-
onclick={() => togglePanel('date')}
|
|
216
|
-
>
|
|
217
|
-
<span class="rb-trigger-label">Date</span>
|
|
218
|
-
<span class="rb-trigger-value">{dateLabel}</span>
|
|
219
|
-
</button>
|
|
220
|
-
<button
|
|
221
|
-
bind:this={guestsTrigger}
|
|
222
|
-
type="button"
|
|
223
|
-
class="rb-trigger"
|
|
224
|
-
aria-haspopup="true"
|
|
225
|
-
aria-expanded={openPanel === 'guests'}
|
|
226
|
-
onclick={() => togglePanel('guests')}
|
|
227
|
-
>
|
|
228
|
-
<span class="rb-trigger-label">Guests</span>
|
|
229
|
-
<span class="rb-trigger-value">{guests} Guest{guests === 1 ? '' : 's'}</span>
|
|
230
|
-
</button>
|
|
231
|
-
<button
|
|
232
|
-
bind:this={timeTrigger}
|
|
233
|
-
type="button"
|
|
234
|
-
class="rb-trigger"
|
|
235
|
-
aria-haspopup="true"
|
|
236
|
-
aria-expanded={openPanel === 'time'}
|
|
237
|
-
onclick={() => togglePanel('time')}
|
|
238
|
-
>
|
|
239
|
-
<span class="rb-trigger-label">Time</span>
|
|
240
|
-
<span class="rb-trigger-value">{time}</span>
|
|
241
|
-
</button>
|
|
242
|
-
{#if showReserveButton}
|
|
243
|
-
<a class="rb-reserve" href={reserveUrl}>Reserve</a>
|
|
244
|
-
{/if}
|
|
245
|
-
</div>
|
|
246
|
-
|
|
247
|
-
<div bind:this={datePanel} popover class="rb-panel rb-panel-date">
|
|
248
|
-
<div class="rb-cal-header">
|
|
249
|
-
<p class="rb-cal-month">{MONTH_NAMES[viewMonth]} {viewYear}</p>
|
|
250
|
-
<div class="rb-cal-nav">
|
|
251
|
-
<button type="button" onclick={prevMonth} disabled={!canGoPrevMonth} aria-label="Previous month">‹</button>
|
|
252
|
-
<button type="button" onclick={nextMonth} aria-label="Next month">›</button>
|
|
253
|
-
</div>
|
|
254
|
-
</div>
|
|
255
|
-
<div class="rb-cal-weekdays">
|
|
256
|
-
{#each WEEKDAY_LABELS as label (label)}
|
|
257
|
-
<span>{label}</span>
|
|
258
|
-
{/each}
|
|
259
|
-
</div>
|
|
260
|
-
<div class="rb-cal-grid">
|
|
261
|
-
{#each calendarCells as cell (cell.iso)}
|
|
262
|
-
<button
|
|
263
|
-
type="button"
|
|
264
|
-
class="rb-cal-cell"
|
|
265
|
-
class:rb-cal-cell-selected={dateIso === cell.iso}
|
|
266
|
-
disabled={isCellDisabled(cell)}
|
|
267
|
-
onclick={() => selectDate(cell)}
|
|
268
|
-
>
|
|
269
|
-
{cell.dayNumber}
|
|
270
|
-
</button>
|
|
271
|
-
{/each}
|
|
272
|
-
</div>
|
|
273
|
-
</div>
|
|
274
|
-
|
|
275
|
-
<ul bind:this={guestsPanel} popover data-match-trigger-width role="listbox" aria-label="Select number of guests" class="rb-panel rb-panel-list">
|
|
276
|
-
{#each guestOptions as n (n)}
|
|
277
|
-
<li role="presentation">
|
|
278
|
-
<button type="button" role="option" aria-selected={n === guests} onclick={() => selectGuests(n)}>
|
|
279
|
-
{n} Guest{n === 1 ? '' : 's'}
|
|
280
|
-
</button>
|
|
281
|
-
</li>
|
|
282
|
-
{/each}
|
|
283
|
-
</ul>
|
|
284
|
-
|
|
285
|
-
<ul bind:this={timePanel} popover data-match-trigger-width role="listbox" aria-label="Select a time" class="rb-panel rb-panel-list">
|
|
286
|
-
{#each timeSlots as slot (slot)}
|
|
287
|
-
<li role="presentation">
|
|
288
|
-
<button type="button" role="option" aria-selected={slot === time} onclick={() => selectTime(slot)}>
|
|
289
|
-
{slot}
|
|
290
|
-
</button>
|
|
291
|
-
</li>
|
|
292
|
-
{/each}
|
|
293
|
-
</ul>
|
|
294
|
-
|
|
295
|
-
<style>
|
|
296
|
-
.rb-bar {
|
|
297
|
-
display: flex;
|
|
298
|
-
width: 100%;
|
|
299
|
-
flex-direction: column;
|
|
300
|
-
align-items: stretch;
|
|
301
|
-
overflow: hidden;
|
|
302
|
-
border-radius: var(--rb-radius, 2px);
|
|
303
|
-
border: 1px solid var(--rb-bar-border, rgba(255, 255, 255, 0.25));
|
|
304
|
-
background: var(--rb-bar-bg, rgba(0, 0, 0, 0.3));
|
|
305
|
-
backdrop-filter: blur(12px);
|
|
306
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
307
|
-
}
|
|
308
|
-
@media (min-width: 640px) {
|
|
309
|
-
.rb-bar {
|
|
310
|
-
flex-direction: row;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
.rb-trigger {
|
|
314
|
-
display: flex;
|
|
315
|
-
flex: 1;
|
|
316
|
-
cursor: pointer;
|
|
317
|
-
flex-direction: column;
|
|
318
|
-
align-items: center;
|
|
319
|
-
gap: 2px;
|
|
320
|
-
padding: 0.75rem 1.5rem;
|
|
321
|
-
text-align: left;
|
|
322
|
-
background: transparent;
|
|
323
|
-
border: none;
|
|
324
|
-
border-top: 1px solid var(--rb-bar-border, rgba(255, 255, 255, 0.2));
|
|
325
|
-
color: var(--rb-text-color, #fff);
|
|
326
|
-
transition: background-color 150ms ease;
|
|
327
|
-
}
|
|
328
|
-
.rb-trigger:first-child {
|
|
329
|
-
border-top: none;
|
|
330
|
-
}
|
|
331
|
-
@media (min-width: 640px) {
|
|
332
|
-
.rb-trigger {
|
|
333
|
-
border-top: none;
|
|
334
|
-
border-right: 1px solid var(--rb-bar-border, rgba(255, 255, 255, 0.2));
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
.rb-trigger:hover {
|
|
338
|
-
background: var(--rb-trigger-hover-bg, rgba(255, 255, 255, 0.1));
|
|
339
|
-
}
|
|
340
|
-
.rb-trigger-label {
|
|
341
|
-
font-size: 10px;
|
|
342
|
-
font-weight: 300;
|
|
343
|
-
text-transform: uppercase;
|
|
344
|
-
letter-spacing: 0.1em;
|
|
345
|
-
}
|
|
346
|
-
.rb-trigger-value {
|
|
347
|
-
font-size: 0.875rem;
|
|
348
|
-
font-weight: 600;
|
|
349
|
-
white-space: nowrap;
|
|
350
|
-
}
|
|
351
|
-
.rb-reserve {
|
|
352
|
-
display: flex;
|
|
353
|
-
align-items: center;
|
|
354
|
-
justify-content: center;
|
|
355
|
-
padding: 1rem 2rem;
|
|
356
|
-
font-size: 1rem;
|
|
357
|
-
font-weight: 600;
|
|
358
|
-
text-transform: uppercase;
|
|
359
|
-
letter-spacing: 0.1em;
|
|
360
|
-
color: #fff;
|
|
361
|
-
text-decoration: none;
|
|
362
|
-
background: var(--rb-accent-bg, #1c3f60);
|
|
363
|
-
transition: background-color 150ms ease;
|
|
364
|
-
}
|
|
365
|
-
.rb-reserve:hover {
|
|
366
|
-
background: var(--rb-accent-hover-bg, #2a557f);
|
|
367
|
-
}
|
|
368
|
-
.rb-panel {
|
|
369
|
-
position: fixed;
|
|
370
|
-
inset: auto;
|
|
371
|
-
margin: 0;
|
|
372
|
-
border-radius: var(--rb-radius, 2px);
|
|
373
|
-
border: 1px solid var(--rb-bar-border, rgba(255, 255, 255, 0.2));
|
|
374
|
-
background: var(--rb-panel-bg, rgba(10, 10, 12, 0.92));
|
|
375
|
-
backdrop-filter: blur(12px);
|
|
376
|
-
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
|
|
377
|
-
color: var(--rb-text-color, #fff);
|
|
378
|
-
}
|
|
379
|
-
.rb-panel-date {
|
|
380
|
-
width: 288px;
|
|
381
|
-
padding: 1rem;
|
|
382
|
-
}
|
|
383
|
-
.rb-cal-header {
|
|
384
|
-
display: flex;
|
|
385
|
-
align-items: center;
|
|
386
|
-
justify-content: space-between;
|
|
387
|
-
}
|
|
388
|
-
.rb-cal-month {
|
|
389
|
-
margin: 0;
|
|
390
|
-
font-size: 0.875rem;
|
|
391
|
-
font-weight: 600;
|
|
392
|
-
}
|
|
393
|
-
.rb-cal-nav {
|
|
394
|
-
display: flex;
|
|
395
|
-
gap: 0.25rem;
|
|
396
|
-
}
|
|
397
|
-
.rb-cal-nav button {
|
|
398
|
-
display: flex;
|
|
399
|
-
height: 28px;
|
|
400
|
-
width: 28px;
|
|
401
|
-
align-items: center;
|
|
402
|
-
justify-content: center;
|
|
403
|
-
border-radius: 9999px;
|
|
404
|
-
border: none;
|
|
405
|
-
background: transparent;
|
|
406
|
-
color: inherit;
|
|
407
|
-
cursor: pointer;
|
|
408
|
-
}
|
|
409
|
-
.rb-cal-nav button:hover:not(:disabled) {
|
|
410
|
-
background: var(--rb-trigger-hover-bg, rgba(255, 255, 255, 0.1));
|
|
411
|
-
}
|
|
412
|
-
.rb-cal-nav button:disabled {
|
|
413
|
-
cursor: default;
|
|
414
|
-
opacity: 0.3;
|
|
415
|
-
}
|
|
416
|
-
.rb-cal-weekdays {
|
|
417
|
-
display: grid;
|
|
418
|
-
grid-template-columns: repeat(7, 1fr);
|
|
419
|
-
margin-top: 0.75rem;
|
|
420
|
-
text-align: center;
|
|
421
|
-
font-size: 10px;
|
|
422
|
-
font-weight: 600;
|
|
423
|
-
text-transform: uppercase;
|
|
424
|
-
letter-spacing: 0.1em;
|
|
425
|
-
opacity: 0.4;
|
|
426
|
-
}
|
|
427
|
-
.rb-cal-grid {
|
|
428
|
-
display: grid;
|
|
429
|
-
grid-template-columns: repeat(7, 1fr);
|
|
430
|
-
row-gap: 0.25rem;
|
|
431
|
-
justify-items: center;
|
|
432
|
-
margin-top: 0.25rem;
|
|
433
|
-
}
|
|
434
|
-
.rb-cal-cell {
|
|
435
|
-
display: flex;
|
|
436
|
-
height: 32px;
|
|
437
|
-
width: 32px;
|
|
438
|
-
align-items: center;
|
|
439
|
-
justify-content: center;
|
|
440
|
-
border-radius: 9999px;
|
|
441
|
-
border: none;
|
|
442
|
-
background: transparent;
|
|
443
|
-
color: inherit;
|
|
444
|
-
font-size: 0.875rem;
|
|
445
|
-
cursor: pointer;
|
|
446
|
-
}
|
|
447
|
-
.rb-cal-cell:hover:not(:disabled) {
|
|
448
|
-
background: var(--rb-trigger-hover-bg, rgba(255, 255, 255, 0.1));
|
|
449
|
-
}
|
|
450
|
-
.rb-cal-cell:disabled {
|
|
451
|
-
cursor: default;
|
|
452
|
-
opacity: 0.25;
|
|
453
|
-
}
|
|
454
|
-
.rb-cal-cell-selected {
|
|
455
|
-
background: var(--rb-text-color, #fff);
|
|
456
|
-
color: var(--rb-bar-bg, #111);
|
|
457
|
-
}
|
|
458
|
-
.rb-panel-list {
|
|
459
|
-
max-height: 16rem;
|
|
460
|
-
width: 10rem;
|
|
461
|
-
list-style: none;
|
|
462
|
-
overflow: auto;
|
|
463
|
-
padding: 0.25rem 0;
|
|
464
|
-
scrollbar-width: thin;
|
|
465
|
-
scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
|
|
466
|
-
}
|
|
467
|
-
.rb-panel-list li {
|
|
468
|
-
display: block;
|
|
469
|
-
}
|
|
470
|
-
.rb-panel-list button {
|
|
471
|
-
display: block;
|
|
472
|
-
width: 100%;
|
|
473
|
-
cursor: pointer;
|
|
474
|
-
white-space: nowrap;
|
|
475
|
-
border: none;
|
|
476
|
-
background: transparent;
|
|
477
|
-
color: inherit;
|
|
478
|
-
padding: 0.5rem 1rem;
|
|
479
|
-
text-align: left;
|
|
480
|
-
font-size: 0.875rem;
|
|
481
|
-
}
|
|
482
|
-
.rb-panel-list button:hover {
|
|
483
|
-
background: var(--rb-trigger-hover-bg, rgba(255, 255, 255, 0.1));
|
|
484
|
-
}
|
|
485
|
-
.rb-panel-list button[aria-selected='true'] {
|
|
486
|
-
background: rgba(255, 255, 255, 0.15);
|
|
487
|
-
font-weight: 600;
|
|
488
|
-
}
|
|
489
|
-
</style>
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
interface Props {
|
|
2
|
-
minGuests?: number;
|
|
3
|
-
maxGuests?: number;
|
|
4
|
-
defaultGuests?: number;
|
|
5
|
-
timeSlots?: string[];
|
|
6
|
-
defaultTime?: string;
|
|
7
|
-
showReserveButton?: boolean;
|
|
8
|
-
reserveHref?: string;
|
|
9
|
-
syncWithUrl?: boolean;
|
|
10
|
-
closedWeekdays?: number[];
|
|
11
|
-
fetchAvailableDates?: (partySize: number) => Promise<string[]>;
|
|
12
|
-
onchange?: (detail: {
|
|
13
|
-
date: string;
|
|
14
|
-
guests: number;
|
|
15
|
-
time: string;
|
|
16
|
-
}) => void;
|
|
17
|
-
}
|
|
18
|
-
declare const ReservationBar: import("svelte").Component<Props, {}, "">;
|
|
19
|
-
type ReservationBar = ReturnType<typeof ReservationBar>;
|
|
20
|
-
export default ReservationBar;
|
package/dist/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { default as ReservationBar } from './ReservationBar.svelte';
|
package/dist/lib/calendar.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export interface CalendarCell {
|
|
2
|
-
date: Date;
|
|
3
|
-
iso: string;
|
|
4
|
-
dayNumber: number;
|
|
5
|
-
inMonth: boolean;
|
|
6
|
-
isPast: boolean;
|
|
7
|
-
isClosedWeekday: boolean;
|
|
8
|
-
}
|
|
9
|
-
export declare const MONTH_NAMES: string[];
|
|
10
|
-
export declare const WEEKDAY_LABELS: string[];
|
|
11
|
-
export declare function toISODate(date: Date): string;
|
|
12
|
-
export declare function startOfDay(date: Date): Date;
|
|
13
|
-
export declare function formatDateLabel(date: Date): string;
|
|
14
|
-
/**
|
|
15
|
-
* Picks the initial selected date: the URL-provided date if valid and not
|
|
16
|
-
* in the past, otherwise today — walking forward past any closed weekdays
|
|
17
|
-
* so the picker never defaults to a day guaranteed to show as closed.
|
|
18
|
-
*/
|
|
19
|
-
export declare function resolveInitialDate(today: Date, closedWeekdays: number[], urlDateIso: string | null): Date;
|
|
20
|
-
export declare function buildCalendarGrid(viewYear: number, viewMonth: number, today: Date, closedWeekdays: number[]): CalendarCell[];
|
package/dist/lib/calendar.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
export const MONTH_NAMES = [
|
|
2
|
-
'January', 'February', 'March', 'April', 'May', 'June',
|
|
3
|
-
'July', 'August', 'September', 'October', 'November', 'December',
|
|
4
|
-
];
|
|
5
|
-
export const WEEKDAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
6
|
-
export function toISODate(date) {
|
|
7
|
-
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
8
|
-
}
|
|
9
|
-
export function startOfDay(date) {
|
|
10
|
-
const d = new Date(date);
|
|
11
|
-
d.setHours(0, 0, 0, 0);
|
|
12
|
-
return d;
|
|
13
|
-
}
|
|
14
|
-
export function formatDateLabel(date) {
|
|
15
|
-
const weekday = date.toLocaleDateString('en-US', { weekday: 'short' });
|
|
16
|
-
const month = date.toLocaleDateString('en-US', { month: 'short' });
|
|
17
|
-
return `${weekday}, ${date.getDate()} ${month}`;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Picks the initial selected date: the URL-provided date if valid and not
|
|
21
|
-
* in the past, otherwise today — walking forward past any closed weekdays
|
|
22
|
-
* so the picker never defaults to a day guaranteed to show as closed.
|
|
23
|
-
*/
|
|
24
|
-
export function resolveInitialDate(today, closedWeekdays, urlDateIso) {
|
|
25
|
-
let initial = startOfDay(today);
|
|
26
|
-
if (urlDateIso) {
|
|
27
|
-
const parsed = startOfDay(new Date(`${urlDateIso}T00:00:00`));
|
|
28
|
-
if (!isNaN(parsed.getTime()) && parsed >= startOfDay(today)) {
|
|
29
|
-
initial = parsed;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
let guard = 0;
|
|
33
|
-
while (closedWeekdays.includes(initial.getDay()) && guard < 7) {
|
|
34
|
-
initial = new Date(initial);
|
|
35
|
-
initial.setDate(initial.getDate() + 1);
|
|
36
|
-
guard++;
|
|
37
|
-
}
|
|
38
|
-
return initial;
|
|
39
|
-
}
|
|
40
|
-
export function buildCalendarGrid(viewYear, viewMonth, today, closedWeekdays) {
|
|
41
|
-
const firstOfMonth = new Date(viewYear, viewMonth, 1);
|
|
42
|
-
const startDay = firstOfMonth.getDay();
|
|
43
|
-
const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
|
|
44
|
-
const daysInPrevMonth = new Date(viewYear, viewMonth, 0).getDate();
|
|
45
|
-
const cells = [];
|
|
46
|
-
for (let i = 0; i < 42; i++) {
|
|
47
|
-
const dayNum = i - startDay + 1;
|
|
48
|
-
let cellDate;
|
|
49
|
-
let inMonth = true;
|
|
50
|
-
if (dayNum < 1) {
|
|
51
|
-
cellDate = new Date(viewYear, viewMonth - 1, daysInPrevMonth + dayNum);
|
|
52
|
-
inMonth = false;
|
|
53
|
-
}
|
|
54
|
-
else if (dayNum > daysInMonth) {
|
|
55
|
-
cellDate = new Date(viewYear, viewMonth + 1, dayNum - daysInMonth);
|
|
56
|
-
inMonth = false;
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
cellDate = new Date(viewYear, viewMonth, dayNum);
|
|
60
|
-
}
|
|
61
|
-
cellDate = startOfDay(cellDate);
|
|
62
|
-
cells.push({
|
|
63
|
-
date: cellDate,
|
|
64
|
-
iso: toISODate(cellDate),
|
|
65
|
-
dayNumber: cellDate.getDate(),
|
|
66
|
-
inMonth,
|
|
67
|
-
isPast: cellDate < today,
|
|
68
|
-
isClosedWeekday: closedWeekdays.includes(cellDate.getDay()),
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
return cells;
|
|
72
|
-
}
|