@dataverse-kit/calendar-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -0
- package/dist/CalendarView-CShiWFsR.d.ts +104 -0
- package/dist/CalendarView-Dd8BHOLE.d.cts +104 -0
- package/dist/core.cjs +192 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +64 -0
- package/dist/core.d.ts +64 -0
- package/dist/core.mjs +151 -0
- package/dist/core.mjs.map +1 -0
- package/dist/dataverse.cjs +300 -0
- package/dist/dataverse.cjs.map +1 -0
- package/dist/dataverse.d.cts +121 -0
- package/dist/dataverse.d.ts +121 -0
- package/dist/dataverse.mjs +263 -0
- package/dist/dataverse.mjs.map +1 -0
- package/dist/fake.cjs +199 -0
- package/dist/fake.cjs.map +1 -0
- package/dist/fake.d.cts +29 -0
- package/dist/fake.d.ts +29 -0
- package/dist/fake.mjs +171 -0
- package/dist/fake.mjs.map +1 -0
- package/dist/index.cjs +568 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.mjs +515 -0
- package/dist/index.mjs.map +1 -0
- package/dist/pcf-dataset.cjs +328 -0
- package/dist/pcf-dataset.cjs.map +1 -0
- package/dist/pcf-dataset.d.cts +98 -0
- package/dist/pcf-dataset.d.ts +98 -0
- package/dist/pcf-dataset.mjs +296 -0
- package/dist/pcf-dataset.mjs.map +1 -0
- package/dist/types-u3Gpxk53.d.cts +108 -0
- package/dist/types-u3Gpxk53.d.ts +108 -0
- package/dist/v9.cjs +555 -0
- package/dist/v9.cjs.map +1 -0
- package/dist/v9.d.cts +18 -0
- package/dist/v9.d.ts +18 -0
- package/dist/v9.mjs +517 -0
- package/dist/v9.mjs.map +1 -0
- package/package.json +104 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The calendar's data contract. Framework-agnostic: nothing here may import React, FullCalendar
|
|
3
|
+
* or Fluent (enforced by .eslintrc.cjs and verified on the built bundle).
|
|
4
|
+
*
|
|
5
|
+
* ★★ A date-time value is ISO-8601 TEXT, and its SHAPE carries its meaning:
|
|
6
|
+
*
|
|
7
|
+
* | shape | kind | means |
|
|
8
|
+
* |------------------------------------|------------|------------------------------------------------|
|
|
9
|
+
* | `2026-09-29` | `date` | a calendar day (all-day) |
|
|
10
|
+
* | `2026-09-29T23:30` / `…T23:30:00` | `floating` | a wall-clock time, the same face in every zone |
|
|
11
|
+
* | `2026-09-30T06:30:00Z` / `…+09:00` | `instant` | one moment, shown in the viewer's zone |
|
|
12
|
+
*
|
|
13
|
+
* That is not an invention: it is exactly how ISO-8601 already distinguishes the three, and they
|
|
14
|
+
* map one-to-one onto Dataverse's DateOnly, TimeZoneIndependent and UserLocal behaviours
|
|
15
|
+
* (see ./dataverse.ts). An event keeps its kind through a drag — a floating event moved on the
|
|
16
|
+
* calendar comes back floating — so a host never has to remember which kind it sent.
|
|
17
|
+
*
|
|
18
|
+
* A `Date` is accepted wherever a value is read, and always means an instant.
|
|
19
|
+
*/
|
|
20
|
+
type CalendarValue = string;
|
|
21
|
+
type ValueKind = 'date' | 'floating' | 'instant';
|
|
22
|
+
/**
|
|
23
|
+
* Minutes EAST of UTC for the zone the calendar is shown in, at a given instant.
|
|
24
|
+
*
|
|
25
|
+
* ★ East-positive, matching PCF's `userSettings.getTimeZoneOffsetMinutes()` (Pacific → -420,
|
|
26
|
+
* Tokyo → +540, MEASURED) and the OPPOSITE sign to JS `Date#getTimezoneOffset()`.
|
|
27
|
+
* ★ A FUNCTION of the instant rather than a number, so daylight-saving changes inside the
|
|
28
|
+
* visible range are handled: the offset for 1 March and 1 April can differ.
|
|
29
|
+
*/
|
|
30
|
+
type ZoneOffset = (instant: Date) => number;
|
|
31
|
+
interface CalendarEvent {
|
|
32
|
+
/** Stable id. The calendar hands it back on every callback. */
|
|
33
|
+
id: string;
|
|
34
|
+
/** See {@link CalendarValue}. A date-shaped start makes the event all-day. */
|
|
35
|
+
start: CalendarValue | Date;
|
|
36
|
+
/**
|
|
37
|
+
* Same kind as `start`. For an all-day event this is EXCLUSIVE — the day AFTER the last day
|
|
38
|
+
* shown — as in iCalendar and FullCalendar. (Dataverse stores it inclusive; the Dataverse
|
|
39
|
+
* helpers convert.) Omitted means a default-length event.
|
|
40
|
+
*/
|
|
41
|
+
end?: CalendarValue | Date;
|
|
42
|
+
title: string;
|
|
43
|
+
/** Any CSS colour. Omitted means the calendar's primary colour. */
|
|
44
|
+
color?: string;
|
|
45
|
+
/** Opaque grouping key — a location, a staff member. Used for colour and filtering. */
|
|
46
|
+
resourceId?: string;
|
|
47
|
+
/** Forces all-day. Normally inferred from a date-shaped `start`. */
|
|
48
|
+
allDay?: boolean;
|
|
49
|
+
/** This event cannot be dragged or resized even when the calendar is editable. */
|
|
50
|
+
readOnly?: boolean;
|
|
51
|
+
/** Anything the host wants back in callbacks. The calendar never reads it. */
|
|
52
|
+
data?: unknown;
|
|
53
|
+
}
|
|
54
|
+
/** A moved or resized event, in the SAME kind as the event's original `start`. */
|
|
55
|
+
interface CalendarEventChange {
|
|
56
|
+
event: CalendarEvent;
|
|
57
|
+
kind: 'move' | 'resize';
|
|
58
|
+
start: CalendarValue;
|
|
59
|
+
/** Exclusive for all-day, as on {@link CalendarEvent.end}. */
|
|
60
|
+
end: CalendarValue | undefined;
|
|
61
|
+
allDay: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* An empty range the user selected. `allDay` selections are `date` values with an EXCLUSIVE
|
|
65
|
+
* end; timed selections are `instant` values (UTC `Z`), converted from the calendar's zone.
|
|
66
|
+
*/
|
|
67
|
+
interface SlotSelection {
|
|
68
|
+
start: CalendarValue;
|
|
69
|
+
end: CalendarValue;
|
|
70
|
+
allDay: boolean;
|
|
71
|
+
}
|
|
72
|
+
/** The visible range, as instants (UTC `Z`). `start` inclusive, `end` exclusive. */
|
|
73
|
+
interface VisibleRange {
|
|
74
|
+
start: CalendarValue;
|
|
75
|
+
end: CalendarValue;
|
|
76
|
+
/** The FullCalendar view name, e.g. `timeGridWeek`. */
|
|
77
|
+
view: string;
|
|
78
|
+
/** The same range as calendar DAYS in the calendar's zone, end exclusive. */
|
|
79
|
+
startDate: CalendarValue;
|
|
80
|
+
endDate: CalendarValue;
|
|
81
|
+
}
|
|
82
|
+
interface BusinessHours {
|
|
83
|
+
/** 0 = Sunday. */
|
|
84
|
+
daysOfWeek: number[];
|
|
85
|
+
/** `HH:mm`, wall-clock in the calendar's zone. */
|
|
86
|
+
startTime: string;
|
|
87
|
+
endTime: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The seam between the calendar and a data source that is PULLED (a code app's data service,
|
|
91
|
+
* a Power Pages `/_api` table). A PCF dataset is PUSHED by the host instead and does not use
|
|
92
|
+
* this — see `./adapters/pcf-dataset`.
|
|
93
|
+
*/
|
|
94
|
+
interface CalendarDataSource {
|
|
95
|
+
readonly capabilities: {
|
|
96
|
+
readonly update: boolean;
|
|
97
|
+
readonly create: boolean;
|
|
98
|
+
readonly open: boolean;
|
|
99
|
+
};
|
|
100
|
+
/** Events overlapping the range. Honour `signal`: ranges change faster than networks answer. */
|
|
101
|
+
load(range: VisibleRange, signal?: AbortSignal): Promise<CalendarEvent[]>;
|
|
102
|
+
/** Persist a move/resize. Reject to make the calendar put the event back. */
|
|
103
|
+
update?(change: CalendarEventChange): Promise<void>;
|
|
104
|
+
create?(slot: SlotSelection): Promise<void> | void;
|
|
105
|
+
open?(event: CalendarEvent): void;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type { BusinessHours as B, CalendarDataSource as C, SlotSelection as S, ValueKind as V, ZoneOffset as Z, CalendarEvent as a, CalendarEventChange as b, CalendarValue as c, VisibleRange as d };
|
package/dist/v9.cjs
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/v9/index.ts
|
|
31
|
+
var v9_exports = {};
|
|
32
|
+
__export(v9_exports, {
|
|
33
|
+
FluentCalendar: () => FluentCalendar,
|
|
34
|
+
fluentCalendarTheme: () => fluentCalendarTheme
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(v9_exports);
|
|
37
|
+
|
|
38
|
+
// src/v9/FluentCalendar.tsx
|
|
39
|
+
var React3 = __toESM(require("react"), 1);
|
|
40
|
+
var import_react_components = require("@fluentui/react-components");
|
|
41
|
+
|
|
42
|
+
// src/view/CalendarView.tsx
|
|
43
|
+
var React2 = __toESM(require("react"), 1);
|
|
44
|
+
var import_react = __toESM(require("@fullcalendar/react"), 1);
|
|
45
|
+
var import_daygrid = __toESM(require("@fullcalendar/react/daygrid"), 1);
|
|
46
|
+
var import_timegrid = __toESM(require("@fullcalendar/react/timegrid"), 1);
|
|
47
|
+
var import_list = __toESM(require("@fullcalendar/react/list"), 1);
|
|
48
|
+
var import_interaction = __toESM(require("@fullcalendar/react/interaction"), 1);
|
|
49
|
+
var import_classic = __toESM(require("@fullcalendar/react/themes/classic"), 1);
|
|
50
|
+
var import_skeleton = require("@fullcalendar/react/skeleton.css");
|
|
51
|
+
var import_theme = require("@fullcalendar/react/themes/classic/theme.css");
|
|
52
|
+
var import_palette = require("@fullcalendar/react/themes/classic/palette.css");
|
|
53
|
+
|
|
54
|
+
// src/core/datetime.ts
|
|
55
|
+
var DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
56
|
+
var FLOATING_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,9})?)?$/;
|
|
57
|
+
var INSTANT_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})$/i;
|
|
58
|
+
var pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
59
|
+
var browserZone = (instant) => -instant.getTimezoneOffset();
|
|
60
|
+
function kindOf(value) {
|
|
61
|
+
if (value instanceof Date) {
|
|
62
|
+
if (Number.isNaN(value.getTime())) throw new RangeError("Invalid Date");
|
|
63
|
+
return "instant";
|
|
64
|
+
}
|
|
65
|
+
const local = DATE_RE.test(value) ? "date" : FLOATING_RE.test(value) ? "floating" : null;
|
|
66
|
+
if (local) {
|
|
67
|
+
const m = FLOATING_RE.exec(value) ?? DATE_RE.exec(value);
|
|
68
|
+
const [, y, mo, d, h = "0", mi = "0", sec = "0"] = m;
|
|
69
|
+
const t = new Date(Date.UTC(+y, +mo - 1, +d, +h, +mi, +sec));
|
|
70
|
+
if (t.getUTCFullYear() !== +y || t.getUTCMonth() !== +mo - 1 || t.getUTCDate() !== +d || +h > 23 || +mi > 59 || +sec > 59) {
|
|
71
|
+
throw new RangeError(`Not a real calendar date or time: ${JSON.stringify(value)}`);
|
|
72
|
+
}
|
|
73
|
+
return local;
|
|
74
|
+
}
|
|
75
|
+
if (INSTANT_RE.test(value) && !Number.isNaN(new Date(value).getTime())) return "instant";
|
|
76
|
+
throw new RangeError(`Not an ISO-8601 date, wall-clock time or instant: ${JSON.stringify(value)}`);
|
|
77
|
+
}
|
|
78
|
+
function toInstant(value) {
|
|
79
|
+
if (value instanceof Date) return value.toISOString();
|
|
80
|
+
if (kindOf(value) !== "instant") throw new RangeError(`Not an instant: ${value}`);
|
|
81
|
+
return new Date(value).toISOString();
|
|
82
|
+
}
|
|
83
|
+
function wallMillis(wall) {
|
|
84
|
+
const m = FLOATING_RE.exec(wall) ?? DATE_RE.exec(wall);
|
|
85
|
+
if (!m) throw new RangeError(`Not a date or wall-clock time: ${wall}`);
|
|
86
|
+
const [, y, mo, d, h = "0", mi = "0", s = "0"] = m;
|
|
87
|
+
return Date.UTC(+y, +mo - 1, +d, +h, +mi, +s);
|
|
88
|
+
}
|
|
89
|
+
function formatWall(ms) {
|
|
90
|
+
const d = new Date(ms);
|
|
91
|
+
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
|
|
92
|
+
}
|
|
93
|
+
function toWall(instant, zone) {
|
|
94
|
+
const at = instant instanceof Date ? instant : new Date(toInstant(instant));
|
|
95
|
+
return formatWall(at.getTime() + zone(at) * 6e4);
|
|
96
|
+
}
|
|
97
|
+
function fromWall(wall, zone) {
|
|
98
|
+
const w = wallMillis(wall);
|
|
99
|
+
const before = zone(new Date(w - 864e5));
|
|
100
|
+
const after = zone(new Date(w + 864e5));
|
|
101
|
+
const fits = (offset) => {
|
|
102
|
+
const t2 = w - offset * 6e4;
|
|
103
|
+
return zone(new Date(t2)) === offset ? t2 : null;
|
|
104
|
+
};
|
|
105
|
+
const candidates = [fits(before), fits(after)].filter((t2) => t2 !== null);
|
|
106
|
+
const t = candidates.length > 0 ? Math.min(...candidates) : w - before * 6e4;
|
|
107
|
+
return new Date(t).toISOString();
|
|
108
|
+
}
|
|
109
|
+
function addDays(date, days) {
|
|
110
|
+
if (kindOf(date) !== "date") throw new RangeError(`addDays needs a date, got ${date}`);
|
|
111
|
+
return formatWall(wallMillis(date) + days * 864e5).slice(0, 10);
|
|
112
|
+
}
|
|
113
|
+
function utcFieldsToWall(d) {
|
|
114
|
+
return formatWall(d.getTime());
|
|
115
|
+
}
|
|
116
|
+
function normaliseFloating(value) {
|
|
117
|
+
return formatWall(wallMillis(value));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/core/colors.ts
|
|
121
|
+
function parseHex(color) {
|
|
122
|
+
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color.trim());
|
|
123
|
+
if (!m) return null;
|
|
124
|
+
const h = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];
|
|
125
|
+
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
|
|
126
|
+
}
|
|
127
|
+
function luminance([r, g, b]) {
|
|
128
|
+
const lin = (c) => {
|
|
129
|
+
const s = c / 255;
|
|
130
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
131
|
+
};
|
|
132
|
+
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
133
|
+
}
|
|
134
|
+
function contrastRatio(a, b) {
|
|
135
|
+
const pa = parseHex(a);
|
|
136
|
+
const pb = parseHex(b);
|
|
137
|
+
if (!pa || !pb) return null;
|
|
138
|
+
const [hi, lo] = [luminance(pa), luminance(pb)].sort((x, y) => y - x);
|
|
139
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
140
|
+
}
|
|
141
|
+
function readableTextOn(background) {
|
|
142
|
+
const onWhite = contrastRatio(background, "#ffffff");
|
|
143
|
+
const onBlack = contrastRatio(background, "#000000");
|
|
144
|
+
if (onWhite === null || onBlack === null) return void 0;
|
|
145
|
+
return onWhite >= onBlack ? "#ffffff" : "#000000";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/view/fcBridge.ts
|
|
149
|
+
function toFcValue(value, zone) {
|
|
150
|
+
const k = kindOf(value);
|
|
151
|
+
if (k === "instant") return toWall(value, zone);
|
|
152
|
+
if (k === "floating") return normaliseFloating(value);
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
function exclusiveAllDayEnd(end, zone) {
|
|
156
|
+
const wall = toFcValue(end, zone);
|
|
157
|
+
if (wall.length === 10) return wall;
|
|
158
|
+
return wall.slice(11, 19) === "00:00:00" ? wall.slice(0, 10) : addDays(wall.slice(0, 10), 1);
|
|
159
|
+
}
|
|
160
|
+
function toFcEvent(event, zone) {
|
|
161
|
+
const originalKind = kindOf(event.start);
|
|
162
|
+
const allDay = event.allDay ?? originalKind === "date";
|
|
163
|
+
const start = allDay ? toFcValue(event.start, zone).slice(0, 10) : toFcValue(event.start, zone);
|
|
164
|
+
const out = {
|
|
165
|
+
id: event.id,
|
|
166
|
+
title: event.title,
|
|
167
|
+
start,
|
|
168
|
+
allDay,
|
|
169
|
+
extendedProps: { source: event, kind: allDay ? "date" : originalKind, originalKind }
|
|
170
|
+
};
|
|
171
|
+
if (event.end !== void 0) out.end = allDay ? exclusiveAllDayEnd(event.end, zone) : toFcValue(event.end, zone);
|
|
172
|
+
if (event.color) {
|
|
173
|
+
out.color = event.color;
|
|
174
|
+
const text = readableTextOn(event.color);
|
|
175
|
+
if (text) out.textColor = text;
|
|
176
|
+
}
|
|
177
|
+
if (event.readOnly) out.editable = false;
|
|
178
|
+
else if (event.end === void 0) out.durationEditable = false;
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
function fromFcDate(d, kind, zone) {
|
|
182
|
+
const wall = utcFieldsToWall(d);
|
|
183
|
+
if (kind === "date") return wall.slice(0, 10);
|
|
184
|
+
if (kind === "floating") return wall;
|
|
185
|
+
return fromWall(wall, zone);
|
|
186
|
+
}
|
|
187
|
+
function asKind(value, from, target, zone) {
|
|
188
|
+
if (from === target || from !== "date") return value;
|
|
189
|
+
return target === "instant" ? fromWall(`${value}T00:00:00`, zone) : `${value}T00:00:00`;
|
|
190
|
+
}
|
|
191
|
+
function canonical(value) {
|
|
192
|
+
const k = kindOf(value);
|
|
193
|
+
return k === "instant" ? toInstant(value) : k === "floating" ? normaliseFloating(value) : value;
|
|
194
|
+
}
|
|
195
|
+
function toChange(fcEvent, kind, zone) {
|
|
196
|
+
const { source, kind: valueKind, originalKind } = fcEvent.extendedProps;
|
|
197
|
+
if (!fcEvent.start) throw new Error(`FullCalendar returned no start for event ${source.id}`);
|
|
198
|
+
const back = (d) => asKind(fromFcDate(d, valueKind, zone), valueKind, originalKind ?? valueKind, zone);
|
|
199
|
+
return {
|
|
200
|
+
event: source,
|
|
201
|
+
kind,
|
|
202
|
+
// ★ A RESIZE never moves the start. Recomputing it from FullCalendar's wall-clock Date is
|
|
203
|
+
// lossy on the fall-back day (the second 01:30 reads back as the first), and toWebApiPatch
|
|
204
|
+
// writes `start` unconditionally — so resizing an end silently moved the start an hour.
|
|
205
|
+
start: kind === "resize" ? canonical(source.start) : back(fcEvent.start),
|
|
206
|
+
end: fcEvent.end ? back(fcEvent.end) : void 0,
|
|
207
|
+
allDay: fcEvent.allDay
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function toSelection(start, end, allDay, zone) {
|
|
211
|
+
const kind = allDay ? "date" : "instant";
|
|
212
|
+
return { start: fromFcDate(start, kind, zone), end: fromFcDate(end, kind, zone), allDay };
|
|
213
|
+
}
|
|
214
|
+
function toVisibleRange(start, end, view, zone) {
|
|
215
|
+
return {
|
|
216
|
+
start: fromFcDate(start, "instant", zone),
|
|
217
|
+
end: fromFcDate(end, "instant", zone),
|
|
218
|
+
view,
|
|
219
|
+
startDate: utcFieldsToWall(start).slice(0, 10),
|
|
220
|
+
endDate: utcFieldsToWall(end).slice(0, 10)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
var nowWall = (zone) => toWall(/* @__PURE__ */ new Date(), zone);
|
|
224
|
+
var sameAllDayness = (spanAllDay, eventAllDay) => spanAllDay === eventAllDay;
|
|
225
|
+
var same = (a, b) => {
|
|
226
|
+
if (a === void 0 || b === void 0) return a === b;
|
|
227
|
+
const ka = kindOf(a);
|
|
228
|
+
if (ka !== kindOf(b)) return false;
|
|
229
|
+
if (ka === "instant") return new Date(a).getTime() === new Date(b).getTime();
|
|
230
|
+
return ka === "floating" ? normaliseFloating(a) === normaliseFloating(b) : a === b;
|
|
231
|
+
};
|
|
232
|
+
function applyPending(events, pending) {
|
|
233
|
+
if (pending.size === 0) return events;
|
|
234
|
+
return events.map((e) => {
|
|
235
|
+
const p = pending.get(e.id);
|
|
236
|
+
return p ? { ...e, start: p.start, end: p.end, allDay: p.allDay } : e;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function settledIds(events, pending) {
|
|
240
|
+
const byId = new Map(events.map((e) => [e.id, e]));
|
|
241
|
+
const done = [];
|
|
242
|
+
for (const [id, p] of pending) {
|
|
243
|
+
const e = byId.get(id);
|
|
244
|
+
if (!e || same(e.start, p.start) && same(e.end, p.end) || p.savedWith !== void 0 && p.savedWith !== events) done.push(id);
|
|
245
|
+
}
|
|
246
|
+
return done;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/view/useOptimisticChanges.ts
|
|
250
|
+
var React = __toESM(require("react"), 1);
|
|
251
|
+
function describe(error) {
|
|
252
|
+
const m = error?.message;
|
|
253
|
+
return typeof m === "string" && m ? m : String(error);
|
|
254
|
+
}
|
|
255
|
+
function useOptimisticChanges(events, onEventChange, onChangeError, zoneRef) {
|
|
256
|
+
const [pending, setPending] = React.useState(() => /* @__PURE__ */ new Map());
|
|
257
|
+
const [error, setError] = React.useState(null);
|
|
258
|
+
const eventsRef = React.useRef(events);
|
|
259
|
+
eventsRef.current = events;
|
|
260
|
+
const latest = React.useRef(/* @__PURE__ */ new Map());
|
|
261
|
+
const seq = React.useRef(0);
|
|
262
|
+
const mounted = React.useRef(true);
|
|
263
|
+
React.useEffect(() => {
|
|
264
|
+
mounted.current = true;
|
|
265
|
+
return () => {
|
|
266
|
+
mounted.current = false;
|
|
267
|
+
};
|
|
268
|
+
}, []);
|
|
269
|
+
React.useEffect(() => {
|
|
270
|
+
const done = settledIds(events, pending);
|
|
271
|
+
if (done.length === 0) return;
|
|
272
|
+
for (const id of done) if (latest.current.get(id) === pending.get(id)?.token) latest.current.delete(id);
|
|
273
|
+
setPending((prev) => {
|
|
274
|
+
const next = new Map(prev);
|
|
275
|
+
for (const id of done) next.delete(id);
|
|
276
|
+
return next;
|
|
277
|
+
});
|
|
278
|
+
}, [events, pending]);
|
|
279
|
+
const handleChange = React.useCallback(
|
|
280
|
+
(kind) => (info) => {
|
|
281
|
+
if (!onEventChange) {
|
|
282
|
+
info.revert();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
let change;
|
|
286
|
+
try {
|
|
287
|
+
change = toChange(info.event, kind, zoneRef.current);
|
|
288
|
+
} catch (e) {
|
|
289
|
+
info.revert();
|
|
290
|
+
setError(describe(e));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
setError(null);
|
|
294
|
+
const id = change.event.id;
|
|
295
|
+
const token = ++seq.current;
|
|
296
|
+
const fail = (err) => {
|
|
297
|
+
const current = latest.current.get(id) === token;
|
|
298
|
+
if (current) latest.current.delete(id);
|
|
299
|
+
onChangeError?.(err, change);
|
|
300
|
+
if (!mounted.current) return;
|
|
301
|
+
setError(`Could not save "${change.event.title}": ${describe(err)}`);
|
|
302
|
+
if (!current) return;
|
|
303
|
+
setPending((prev) => {
|
|
304
|
+
if (!prev.has(id)) return prev;
|
|
305
|
+
const next = new Map(prev);
|
|
306
|
+
next.delete(id);
|
|
307
|
+
return next;
|
|
308
|
+
});
|
|
309
|
+
try {
|
|
310
|
+
info.revert();
|
|
311
|
+
} catch {
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
latest.current.set(id, token);
|
|
315
|
+
let result;
|
|
316
|
+
try {
|
|
317
|
+
result = onEventChange(change);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
fail(err);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (!result || typeof result.then !== "function") {
|
|
323
|
+
if (latest.current.get(id) === token) latest.current.delete(id);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
setPending((prev) => new Map(prev).set(id, { start: change.start, end: change.end, allDay: change.allDay, token }));
|
|
327
|
+
result.then(
|
|
328
|
+
() => {
|
|
329
|
+
if (!mounted.current || latest.current.get(id) !== token) return;
|
|
330
|
+
const savedWith = eventsRef.current;
|
|
331
|
+
setPending((prev) => {
|
|
332
|
+
const p = prev.get(id);
|
|
333
|
+
if (!p || p.token !== token) return prev;
|
|
334
|
+
return new Map(prev).set(id, { ...p, savedWith });
|
|
335
|
+
});
|
|
336
|
+
},
|
|
337
|
+
fail
|
|
338
|
+
);
|
|
339
|
+
},
|
|
340
|
+
[onEventChange, onChangeError, zoneRef]
|
|
341
|
+
);
|
|
342
|
+
return { pending, error, dismissError: React.useCallback(() => setError(null), []), handleChange };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/view/theme.ts
|
|
346
|
+
var VAR = {
|
|
347
|
+
primary: "--fc-classic-primary",
|
|
348
|
+
primaryForeground: "--fc-classic-primary-foreground",
|
|
349
|
+
background: "--fc-classic-background",
|
|
350
|
+
foreground: "--fc-classic-foreground",
|
|
351
|
+
mutedForeground: "--fc-classic-muted-foreground",
|
|
352
|
+
faint: "--fc-classic-faint",
|
|
353
|
+
muted: "--fc-classic-muted",
|
|
354
|
+
strong: "--fc-classic-strong",
|
|
355
|
+
border: "--fc-classic-border",
|
|
356
|
+
strongBorder: "--fc-classic-strong-border",
|
|
357
|
+
today: "--fc-classic-today",
|
|
358
|
+
highlight: "--fc-classic-highlight",
|
|
359
|
+
now: "--fc-classic-now",
|
|
360
|
+
button: "--fc-classic-button",
|
|
361
|
+
buttonForeground: "--fc-classic-button-foreground",
|
|
362
|
+
buttonBorder: "--fc-classic-button-border",
|
|
363
|
+
buttonStrong: "--fc-classic-button-strong",
|
|
364
|
+
buttonStrongBorder: "--fc-classic-button-strong-border"
|
|
365
|
+
};
|
|
366
|
+
function themeStyle(theme) {
|
|
367
|
+
const style = {};
|
|
368
|
+
if (!theme) return style;
|
|
369
|
+
for (const [key, cssVar] of Object.entries(VAR)) {
|
|
370
|
+
const v = theme[key];
|
|
371
|
+
if (v) style[cssVar] = v;
|
|
372
|
+
}
|
|
373
|
+
if (theme.fontFamily) style.fontFamily = theme.fontFamily;
|
|
374
|
+
if (theme.fontSize) style.fontSize = theme.fontSize;
|
|
375
|
+
if (theme.background) style.background = theme.background;
|
|
376
|
+
if (theme.foreground) style.color = theme.foreground;
|
|
377
|
+
return style;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/view/CalendarView.tsx
|
|
381
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
382
|
+
var FC_VIEW = {
|
|
383
|
+
month: "dayGridMonth",
|
|
384
|
+
week: "timeGridWeek",
|
|
385
|
+
workWeek: "timeGridWorkWeek",
|
|
386
|
+
day: "timeGridDay",
|
|
387
|
+
list: "listWeek"
|
|
388
|
+
};
|
|
389
|
+
var VIEW_LABEL = {
|
|
390
|
+
month: "Month",
|
|
391
|
+
week: "Week",
|
|
392
|
+
workWeek: "Work week",
|
|
393
|
+
day: "Day",
|
|
394
|
+
list: "List"
|
|
395
|
+
};
|
|
396
|
+
var asChangeInfo = (info) => ({
|
|
397
|
+
event: {
|
|
398
|
+
start: info.event.start,
|
|
399
|
+
end: info.event.end,
|
|
400
|
+
allDay: info.event.allDay,
|
|
401
|
+
extendedProps: info.event.extendedProps
|
|
402
|
+
},
|
|
403
|
+
revert: () => info.revert()
|
|
404
|
+
});
|
|
405
|
+
var bannerStyle = {
|
|
406
|
+
display: "flex",
|
|
407
|
+
alignItems: "center",
|
|
408
|
+
gap: 8,
|
|
409
|
+
padding: "6px 10px",
|
|
410
|
+
marginBottom: 6,
|
|
411
|
+
borderRadius: 4,
|
|
412
|
+
border: "1px solid currentColor"
|
|
413
|
+
};
|
|
414
|
+
function CalendarView(props) {
|
|
415
|
+
const zone = props.zone ?? browserZone;
|
|
416
|
+
const zoneRef = React2.useRef(zone);
|
|
417
|
+
zoneRef.current = zone;
|
|
418
|
+
const { pending, error, dismissError, handleChange } = useOptimisticChanges(
|
|
419
|
+
props.events,
|
|
420
|
+
props.onEventChange,
|
|
421
|
+
props.onChangeError,
|
|
422
|
+
zoneRef
|
|
423
|
+
);
|
|
424
|
+
const fcEvents = React2.useMemo(
|
|
425
|
+
() => applyPending(props.events, pending).map((e) => toFcEvent(e, zone)),
|
|
426
|
+
[props.events, pending, zone]
|
|
427
|
+
);
|
|
428
|
+
const views = props.views ?? ["month", "week", "day", "list"];
|
|
429
|
+
const optional = {};
|
|
430
|
+
if (props.firstDay !== void 0) optional.firstDay = props.firstDay;
|
|
431
|
+
if (props.slotMinTime !== void 0) optional.slotMinTime = props.slotMinTime;
|
|
432
|
+
if (props.slotMaxTime !== void 0) optional.slotMaxTime = props.slotMaxTime;
|
|
433
|
+
if (props.initialDate !== void 0) optional.initialDate = props.initialDate;
|
|
434
|
+
if (props.classNames?.button) optional.buttonClass = props.classNames.button;
|
|
435
|
+
if (props.classNames?.toolbar) optional.headerToolbarClass = props.classNames.toolbar;
|
|
436
|
+
if (props.businessHours) optional.businessHours = props.businessHours.map((b) => ({ ...b, daysOfWeek: [...b.daysOfWeek] }));
|
|
437
|
+
const selectable = props.selectable ?? !!props.onSelectRange;
|
|
438
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
439
|
+
"div",
|
|
440
|
+
{
|
|
441
|
+
className: props.className,
|
|
442
|
+
style: themeStyle(props.theme),
|
|
443
|
+
role: "region",
|
|
444
|
+
"aria-label": props["aria-label"] ?? "Calendar",
|
|
445
|
+
"aria-busy": props.loading || void 0,
|
|
446
|
+
"data-calendar-kit": "",
|
|
447
|
+
children: [
|
|
448
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { role: "alert", style: { ...bannerStyle, color: props.theme?.error ?? "#a4262c" }, children: [
|
|
449
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flex: 1 }, children: error }),
|
|
450
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: dismissError, "aria-label": "Dismiss", style: { font: "inherit" }, children: "\xD7" })
|
|
451
|
+
] }),
|
|
452
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
453
|
+
import_react.default,
|
|
454
|
+
{
|
|
455
|
+
plugins: [import_daygrid.default, import_timegrid.default, import_list.default, import_interaction.default, import_classic.default],
|
|
456
|
+
timeZone: "UTC",
|
|
457
|
+
now: () => nowWall(zoneRef.current),
|
|
458
|
+
initialView: FC_VIEW[props.view ?? "week"],
|
|
459
|
+
views: { timeGridWorkWeek: { type: "timeGridWeek", hiddenDays: [0, 6] } },
|
|
460
|
+
headerToolbar: {
|
|
461
|
+
start: "today prev,next",
|
|
462
|
+
center: "title",
|
|
463
|
+
end: views.map((v) => FC_VIEW[v]).join(",")
|
|
464
|
+
},
|
|
465
|
+
buttons: {
|
|
466
|
+
today: { text: "Today" },
|
|
467
|
+
...Object.fromEntries(views.map((v) => [FC_VIEW[v], { text: VIEW_LABEL[v] }]))
|
|
468
|
+
},
|
|
469
|
+
height: props.height ?? "auto",
|
|
470
|
+
events: fcEvents,
|
|
471
|
+
editable: !!props.editable && !!props.onEventChange,
|
|
472
|
+
selectable,
|
|
473
|
+
selectMirror: true,
|
|
474
|
+
nowIndicator: true,
|
|
475
|
+
dayMaxEvents: true,
|
|
476
|
+
...optional,
|
|
477
|
+
scrollTime: props.scrollTime ?? "08:00:00",
|
|
478
|
+
eventAllow: (span, moving) => moving ? sameAllDayness(span.allDay, moving.allDay) : true,
|
|
479
|
+
datesSet: (info) => props.onRangeChange?.(toVisibleRange(info.start, info.end, info.view.type, zoneRef.current)),
|
|
480
|
+
eventClick: (info) => {
|
|
481
|
+
info.jsEvent.preventDefault();
|
|
482
|
+
const source = info.event.extendedProps.source;
|
|
483
|
+
if (source) props.onEventClick?.(source);
|
|
484
|
+
},
|
|
485
|
+
select: (info) => {
|
|
486
|
+
props.onSelectRange?.(toSelection(info.start, info.end, info.allDay, zoneRef.current));
|
|
487
|
+
info.view.calendar.unselect();
|
|
488
|
+
},
|
|
489
|
+
eventDrop: (info) => handleChange("move")(asChangeInfo(info)),
|
|
490
|
+
eventResize: (info) => handleChange("resize")(asChangeInfo(info))
|
|
491
|
+
}
|
|
492
|
+
)
|
|
493
|
+
]
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/v9/FluentCalendar.tsx
|
|
499
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
500
|
+
var fluentCalendarTheme = {
|
|
501
|
+
primary: import_react_components.tokens.colorBrandBackground,
|
|
502
|
+
primaryForeground: import_react_components.tokens.colorNeutralForegroundOnBrand,
|
|
503
|
+
background: import_react_components.tokens.colorNeutralBackground1,
|
|
504
|
+
foreground: import_react_components.tokens.colorNeutralForeground1,
|
|
505
|
+
mutedForeground: import_react_components.tokens.colorNeutralForeground3,
|
|
506
|
+
faint: import_react_components.tokens.colorNeutralBackground2,
|
|
507
|
+
muted: import_react_components.tokens.colorNeutralBackground3,
|
|
508
|
+
strong: import_react_components.tokens.colorNeutralBackground5,
|
|
509
|
+
border: import_react_components.tokens.colorNeutralStroke2,
|
|
510
|
+
strongBorder: import_react_components.tokens.colorNeutralStroke1,
|
|
511
|
+
today: import_react_components.tokens.colorBrandBackground2,
|
|
512
|
+
highlight: import_react_components.tokens.colorBrandBackground2Hover,
|
|
513
|
+
now: import_react_components.tokens.colorPaletteRedBorder2,
|
|
514
|
+
button: import_react_components.tokens.colorNeutralBackground1,
|
|
515
|
+
buttonForeground: import_react_components.tokens.colorNeutralForeground1,
|
|
516
|
+
buttonBorder: import_react_components.tokens.colorNeutralStroke1,
|
|
517
|
+
buttonStrong: import_react_components.tokens.colorNeutralBackground1Selected,
|
|
518
|
+
buttonStrongBorder: import_react_components.tokens.colorNeutralStroke1Pressed,
|
|
519
|
+
fontFamily: import_react_components.tokens.fontFamilyBase,
|
|
520
|
+
fontSize: import_react_components.tokens.fontSizeBase300,
|
|
521
|
+
error: import_react_components.tokens.colorPaletteRedForeground1
|
|
522
|
+
};
|
|
523
|
+
var useStyles = (0, import_react_components.makeStyles)({
|
|
524
|
+
// ★ Applied through FullCalendar's `buttonClass` option, NOT a `& .fc-button` selector: v7
|
|
525
|
+
// hashes its class names and that selector matched 0 buttons (measured).
|
|
526
|
+
button: {
|
|
527
|
+
borderRadius: import_react_components.tokens.borderRadiusMedium,
|
|
528
|
+
fontFamily: import_react_components.tokens.fontFamilyBase,
|
|
529
|
+
fontWeight: import_react_components.tokens.fontWeightSemibold
|
|
530
|
+
},
|
|
531
|
+
toolbar: {
|
|
532
|
+
gap: import_react_components.tokens.spacingHorizontalS
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
function FluentCalendar(props) {
|
|
536
|
+
const styles = useStyles();
|
|
537
|
+
const theme = React3.useMemo(() => ({ ...fluentCalendarTheme, ...props.theme }), [props.theme]);
|
|
538
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
539
|
+
CalendarView,
|
|
540
|
+
{
|
|
541
|
+
...props,
|
|
542
|
+
theme,
|
|
543
|
+
classNames: {
|
|
544
|
+
button: (0, import_react_components.mergeClasses)(styles.button, props.classNames?.button),
|
|
545
|
+
toolbar: (0, import_react_components.mergeClasses)(styles.toolbar, props.classNames?.toolbar)
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
551
|
+
0 && (module.exports = {
|
|
552
|
+
FluentCalendar,
|
|
553
|
+
fluentCalendarTheme
|
|
554
|
+
});
|
|
555
|
+
//# sourceMappingURL=v9.cjs.map
|