@joewinke/jatui 0.41.0 → 0.42.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/CLAUDE.md +9 -3
- package/dist/components/AnimatedNumber.svelte +22 -4
- package/dist/components/BookingScheduler.svelte +412 -0
- package/dist/components/BookingScheduler.svelte.d.ts +66 -0
- package/dist/components/CalendarPicker.svelte +190 -205
- package/dist/components/CalendarPicker.svelte.d.ts +19 -1
- package/dist/components/DatePicker.svelte +419 -0
- package/dist/components/DatePicker.svelte.d.ts +44 -0
- package/dist/components/DateRangePicker.svelte +471 -5
- package/dist/components/DateRangePicker.svelte.d.ts +32 -3
- package/dist/components/EventCalendar.svelte +1324 -0
- package/dist/components/EventCalendar.svelte.d.ts +101 -0
- package/dist/components/TimeSlotPicker.svelte +174 -108
- package/dist/components/TimeSlotPicker.svelte.d.ts +27 -0
- package/dist/components/calendar/CalendarHeader.svelte +98 -0
- package/dist/components/calendar/CalendarHeader.svelte.d.ts +24 -0
- package/dist/components/calendar/MonthGrid.svelte +361 -0
- package/dist/components/calendar/MonthGrid.svelte.d.ts +55 -0
- package/dist/components/calendar/calendarGrid.d.ts +219 -0
- package/dist/components/calendar/calendarGrid.js +494 -0
- package/dist/components/calendar/createCalendarGrid.d.ts +25 -0
- package/dist/components/calendar/createCalendarGrid.js +57 -0
- package/dist/components/calendar/dayOverlap.d.ts +92 -0
- package/dist/components/calendar/dayOverlap.js +147 -0
- package/dist/components/calendar/eventCalendar.d.ts +75 -0
- package/dist/components/calendar/eventCalendar.js +187 -0
- package/dist/components/calendar/index.d.ts +32 -0
- package/dist/components/calendar/index.js +25 -0
- package/dist/components/calendar/monthPacking.d.ts +97 -0
- package/dist/components/calendar/monthPacking.js +254 -0
- package/dist/components/calendar/timeSlots.d.ts +57 -0
- package/dist/components/calendar/timeSlots.js +163 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +11 -0
- package/dist/showcase/Globe.svelte +1 -1
- package/package.json +6 -1
package/CLAUDE.md
CHANGED
|
@@ -46,13 +46,19 @@ Then import:
|
|
|
46
46
|
|
|
47
47
|
| Component | Purpose | Key Props |
|
|
48
48
|
|-----------|---------|-----------|
|
|
49
|
-
| `CalendarPicker` |
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
49
|
+
| `CalendarPicker` | Booking month calendar on the shared grid: `role="grid"` with APG keys (arrows, Home/End, PageUp/PageDown, Shift+Page = year), availability dots in status tokens (announced in each day's name), booking-window rules, static accent map. Six-week `getAvailability` range as before. | `onSelect`, `selectedDate` (bindable), `getAvailability`, `isDateEnabled`, `maxDaysInAdvance` (90), `minNoticeHours` (24), `weekdaysOnly` (true), `accentColor`, `weekStart?` (0 \| 'locale'), `locale?`, `showLegend?`, `size?` |
|
|
50
|
+
| `DatePicker` | Date field replacing `<input type="date">`: trigger + `use:floating` popover grid, typed entry in the LOCALE's order (MM/DD/YYYY en-US, DD/MM/YYYY en-GB — commits on Enter/blur, reverts on Esc or unreadable input), Today, optional pending/Apply step. `name` posts the ISO value; `required` validates natively. | `value` (bindable `YYYY-MM-DD \| null`), `open` (bindable), `onChange(value, date)`, `label`, `name`, `min`/`max` (ISO or Date), `isDateEnabled`, `weekdaysOnly`, `weekStart` ('locale'), `locale`, `format`, `commit` ('instant' \| 'apply'), `typed`, `clearable`, `error`, `size` |
|
|
51
|
+
| `DateRangePicker` | Two modes. `presets` (default, unchanged): the filter-bar dropdown with string presets + native From/To. `mode="calendar"`: two-month range calendar — preset sidebar (Today … Last year + All time), hover/keyboard preview band, typed Start/End, "N days selected", Apply/Cancel. Calendar mode still calls `onRangeChange(presetId \| 'custom', from, to)`. | `selectedRange`, `customFrom`/`customTo`, `onRangeChange`, `compact` · calendar: `mode`, `value` (bindable `{start,end}` ISO), `onChange(range, presetId)`, `presets: RangePreset[]`, `months` (1\|2), `commit` ('apply'), `min`/`max`, `isDateEnabled`, `weekStart`, `nameStart`/`nameEnd` |
|
|
52
|
+
| `TimeSlotPicker` | Slots grouped morning/afternoon/evening as one `role="radiogroup"` (`role="radio"` + `aria-checked`, full spoken date+time): roving arrows skip unavailable slots, Up/Down jump a visual row, Home/End, Space/Enter choose. Times and grouping follow `timeZone`'s wall clock. | `slots: TimeSlot[]`, `onSelect`, `selectedSlot`, `loading`, `accentColor`, `hourCycle?` ('h12' \| 'h23'), `timeZone?` (IANA), `locale?`, `selectOnFocus?`, `timezone?` (snippet row above the slots) |
|
|
53
|
+
| `BookingScheduler` | Calendly-shaped composition over `CalendarPicker` + `TimeSlotPicker`: details pane (host, duration, location, zone), time-zone select + 12h/24h toggle, slot list, Confirm. Container-query layout (3 panes → 2 → 1). Host supplies data; nothing is booked here. | `getSlots(date, timeZone)` (sync/async, stale responses dropped), `getAvailability?`, `selectedDate`/`selectedSlot`/`timeZone`/`hourCycle` (bindable), `title`, `durationMinutes`, `hostName`, `location`, `description`, `details?` (snippet), `onConfirm(slot, {timeZone})`, `confirming` |
|
|
54
|
+
| `EventCalendar` | Month / week / day / agenda event calendar, engine upstreamed from MOS `CalendarGrid`: one lane allocator per month row (`packWeek`/`computeVisibility`, measured lane capacity), "+N more" expands the week IN PLACE, week/day time grid with side-by-side overlap layout (`layoutDay`) + all-day bar row + now-line, agenda list, month-switcher pill (the shared month panel), details popover snippet, HTML5 drag-to-move, drag- or Shift+arrow range select. Event colours from `--jatui-event-N{,-bg,-fg}`. Generic over your event type. | `events: CalendarEventItem[]` (`{id,title,start,end?,allDay?,color?: 1-6 \| semantic,…}`), `view`/`date` (bindable), `views`, `weekStart`, `locale`, `hourCycle`, `dayStartHour`/`dayEndHour`, `hourHeight`, `height`, `eventDetails?`/`eventContent?`/`toolbarEnd?` (snippets), `moreAction`, `onEventClick`, `onDayClick`, `onCreate(date, hour?)`, `onEventMove(ev, {date, hour?, allDay})`, `onRangeSelect`, `onRangeChange(range, view)` |
|
|
55
|
+
| `CalendarMonthGrid` | The shared month panel every picker uses — exported for custom surfaces. `<table role="grid">`, roving tabindex, range band that bridges cell gaps and rounds at week edges, disabled days stay focusable (`aria-disabled`), `day` snippet + `describeDay` for extra per-day content/name. Pair with `createCalendarGrid()`. | `month`, `focusedDate`, `onFocusDate`, `onSelect`, `selected?`, `range?`, `isDisabled?`, `min`/`max`, `weekStart`, `locale`, `showOutsideDays`, `fixedWeeks`, `size`, `onHover?`, `day?`, `describeDay?` |
|
|
52
56
|
| `AvailabilityModal` | Date override editor (modal) | `date`, `overrides`, `onSave`, `onClose` |
|
|
53
57
|
| `BookingForm` | Guest info form for booking | `onSubmit`, `submitting`, `requirePhone`, `accentColor`, `showAttendeeOption` |
|
|
54
58
|
| `CountdownTimer` | Countdown to target date | `targetDate: Date`, `label` |
|
|
55
59
|
|
|
60
|
+
**Calendar core (`jatui/calendar`, also on the root).** Plain `Date` + `Intl`, no date library. `createCalendarGrid({ initial?, min?, max?, months? })` (getters) → reactive `viewMonth`, `panels`, `focusedDate`, `canPrev`/`canNext`, `focus(date)` (panels follow), `shift(n)`, `reset(date)`. Pure helpers: `buildMonthMatrix(month, weekStart, fixedWeeks?)`, `nextFocusDate(date, key, {weekStart, min, max, rtl, shiftKey})` (the APG key map), `weekStartForLocale` (`Intl.Locale#getWeekInfo` + CLDR fallback), `parseTypedDate`/`formatTypedDate`/`datePlaceholder` (locale field order), `toIsoDate`/`parseIsoDate` (local days, never UTC-shifted), ranges (`pickRangeDate`, `rangeCellState`, `rangeDayCount`, `formatRangeLabel`), presets (`defaultRangePresets()`, `RangePreset = {id, label, range: IsoDateRange \| (now) => IsoDateRange}`, `matchPreset`), time slots (`formatSlotTime`, `groupSlotsByDayPart`, `nextSlotIndex`, `localeHourCycle`, `commonTimeZones`, `timeZoneOffsetLabel`), and the MOS event engine with its tests (`packWeek`, `computeVisibility`, `layoutDay`, `eventInterval` — generic over any `PackableEvent {id, title, start, end?, allDay?}`, so MOS's `CalendarEvent` fits as-is). Demos: `/components/date-picker`, `date-range-picker`, `calendar-picker`, `time-slot-picker`, `booking-scheduler`, `event-calendar`, `calendar-month-grid`.
|
|
61
|
+
|
|
56
62
|
### Display & Feedback
|
|
57
63
|
|
|
58
64
|
| Component | Purpose | Key Props |
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { untrack } from 'svelte';
|
|
3
|
-
import { Tween } from 'svelte/motion';
|
|
4
3
|
import { cubicOut } from 'svelte/easing';
|
|
5
4
|
import { getMotionContext, motionDuration } from '../motion/level';
|
|
6
5
|
|
|
@@ -21,13 +20,32 @@
|
|
|
21
20
|
|
|
22
21
|
// ── Tween mode ─────────────────────────────────────────────────────────
|
|
23
22
|
let tweenEl: HTMLSpanElement | undefined = $state();
|
|
24
|
-
|
|
23
|
+
// A hand-rolled rAF tween, NOT svelte/motion's Tween: importing svelte/motion
|
|
24
|
+
// creates a prefers-reduced-motion MediaQuery at MODULE load, which throws
|
|
25
|
+
// 'window.matchMedia is not a function' in any jsdom test that merely
|
|
26
|
+
// imports 'jatui' (jatui-oozkb). Reduced motion comes from motionDuration,
|
|
27
|
+
// which is 0 under prefers-reduced-motion.
|
|
28
|
+
let tweenCurrent = $state(untrack(() => value));
|
|
25
29
|
$effect(() => {
|
|
26
30
|
if (mode !== 'tween') return;
|
|
27
31
|
const target = value;
|
|
28
32
|
// Level-aware (data-motion) and instant under prefers-reduced-motion.
|
|
29
33
|
const ms = motionDuration(duration ?? 'move', getMotionContext(tweenEl ?? null));
|
|
30
|
-
|
|
34
|
+
const from = untrack(() => tweenCurrent);
|
|
35
|
+
if (ms <= 0 || from === target || typeof requestAnimationFrame !== 'function') {
|
|
36
|
+
tweenCurrent = target;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let frame = 0;
|
|
40
|
+
let start: number | null = null;
|
|
41
|
+
const step = (now: number) => {
|
|
42
|
+
start ??= now;
|
|
43
|
+
const t = Math.min(1, (now - start) / ms);
|
|
44
|
+
tweenCurrent = from + (target - from) * cubicOut(t);
|
|
45
|
+
if (t < 1) frame = requestAnimationFrame(step);
|
|
46
|
+
};
|
|
47
|
+
frame = requestAnimationFrame(step);
|
|
48
|
+
return () => cancelAnimationFrame(frame);
|
|
31
49
|
});
|
|
32
50
|
/** Decimals to show mid-tween: the target's own, unless `format` decides. */
|
|
33
51
|
const tweenDecimals = $derived.by(() => {
|
|
@@ -36,7 +54,7 @@
|
|
|
36
54
|
return s.includes('.') && !s.includes('e') ? s.split('.')[1].length : 0;
|
|
37
55
|
});
|
|
38
56
|
const tweenText = $derived.by(() => {
|
|
39
|
-
const v =
|
|
57
|
+
const v = tweenCurrent;
|
|
40
58
|
const shown = tweenDecimals == null ? v : Number(v.toFixed(tweenDecimals));
|
|
41
59
|
return formatNumber(shown, format);
|
|
42
60
|
});
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* BookingScheduler — the Calendly-shaped booking surface, composed from the
|
|
4
|
+
* existing jatui pieces: a details pane, `CalendarPicker` (availability dots,
|
|
5
|
+
* booking-window rules) and `TimeSlotPicker` (radiogroup keys, 12/24h, time
|
|
6
|
+
* zone), with a time-zone select and a 12h/24h toggle between them.
|
|
7
|
+
*
|
|
8
|
+
* The host owns the data: `getSlots(date, timeZone)` returns the slots for a
|
|
9
|
+
* day (sync or async; stale responses are dropped), `getAvailability` feeds
|
|
10
|
+
* the dots. Nothing is booked here — `onConfirm(slot, { timeZone })` hands the
|
|
11
|
+
* choice back, and `BookingForm` usually comes next.
|
|
12
|
+
*
|
|
13
|
+
* Layout follows its own width (container queries): three panes side by side
|
|
14
|
+
* when wide, details above calendar + times when medium, one column on a phone.
|
|
15
|
+
* Built from BoardUI's public meeting-scheduler behaviour (jatui-lfxyy); no
|
|
16
|
+
* BoardUI source is copied and there are no built-in names or mock slots.
|
|
17
|
+
*/
|
|
18
|
+
import type { Snippet } from 'svelte';
|
|
19
|
+
import type { AvailabilityCounts, TimeSlot } from '../types/booking';
|
|
20
|
+
import CalendarPicker from './CalendarPicker.svelte';
|
|
21
|
+
import TimeSlotPicker from './TimeSlotPicker.svelte';
|
|
22
|
+
import {
|
|
23
|
+
commonTimeZones,
|
|
24
|
+
formatSlotLabel,
|
|
25
|
+
localeHourCycle,
|
|
26
|
+
localTimeZone,
|
|
27
|
+
timeZoneCity,
|
|
28
|
+
timeZoneOffsetLabel,
|
|
29
|
+
type HourCycle
|
|
30
|
+
} from './calendar/timeSlots';
|
|
31
|
+
import type { Weekday } from './calendar/calendarGrid';
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
getSlots,
|
|
35
|
+
getAvailability,
|
|
36
|
+
isDateEnabled,
|
|
37
|
+
maxDaysInAdvance = 90,
|
|
38
|
+
minNoticeHours = 24,
|
|
39
|
+
weekdaysOnly = true,
|
|
40
|
+
selectedDate = $bindable(null),
|
|
41
|
+
selectedSlot = $bindable(null),
|
|
42
|
+
timeZone = $bindable(localTimeZone()),
|
|
43
|
+
hourCycle = $bindable(),
|
|
44
|
+
timeZones,
|
|
45
|
+
locale = 'en-US',
|
|
46
|
+
weekStart = 0,
|
|
47
|
+
title,
|
|
48
|
+
durationMinutes,
|
|
49
|
+
hostName,
|
|
50
|
+
hostAvatarUrl,
|
|
51
|
+
description,
|
|
52
|
+
location,
|
|
53
|
+
details,
|
|
54
|
+
showDetails = true,
|
|
55
|
+
showTimeZone = true,
|
|
56
|
+
showHourCycleToggle = true,
|
|
57
|
+
onSelectDate,
|
|
58
|
+
onSelectSlot,
|
|
59
|
+
onConfirm,
|
|
60
|
+
confirmLabel = 'Confirm',
|
|
61
|
+
confirming = false,
|
|
62
|
+
accentColor = 'primary',
|
|
63
|
+
class: className = ''
|
|
64
|
+
}: {
|
|
65
|
+
/** Slots for one day, in `timeZone`. Sync or async; a newer request wins. */
|
|
66
|
+
getSlots: (date: Date, timeZone: string) => TimeSlot[] | Promise<TimeSlot[]>;
|
|
67
|
+
getAvailability?: (startDate: Date, endDate: Date) => Promise<AvailabilityCounts>;
|
|
68
|
+
isDateEnabled?: (date: Date) => boolean;
|
|
69
|
+
maxDaysInAdvance?: number;
|
|
70
|
+
minNoticeHours?: number;
|
|
71
|
+
weekdaysOnly?: boolean;
|
|
72
|
+
selectedDate?: Date | null;
|
|
73
|
+
selectedSlot?: TimeSlot | null;
|
|
74
|
+
/** IANA zone the slots are shown in (bindable). Defaults to the viewer's zone. */
|
|
75
|
+
timeZone?: string;
|
|
76
|
+
/** 'h12' | 'h23' (bindable). Defaults to the locale's convention. */
|
|
77
|
+
hourCycle?: HourCycle;
|
|
78
|
+
/** Zones offered in the picker. Defaults to the viewer's zone + common ones. */
|
|
79
|
+
timeZones?: string[];
|
|
80
|
+
locale?: string;
|
|
81
|
+
weekStart?: Weekday | 'locale';
|
|
82
|
+
/** Meeting name, e.g. "Intro call". */
|
|
83
|
+
title?: string;
|
|
84
|
+
durationMinutes?: number;
|
|
85
|
+
hostName?: string;
|
|
86
|
+
hostAvatarUrl?: string;
|
|
87
|
+
description?: string;
|
|
88
|
+
/** "Video call", "Phone", an address… */
|
|
89
|
+
location?: string;
|
|
90
|
+
/** Replaces the built-in details pane. */
|
|
91
|
+
details?: Snippet;
|
|
92
|
+
showDetails?: boolean;
|
|
93
|
+
showTimeZone?: boolean;
|
|
94
|
+
showHourCycleToggle?: boolean;
|
|
95
|
+
onSelectDate?: (date: Date) => void;
|
|
96
|
+
onSelectSlot?: (slot: TimeSlot) => void;
|
|
97
|
+
/** Shows a Confirm button once a slot is chosen. */
|
|
98
|
+
onConfirm?: (slot: TimeSlot, ctx: { timeZone: string }) => void;
|
|
99
|
+
confirmLabel?: string;
|
|
100
|
+
/** Busy state for the Confirm button. */
|
|
101
|
+
confirming?: boolean;
|
|
102
|
+
accentColor?: string;
|
|
103
|
+
class?: string;
|
|
104
|
+
} = $props();
|
|
105
|
+
|
|
106
|
+
const cycle = $derived<HourCycle>(hourCycle ?? localeHourCycle(locale));
|
|
107
|
+
const zones = $derived(timeZones ?? commonTimeZones(timeZone ? [timeZone] : []));
|
|
108
|
+
const uid = `jbs-${Math.random().toString(36).slice(2, 8)}`;
|
|
109
|
+
|
|
110
|
+
let slots = $state<TimeSlot[]>([]);
|
|
111
|
+
let loading = $state(false);
|
|
112
|
+
let requestId = 0;
|
|
113
|
+
|
|
114
|
+
async function loadSlots(date: Date, zone: string) {
|
|
115
|
+
const id = ++requestId;
|
|
116
|
+
loading = true;
|
|
117
|
+
try {
|
|
118
|
+
const result = await getSlots(date, zone);
|
|
119
|
+
if (id !== requestId) return;
|
|
120
|
+
slots = result;
|
|
121
|
+
// Drop a selection the new list no longer offers.
|
|
122
|
+
if (selectedSlot && !result.some((s) => s.available && s.start.getTime() === selectedSlot!.start.getTime())) {
|
|
123
|
+
selectedSlot = null;
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
if (id === requestId) slots = [];
|
|
127
|
+
} finally {
|
|
128
|
+
if (id === requestId) loading = false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
$effect(() => {
|
|
133
|
+
if (selectedDate) void loadSlots(selectedDate, timeZone);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
function handleDate(date: Date) {
|
|
137
|
+
selectedDate = date;
|
|
138
|
+
selectedSlot = null;
|
|
139
|
+
onSelectDate?.(date);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function handleSlot(slot: TimeSlot) {
|
|
143
|
+
selectedSlot = slot;
|
|
144
|
+
onSelectSlot?.(slot);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const dayHeading = $derived(
|
|
148
|
+
selectedDate
|
|
149
|
+
? new Intl.DateTimeFormat(locale, { weekday: 'long', month: 'long', day: 'numeric' }).format(selectedDate)
|
|
150
|
+
: ''
|
|
151
|
+
);
|
|
152
|
+
const durationLabel = $derived.by(() => {
|
|
153
|
+
if (!durationMinutes) return '';
|
|
154
|
+
const h = Math.floor(durationMinutes / 60);
|
|
155
|
+
const m = durationMinutes % 60;
|
|
156
|
+
return [h ? `${h} hr` : '', m ? `${m} min` : ''].filter(Boolean).join(' ');
|
|
157
|
+
});
|
|
158
|
+
const initials = $derived(
|
|
159
|
+
(hostName ?? '')
|
|
160
|
+
.split(/\s+/)
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
.slice(0, 2)
|
|
163
|
+
.map((w) => w[0]?.toUpperCase())
|
|
164
|
+
.join('')
|
|
165
|
+
);
|
|
166
|
+
</script>
|
|
167
|
+
|
|
168
|
+
<div class="jatui-booking {className}" class:has-details={showDetails && (details || title || hostName)}>
|
|
169
|
+
{#if showDetails && (details || title || hostName || description)}
|
|
170
|
+
<section class="jatui-booking__details" aria-label="Meeting details">
|
|
171
|
+
{#if details}
|
|
172
|
+
{@render details()}
|
|
173
|
+
{:else}
|
|
174
|
+
{#if hostName}
|
|
175
|
+
<div class="jatui-booking__host">
|
|
176
|
+
{#if hostAvatarUrl}
|
|
177
|
+
<img src={hostAvatarUrl} alt="" class="jatui-booking__avatar" />
|
|
178
|
+
{:else}
|
|
179
|
+
<span class="jatui-booking__avatar jatui-booking__avatar--initials" aria-hidden="true">{initials}</span>
|
|
180
|
+
{/if}
|
|
181
|
+
<span class="jatui-booking__host-name">{hostName}</span>
|
|
182
|
+
</div>
|
|
183
|
+
{/if}
|
|
184
|
+
{#if title}<h2 class="jatui-booking__title">{title}</h2>{/if}
|
|
185
|
+
<ul class="jatui-booking__meta">
|
|
186
|
+
{#if durationLabel}
|
|
187
|
+
<li>
|
|
188
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true"><circle cx="12" cy="12" r="9" /><path stroke-linecap="round" d="M12 7v5l3 2" /></svg>
|
|
189
|
+
{durationLabel}
|
|
190
|
+
</li>
|
|
191
|
+
{/if}
|
|
192
|
+
{#if location}
|
|
193
|
+
<li>
|
|
194
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 21s-7-6.2-7-11.5A7 7 0 0119 9.5C19 14.8 12 21 12 21z" /><circle cx="12" cy="9.5" r="2.5" /></svg>
|
|
195
|
+
{location}
|
|
196
|
+
</li>
|
|
197
|
+
{/if}
|
|
198
|
+
{#if showTimeZone}
|
|
199
|
+
<li>
|
|
200
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true"><circle cx="12" cy="12" r="9" /><path d="M3 12h18M12 3a14 14 0 010 18M12 3a14 14 0 000 18" /></svg>
|
|
201
|
+
{timeZoneCity(timeZone)} ({timeZoneOffsetLabel(timeZone)})
|
|
202
|
+
</li>
|
|
203
|
+
{/if}
|
|
204
|
+
</ul>
|
|
205
|
+
{#if description}<p class="jatui-booking__desc">{description}</p>{/if}
|
|
206
|
+
{/if}
|
|
207
|
+
</section>
|
|
208
|
+
{/if}
|
|
209
|
+
|
|
210
|
+
<section class="jatui-booking__calendar" aria-label="Choose a day">
|
|
211
|
+
<CalendarPicker
|
|
212
|
+
onSelect={handleDate}
|
|
213
|
+
bind:selectedDate
|
|
214
|
+
{getAvailability}
|
|
215
|
+
{isDateEnabled}
|
|
216
|
+
{maxDaysInAdvance}
|
|
217
|
+
{minNoticeHours}
|
|
218
|
+
{weekdaysOnly}
|
|
219
|
+
{accentColor}
|
|
220
|
+
{locale}
|
|
221
|
+
{weekStart}
|
|
222
|
+
size="sm"
|
|
223
|
+
/>
|
|
224
|
+
</section>
|
|
225
|
+
|
|
226
|
+
<section class="jatui-booking__times" aria-labelledby="{uid}-times">
|
|
227
|
+
<h3 id="{uid}-times" class="jatui-booking__times-heading">
|
|
228
|
+
{dayHeading || 'Pick a day'}
|
|
229
|
+
</h3>
|
|
230
|
+
{#if showTimeZone || showHourCycleToggle}
|
|
231
|
+
<div class="jatui-booking__tz">
|
|
232
|
+
{#if showTimeZone}
|
|
233
|
+
<label class="jatui-booking__tz-label" for="{uid}-tz">Time zone</label>
|
|
234
|
+
<select id="{uid}-tz" class="select select-sm jatui-booking__tz-select" bind:value={timeZone}>
|
|
235
|
+
{#each zones as z (z)}
|
|
236
|
+
<option value={z}>{timeZoneCity(z)} ({timeZoneOffsetLabel(z)})</option>
|
|
237
|
+
{/each}
|
|
238
|
+
</select>
|
|
239
|
+
{/if}
|
|
240
|
+
{#if showHourCycleToggle}
|
|
241
|
+
<div class="join" role="group" aria-label="Clock format">
|
|
242
|
+
<button
|
|
243
|
+
type="button"
|
|
244
|
+
class="join-item btn btn-xs {cycle === 'h12' ? 'btn-active' : 'btn-ghost'}"
|
|
245
|
+
aria-pressed={cycle === 'h12'}
|
|
246
|
+
onclick={() => (hourCycle = 'h12')}>12h</button
|
|
247
|
+
>
|
|
248
|
+
<button
|
|
249
|
+
type="button"
|
|
250
|
+
class="join-item btn btn-xs {cycle === 'h23' ? 'btn-active' : 'btn-ghost'}"
|
|
251
|
+
aria-pressed={cycle === 'h23'}
|
|
252
|
+
onclick={() => (hourCycle = 'h23')}>24h</button
|
|
253
|
+
>
|
|
254
|
+
</div>
|
|
255
|
+
{/if}
|
|
256
|
+
</div>
|
|
257
|
+
{/if}
|
|
258
|
+
|
|
259
|
+
{#if selectedDate}
|
|
260
|
+
<TimeSlotPicker
|
|
261
|
+
{slots}
|
|
262
|
+
{loading}
|
|
263
|
+
onSelect={handleSlot}
|
|
264
|
+
{selectedSlot}
|
|
265
|
+
{accentColor}
|
|
266
|
+
hourCycle={cycle}
|
|
267
|
+
{timeZone}
|
|
268
|
+
{locale}
|
|
269
|
+
/>
|
|
270
|
+
{:else}
|
|
271
|
+
<p class="jatui-booking__hint">Available times appear here once you pick a day.</p>
|
|
272
|
+
{/if}
|
|
273
|
+
|
|
274
|
+
{#if onConfirm && selectedSlot}
|
|
275
|
+
<div class="jatui-booking__confirm">
|
|
276
|
+
<p class="jatui-booking__summary" aria-live="polite">
|
|
277
|
+
{formatSlotLabel(selectedSlot.start, { hourCycle: cycle, timeZone, locale })}
|
|
278
|
+
</p>
|
|
279
|
+
<button
|
|
280
|
+
type="button"
|
|
281
|
+
class="btn btn-primary btn-sm"
|
|
282
|
+
disabled={confirming}
|
|
283
|
+
aria-busy={confirming}
|
|
284
|
+
onclick={() => selectedSlot && onConfirm?.(selectedSlot, { timeZone })}
|
|
285
|
+
>
|
|
286
|
+
{#if confirming}<span class="loading loading-spinner loading-xs" aria-hidden="true"></span>{/if}
|
|
287
|
+
{confirmLabel}
|
|
288
|
+
</button>
|
|
289
|
+
</div>
|
|
290
|
+
{/if}
|
|
291
|
+
</section>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<style>
|
|
295
|
+
.jatui-booking {
|
|
296
|
+
container-type: inline-size;
|
|
297
|
+
display: grid;
|
|
298
|
+
gap: 1.5rem;
|
|
299
|
+
grid-template-columns: minmax(0, 1fr);
|
|
300
|
+
}
|
|
301
|
+
@container (min-width: 40rem) {
|
|
302
|
+
.jatui-booking {
|
|
303
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
304
|
+
}
|
|
305
|
+
.jatui-booking__details {
|
|
306
|
+
grid-column: 1 / -1;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
@container (min-width: 60rem) {
|
|
310
|
+
.jatui-booking.has-details {
|
|
311
|
+
grid-template-columns: minmax(12rem, 0.8fr) minmax(0, 1.1fr) minmax(0, 1fr);
|
|
312
|
+
}
|
|
313
|
+
.jatui-booking.has-details .jatui-booking__details {
|
|
314
|
+
grid-column: auto;
|
|
315
|
+
padding-inline-end: 1.5rem;
|
|
316
|
+
border-inline-end: 1px solid var(--jatui-separator, var(--color-base-300));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
.jatui-booking__host {
|
|
320
|
+
display: flex;
|
|
321
|
+
align-items: center;
|
|
322
|
+
gap: 0.5rem;
|
|
323
|
+
margin-block-end: 0.75rem;
|
|
324
|
+
}
|
|
325
|
+
.jatui-booking__avatar {
|
|
326
|
+
width: 2.25rem;
|
|
327
|
+
height: 2.25rem;
|
|
328
|
+
border-radius: 9999px;
|
|
329
|
+
object-fit: cover;
|
|
330
|
+
}
|
|
331
|
+
.jatui-booking__avatar--initials {
|
|
332
|
+
display: inline-flex;
|
|
333
|
+
align-items: center;
|
|
334
|
+
justify-content: center;
|
|
335
|
+
font-size: 0.8125rem;
|
|
336
|
+
font-weight: 600;
|
|
337
|
+
background: var(--color-base-200);
|
|
338
|
+
color: color-mix(in oklab, var(--color-base-content) 75%, transparent);
|
|
339
|
+
}
|
|
340
|
+
.jatui-booking__host-name {
|
|
341
|
+
font-size: 0.875rem;
|
|
342
|
+
color: color-mix(in oklab, var(--color-base-content) 65%, transparent);
|
|
343
|
+
}
|
|
344
|
+
.jatui-booking__title {
|
|
345
|
+
margin: 0 0 0.75rem;
|
|
346
|
+
font-size: 1.25rem;
|
|
347
|
+
font-weight: 700;
|
|
348
|
+
line-height: 1.25;
|
|
349
|
+
}
|
|
350
|
+
.jatui-booking__meta {
|
|
351
|
+
display: flex;
|
|
352
|
+
flex-direction: column;
|
|
353
|
+
gap: 0.5rem;
|
|
354
|
+
margin: 0;
|
|
355
|
+
padding: 0;
|
|
356
|
+
list-style: none;
|
|
357
|
+
font-size: 0.875rem;
|
|
358
|
+
color: color-mix(in oklab, var(--color-base-content) 70%, transparent);
|
|
359
|
+
}
|
|
360
|
+
.jatui-booking__meta li {
|
|
361
|
+
display: flex;
|
|
362
|
+
align-items: center;
|
|
363
|
+
gap: 0.5rem;
|
|
364
|
+
}
|
|
365
|
+
.jatui-booking__desc {
|
|
366
|
+
margin: 1rem 0 0;
|
|
367
|
+
font-size: 0.875rem;
|
|
368
|
+
line-height: 1.55;
|
|
369
|
+
color: color-mix(in oklab, var(--color-base-content) 70%, transparent);
|
|
370
|
+
}
|
|
371
|
+
.jatui-booking__times-heading {
|
|
372
|
+
margin: 0 0 0.75rem;
|
|
373
|
+
font-size: 1rem;
|
|
374
|
+
font-weight: 600;
|
|
375
|
+
}
|
|
376
|
+
.jatui-booking__tz {
|
|
377
|
+
display: flex;
|
|
378
|
+
flex-wrap: wrap;
|
|
379
|
+
align-items: center;
|
|
380
|
+
gap: 0.5rem;
|
|
381
|
+
margin-block-end: 1rem;
|
|
382
|
+
}
|
|
383
|
+
.jatui-booking__tz-label {
|
|
384
|
+
font-size: 0.75rem;
|
|
385
|
+
color: color-mix(in oklab, var(--color-base-content) 55%, transparent);
|
|
386
|
+
}
|
|
387
|
+
.jatui-booking__tz-select {
|
|
388
|
+
flex: 1;
|
|
389
|
+
min-width: 10rem;
|
|
390
|
+
}
|
|
391
|
+
.jatui-booking__hint {
|
|
392
|
+
margin: 2rem 0;
|
|
393
|
+
font-size: 0.875rem;
|
|
394
|
+
text-align: center;
|
|
395
|
+
color: color-mix(in oklab, var(--color-base-content) 50%, transparent);
|
|
396
|
+
}
|
|
397
|
+
.jatui-booking__confirm {
|
|
398
|
+
display: flex;
|
|
399
|
+
flex-wrap: wrap;
|
|
400
|
+
align-items: center;
|
|
401
|
+
justify-content: space-between;
|
|
402
|
+
gap: 0.75rem;
|
|
403
|
+
margin-block-start: 1.25rem;
|
|
404
|
+
padding-block-start: 1rem;
|
|
405
|
+
border-block-start: 1px solid var(--jatui-separator, var(--color-base-300));
|
|
406
|
+
}
|
|
407
|
+
.jatui-booking__summary {
|
|
408
|
+
margin: 0;
|
|
409
|
+
font-size: 0.875rem;
|
|
410
|
+
font-weight: 500;
|
|
411
|
+
}
|
|
412
|
+
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BookingScheduler — the Calendly-shaped booking surface, composed from the
|
|
3
|
+
* existing jatui pieces: a details pane, `CalendarPicker` (availability dots,
|
|
4
|
+
* booking-window rules) and `TimeSlotPicker` (radiogroup keys, 12/24h, time
|
|
5
|
+
* zone), with a time-zone select and a 12h/24h toggle between them.
|
|
6
|
+
*
|
|
7
|
+
* The host owns the data: `getSlots(date, timeZone)` returns the slots for a
|
|
8
|
+
* day (sync or async; stale responses are dropped), `getAvailability` feeds
|
|
9
|
+
* the dots. Nothing is booked here — `onConfirm(slot, { timeZone })` hands the
|
|
10
|
+
* choice back, and `BookingForm` usually comes next.
|
|
11
|
+
*
|
|
12
|
+
* Layout follows its own width (container queries): three panes side by side
|
|
13
|
+
* when wide, details above calendar + times when medium, one column on a phone.
|
|
14
|
+
* Built from BoardUI's public meeting-scheduler behaviour (jatui-lfxyy); no
|
|
15
|
+
* BoardUI source is copied and there are no built-in names or mock slots.
|
|
16
|
+
*/
|
|
17
|
+
import type { Snippet } from 'svelte';
|
|
18
|
+
import type { AvailabilityCounts, TimeSlot } from '../types/booking';
|
|
19
|
+
import { type HourCycle } from './calendar/timeSlots';
|
|
20
|
+
import type { Weekday } from './calendar/calendarGrid';
|
|
21
|
+
type $$ComponentProps = {
|
|
22
|
+
/** Slots for one day, in `timeZone`. Sync or async; a newer request wins. */
|
|
23
|
+
getSlots: (date: Date, timeZone: string) => TimeSlot[] | Promise<TimeSlot[]>;
|
|
24
|
+
getAvailability?: (startDate: Date, endDate: Date) => Promise<AvailabilityCounts>;
|
|
25
|
+
isDateEnabled?: (date: Date) => boolean;
|
|
26
|
+
maxDaysInAdvance?: number;
|
|
27
|
+
minNoticeHours?: number;
|
|
28
|
+
weekdaysOnly?: boolean;
|
|
29
|
+
selectedDate?: Date | null;
|
|
30
|
+
selectedSlot?: TimeSlot | null;
|
|
31
|
+
/** IANA zone the slots are shown in (bindable). Defaults to the viewer's zone. */
|
|
32
|
+
timeZone?: string;
|
|
33
|
+
/** 'h12' | 'h23' (bindable). Defaults to the locale's convention. */
|
|
34
|
+
hourCycle?: HourCycle;
|
|
35
|
+
/** Zones offered in the picker. Defaults to the viewer's zone + common ones. */
|
|
36
|
+
timeZones?: string[];
|
|
37
|
+
locale?: string;
|
|
38
|
+
weekStart?: Weekday | 'locale';
|
|
39
|
+
/** Meeting name, e.g. "Intro call". */
|
|
40
|
+
title?: string;
|
|
41
|
+
durationMinutes?: number;
|
|
42
|
+
hostName?: string;
|
|
43
|
+
hostAvatarUrl?: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
/** "Video call", "Phone", an address… */
|
|
46
|
+
location?: string;
|
|
47
|
+
/** Replaces the built-in details pane. */
|
|
48
|
+
details?: Snippet;
|
|
49
|
+
showDetails?: boolean;
|
|
50
|
+
showTimeZone?: boolean;
|
|
51
|
+
showHourCycleToggle?: boolean;
|
|
52
|
+
onSelectDate?: (date: Date) => void;
|
|
53
|
+
onSelectSlot?: (slot: TimeSlot) => void;
|
|
54
|
+
/** Shows a Confirm button once a slot is chosen. */
|
|
55
|
+
onConfirm?: (slot: TimeSlot, ctx: {
|
|
56
|
+
timeZone: string;
|
|
57
|
+
}) => void;
|
|
58
|
+
confirmLabel?: string;
|
|
59
|
+
/** Busy state for the Confirm button. */
|
|
60
|
+
confirming?: boolean;
|
|
61
|
+
accentColor?: string;
|
|
62
|
+
class?: string;
|
|
63
|
+
};
|
|
64
|
+
declare const BookingScheduler: import("svelte").Component<$$ComponentProps, {}, "hourCycle" | "timeZone" | "selectedSlot" | "selectedDate">;
|
|
65
|
+
type BookingScheduler = ReturnType<typeof BookingScheduler>;
|
|
66
|
+
export default BookingScheduler;
|