@remit/ui 0.0.156 → 0.0.158
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 +4 -2
- package/src/calendar.css +4 -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 +34 -83
- 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/event-detail.render.test.ts +31 -0
- package/src/components/event-detail.stories.tsx +17 -0
- package/src/components/event-detail.tsx +28 -19
- package/src/components/nav-sidebar.tsx +3 -3
- package/src/index.ts +18 -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
|
@@ -560,9 +560,9 @@ export interface NavSidebarProps
|
|
|
560
560
|
*/
|
|
561
561
|
linkComponent?: NavLinkComponent;
|
|
562
562
|
/**
|
|
563
|
-
* Whether the calendar destination sits under the daily brief.
|
|
564
|
-
*
|
|
565
|
-
*
|
|
563
|
+
* Whether the calendar destination sits under the daily brief. Hidden by
|
|
564
|
+
* default so a host with no calendar route behind it does not offer an entry
|
|
565
|
+
* that leads nowhere.
|
|
566
566
|
*/
|
|
567
567
|
calendarNav?: "hidden" | "shown";
|
|
568
568
|
/** Every saved query, most recently saved first. See `SavedSearchesGroup`. */
|
package/src/index.ts
CHANGED
|
@@ -147,6 +147,14 @@ export {
|
|
|
147
147
|
CalendarEventChip,
|
|
148
148
|
type CalendarEventChipProps,
|
|
149
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";
|
|
150
158
|
export {
|
|
151
159
|
CalendarList,
|
|
152
160
|
type CalendarListProps,
|
|
@@ -881,6 +889,16 @@ export {
|
|
|
881
889
|
type CalendarColorClasses,
|
|
882
890
|
calendarColorClasses,
|
|
883
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";
|
|
884
902
|
export {
|
|
885
903
|
buildCidResolver,
|
|
886
904
|
type CidResolvableBodyPart,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Density } from "../components/app-shell-types.js";
|
|
2
|
+
import type {
|
|
3
|
+
CalendarColorId,
|
|
4
|
+
RsvpState,
|
|
5
|
+
} from "../components/calendar-types.js";
|
|
6
|
+
import { calendarColorClasses } from "./calendar-color.js";
|
|
7
|
+
import { cn } from "./cn.js";
|
|
8
|
+
|
|
9
|
+
export interface CalendarEventShell {
|
|
10
|
+
color: CalendarColorId;
|
|
11
|
+
/**
|
|
12
|
+
* `row` is the horizontal pill of the all-day band and the month grid.
|
|
13
|
+
* `column` is the block that fills its slot in a time grid.
|
|
14
|
+
*/
|
|
15
|
+
layout: "row" | "column";
|
|
16
|
+
density: Density;
|
|
17
|
+
/** The reader's own reply. A declined event dims; it never recolours. */
|
|
18
|
+
rsvp: RsvpState;
|
|
19
|
+
status: "confirmed" | "tentative";
|
|
20
|
+
selected: boolean;
|
|
21
|
+
/** Two lines rather than one: a title with something under it. */
|
|
22
|
+
stacked: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The coloured body of one event: the hue, the RSVP, the selection, and the box
|
|
27
|
+
* they sit in.
|
|
28
|
+
*
|
|
29
|
+
* `CalendarEventChip` draws this body inside a button of its own.
|
|
30
|
+
* `CalendarGrid` cannot — its engine builds the event's element and takes a
|
|
31
|
+
* class string for it — so there the engine's element *is* the body, styled
|
|
32
|
+
* from here. Neither surface restates the other.
|
|
33
|
+
*
|
|
34
|
+
* How the body is sized is left to the surface: the chip stretches it beside a
|
|
35
|
+
* leading slot, and the grid's engine has already positioned it.
|
|
36
|
+
*/
|
|
37
|
+
export function calendarEventBodyClasses(shell: CalendarEventShell): string {
|
|
38
|
+
const hue = calendarColorClasses(shell.color);
|
|
39
|
+
const provisional =
|
|
40
|
+
shell.rsvp === "tentative" || shell.status === "tentative";
|
|
41
|
+
return cn(
|
|
42
|
+
"flex min-w-0 overflow-hidden rounded-sm border-l-2 px-1.5 py-0.5",
|
|
43
|
+
hue.soft,
|
|
44
|
+
hue.text,
|
|
45
|
+
hue.rail,
|
|
46
|
+
shell.layout === "column" || shell.stacked
|
|
47
|
+
? "flex-col"
|
|
48
|
+
: "items-center gap-1.5",
|
|
49
|
+
provisional && "border-y border-r border-dashed",
|
|
50
|
+
provisional && hue.border,
|
|
51
|
+
shell.rsvp === "declined" && "opacity-60",
|
|
52
|
+
shell.selected && "ring-2 ring-ring",
|
|
53
|
+
shell.density === "compact" ? "text-2xs" : "text-xs",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
isDraggedSelection,
|
|
5
|
+
pointPick,
|
|
6
|
+
rangePick,
|
|
7
|
+
} from "./calendar-slot-pick.js";
|
|
8
|
+
|
|
9
|
+
describe("pointPick", () => {
|
|
10
|
+
it("drafts an hour from the point that was clicked", () => {
|
|
11
|
+
assert.deepEqual(pointPick("2026-06-10T14:30:00+02:00", false), {
|
|
12
|
+
date: "2026-06-10",
|
|
13
|
+
startTime: "14:30",
|
|
14
|
+
endTime: "15:30",
|
|
15
|
+
allDay: false,
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("reads the clock off the string, not off the host's zone", () => {
|
|
20
|
+
/* The same instant, written in two zones. A pick reads the wall clock the
|
|
21
|
+
grid drew, so each keeps its own — a Date would collapse both to the
|
|
22
|
+
runner's zone. */
|
|
23
|
+
assert.equal(
|
|
24
|
+
pointPick("2026-06-10T09:00:00+02:00", false).startTime,
|
|
25
|
+
"09:00",
|
|
26
|
+
);
|
|
27
|
+
assert.equal(pointPick("2026-06-10T07:00:00Z", false).startTime, "07:00");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("carries an all-day point with no clock at all", () => {
|
|
31
|
+
assert.deepEqual(pointPick("2026-06-10", true), {
|
|
32
|
+
date: "2026-06-10",
|
|
33
|
+
startTime: "",
|
|
34
|
+
endTime: "",
|
|
35
|
+
allDay: true,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("rolls a draft that runs past midnight round the clock", () => {
|
|
40
|
+
assert.equal(
|
|
41
|
+
pointPick("2026-06-10T23:30:00+02:00", false).endTime,
|
|
42
|
+
"00:30",
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("rangePick", () => {
|
|
48
|
+
it("keeps both ends of a dragged range", () => {
|
|
49
|
+
assert.deepEqual(
|
|
50
|
+
rangePick(
|
|
51
|
+
"2026-06-10T09:00:00+02:00",
|
|
52
|
+
"2026-06-10T11:30:00+02:00",
|
|
53
|
+
false,
|
|
54
|
+
),
|
|
55
|
+
{
|
|
56
|
+
date: "2026-06-10",
|
|
57
|
+
startTime: "09:00",
|
|
58
|
+
endTime: "11:30",
|
|
59
|
+
allDay: false,
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("drops the clock off an all-day range", () => {
|
|
65
|
+
assert.deepEqual(rangePick("2026-06-10", "2026-06-13", true), {
|
|
66
|
+
date: "2026-06-10",
|
|
67
|
+
startTime: "",
|
|
68
|
+
endTime: "",
|
|
69
|
+
allDay: true,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("isDraggedSelection", () => {
|
|
75
|
+
const slot = 30;
|
|
76
|
+
|
|
77
|
+
it("calls one slot a click, not a drag", () => {
|
|
78
|
+
assert.equal(
|
|
79
|
+
isDraggedSelection(
|
|
80
|
+
"2026-06-10T09:00:00+02:00",
|
|
81
|
+
"2026-06-10T09:30:00+02:00",
|
|
82
|
+
false,
|
|
83
|
+
slot,
|
|
84
|
+
),
|
|
85
|
+
false,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("calls anything wider than a slot a drag", () => {
|
|
90
|
+
assert.equal(
|
|
91
|
+
isDraggedSelection(
|
|
92
|
+
"2026-06-10T09:00:00+02:00",
|
|
93
|
+
"2026-06-10T10:00:00+02:00",
|
|
94
|
+
false,
|
|
95
|
+
slot,
|
|
96
|
+
),
|
|
97
|
+
true,
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("measures against the slot in force, so a coarser grid clicks wider", () => {
|
|
102
|
+
assert.equal(
|
|
103
|
+
isDraggedSelection(
|
|
104
|
+
"2026-06-10T09:00:00+02:00",
|
|
105
|
+
"2026-06-10T10:00:00+02:00",
|
|
106
|
+
false,
|
|
107
|
+
60,
|
|
108
|
+
),
|
|
109
|
+
false,
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("calls a single all-day cell a click", () => {
|
|
114
|
+
assert.equal(
|
|
115
|
+
isDraggedSelection("2026-06-10", "2026-06-11", true, slot),
|
|
116
|
+
false,
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("calls two all-day cells a drag", () => {
|
|
121
|
+
assert.equal(
|
|
122
|
+
isDraggedSelection("2026-06-10", "2026-06-12", true, slot),
|
|
123
|
+
true,
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("survives a day that changed length under it", () => {
|
|
128
|
+
/* The Amsterdam DST spring forward: 25 to 26 October is 25 hours long, and
|
|
129
|
+
rounding is what keeps that one cell a click. */
|
|
130
|
+
assert.equal(
|
|
131
|
+
isDraggedSelection(
|
|
132
|
+
"2026-10-25T00:00:00+02:00",
|
|
133
|
+
"2026-10-26T00:00:00+01:00",
|
|
134
|
+
true,
|
|
135
|
+
slot,
|
|
136
|
+
),
|
|
137
|
+
false,
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { CalendarSlotPick } from "../components/calendar-types.js";
|
|
2
|
+
import { addMinutesToClock } from "./event-phrase.js";
|
|
3
|
+
|
|
4
|
+
/** How long a new event is when nobody said — the hour every other path starts from. */
|
|
5
|
+
export const DRAFT_MINUTES = 60;
|
|
6
|
+
|
|
7
|
+
const MS_PER_MINUTE = 60_000;
|
|
8
|
+
const MS_PER_DAY = 86_400_000;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A pick is read off the ISO strings the calendar hands the callback, never off
|
|
12
|
+
* its `Date`. The grid runs in the calendar's own zone; a `Date` read through
|
|
13
|
+
* `getHours()` is that instant in the host's zone, so a UTC runner would draft
|
|
14
|
+
* 09:00 for a click on 11:00.
|
|
15
|
+
*/
|
|
16
|
+
export function rangePick(
|
|
17
|
+
startStr: string,
|
|
18
|
+
endStr: string,
|
|
19
|
+
allDay: boolean,
|
|
20
|
+
): CalendarSlotPick {
|
|
21
|
+
if (allDay) return allDayPick(startStr);
|
|
22
|
+
return {
|
|
23
|
+
date: startStr.slice(0, 10),
|
|
24
|
+
startTime: startStr.slice(11, 16),
|
|
25
|
+
endTime: endStr.slice(11, 16),
|
|
26
|
+
allDay: false,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A single point on the grid, an hour long because nothing said otherwise. */
|
|
31
|
+
export function pointPick(dateStr: string, allDay: boolean): CalendarSlotPick {
|
|
32
|
+
if (allDay) return allDayPick(dateStr);
|
|
33
|
+
const startTime = dateStr.slice(11, 16);
|
|
34
|
+
return {
|
|
35
|
+
date: dateStr.slice(0, 10),
|
|
36
|
+
startTime,
|
|
37
|
+
endTime: addMinutesToClock(startTime, DRAFT_MINUTES),
|
|
38
|
+
allDay: false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function allDayPick(dateStr: string): CalendarSlotPick {
|
|
43
|
+
return {
|
|
44
|
+
date: dateStr.slice(0, 10),
|
|
45
|
+
startTime: "",
|
|
46
|
+
endTime: "",
|
|
47
|
+
allDay: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether a selection was dragged across the grid rather than landing on it.
|
|
53
|
+
* One slot, or one day, is as small as a selection gets, which is exactly what
|
|
54
|
+
* a click leaves behind — and a click is already a pick of its own.
|
|
55
|
+
*/
|
|
56
|
+
export function isDraggedSelection(
|
|
57
|
+
startStr: string,
|
|
58
|
+
endStr: string,
|
|
59
|
+
allDay: boolean,
|
|
60
|
+
slotMinutes: number,
|
|
61
|
+
): boolean {
|
|
62
|
+
const span = Date.parse(endStr) - Date.parse(startStr);
|
|
63
|
+
if (allDay) return Math.round(span / MS_PER_DAY) > 1;
|
|
64
|
+
return span / MS_PER_MINUTE > slotMinutes;
|
|
65
|
+
}
|