@mulmoclaude/core 1.6.0 → 1.7.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/assets/helps/google-calendar-collection.md +57 -3
- package/assets/helps/google.md +3 -2
- package/dist/calendarGrid-CaS9er8i.cjs +576 -0
- package/dist/calendarGrid-CaS9er8i.cjs.map +1 -0
- package/dist/calendarGrid-CrGxeCOG.js +397 -0
- package/dist/calendarGrid-CrGxeCOG.js.map +1 -0
- package/dist/collection/index.cjs +44 -44
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.js +2 -2
- package/dist/collection/registry/server/index.cjs +2 -2
- package/dist/collection/registry/server/index.js +2 -2
- package/dist/collection/server/index.cjs +4 -4
- package/dist/collection/server/index.js +3 -3
- package/dist/collection-watchers/index.cjs +5 -5
- package/dist/collection-watchers/index.cjs.map +1 -1
- package/dist/collection-watchers/index.js +4 -4
- package/dist/{discovery--kRUWed0.cjs → discovery-DYcR83qx.cjs} +64 -53
- package/dist/discovery-DYcR83qx.cjs.map +1 -0
- package/dist/{discovery-CKR4TOX3.js → discovery-_fjsluLx.js} +43 -32
- package/dist/discovery-_fjsluLx.js.map +1 -0
- package/dist/feeds/index.cjs +4 -4
- package/dist/feeds/index.js +2 -2
- package/dist/feeds/server/index.cjs +6 -6
- package/dist/feeds/server/index.js +4 -4
- package/dist/google/apiClient.d.ts +6 -0
- package/dist/google/calendar.d.ts +79 -8
- package/dist/google/calendarPushState.d.ts +17 -0
- package/dist/google/collectionPush.d.ts +64 -0
- package/dist/google/collectionSync.d.ts +10 -0
- package/dist/google/index.cjs +712 -36
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.d.ts +7 -3
- package/dist/google/index.js +683 -37
- package/dist/google/index.js.map +1 -1
- package/dist/google/pushDateTime.d.ts +10 -0
- package/dist/google/pushPlan.d.ts +73 -0
- package/dist/{ingestTypes-xX8VpQqh.cjs → ingestTypes-D1GQdG8e.cjs} +3 -3
- package/dist/{ingestTypes-xX8VpQqh.cjs.map → ingestTypes-D1GQdG8e.cjs.map} +1 -1
- package/dist/{ingestTypes-L59cIGgX.js → ingestTypes-XReG-li7.js} +2 -2
- package/dist/{ingestTypes-L59cIGgX.js.map → ingestTypes-XReG-li7.js.map} +1 -1
- package/dist/{promptSafety-CQ5Un4wi.js → promptSafety-C--op_6a.js} +3 -269
- package/dist/promptSafety-C--op_6a.js.map +1 -0
- package/dist/{promptSafety-DfZYNtjK.cjs → promptSafety-FD8cQn3e.cjs} +8 -358
- package/dist/promptSafety-FD8cQn3e.cjs.map +1 -0
- package/dist/remote-host/index.cjs +8 -0
- package/dist/remote-host/index.cjs.map +1 -1
- package/dist/remote-host/index.d.ts +21 -0
- package/dist/remote-host/index.js +8 -1
- package/dist/remote-host/index.js.map +1 -1
- package/dist/{server-D_6FLygK.js → server-BlIrtGKl.js} +4 -4
- package/dist/{server-D_6FLygK.js.map → server-BlIrtGKl.js.map} +1 -1
- package/dist/{server-gEOeV_Nv.cjs → server-l4TZ5Lbf.cjs} +21 -21
- package/dist/{server-gEOeV_Nv.cjs.map → server-l4TZ5Lbf.cjs.map} +1 -1
- package/package.json +1 -1
- package/dist/discovery--kRUWed0.cjs.map +0 -1
- package/dist/discovery-CKR4TOX3.js.map +0 -1
- package/dist/ids-BnTh3Hl0.cjs +0 -226
- package/dist/ids-BnTh3Hl0.cjs.map +0 -1
- package/dist/ids-s4GfmE93.js +0 -131
- package/dist/ids-s4GfmE93.js.map +0 -1
- package/dist/promptSafety-CQ5Un4wi.js.map +0 -1
- package/dist/promptSafety-DfZYNtjK.cjs.map +0 -1
|
@@ -6,10 +6,22 @@ events on a schedule and writes them as records — **without calling you**. No
|
|
|
6
6
|
tool call, no tokens spent per sync, so hourly syncing is free.
|
|
7
7
|
|
|
8
8
|
This is the mechanism for *keeping a collection fresh*. For one-off reads and
|
|
9
|
-
for
|
|
9
|
+
for deleting events, use the `google` tool — see
|
|
10
10
|
[The `google` tool](google.md). There is no bundled calendar collection: you
|
|
11
11
|
author the schema when the user asks for one.
|
|
12
12
|
|
|
13
|
+
## Both directions, but only one of them automatic
|
|
14
|
+
|
|
15
|
+
| Direction | How | When it runs |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| Google → collection | the `googleCalendar` block | hourly, on creation, and on the **Sync** button |
|
|
18
|
+
| collection → Google | the **Push to Google** button in the collection view | only when the user clicks it |
|
|
19
|
+
|
|
20
|
+
There is no automatic write-back and no setting that enables one. If a user
|
|
21
|
+
asks for "two-way sync", tell them plainly: the pull is automatic, the push is
|
|
22
|
+
a button they press. Do not go looking for a config key for it — there isn't
|
|
23
|
+
one, and hunting for it is what makes this conversation go in circles.
|
|
24
|
+
|
|
13
25
|
## Requirements
|
|
14
26
|
|
|
15
27
|
The user's Google account must be linked (`google` tool, `kind: "status"`). If
|
|
@@ -81,8 +93,50 @@ the calendar day view then draws each record as a proportional time block.
|
|
|
81
93
|
records on that first pass.
|
|
82
94
|
|
|
83
95
|
Records are ordinary collection records: the user can open, filter, and view
|
|
84
|
-
them like any other.
|
|
85
|
-
|
|
96
|
+
them like any other.
|
|
97
|
+
|
|
98
|
+
## Pushing local work back — the Push to Google button
|
|
99
|
+
|
|
100
|
+
The collection view has a **Push to Google** button next to Sync. It creates
|
|
101
|
+
events for records added locally and updates events for records edited locally.
|
|
102
|
+
|
|
103
|
+
**Order matters, and this is the one thing to warn users about.** A pull
|
|
104
|
+
overwrites a locally edited record as soon as Google reports any change to that
|
|
105
|
+
event. So: push first, then sync. Syncing first can discard the edit that was
|
|
106
|
+
waiting to be pushed.
|
|
107
|
+
|
|
108
|
+
What the button does and deliberately does not do:
|
|
109
|
+
|
|
110
|
+
- **Creates** an event for a record that never came from a sync.
|
|
111
|
+
- **Updates** only the fields the user actually changed, so attendees,
|
|
112
|
+
reminders and recurrence rules stay untouched.
|
|
113
|
+
- **Never deletes.** A record deleted locally leaves its Google event alone —
|
|
114
|
+
a Google delete removes the event for every attendee and cannot be undone.
|
|
115
|
+
The count is reported so the user knows it was skipped; deleting for real is
|
|
116
|
+
the `google` tool's `calendarDeleteEvent`, after confirming with them.
|
|
117
|
+
- **Skips a record edited on both sides** and reports it, rather than picking a
|
|
118
|
+
winner. The user resolves it by editing one side to match.
|
|
119
|
+
- Pushes only `summary`, `start`, `end` and `colorId` — `htmlLink` and `status`
|
|
120
|
+
are read-only in Google, so a column mapped to either is ignored.
|
|
121
|
+
|
|
122
|
+
Reasons a record can be reported as skipped:
|
|
123
|
+
|
|
124
|
+
- **Its record id cannot be a Google event id.** Google requires 5-1024
|
|
125
|
+
characters from `0-9a-v`. Records created through the UI get a valid
|
|
126
|
+
generated id; a semantic id you authored (`team-standup`) cannot be used. Fix
|
|
127
|
+
by recreating the record without setting the primary field.
|
|
128
|
+
- **No `start` / `end` is mapped, on a record being CREATED.** An event cannot
|
|
129
|
+
be created without a span. Editing an existing event is unaffected — a changed
|
|
130
|
+
title or colour is patched on its own.
|
|
131
|
+
- **The calendar reports no timezone**, and the stored clock carries no offset
|
|
132
|
+
to fall back on.
|
|
133
|
+
- **Clearing an event colour** — Google has no way to unset one.
|
|
134
|
+
|
|
135
|
+
If the user only has `reader` access to the calendar, the whole push is refused
|
|
136
|
+
with that reason rather than failing event by event. A calendar the user can
|
|
137
|
+
reach by id but has not added to their calendar list has no role to check, so
|
|
138
|
+
the push goes ahead and reports Google's own refusal if the write turns out not
|
|
139
|
+
to be allowed — being unlisted is not treated as being read-only.
|
|
86
140
|
|
|
87
141
|
## Not for this
|
|
88
142
|
|
package/assets/helps/google.md
CHANGED
|
@@ -15,10 +15,11 @@ the original call — do not fall back to guessing.
|
|
|
15
15
|
| --- | --- |
|
|
16
16
|
| Read or change specific events / tasks / files, right now | this tool |
|
|
17
17
|
| A collection that mirrors a calendar and keeps itself fresh | a `googleCalendar` block — see [Google Calendar sync](google-calendar-collection.md) |
|
|
18
|
+
| Records the user added/edited in a calendar collection to reach Google | the collection view's **Push to Google** button, not this tool |
|
|
18
19
|
|
|
19
20
|
The collection route costs no tokens per refresh, so prefer it for anything
|
|
20
|
-
recurring. Use this tool for one-off work and for
|
|
21
|
-
|
|
21
|
+
recurring. Use this tool for one-off work, and for deleting — the push button
|
|
22
|
+
never deletes.
|
|
22
23
|
|
|
23
24
|
## Calendar
|
|
24
25
|
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
//#region src/collection/core/fieldText.ts
|
|
2
|
+
/** A field value that has a meaningful text form. Dates are included because
|
|
3
|
+
* a JSON record can carry one once it has been revived. */
|
|
4
|
+
var isTextable = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date;
|
|
5
|
+
/** The field's text, or `null` when it has no meaningful one (absent, or an
|
|
6
|
+
* array/object that would stringify to `"[object Object]"`).
|
|
7
|
+
*
|
|
8
|
+
* Returning `null` rather than `""` keeps "the field is empty" distinct from
|
|
9
|
+
* "the field can't be text" — a matcher must not treat an object-valued field
|
|
10
|
+
* as an empty string and match `""`. */
|
|
11
|
+
function fieldTextOrNull(value) {
|
|
12
|
+
if (value === void 0 || value === null) return null;
|
|
13
|
+
if (!isTextable(value)) return null;
|
|
14
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString();
|
|
15
|
+
return String(value);
|
|
16
|
+
}
|
|
17
|
+
/** The field's text, or `fallback` (default `""`) when it has none. Use where
|
|
18
|
+
* a string is required and an empty one is a safe stand-in — display, sort
|
|
19
|
+
* keys, CSV cells. Where the distinction matters, use {@link fieldTextOrNull}. */
|
|
20
|
+
function fieldText(value, fallback = "") {
|
|
21
|
+
return fieldTextOrNull(value) ?? fallback;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/collection/core/schema.ts
|
|
25
|
+
/** Declarative retriever kinds a Feed's `ingest.kind` may declare. The host's
|
|
26
|
+
* feeds engine dispatches on these; they live here (with the schema contract)
|
|
27
|
+
* so the schema validator can enforce them. The host re-exports these from
|
|
28
|
+
* `server/workspace/feeds/ingestTypes.ts`. */
|
|
29
|
+
var INGEST_KINDS = [
|
|
30
|
+
"rss",
|
|
31
|
+
"atom",
|
|
32
|
+
"http-json"
|
|
33
|
+
];
|
|
34
|
+
/** The agent-performed ingest kind. Instead of a declarative fetch, the host
|
|
35
|
+
* dispatches a hidden background chat (origin `system`) in `ingest.role`,
|
|
36
|
+
* seeded with `ingest.template` + a summary of every record, on the
|
|
37
|
+
* `ingest.schedule` cadence; the worker edits records via the collections io
|
|
38
|
+
* layer. Kept separate from {@link INGEST_KINDS} (which the declarative
|
|
39
|
+
* retriever registry keys on) so the schema validator can model `ingest` as a
|
|
40
|
+
* discriminated union without the feeds engine gaining an "agent" retriever. */
|
|
41
|
+
var AGENT_INGEST_KIND = "agent";
|
|
42
|
+
/** Refresh cadences a Feed's `ingest.schedule` may declare. */
|
|
43
|
+
var FEED_SCHEDULES = [
|
|
44
|
+
"hourly",
|
|
45
|
+
"daily",
|
|
46
|
+
"weekly",
|
|
47
|
+
"on-demand"
|
|
48
|
+
];
|
|
49
|
+
/** derived/embed/backlinks/rollup/toggle/flag are host-computed or
|
|
50
|
+
* projected — never written to the record JSON, so required / value
|
|
51
|
+
* checks and edit-draft slots must not apply to them. THE single source
|
|
52
|
+
* for "computed" — lives here (zod-free at runtime) so browser code
|
|
53
|
+
* (`./draft`) and the zod record compiler (`./recordZ`, which re-exports
|
|
54
|
+
* it) share one set instead of drifting copies. */
|
|
55
|
+
var COMPUTED_TYPES = /* @__PURE__ */ new Set([
|
|
56
|
+
"derived",
|
|
57
|
+
"embed",
|
|
58
|
+
"backlinks",
|
|
59
|
+
"rollup",
|
|
60
|
+
"toggle",
|
|
61
|
+
"flag"
|
|
62
|
+
]);
|
|
63
|
+
/** Narrowing guard: true when `every` is the field-driven arm. */
|
|
64
|
+
function isFieldDrivenEvery(every) {
|
|
65
|
+
return "fromField" in every;
|
|
66
|
+
}
|
|
67
|
+
/** Which storage backend serves this schema's records. Derived, not stored:
|
|
68
|
+
* existing schemas carry no `storage` key and must keep resolving exactly
|
|
69
|
+
* as before (`dataSource` ⇒ csv, else file). */
|
|
70
|
+
function storageKindFor(schema) {
|
|
71
|
+
if (schema.dataSource !== void 0) return "csv";
|
|
72
|
+
return schema.storage?.type ?? "file";
|
|
73
|
+
}
|
|
74
|
+
/** True when `schema` declares an external `dataSource` — i.e. the
|
|
75
|
+
* collection is READ-ONLY through every UI/tool write path (updates
|
|
76
|
+
* happen by editing/replacing the data file itself). Isomorphic: both
|
|
77
|
+
* the server write guards and the client's control hiding key off this
|
|
78
|
+
* one predicate. */
|
|
79
|
+
function isReadOnlySchema(schema) {
|
|
80
|
+
return schema.dataSource !== void 0;
|
|
81
|
+
}
|
|
82
|
+
/** Resolve an `embed` field's target record id: the fixed `id`, or the value
|
|
83
|
+
* of the sibling `idField` on this record (empty string when neither applies
|
|
84
|
+
* — the caller renders that as "no record"). Pure + isomorphic so the server
|
|
85
|
+
* projection (`derive.ts`) and the client preview (`useCollectionRendering`)
|
|
86
|
+
* resolve embeds identically. Non-`embed` fields resolve to "no record". */
|
|
87
|
+
function embedTargetId(field, record) {
|
|
88
|
+
if (field.type !== "embed") return "";
|
|
89
|
+
if (field.id) return field.id;
|
|
90
|
+
if (field.idField && record) return fieldText(record[field.idField]);
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/collection/core/ids.ts
|
|
95
|
+
var SAFE_SLUG_PATTERN = /^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
|
|
96
|
+
var SAFE_RECORD_ID_PATTERN = /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/;
|
|
97
|
+
/** True when `value` is a well-formed collection slug (alphanumeric /
|
|
98
|
+
* hyphen / underscore, no path separators). The pattern admits no `/`,
|
|
99
|
+
* `\`, or `.`, so a passing value is trivially also a safe basename —
|
|
100
|
+
* validation callers need no `path.basename` round-trip (path-building
|
|
101
|
+
* callers use `../server/paths#safeSlugName`, which adds it). */
|
|
102
|
+
function isSafeSlug(value) {
|
|
103
|
+
return typeof value === "string" && SAFE_SLUG_PATTERN.test(value);
|
|
104
|
+
}
|
|
105
|
+
/** True when `value` is a well-formed record id (slug charset plus interior
|
|
106
|
+
* dots), with any `..` substring rejected explicitly. Validation-only
|
|
107
|
+
* counterpart of `../server/paths#safeRecordId`. */
|
|
108
|
+
function isSafeRecordId(value) {
|
|
109
|
+
if (typeof value !== "string" || !SAFE_RECORD_ID_PATTERN.test(value)) return false;
|
|
110
|
+
return !value.includes("..");
|
|
111
|
+
}
|
|
112
|
+
var DEFAULT_UNIQUE_ID_ATTEMPTS = 8;
|
|
113
|
+
/** Pick an id not already in `existing`, re-rolling `generate()` up to
|
|
114
|
+
* `maxAttempts` times before giving up and returning the last candidate.
|
|
115
|
+
* Collisions on a wide id space are astronomically unlikely, so a caller's
|
|
116
|
+
* own overwrite guard is the final backstop rather than an unbounded loop. */
|
|
117
|
+
function generateUniqueId(existing, generate, maxAttempts = DEFAULT_UNIQUE_ID_ATTEMPTS) {
|
|
118
|
+
let candidate = generate();
|
|
119
|
+
for (let attempt = 0; attempt < maxAttempts && existing.has(candidate); attempt++) candidate = generate();
|
|
120
|
+
return candidate;
|
|
121
|
+
}
|
|
122
|
+
/** An id not already used by any loaded record: collects the in-memory primary
|
|
123
|
+
* keys and re-rolls `generate()` against them (see {@link generateUniqueId}).
|
|
124
|
+
* The pure core of the view's create-form id prefill. */
|
|
125
|
+
function nextUniqueItemId(items, primaryKey, generate, maxAttempts = DEFAULT_UNIQUE_ID_ATTEMPTS) {
|
|
126
|
+
return generateUniqueId(new Set(items.map((item) => fieldText(item[primaryKey]))), generate, maxAttempts);
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/collection/core/calendarGrid.ts
|
|
130
|
+
var MS_PER_DAY = 864e5;
|
|
131
|
+
var ISO_DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
132
|
+
var TWO_DIGIT_RE = /^\d{2}$/;
|
|
133
|
+
var CLOCK_RE = /(\d{1,2}):(\d{2})/g;
|
|
134
|
+
var RANGE_SEP_RE = /[-–—~〜~]/;
|
|
135
|
+
/** Minutes in a full day — the timeline's vertical extent. */
|
|
136
|
+
var MINUTES_PER_DAY = 1440;
|
|
137
|
+
function pad2(value) {
|
|
138
|
+
return String(value).padStart(2, "0");
|
|
139
|
+
}
|
|
140
|
+
/** Canonical `YYYY-MM-DD` string for a civil date. */
|
|
141
|
+
function ymdKey(ymd) {
|
|
142
|
+
return `${String(ymd.year).padStart(4, "0")}-${pad2(ymd.month)}-${pad2(ymd.day)}`;
|
|
143
|
+
}
|
|
144
|
+
/** Strictly parse a `YYYY-MM-DD` string into a civil date, rejecting
|
|
145
|
+
* anything that isn't a real calendar day (e.g. `2026-02-30`, `2026-13-01`).
|
|
146
|
+
* Returns null for non-strings and malformed values so callers can route
|
|
147
|
+
* records with no usable date into the "no date" tray rather than crash. */
|
|
148
|
+
function parseIsoDate(value) {
|
|
149
|
+
if (typeof value !== "string") return null;
|
|
150
|
+
const match = ISO_DATE_RE.exec(value.trim());
|
|
151
|
+
if (!match) return null;
|
|
152
|
+
const year = Number(match[1]);
|
|
153
|
+
const month = Number(match[2]);
|
|
154
|
+
const day = Number(match[3]);
|
|
155
|
+
const probe = new Date(Date.UTC(year, month - 1, day));
|
|
156
|
+
if (probe.getUTCFullYear() !== year || probe.getUTCMonth() !== month - 1 || probe.getUTCDate() !== day) return null;
|
|
157
|
+
return {
|
|
158
|
+
year,
|
|
159
|
+
month,
|
|
160
|
+
day
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** Minutes-of-day for an `HH:MM` pair, or null when out of range. */
|
|
164
|
+
function clockToMinutes(hours, minutes) {
|
|
165
|
+
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null;
|
|
166
|
+
return hours * 60 + minutes;
|
|
167
|
+
}
|
|
168
|
+
/** Strictly parse a `YYYY-MM-DDTHH:MM` (optional `:SS`) datetime into its
|
|
169
|
+
* civil date and minutes-of-day. Returns null for anything that isn't a real
|
|
170
|
+
* calendar day or a valid 24h clock. */
|
|
171
|
+
function parseIsoDateTime(value) {
|
|
172
|
+
if (typeof value !== "string") return null;
|
|
173
|
+
const trimmed = value.trim();
|
|
174
|
+
const tIndex = trimmed.indexOf("T");
|
|
175
|
+
if (tIndex === -1) return null;
|
|
176
|
+
const ymd = parseIsoDate(trimmed.slice(0, tIndex));
|
|
177
|
+
if (!ymd) return null;
|
|
178
|
+
const parts = trimmed.slice(tIndex + 1).split(":");
|
|
179
|
+
if (parts.length < 2 || parts.length > 3 || !parts.every((part) => TWO_DIGIT_RE.test(part))) return null;
|
|
180
|
+
const minutes = clockToMinutes(Number(parts[0]), Number(parts[1]));
|
|
181
|
+
if (minutes === null) return null;
|
|
182
|
+
return {
|
|
183
|
+
ymd,
|
|
184
|
+
minutes
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** Civil date from either a `YYYY-MM-DD` or a `YYYY-MM-DDTHH:MM` value, so the
|
|
188
|
+
* month grid buckets date-only and datetime anchors alike. */
|
|
189
|
+
function dateOf(value) {
|
|
190
|
+
return parseIsoDate(value) ?? parseIsoDateTime(value)?.ymd ?? null;
|
|
191
|
+
}
|
|
192
|
+
/** Minutes-of-day from a datetime value, or null for date-only / invalid. */
|
|
193
|
+
function timeOf(value) {
|
|
194
|
+
return parseIsoDateTime(value)?.minutes ?? null;
|
|
195
|
+
}
|
|
196
|
+
/** Parse a free-form time-string field into start/end minutes-of-day.
|
|
197
|
+
* Handles the common shapes in user data:
|
|
198
|
+
* "14:00-17:00" → { start: 840, end: 1020 } (range → block)
|
|
199
|
+
* "17:00-" → { start: 1020, end: null } (open end → single line)
|
|
200
|
+
* "16:30" → { start: 990, end: null } (point in time → single line)
|
|
201
|
+
* "終日" / "" → null (no clock → all-day)
|
|
202
|
+
* Returns null when no clock token is parseable. */
|
|
203
|
+
function parseTimeRange(value) {
|
|
204
|
+
if (typeof value !== "string") return null;
|
|
205
|
+
const text = value.trim();
|
|
206
|
+
if (!text) return null;
|
|
207
|
+
const tokens = [...text.matchAll(CLOCK_RE)];
|
|
208
|
+
if (tokens.length === 0) return null;
|
|
209
|
+
const minutesOf = (match) => clockToMinutes(Number(match[1]), Number(match[2]));
|
|
210
|
+
if (!RANGE_SEP_RE.test(text)) {
|
|
211
|
+
const startMin = minutesOf(tokens[0]);
|
|
212
|
+
return startMin === null ? null : {
|
|
213
|
+
startMin,
|
|
214
|
+
endMin: null
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const sepIndex = text.search(RANGE_SEP_RE);
|
|
218
|
+
let startMin = null;
|
|
219
|
+
let endMin = null;
|
|
220
|
+
for (const token of tokens) if ((token.index ?? 0) < sepIndex) startMin = minutesOf(token);
|
|
221
|
+
else endMin = minutesOf(token);
|
|
222
|
+
if (startMin === null) return null;
|
|
223
|
+
return {
|
|
224
|
+
startMin,
|
|
225
|
+
endMin
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function ymdToUtcMs(ymd) {
|
|
229
|
+
return Date.UTC(ymd.year, ymd.month - 1, ymd.day);
|
|
230
|
+
}
|
|
231
|
+
function utcMsToYmd(epochMs) {
|
|
232
|
+
const date = new Date(epochMs);
|
|
233
|
+
return {
|
|
234
|
+
year: date.getUTCFullYear(),
|
|
235
|
+
month: date.getUTCMonth() + 1,
|
|
236
|
+
day: date.getUTCDate()
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/** Chronological comparison: negative if `left` precedes `right`, 0 if the
|
|
240
|
+
* same day, positive if after. */
|
|
241
|
+
function compareYmd(left, right) {
|
|
242
|
+
return ymdToUtcMs(left) - ymdToUtcMs(right);
|
|
243
|
+
}
|
|
244
|
+
/** True iff `day` falls within the inclusive span `[span.start, span.end]`. */
|
|
245
|
+
function spanCoversDay(span, day) {
|
|
246
|
+
return compareYmd(span.start, day) <= 0 && compareYmd(day, span.end) <= 0;
|
|
247
|
+
}
|
|
248
|
+
/** Build the 6×7 (42-cell) grid for the given month, including the
|
|
249
|
+
* leading/trailing days of the adjacent months so every week is full.
|
|
250
|
+
* `month` is 1-12. `weekStartsOn` is 0 (Sunday) … 6 (Saturday). */
|
|
251
|
+
function buildMonthGrid(year, month, weekStartsOn = 0) {
|
|
252
|
+
const lead = (new Date(Date.UTC(year, month - 1, 1)).getUTCDay() - weekStartsOn + 7) % 7;
|
|
253
|
+
const startMs = Date.UTC(year, month - 1, 1) - lead * MS_PER_DAY;
|
|
254
|
+
const cells = [];
|
|
255
|
+
for (let i = 0; i < 42; i++) {
|
|
256
|
+
const ymd = utcMsToYmd(startMs + i * MS_PER_DAY);
|
|
257
|
+
cells.push({
|
|
258
|
+
ymd,
|
|
259
|
+
inMonth: ymd.year === year && ymd.month === month,
|
|
260
|
+
key: ymdKey(ymd)
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return cells;
|
|
264
|
+
}
|
|
265
|
+
/** Resolve a record's calendar span from its date/datetime fields. Returns
|
|
266
|
+
* null when the anchor date is missing/invalid (→ the caller's "no date"
|
|
267
|
+
* tray). An end date that is missing, invalid, or earlier than the start
|
|
268
|
+
* collapses to a single-day span — never an inverted range.
|
|
269
|
+
*
|
|
270
|
+
* Times for the day (time-allocation) view come from, in priority order:
|
|
271
|
+
* 1. the clock on a `datetime` anchor/end value, else
|
|
272
|
+
* 2. `timeField` — a separate free-form time-string column (e.g. "14:00-17:00").
|
|
273
|
+
* A record with no resolvable clock has `startMin === endMin === null`. */
|
|
274
|
+
function recordSpan(item, anchorField, endField, timeField) {
|
|
275
|
+
const startRaw = item[anchorField];
|
|
276
|
+
const start = dateOf(startRaw);
|
|
277
|
+
if (!start) return null;
|
|
278
|
+
let end = start;
|
|
279
|
+
let startMin = timeOf(startRaw);
|
|
280
|
+
let endMin = null;
|
|
281
|
+
if (endField) {
|
|
282
|
+
const endRaw = item[endField];
|
|
283
|
+
const parsedEnd = dateOf(endRaw);
|
|
284
|
+
if (parsedEnd && compareYmd(parsedEnd, start) >= 0) {
|
|
285
|
+
end = parsedEnd;
|
|
286
|
+
endMin = timeOf(endRaw);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (timeField && startMin === null && endMin === null) {
|
|
290
|
+
const range = parseTimeRange(item[timeField]);
|
|
291
|
+
if (range) ({startMin, endMin} = range);
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
item,
|
|
295
|
+
start,
|
|
296
|
+
end,
|
|
297
|
+
startMin,
|
|
298
|
+
endMin
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/** Split records into those that land on the calendar (with their spans)
|
|
302
|
+
* and those with no usable anchor date (the "no date" tray). Spans are
|
|
303
|
+
* sorted by start day so same-day stacking is stable across renders. */
|
|
304
|
+
function bucketRecords(items, anchorField, endField, timeField) {
|
|
305
|
+
const spans = [];
|
|
306
|
+
const noDate = [];
|
|
307
|
+
for (const item of items) {
|
|
308
|
+
const span = recordSpan(item, anchorField, endField, timeField);
|
|
309
|
+
if (span) spans.push(span);
|
|
310
|
+
else noDate.push(item);
|
|
311
|
+
}
|
|
312
|
+
spans.sort((left, right) => compareYmd(left.start, right.start));
|
|
313
|
+
return {
|
|
314
|
+
spans,
|
|
315
|
+
noDate
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/** Project a record's span onto a single day for the time-allocation view, or
|
|
319
|
+
* null when the span doesn't cover that day. */
|
|
320
|
+
function daySlice(span, day) {
|
|
321
|
+
if (!spanCoversDay(span, day)) return null;
|
|
322
|
+
const hasStart = span.startMin !== null;
|
|
323
|
+
const hasEnd = span.endMin !== null;
|
|
324
|
+
if (!hasStart && !hasEnd) return {
|
|
325
|
+
kind: "allDay",
|
|
326
|
+
startMin: 0,
|
|
327
|
+
endMin: MINUTES_PER_DAY,
|
|
328
|
+
bleedsBefore: false,
|
|
329
|
+
bleedsAfter: false
|
|
330
|
+
};
|
|
331
|
+
const singleDay = compareYmd(span.start, span.end) === 0;
|
|
332
|
+
const isStartDay = compareYmd(day, span.start) === 0;
|
|
333
|
+
const isEndDay = compareYmd(day, span.end) === 0;
|
|
334
|
+
if (singleDay && hasStart && !hasEnd) return {
|
|
335
|
+
kind: "line",
|
|
336
|
+
startMin: span.startMin,
|
|
337
|
+
endMin: span.startMin,
|
|
338
|
+
bleedsBefore: false,
|
|
339
|
+
bleedsAfter: false
|
|
340
|
+
};
|
|
341
|
+
const startMin = isStartDay && hasStart ? span.startMin : 0;
|
|
342
|
+
const endMin = isEndDay && hasEnd ? span.endMin : MINUTES_PER_DAY;
|
|
343
|
+
if (singleDay && endMin <= startMin) return {
|
|
344
|
+
kind: "line",
|
|
345
|
+
startMin,
|
|
346
|
+
endMin: startMin,
|
|
347
|
+
bleedsBefore: false,
|
|
348
|
+
bleedsAfter: false
|
|
349
|
+
};
|
|
350
|
+
return {
|
|
351
|
+
kind: "block",
|
|
352
|
+
startMin,
|
|
353
|
+
endMin,
|
|
354
|
+
bleedsBefore: !isStartDay,
|
|
355
|
+
bleedsAfter: !isEndDay
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function assignLanes(blocks) {
|
|
359
|
+
const order = [...blocks.keys()].sort((left, right) => blocks[left].startMin - blocks[right].startMin || blocks[left].endMin - blocks[right].endMin);
|
|
360
|
+
const result = blocks.map(() => ({
|
|
361
|
+
lane: 0,
|
|
362
|
+
lanes: 1
|
|
363
|
+
}));
|
|
364
|
+
let cluster = [];
|
|
365
|
+
let clusterEnd = Number.NEGATIVE_INFINITY;
|
|
366
|
+
const laneEnds = [];
|
|
367
|
+
const flush = () => {
|
|
368
|
+
for (const index of cluster) result[index].lanes = laneEnds.length;
|
|
369
|
+
cluster = [];
|
|
370
|
+
laneEnds.length = 0;
|
|
371
|
+
clusterEnd = Number.NEGATIVE_INFINITY;
|
|
372
|
+
};
|
|
373
|
+
for (const index of order) {
|
|
374
|
+
const block = blocks[index];
|
|
375
|
+
if (cluster.length > 0 && block.startMin >= clusterEnd) flush();
|
|
376
|
+
let lane = laneEnds.findIndex((end) => end <= block.startMin);
|
|
377
|
+
if (lane === -1) {
|
|
378
|
+
lane = laneEnds.length;
|
|
379
|
+
laneEnds.push(block.endMin);
|
|
380
|
+
} else laneEnds[lane] = block.endMin;
|
|
381
|
+
result[index].lane = lane;
|
|
382
|
+
cluster.push(index);
|
|
383
|
+
clusterEnd = Math.max(clusterEnd, block.endMin);
|
|
384
|
+
}
|
|
385
|
+
flush();
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
/** Month label key inputs — returns the 1st of the month as a `Date` so the
|
|
389
|
+
* component can feed it to `Intl.DateTimeFormat(locale, …)` for a localized
|
|
390
|
+
* "April 2026" header without this module taking a locale dependency. */
|
|
391
|
+
function monthAnchorDate(year, month) {
|
|
392
|
+
return new Date(Date.UTC(year, month - 1, 1));
|
|
393
|
+
}
|
|
394
|
+
//#endregion
|
|
395
|
+
Object.defineProperty(exports, "AGENT_INGEST_KIND", {
|
|
396
|
+
enumerable: true,
|
|
397
|
+
get: function() {
|
|
398
|
+
return AGENT_INGEST_KIND;
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
Object.defineProperty(exports, "COMPUTED_TYPES", {
|
|
402
|
+
enumerable: true,
|
|
403
|
+
get: function() {
|
|
404
|
+
return COMPUTED_TYPES;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
Object.defineProperty(exports, "FEED_SCHEDULES", {
|
|
408
|
+
enumerable: true,
|
|
409
|
+
get: function() {
|
|
410
|
+
return FEED_SCHEDULES;
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
Object.defineProperty(exports, "INGEST_KINDS", {
|
|
414
|
+
enumerable: true,
|
|
415
|
+
get: function() {
|
|
416
|
+
return INGEST_KINDS;
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
Object.defineProperty(exports, "MINUTES_PER_DAY", {
|
|
420
|
+
enumerable: true,
|
|
421
|
+
get: function() {
|
|
422
|
+
return MINUTES_PER_DAY;
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
Object.defineProperty(exports, "SAFE_RECORD_ID_PATTERN", {
|
|
426
|
+
enumerable: true,
|
|
427
|
+
get: function() {
|
|
428
|
+
return SAFE_RECORD_ID_PATTERN;
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
Object.defineProperty(exports, "SAFE_SLUG_PATTERN", {
|
|
432
|
+
enumerable: true,
|
|
433
|
+
get: function() {
|
|
434
|
+
return SAFE_SLUG_PATTERN;
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
Object.defineProperty(exports, "assignLanes", {
|
|
438
|
+
enumerable: true,
|
|
439
|
+
get: function() {
|
|
440
|
+
return assignLanes;
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
Object.defineProperty(exports, "bucketRecords", {
|
|
444
|
+
enumerable: true,
|
|
445
|
+
get: function() {
|
|
446
|
+
return bucketRecords;
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
Object.defineProperty(exports, "buildMonthGrid", {
|
|
450
|
+
enumerable: true,
|
|
451
|
+
get: function() {
|
|
452
|
+
return buildMonthGrid;
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
Object.defineProperty(exports, "compareYmd", {
|
|
456
|
+
enumerable: true,
|
|
457
|
+
get: function() {
|
|
458
|
+
return compareYmd;
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
Object.defineProperty(exports, "dateOf", {
|
|
462
|
+
enumerable: true,
|
|
463
|
+
get: function() {
|
|
464
|
+
return dateOf;
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
Object.defineProperty(exports, "daySlice", {
|
|
468
|
+
enumerable: true,
|
|
469
|
+
get: function() {
|
|
470
|
+
return daySlice;
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
Object.defineProperty(exports, "embedTargetId", {
|
|
474
|
+
enumerable: true,
|
|
475
|
+
get: function() {
|
|
476
|
+
return embedTargetId;
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
Object.defineProperty(exports, "fieldText", {
|
|
480
|
+
enumerable: true,
|
|
481
|
+
get: function() {
|
|
482
|
+
return fieldText;
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
Object.defineProperty(exports, "fieldTextOrNull", {
|
|
486
|
+
enumerable: true,
|
|
487
|
+
get: function() {
|
|
488
|
+
return fieldTextOrNull;
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
Object.defineProperty(exports, "generateUniqueId", {
|
|
492
|
+
enumerable: true,
|
|
493
|
+
get: function() {
|
|
494
|
+
return generateUniqueId;
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
Object.defineProperty(exports, "isFieldDrivenEvery", {
|
|
498
|
+
enumerable: true,
|
|
499
|
+
get: function() {
|
|
500
|
+
return isFieldDrivenEvery;
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
Object.defineProperty(exports, "isReadOnlySchema", {
|
|
504
|
+
enumerable: true,
|
|
505
|
+
get: function() {
|
|
506
|
+
return isReadOnlySchema;
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
Object.defineProperty(exports, "isSafeRecordId", {
|
|
510
|
+
enumerable: true,
|
|
511
|
+
get: function() {
|
|
512
|
+
return isSafeRecordId;
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
Object.defineProperty(exports, "isSafeSlug", {
|
|
516
|
+
enumerable: true,
|
|
517
|
+
get: function() {
|
|
518
|
+
return isSafeSlug;
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
Object.defineProperty(exports, "monthAnchorDate", {
|
|
522
|
+
enumerable: true,
|
|
523
|
+
get: function() {
|
|
524
|
+
return monthAnchorDate;
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
Object.defineProperty(exports, "nextUniqueItemId", {
|
|
528
|
+
enumerable: true,
|
|
529
|
+
get: function() {
|
|
530
|
+
return nextUniqueItemId;
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
Object.defineProperty(exports, "parseIsoDate", {
|
|
534
|
+
enumerable: true,
|
|
535
|
+
get: function() {
|
|
536
|
+
return parseIsoDate;
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
Object.defineProperty(exports, "parseIsoDateTime", {
|
|
540
|
+
enumerable: true,
|
|
541
|
+
get: function() {
|
|
542
|
+
return parseIsoDateTime;
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
Object.defineProperty(exports, "parseTimeRange", {
|
|
546
|
+
enumerable: true,
|
|
547
|
+
get: function() {
|
|
548
|
+
return parseTimeRange;
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
Object.defineProperty(exports, "recordSpan", {
|
|
552
|
+
enumerable: true,
|
|
553
|
+
get: function() {
|
|
554
|
+
return recordSpan;
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
Object.defineProperty(exports, "spanCoversDay", {
|
|
558
|
+
enumerable: true,
|
|
559
|
+
get: function() {
|
|
560
|
+
return spanCoversDay;
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
Object.defineProperty(exports, "storageKindFor", {
|
|
564
|
+
enumerable: true,
|
|
565
|
+
get: function() {
|
|
566
|
+
return storageKindFor;
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
Object.defineProperty(exports, "ymdKey", {
|
|
570
|
+
enumerable: true,
|
|
571
|
+
get: function() {
|
|
572
|
+
return ymdKey;
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
//# sourceMappingURL=calendarGrid-CaS9er8i.cjs.map
|