@remit/ui 0.0.155 → 0.0.157
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/package.json +5 -2
- package/src/calendar.css +4 -0
- package/src/components/agenda-composer.render.test.ts +196 -0
- package/src/components/agenda-composer.stories.tsx +225 -0
- package/src/components/agenda-composer.tsx +283 -0
- package/src/components/agenda-flow.render.test.ts +217 -0
- package/src/components/agenda-flow.stories.tsx +215 -0
- package/src/components/agenda-flow.tsx +887 -0
- package/src/components/agenda-panels.render.test.ts +252 -0
- package/src/components/agenda-panels.stories.tsx +207 -0
- package/src/components/agenda-panels.tsx +421 -0
- package/src/components/calendar-event-chip-content.tsx +102 -0
- package/src/components/calendar-event-chip.stories.tsx +5 -7
- package/src/components/calendar-event-chip.tsx +47 -47
- package/src/components/calendar-grid.render.test.ts +309 -0
- package/src/components/calendar-grid.stories.tsx +203 -0
- package/src/components/calendar-grid.tsx +370 -0
- package/src/components/calendar-types.ts +85 -0
- package/src/index.ts +82 -0
- package/src/lib/agenda-time.test.ts +539 -0
- package/src/lib/agenda-time.ts +505 -0
- package/src/lib/calendar-event-shell.ts +55 -0
- package/src/lib/calendar-slot-pick.test.ts +140 -0
- package/src/lib/calendar-slot-pick.ts +65 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CalendarRef,
|
|
3
|
+
EventDisplayInfo,
|
|
4
|
+
EventInput,
|
|
5
|
+
} from "@fullcalendar/react";
|
|
6
|
+
import FullCalendar from "@fullcalendar/react";
|
|
7
|
+
import dayGridPlugin from "@fullcalendar/react/daygrid";
|
|
8
|
+
import interactionPlugin from "@fullcalendar/react/interaction";
|
|
9
|
+
import listPlugin from "@fullcalendar/react/list";
|
|
10
|
+
import multiMonthPlugin from "@fullcalendar/react/multimonth";
|
|
11
|
+
import timeGridPlugin from "@fullcalendar/react/timegrid";
|
|
12
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
13
|
+
import { calendarEventBodyClasses } from "../lib/calendar-event-shell.js";
|
|
14
|
+
import {
|
|
15
|
+
isDraggedSelection,
|
|
16
|
+
pointPick,
|
|
17
|
+
rangePick,
|
|
18
|
+
} from "../lib/calendar-slot-pick.js";
|
|
19
|
+
import { cn } from "../lib/cn.js";
|
|
20
|
+
import type { Density } from "./app-shell-types.js";
|
|
21
|
+
import { CalendarEventChipContent } from "./calendar-event-chip-content.js";
|
|
22
|
+
import type {
|
|
23
|
+
CalendarColorId,
|
|
24
|
+
CalendarEventData,
|
|
25
|
+
CalendarSlotPick,
|
|
26
|
+
CalendarViewId,
|
|
27
|
+
RsvpState,
|
|
28
|
+
ZoneCertainty,
|
|
29
|
+
} from "./calendar-types.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One continuous strip of time, at whichever zoom the view names. Every pixel
|
|
33
|
+
* comes from the kit's tokens: the calendar engine ships a structural skeleton
|
|
34
|
+
* sheet and no theme, and it is styled entirely through its per-element
|
|
35
|
+
* `*Class` props, so no rule here overrides one of the library's own.
|
|
36
|
+
*
|
|
37
|
+
* The component is presentational. It holds no events of its own, reads no
|
|
38
|
+
* clock of its own, and every gesture leaves through a callback.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const PLUGINS = [
|
|
42
|
+
dayGridPlugin,
|
|
43
|
+
timeGridPlugin,
|
|
44
|
+
listPlugin,
|
|
45
|
+
multiMonthPlugin,
|
|
46
|
+
interactionPlugin,
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const FC_VIEW: Record<CalendarViewId, string> = {
|
|
50
|
+
year: "multiMonthYear",
|
|
51
|
+
month: "dayGridMonth",
|
|
52
|
+
week: "timeGridWeek",
|
|
53
|
+
day: "timeGridDay",
|
|
54
|
+
agenda: "listWeek",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/* A week or a day names the date in its header. A month grid repeats the same
|
|
58
|
+
seven weekdays down the page, so the header names the weekday and the cells
|
|
59
|
+
own the dates; a year has room for one letter. */
|
|
60
|
+
const DAY_HEADER_FORMAT: Record<
|
|
61
|
+
CalendarViewId,
|
|
62
|
+
{ weekday: "short" | "narrow"; day?: "numeric" }
|
|
63
|
+
> = {
|
|
64
|
+
year: { weekday: "narrow" },
|
|
65
|
+
month: { weekday: "short" },
|
|
66
|
+
week: { weekday: "short", day: "numeric" },
|
|
67
|
+
day: { weekday: "short", day: "numeric" },
|
|
68
|
+
agenda: { weekday: "short", day: "numeric" },
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Views that draw every event as a horizontal pill rather than a block. */
|
|
72
|
+
const ROW_VIEWS = new Set<CalendarViewId>(["year", "month", "agenda"]);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Density is a real change in how much of a day fits on screen, not a padding
|
|
76
|
+
* tweak: a tighter setting halves the slot height and drops the time off short
|
|
77
|
+
* chips.
|
|
78
|
+
*/
|
|
79
|
+
const DENSITY: Record<
|
|
80
|
+
Density,
|
|
81
|
+
{ slotMinutes: number; slotMinHeight: number; eventShortHeight: number }
|
|
82
|
+
> = {
|
|
83
|
+
comfortable: { slotMinutes: 30, slotMinHeight: 26, eventShortHeight: 34 },
|
|
84
|
+
compact: { slotMinutes: 60, slotMinHeight: 15, eventShortHeight: 22 },
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const FALLBACK_COLOR: CalendarColorId = "cal-1";
|
|
88
|
+
|
|
89
|
+
export interface CalendarGridProps {
|
|
90
|
+
view: CalendarViewId;
|
|
91
|
+
/** The day the view is centred on; changing it moves the grid. */
|
|
92
|
+
date: string;
|
|
93
|
+
events: CalendarEventData[];
|
|
94
|
+
/** Which calendar owns which hue. Anything missing falls back to cal-1. */
|
|
95
|
+
colorByCalendarId: Record<string, CalendarColorId>;
|
|
96
|
+
density: Density;
|
|
97
|
+
selectedEventId: string;
|
|
98
|
+
/** IANA zone the grid's clock runs on, and the zone a pick is read in. */
|
|
99
|
+
timeZone: string;
|
|
100
|
+
/** The instant the grid calls now: the today marker and the now line read it. */
|
|
101
|
+
now: string;
|
|
102
|
+
onSelectEvent: (eventId: string) => void;
|
|
103
|
+
onPickSlot: (pick: CalendarSlotPick) => void;
|
|
104
|
+
/** The range title the grid computed, e.g. "8 – 14 Jun 2026". */
|
|
105
|
+
onRangeChange: (title: string) => void;
|
|
106
|
+
className?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* What drawing one event needs, keyed by id. The engine renders the element and
|
|
111
|
+
* hands the callback its own `EventApi`, so the data is looked up rather than
|
|
112
|
+
* smuggled through `extendedProps` and cast back out.
|
|
113
|
+
*/
|
|
114
|
+
interface GridEvent {
|
|
115
|
+
color: CalendarColorId;
|
|
116
|
+
rsvp: RsvpState;
|
|
117
|
+
status: "confirmed" | "tentative";
|
|
118
|
+
hasThread: boolean;
|
|
119
|
+
isRecurring: boolean;
|
|
120
|
+
zoneCertainty: ZoneCertainty;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The placeholder the engine drags under the pointer, which is not one of ours. */
|
|
124
|
+
const MIRROR_EVENT: GridEvent = {
|
|
125
|
+
color: FALLBACK_COLOR,
|
|
126
|
+
rsvp: "accepted",
|
|
127
|
+
status: "confirmed",
|
|
128
|
+
hasThread: false,
|
|
129
|
+
isRecurring: false,
|
|
130
|
+
zoneCertainty: "local",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
function toInput(event: CalendarEventData): EventInput {
|
|
134
|
+
return {
|
|
135
|
+
id: event.id,
|
|
136
|
+
title: event.title,
|
|
137
|
+
start: event.start,
|
|
138
|
+
end: event.end,
|
|
139
|
+
allDay: event.allDay,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function CalendarGrid({
|
|
144
|
+
view,
|
|
145
|
+
date,
|
|
146
|
+
events,
|
|
147
|
+
colorByCalendarId,
|
|
148
|
+
density,
|
|
149
|
+
selectedEventId,
|
|
150
|
+
timeZone,
|
|
151
|
+
now,
|
|
152
|
+
onSelectEvent,
|
|
153
|
+
onPickSlot,
|
|
154
|
+
onRangeChange,
|
|
155
|
+
className,
|
|
156
|
+
}: CalendarGridProps) {
|
|
157
|
+
const calendarRef = useRef<CalendarRef>(null);
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
calendarRef.current?.getApi().changeView(FC_VIEW[view]);
|
|
161
|
+
}, [view]);
|
|
162
|
+
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
calendarRef.current?.getApi().gotoDate(date);
|
|
165
|
+
}, [date]);
|
|
166
|
+
|
|
167
|
+
const eventInputs = useMemo(() => events.map(toInput), [events]);
|
|
168
|
+
|
|
169
|
+
const byId = useMemo(
|
|
170
|
+
() =>
|
|
171
|
+
new Map<string, GridEvent>(
|
|
172
|
+
events.map((event) => [
|
|
173
|
+
event.id,
|
|
174
|
+
{
|
|
175
|
+
color: colorByCalendarId[event.calendarId] ?? FALLBACK_COLOR,
|
|
176
|
+
rsvp: event.myRsvp,
|
|
177
|
+
status: event.status,
|
|
178
|
+
hasThread: event.threadId !== "",
|
|
179
|
+
isRecurring: event.recurrenceRule !== "",
|
|
180
|
+
zoneCertainty: event.zoneCertainty,
|
|
181
|
+
},
|
|
182
|
+
]),
|
|
183
|
+
),
|
|
184
|
+
[events, colorByCalendarId],
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const lookup = useMemo(
|
|
188
|
+
() => (id: string) => byId.get(id) ?? MIRROR_EVENT,
|
|
189
|
+
[byId],
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const isRowEvent = useMemo(
|
|
193
|
+
() => (info: EventDisplayInfo) => info.event.allDay || ROW_VIEWS.has(view),
|
|
194
|
+
[view],
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
/* The event element the engine built, dressed as the body of a
|
|
198
|
+
`CalendarEventChip`: same box, same hue, same states, one definition. A
|
|
199
|
+
grid cell is tight whatever the density does to the slots around it. */
|
|
200
|
+
const eventBody = useMemo(
|
|
201
|
+
() => (info: EventDisplayInfo) => {
|
|
202
|
+
const event = lookup(info.event.id);
|
|
203
|
+
const isRow = isRowEvent(info);
|
|
204
|
+
return cn(
|
|
205
|
+
calendarEventBodyClasses({
|
|
206
|
+
color: event.color,
|
|
207
|
+
layout: isRow ? "row" : "column",
|
|
208
|
+
density: "compact",
|
|
209
|
+
rsvp: event.rsvp,
|
|
210
|
+
status: event.status,
|
|
211
|
+
selected: info.event.id === selectedEventId,
|
|
212
|
+
stacked: false,
|
|
213
|
+
}),
|
|
214
|
+
"cursor-pointer outline-none transition-colors",
|
|
215
|
+
"focus-visible:ring-2 focus-visible:ring-ring",
|
|
216
|
+
/* Tighter than a chip drawn on its own: a grid cell is smaller than
|
|
217
|
+
anywhere else an event lands, and an all-day pill has to fit a band
|
|
218
|
+
one line high. */
|
|
219
|
+
isRow ? "my-px px-1 py-0" : "px-1 py-0.5",
|
|
220
|
+
);
|
|
221
|
+
},
|
|
222
|
+
[lookup, isRowEvent, selectedEventId],
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const slot = DENSITY[density];
|
|
226
|
+
const isTight = density === "compact";
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<div className={cn("h-full min-h-0 w-full text-fg", className)}>
|
|
230
|
+
<FullCalendar
|
|
231
|
+
ref={calendarRef}
|
|
232
|
+
plugins={PLUGINS}
|
|
233
|
+
initialView={FC_VIEW[view]}
|
|
234
|
+
initialDate={date}
|
|
235
|
+
now={now}
|
|
236
|
+
timeZone={timeZone}
|
|
237
|
+
height="100%"
|
|
238
|
+
headerToolbar={false}
|
|
239
|
+
firstDay={1}
|
|
240
|
+
nowIndicator
|
|
241
|
+
selectable
|
|
242
|
+
selectMirror
|
|
243
|
+
editable={false}
|
|
244
|
+
/* Events take focus and answer Enter, the way the toolbar's controls
|
|
245
|
+
do — the grid is not reachable by pointer only. */
|
|
246
|
+
eventInteractive
|
|
247
|
+
expandRows
|
|
248
|
+
allDayText="All day"
|
|
249
|
+
dayMaxEvents={isTight ? 2 : 3}
|
|
250
|
+
eventMaxStack={3}
|
|
251
|
+
moreLinkClick="popover"
|
|
252
|
+
moreLinkText={(num) => `+${num}`}
|
|
253
|
+
slotMinTime="07:00:00"
|
|
254
|
+
slotMaxTime="23:00:00"
|
|
255
|
+
scrollTime="08:30:00"
|
|
256
|
+
scrollTimeReset={false}
|
|
257
|
+
slotDuration={{ minutes: slot.slotMinutes }}
|
|
258
|
+
slotMinHeight={slot.slotMinHeight}
|
|
259
|
+
eventShortHeight={slot.eventShortHeight}
|
|
260
|
+
displayEventEnd={false}
|
|
261
|
+
eventTimeFormat={{ hour: "2-digit", minute: "2-digit", hour12: false }}
|
|
262
|
+
slotHeaderFormat={{ hour: "2-digit", minute: "2-digit", hour12: false }}
|
|
263
|
+
dayHeaderFormat={DAY_HEADER_FORMAT[view]}
|
|
264
|
+
events={eventInputs}
|
|
265
|
+
eventClick={(info) => onSelectEvent(info.event.id)}
|
|
266
|
+
/* One gesture, two readings, and each owns a shape the other cannot
|
|
267
|
+
reach. A point is `dateClick`'s: a finger has to hold a full second
|
|
268
|
+
before the library will call it a selection, and a selection cancels
|
|
269
|
+
the scroll the finger might have meant, so a tap has nowhere else to
|
|
270
|
+
land. A dragged range is `select`'s.
|
|
271
|
+
|
|
272
|
+
Acting on both would report a point twice, and the second report
|
|
273
|
+
arrives late — the library finishes a click a task after the pointer
|
|
274
|
+
is up — by which time the pane the first report opened has rebuilt
|
|
275
|
+
the grid and left the second reading a detached cell. */
|
|
276
|
+
dateClick={(info) => onPickSlot(pointPick(info.dateStr, info.allDay))}
|
|
277
|
+
select={(info) => {
|
|
278
|
+
if (
|
|
279
|
+
!isDraggedSelection(
|
|
280
|
+
info.startStr,
|
|
281
|
+
info.endStr,
|
|
282
|
+
info.allDay,
|
|
283
|
+
slot.slotMinutes,
|
|
284
|
+
)
|
|
285
|
+
) {
|
|
286
|
+
/* The point reading is answering this one. Left alone the
|
|
287
|
+
selection would stay lit under the draft, a slot wide where
|
|
288
|
+
the draft is an hour, until a click somewhere else. */
|
|
289
|
+
info.view.calendar.unselect();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
onPickSlot(rangePick(info.startStr, info.endStr, info.allDay));
|
|
293
|
+
}}
|
|
294
|
+
datesSet={(info) => onRangeChange(info.view.title)}
|
|
295
|
+
/* ---- chrome, all of it ours ---- */
|
|
296
|
+
viewClass="bg-surface"
|
|
297
|
+
tableClass="border-line"
|
|
298
|
+
tableHeaderClass="bg-surface"
|
|
299
|
+
fillerClass="border-line"
|
|
300
|
+
dayHeaderRowClass="h-pane-header"
|
|
301
|
+
dayHeaderClass={(info) =>
|
|
302
|
+
cn(
|
|
303
|
+
"border-b border-line bg-surface",
|
|
304
|
+
info.isToday ? "text-accent" : "text-fg-subtle",
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
dayHeaderInnerClass="px-1 py-1 text-2xs font-semibold uppercase tracking-wider"
|
|
308
|
+
dayHeaderDividerClass="border-b border-line"
|
|
309
|
+
dayCellClass={(info) =>
|
|
310
|
+
cn("border-line", info.isOther && "bg-surface-sunken")
|
|
311
|
+
}
|
|
312
|
+
dayLaneClass={(info) => cn(info.isToday && "bg-accent-soft/40")}
|
|
313
|
+
dayCellTopInnerClass={(info) =>
|
|
314
|
+
cn(
|
|
315
|
+
"px-1 py-0.5 text-2xs tabular-nums",
|
|
316
|
+
info.isToday ? "font-semibold text-accent" : "text-fg-muted",
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
dayRowClass="border-line"
|
|
320
|
+
slotLaneClass={(info) =>
|
|
321
|
+
cn("border-line", info.isMinor && "border-dashed")
|
|
322
|
+
}
|
|
323
|
+
slotHeaderClass="border-line"
|
|
324
|
+
slotHeaderInnerClass="pr-1.5 text-2xs tabular-nums text-fg-subtle"
|
|
325
|
+
slotHeaderDividerClass="border-r border-line"
|
|
326
|
+
allDayHeaderClass="border-line"
|
|
327
|
+
allDayHeaderInnerClass="px-1 text-2xs uppercase tracking-wider text-fg-subtle"
|
|
328
|
+
allDayDividerClass="border-b border-line-strong"
|
|
329
|
+
nonBusinessHoursClass="bg-surface-sunken"
|
|
330
|
+
highlightClass="bg-accent-soft"
|
|
331
|
+
nowIndicatorLineClass="border-t border-danger"
|
|
332
|
+
nowIndicatorDotClass="bg-danger"
|
|
333
|
+
moreLinkClass="rounded-sm bg-surface-sunken text-2xs font-semibold text-fg-muted hover:bg-line hover:text-fg"
|
|
334
|
+
moreLinkInnerClass="px-1 py-0.5"
|
|
335
|
+
popoverClass="rounded-lg border border-line bg-surface-raised shadow-xl shadow-black/20"
|
|
336
|
+
popoverCloseClass="text-fg-subtle hover:text-fg"
|
|
337
|
+
listDaysClass="bg-surface"
|
|
338
|
+
listDayClass="border-line"
|
|
339
|
+
listDayHeaderClass="border-y border-line bg-surface-sunken"
|
|
340
|
+
listDayHeaderInnerClass="flex h-section-row items-center px-row-inset text-2xs font-semibold uppercase tracking-wider text-fg-subtle"
|
|
341
|
+
listDayBodyClass="border-line"
|
|
342
|
+
noEventsClass="p-10 text-center"
|
|
343
|
+
noEventsInnerClass="text-sm text-fg-muted"
|
|
344
|
+
noEventsContent="Nothing scheduled"
|
|
345
|
+
singleMonthClass="p-2"
|
|
346
|
+
singleMonthHeaderClass="pb-1"
|
|
347
|
+
singleMonthHeaderInnerClass="text-xs font-semibold text-fg"
|
|
348
|
+
eventClass={eventBody}
|
|
349
|
+
listItemEventClass={eventBody}
|
|
350
|
+
eventContent={(info) => {
|
|
351
|
+
const event = lookup(info.event.id);
|
|
352
|
+
/* A column an hour wide has room for a time or a title, never
|
|
353
|
+
both — the title is the one worth keeping. */
|
|
354
|
+
const showTime = info.timeText !== "" && !isTight && !info.isNarrow;
|
|
355
|
+
return (
|
|
356
|
+
<CalendarEventChipContent
|
|
357
|
+
title={info.event.title}
|
|
358
|
+
timeText={showTime ? info.timeText : ""}
|
|
359
|
+
layout={isRowEvent(info) ? "row" : "column"}
|
|
360
|
+
rsvp={event.rsvp}
|
|
361
|
+
hasThread={event.hasThread}
|
|
362
|
+
isRecurring={event.isRecurring}
|
|
363
|
+
zoneCertainty={event.zoneCertainty}
|
|
364
|
+
/>
|
|
365
|
+
);
|
|
366
|
+
}}
|
|
367
|
+
/>
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
@@ -82,6 +82,91 @@ export interface CalendarEventData {
|
|
|
82
82
|
status: "confirmed" | "tentative";
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* One day, with everything on it already sorted and measured. The surfaces that
|
|
87
|
+
* render a day take this rather than a flat event list, so the arithmetic is
|
|
88
|
+
* done once by whoever owns the events.
|
|
89
|
+
*/
|
|
90
|
+
export interface CalendarDay {
|
|
91
|
+
/** `YYYY-MM-DD`. */
|
|
92
|
+
date: string;
|
|
93
|
+
weekdayLabel: string;
|
|
94
|
+
dayNumber: number;
|
|
95
|
+
isToday: boolean;
|
|
96
|
+
/** Ascending by start. */
|
|
97
|
+
timed: CalendarEventData[];
|
|
98
|
+
allDay: CalendarEventData[];
|
|
99
|
+
/** Minutes of the day covered by at least one timed event. */
|
|
100
|
+
busyMinutes: number;
|
|
101
|
+
/**
|
|
102
|
+
* Every pile-up on the day: one group per event that something else runs
|
|
103
|
+
* into, holding that event and everything overlapping it. Members all meet
|
|
104
|
+
* the event the group is built around, not necessarily each other — which is
|
|
105
|
+
* what a grid has to lay out. Empty when the day is clean.
|
|
106
|
+
*/
|
|
107
|
+
conflicts: string[][];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** An empty slot a reader picked, wherever they picked it — a grid, a list. */
|
|
111
|
+
export interface CalendarSlotPick {
|
|
112
|
+
/** `YYYY-MM-DD`. */
|
|
113
|
+
date: string;
|
|
114
|
+
/** `HH:MM`, empty when the pick landed in the all-day band. */
|
|
115
|
+
startTime: string;
|
|
116
|
+
endTime: string;
|
|
117
|
+
allDay: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** One reading of a phrase the parser could not settle on its own. */
|
|
121
|
+
export interface PhraseChoiceOption {
|
|
122
|
+
id: string;
|
|
123
|
+
label: string;
|
|
124
|
+
/** Empty leaves the reading from the rest of the sentence alone. */
|
|
125
|
+
date: string;
|
|
126
|
+
startTime: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface PhraseChoice {
|
|
130
|
+
id: string;
|
|
131
|
+
question: string;
|
|
132
|
+
/** The words the question is about. */
|
|
133
|
+
source: string;
|
|
134
|
+
options: PhraseChoiceOption[];
|
|
135
|
+
chosenId: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* What a reader made of a typed sentence, with the words each reading came
|
|
140
|
+
* from. Every field is presentational: the composer renders it and never parses
|
|
141
|
+
* anything itself, so a parser can be swapped without touching the surface.
|
|
142
|
+
*/
|
|
143
|
+
export interface AgendaParse {
|
|
144
|
+
title: string;
|
|
145
|
+
date: string;
|
|
146
|
+
dateText: string;
|
|
147
|
+
startTime: string;
|
|
148
|
+
startTimeText: string;
|
|
149
|
+
endTime: string;
|
|
150
|
+
durationMinutes: number;
|
|
151
|
+
durationText: string;
|
|
152
|
+
attendees: string[];
|
|
153
|
+
attendeesText: string;
|
|
154
|
+
location: string;
|
|
155
|
+
locationText: string;
|
|
156
|
+
/** Human-readable rule, empty for a one-off. */
|
|
157
|
+
repeat: string;
|
|
158
|
+
repeatText: string;
|
|
159
|
+
/** Defaults the reader applied on its own authority. */
|
|
160
|
+
assumptions: string[];
|
|
161
|
+
/** What the sentence never said. */
|
|
162
|
+
unresolved: string[];
|
|
163
|
+
/** What the sentence said two ways. */
|
|
164
|
+
choices: PhraseChoice[];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Choice id → option id, as answered by the person typing. */
|
|
168
|
+
export type ChoicePicks = Readonly<Record<string, string>>;
|
|
169
|
+
|
|
85
170
|
/**
|
|
86
171
|
* One clock a zoneless time could be on. Present only when the source genuinely
|
|
87
172
|
* did not say which, and the reader is the one who settles it.
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,29 @@ export {
|
|
|
11
11
|
type EnvelopeAddress,
|
|
12
12
|
} from "./components/address-display.js";
|
|
13
13
|
export { AddressTag, type AddressTagProps } from "./components/address-tag.js";
|
|
14
|
+
export {
|
|
15
|
+
AgendaComposer,
|
|
16
|
+
type AgendaComposerProps,
|
|
17
|
+
AgendaPhraseField,
|
|
18
|
+
type AgendaPhraseFieldProps,
|
|
19
|
+
PhraseReading,
|
|
20
|
+
} from "./components/agenda-composer.js";
|
|
21
|
+
export {
|
|
22
|
+
type AgendaDensity,
|
|
23
|
+
AgendaFlow,
|
|
24
|
+
type AgendaFlowProps,
|
|
25
|
+
type AgendaScrollTarget,
|
|
26
|
+
} from "./components/agenda-flow.js";
|
|
27
|
+
export {
|
|
28
|
+
AgendaDensityControl,
|
|
29
|
+
type AgendaDensityControlProps,
|
|
30
|
+
FreeTimeList,
|
|
31
|
+
type FreeTimeListProps,
|
|
32
|
+
NextUpCard,
|
|
33
|
+
type NextUpCardProps,
|
|
34
|
+
PositionMap,
|
|
35
|
+
type PositionMapProps,
|
|
36
|
+
} from "./components/agenda-panels.js";
|
|
14
37
|
export {
|
|
15
38
|
AppPasswordHint,
|
|
16
39
|
type AppPasswordHintProps,
|
|
@@ -124,6 +147,14 @@ export {
|
|
|
124
147
|
CalendarEventChip,
|
|
125
148
|
type CalendarEventChipProps,
|
|
126
149
|
} from "./components/calendar-event-chip.js";
|
|
150
|
+
export {
|
|
151
|
+
CalendarEventChipContent,
|
|
152
|
+
type CalendarEventChipContentProps,
|
|
153
|
+
} from "./components/calendar-event-chip-content.js";
|
|
154
|
+
export {
|
|
155
|
+
CalendarGrid,
|
|
156
|
+
type CalendarGridProps,
|
|
157
|
+
} from "./components/calendar-grid.js";
|
|
127
158
|
export {
|
|
128
159
|
CalendarList,
|
|
129
160
|
type CalendarListProps,
|
|
@@ -137,14 +168,20 @@ export {
|
|
|
137
168
|
segmentClassName,
|
|
138
169
|
} from "./components/calendar-toolbar.js";
|
|
139
170
|
export {
|
|
171
|
+
type AgendaParse,
|
|
140
172
|
type CalendarAttendee,
|
|
141
173
|
type CalendarColorId,
|
|
174
|
+
type CalendarDay,
|
|
142
175
|
type CalendarDescriptor,
|
|
143
176
|
type CalendarEventData,
|
|
177
|
+
type CalendarSlotPick,
|
|
144
178
|
type CalendarViewId,
|
|
179
|
+
type ChoicePicks,
|
|
145
180
|
calendarColorIds,
|
|
146
181
|
type EventDraft,
|
|
147
182
|
type EventSuggestion,
|
|
183
|
+
type PhraseChoice,
|
|
184
|
+
type PhraseChoiceOption,
|
|
148
185
|
type RecurrenceScope,
|
|
149
186
|
type RsvpState,
|
|
150
187
|
type ZoneCertainty,
|
|
@@ -797,6 +834,41 @@ export {
|
|
|
797
834
|
sanitizeAdoptedHtml,
|
|
798
835
|
sanitizeQuotedHtml,
|
|
799
836
|
} from "./lib/adopted-html.js";
|
|
837
|
+
export {
|
|
838
|
+
type AgendaRow,
|
|
839
|
+
addDays,
|
|
840
|
+
type BusySpan,
|
|
841
|
+
buildAgendaRows,
|
|
842
|
+
buildCalendarDay,
|
|
843
|
+
busyMinutesOf,
|
|
844
|
+
busySpansOn,
|
|
845
|
+
type ClashOptions,
|
|
846
|
+
clashesWith,
|
|
847
|
+
conflictsOf,
|
|
848
|
+
DAY_END_MINUTE,
|
|
849
|
+
DAY_START_MINUTE,
|
|
850
|
+
datesBetween,
|
|
851
|
+
FREE_MINUTES,
|
|
852
|
+
type FreeStretch,
|
|
853
|
+
formatMinute,
|
|
854
|
+
formatRunLabel,
|
|
855
|
+
formatShortDay,
|
|
856
|
+
formatSpan,
|
|
857
|
+
freeAhead,
|
|
858
|
+
freeStretchesOn,
|
|
859
|
+
groupOverlapping,
|
|
860
|
+
isClearDay,
|
|
861
|
+
isEmptyDay,
|
|
862
|
+
minuteOfDay,
|
|
863
|
+
monthLabel,
|
|
864
|
+
type NextUp,
|
|
865
|
+
readNextUp,
|
|
866
|
+
shortMonthLabel,
|
|
867
|
+
type WallSpan,
|
|
868
|
+
wallSpanOn,
|
|
869
|
+
weekdayLongLabel,
|
|
870
|
+
weekdayShortLabel,
|
|
871
|
+
} from "./lib/agenda-time.js";
|
|
800
872
|
export {
|
|
801
873
|
DEFAULT_ATTACHMENT_FILENAME,
|
|
802
874
|
formatByteSize,
|
|
@@ -817,6 +889,16 @@ export {
|
|
|
817
889
|
type CalendarColorClasses,
|
|
818
890
|
calendarColorClasses,
|
|
819
891
|
} from "./lib/calendar-color.js";
|
|
892
|
+
export {
|
|
893
|
+
type CalendarEventShell,
|
|
894
|
+
calendarEventBodyClasses,
|
|
895
|
+
} from "./lib/calendar-event-shell.js";
|
|
896
|
+
export {
|
|
897
|
+
DRAFT_MINUTES,
|
|
898
|
+
isDraggedSelection,
|
|
899
|
+
pointPick,
|
|
900
|
+
rangePick,
|
|
901
|
+
} from "./lib/calendar-slot-pick.js";
|
|
820
902
|
export {
|
|
821
903
|
buildCidResolver,
|
|
822
904
|
type CidResolvableBodyPart,
|