@mulmoclaude/collection-plugin 0.2.2 → 0.3.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.
Files changed (99) hide show
  1. package/dist/calendarGrid-DZeLwBj9.cjs +778 -0
  2. package/dist/calendarGrid-DZeLwBj9.cjs.map +1 -0
  3. package/dist/calendarGrid-ggmFTZju.js +587 -0
  4. package/dist/calendarGrid-ggmFTZju.js.map +1 -0
  5. package/dist/chunk-CKQMccvm.cjs +28 -0
  6. package/dist/core/actionVisible.d.ts +1 -0
  7. package/dist/core/actionVisible.d.ts.map +1 -0
  8. package/dist/core/calendarGrid.d.ts +1 -0
  9. package/dist/core/calendarGrid.d.ts.map +1 -0
  10. package/dist/core/deriveAll.d.ts +2 -1
  11. package/dist/core/deriveAll.d.ts.map +1 -0
  12. package/dist/core/derivedFormula.d.ts +1 -0
  13. package/dist/core/derivedFormula.d.ts.map +1 -0
  14. package/dist/core/draft.d.ts +3 -2
  15. package/dist/core/draft.d.ts.map +1 -0
  16. package/dist/core/enumColors.d.ts +2 -1
  17. package/dist/core/enumColors.d.ts.map +1 -0
  18. package/dist/core/itemLabel.d.ts +2 -1
  19. package/dist/core/itemLabel.d.ts.map +1 -0
  20. package/dist/core/presentCollection.d.ts +2 -1
  21. package/dist/core/presentCollection.d.ts.map +1 -0
  22. package/dist/core/schema.d.ts +1 -0
  23. package/dist/core/schema.d.ts.map +1 -0
  24. package/dist/core/sortItems.d.ts +2 -1
  25. package/dist/core/sortItems.d.ts.map +1 -0
  26. package/dist/core/uiTypes.d.ts +29 -1
  27. package/dist/core/uiTypes.d.ts.map +1 -0
  28. package/dist/{deriveAll-B5spcUIb.js → deriveAll-BHcs1erT.js} +2 -20
  29. package/dist/deriveAll-BHcs1erT.js.map +1 -0
  30. package/dist/{deriveAll-Do0v6Xjb.cjs → deriveAll-BJ0Lvm1Q.cjs} +1 -31
  31. package/dist/deriveAll-BJ0Lvm1Q.cjs.map +1 -0
  32. package/dist/index.cjs +37 -619
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.ts +12 -11
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +3 -585
  37. package/dist/index.js.map +1 -1
  38. package/dist/schema-DyfSfVzh.js +21 -0
  39. package/dist/schema-DyfSfVzh.js.map +1 -0
  40. package/dist/schema-LrmrgW12.cjs +32 -0
  41. package/dist/schema-LrmrgW12.cjs.map +1 -0
  42. package/dist/server/atomic.d.ts +1 -0
  43. package/dist/server/atomic.d.ts.map +1 -0
  44. package/dist/server/delete.d.ts +2 -1
  45. package/dist/server/delete.d.ts.map +1 -0
  46. package/dist/server/derive.d.ts +4 -3
  47. package/dist/server/derive.d.ts.map +1 -0
  48. package/dist/server/discoveredCollection.d.ts +2 -1
  49. package/dist/server/discoveredCollection.d.ts.map +1 -0
  50. package/dist/server/discovery.d.ts +4 -3
  51. package/dist/server/discovery.d.ts.map +1 -0
  52. package/dist/server/host.d.ts +1 -0
  53. package/dist/server/host.d.ts.map +1 -0
  54. package/dist/server/index.d.ts +12 -11
  55. package/dist/server/index.d.ts.map +1 -0
  56. package/dist/server/io.d.ts +3 -2
  57. package/dist/server/io.d.ts.map +1 -0
  58. package/dist/server/paths.d.ts +1 -0
  59. package/dist/server/paths.d.ts.map +1 -0
  60. package/dist/server/spawn.d.ts +3 -2
  61. package/dist/server/spawn.d.ts.map +1 -0
  62. package/dist/server/templatePath.d.ts +1 -0
  63. package/dist/server/templatePath.d.ts.map +1 -0
  64. package/dist/server/util.d.ts +1 -0
  65. package/dist/server/util.d.ts.map +1 -0
  66. package/dist/server/validate.d.ts +3 -2
  67. package/dist/server/validate.d.ts.map +1 -0
  68. package/dist/server/views.d.ts +3 -2
  69. package/dist/server/views.d.ts.map +1 -0
  70. package/dist/server.cjs +6 -26
  71. package/dist/server.cjs.map +1 -1
  72. package/dist/server.js +2 -1
  73. package/dist/server.js.map +1 -1
  74. package/dist/style.css +2121 -0
  75. package/dist/vue/components/CollectionCalendarView.vue.d.ts +27 -0
  76. package/dist/vue/components/CollectionCalendarView.vue.d.ts.map +1 -0
  77. package/dist/vue/components/CollectionDayView.vue.d.ts +40 -0
  78. package/dist/vue/components/CollectionDayView.vue.d.ts.map +1 -0
  79. package/dist/vue/components/CollectionEmbedView.vue.d.ts +9 -0
  80. package/dist/vue/components/CollectionEmbedView.vue.d.ts.map +1 -0
  81. package/dist/vue/components/CollectionKanbanView.vue.d.ts +23 -0
  82. package/dist/vue/components/CollectionKanbanView.vue.d.ts.map +1 -0
  83. package/dist/vue/components/CollectionRecordModal.vue.d.ts +18 -0
  84. package/dist/vue/components/CollectionRecordModal.vue.d.ts.map +1 -0
  85. package/dist/vue/components/CollectionRecordPanel.vue.d.ts +46 -0
  86. package/dist/vue/components/CollectionRecordPanel.vue.d.ts.map +1 -0
  87. package/dist/vue/index.d.ts +10 -0
  88. package/dist/vue/index.d.ts.map +1 -0
  89. package/dist/vue/uiContext.d.ts +30 -0
  90. package/dist/vue/uiContext.d.ts.map +1 -0
  91. package/dist/vue/useCollectionRendering.d.ts +30 -0
  92. package/dist/vue/useCollectionRendering.d.ts.map +1 -0
  93. package/dist/vue.cjs +1624 -0
  94. package/dist/vue.cjs.map +1 -0
  95. package/dist/vue.js +1613 -0
  96. package/dist/vue.js.map +1 -0
  97. package/package.json +21 -6
  98. package/dist/deriveAll-B5spcUIb.js.map +0 -1
  99. package/dist/deriveAll-Do0v6Xjb.cjs.map +0 -1
@@ -0,0 +1,778 @@
1
+ //#region src/core/enumColors.ts
2
+ var PALETTE = [
3
+ {
4
+ card: "border-indigo-200 bg-indigo-50 text-indigo-600 hover:bg-indigo-100",
5
+ dot: "bg-indigo-500",
6
+ badge: "bg-indigo-100 text-indigo-700",
7
+ border: "border-indigo-200"
8
+ },
9
+ {
10
+ card: "border-sky-200 bg-sky-50 text-sky-600 hover:bg-sky-100",
11
+ dot: "bg-sky-500",
12
+ badge: "bg-sky-100 text-sky-700",
13
+ border: "border-sky-200"
14
+ },
15
+ {
16
+ card: "border-cyan-200 bg-cyan-50 text-cyan-600 hover:bg-cyan-100",
17
+ dot: "bg-cyan-500",
18
+ badge: "bg-cyan-100 text-cyan-700",
19
+ border: "border-cyan-200"
20
+ },
21
+ {
22
+ card: "border-teal-200 bg-teal-50 text-teal-600 hover:bg-teal-100",
23
+ dot: "bg-teal-500",
24
+ badge: "bg-teal-100 text-teal-700",
25
+ border: "border-teal-200"
26
+ },
27
+ {
28
+ card: "border-emerald-200 bg-emerald-50 text-emerald-600 hover:bg-emerald-100",
29
+ dot: "bg-emerald-500",
30
+ badge: "bg-emerald-100 text-emerald-700",
31
+ border: "border-emerald-200"
32
+ },
33
+ {
34
+ card: "border-lime-200 bg-lime-50 text-lime-600 hover:bg-lime-100",
35
+ dot: "bg-lime-500",
36
+ badge: "bg-lime-100 text-lime-700",
37
+ border: "border-lime-200"
38
+ },
39
+ {
40
+ card: "border-violet-200 bg-violet-50 text-violet-600 hover:bg-violet-100",
41
+ dot: "bg-violet-500",
42
+ badge: "bg-violet-100 text-violet-700",
43
+ border: "border-violet-200"
44
+ },
45
+ {
46
+ card: "border-fuchsia-200 bg-fuchsia-50 text-fuchsia-600 hover:bg-fuchsia-100",
47
+ dot: "bg-fuchsia-500",
48
+ badge: "bg-fuchsia-100 text-fuchsia-700",
49
+ border: "border-fuchsia-200"
50
+ }
51
+ ];
52
+ /** Neutral styling for the empty / Uncategorized bucket — never a palette
53
+ * colour, so an unset or unknown value reads grey across every surface. */
54
+ var ENUM_NEUTRAL = {
55
+ card: "border-slate-200 bg-slate-50 text-slate-500 hover:bg-slate-100",
56
+ dot: "bg-slate-300",
57
+ badge: "bg-slate-100 text-slate-500",
58
+ border: "border-slate-200"
59
+ };
60
+ /** The urgent notification colour (red), matching the bell's `urgent`
61
+ * severity. The first value a schema's `notifyWhen` flags reads this. */
62
+ var ENUM_ALERT = {
63
+ card: "border-red-200 bg-red-50 text-red-600 hover:bg-red-100",
64
+ dot: "bg-red-500",
65
+ badge: "bg-red-100 text-red-700",
66
+ border: "border-red-200"
67
+ };
68
+ /** The nudge notification colour (amber), matching the bell's `nudge`
69
+ * severity. Flagged `notifyWhen` values after the first read this. */
70
+ var ENUM_NUDGE = {
71
+ card: "border-amber-200 bg-amber-50 text-amber-600 hover:bg-amber-100",
72
+ dot: "bg-amber-500",
73
+ badge: "bg-amber-100 text-amber-700",
74
+ border: "border-amber-200"
75
+ };
76
+ /** Classes for the enum value at `index` in its field's `values` array. A
77
+ * negative index (value unset or not among the declared values) reads
78
+ * neutral. */
79
+ function enumColorClasses(index) {
80
+ if (index < 0) return ENUM_NEUTRAL;
81
+ return PALETTE[index % PALETTE.length] ?? ENUM_NEUTRAL;
82
+ }
83
+ /** Index of `value` within an enum field's declared `values`, or -1 when the
84
+ * value is empty / unknown (→ neutral). */
85
+ function enumValueIndex(values, value) {
86
+ if (value === void 0 || value === null || value === "") return -1;
87
+ return values?.indexOf(String(value)) ?? -1;
88
+ }
89
+ /** The flagged values when `fieldKey` is the schema's `notifyWhen` target (the
90
+ * "notification enum"); undefined for every other field. */
91
+ function notifyValuesFor(schema, fieldKey) {
92
+ const spec = schema.notifyWhen;
93
+ return spec && spec.field === fieldKey ? spec.in : void 0;
94
+ }
95
+ /** Resolve a value's colour for enum field `fieldKey`:
96
+ * - Notification enum (`notifyWhen` targets it): the first flagged value (in
97
+ * `notifyWhen.in` order, the most urgent) reads notification red, the rest
98
+ * amber, and every non-flagged value neutral grey.
99
+ * - Any other enum: the standard palette by the value's declared index. */
100
+ function resolveEnumColor(schema, fieldKey, value) {
101
+ const notifyValues = notifyValuesFor(schema, fieldKey);
102
+ if (notifyValues) {
103
+ const str = value === void 0 || value === null ? "" : String(value);
104
+ const rank = notifyValues.indexOf(str);
105
+ if (rank < 0) return ENUM_NEUTRAL;
106
+ return rank === 0 ? ENUM_ALERT : ENUM_NUDGE;
107
+ }
108
+ return enumColorClasses(enumValueIndex(schema.fields[fieldKey]?.values, value));
109
+ }
110
+ //#endregion
111
+ //#region src/core/actionVisible.ts
112
+ /** Core matcher:
113
+ * - no `when` ⇒ always true (visible);
114
+ * - otherwise true only when `record[when.field]` is present and its
115
+ * stringified value is one of `when.in`.
116
+ * A missing/undefined/null field is treated as "not a match"
117
+ * (hidden), so a status-gated target never shows on a record that
118
+ * lacks the status. */
119
+ function whenMatches(when, record) {
120
+ if (!when) return true;
121
+ const value = record[when.field];
122
+ if (value === void 0 || value === null) return false;
123
+ return when.in.includes(String(value));
124
+ }
125
+ /** True when the action's button should render against `record`
126
+ * (see whenMatches). */
127
+ function actionVisible(action, record) {
128
+ return whenMatches(action.when, record);
129
+ }
130
+ /** True when the field should render against `record`. A field with
131
+ * no `when` is always shown; otherwise it's shown only when the
132
+ * record matches (e.g. hide a rating field until `visited` is true).
133
+ * Purely presentational — a hidden field's stored value is never
134
+ * altered, so toggling the gate back on restores it. */
135
+ function fieldVisible(field, record) {
136
+ return whenMatches(field.when, record);
137
+ }
138
+ //#endregion
139
+ //#region src/core/draft.ts
140
+ /** A fresh, empty row draft for a `table` field's sub-schema. */
141
+ function emptyRow(subFields) {
142
+ const text = {};
143
+ const bool = {};
144
+ const boolOriginallyPresent = {};
145
+ const boolTouched = {};
146
+ for (const [subKey, subField] of Object.entries(subFields)) if (subField.type === "boolean") {
147
+ bool[subKey] = false;
148
+ boolOriginallyPresent[subKey] = false;
149
+ boolTouched[subKey] = false;
150
+ } else text[subKey] = "";
151
+ return {
152
+ text,
153
+ bool,
154
+ boolOriginallyPresent,
155
+ boolTouched
156
+ };
157
+ }
158
+ /** Build a row draft from an existing persisted row. */
159
+ function rowFromItem(item, subFields) {
160
+ const text = {};
161
+ const bool = {};
162
+ const boolOriginallyPresent = {};
163
+ const boolTouched = {};
164
+ for (const [subKey, subField] of Object.entries(subFields)) {
165
+ const raw = item[subKey];
166
+ if (subField.type === "boolean") {
167
+ bool[subKey] = raw === true;
168
+ boolOriginallyPresent[subKey] = typeof raw === "boolean";
169
+ boolTouched[subKey] = false;
170
+ } else text[subKey] = raw === void 0 || raw === null ? "" : String(raw);
171
+ }
172
+ return {
173
+ text,
174
+ bool,
175
+ boolOriginallyPresent,
176
+ boolTouched
177
+ };
178
+ }
179
+ /** Decide whether a boolean field's draft value should be emitted (vs.
180
+ * omitted so a downstream default applies). */
181
+ function shouldEmitBoolean(state, key, field) {
182
+ return Boolean(state.boolOriginallyPresent[key] || state.boolTouched[key] || field.required);
183
+ }
184
+ /** Convert a scalar draft slot to its persisted form. `undefined` = omit. */
185
+ function scalarDraftToValue(raw, fieldType) {
186
+ if (raw === void 0 || raw === "") return void 0;
187
+ if (fieldType === "number" || fieldType === "money") {
188
+ const num = Number(raw);
189
+ return Number.isFinite(num) ? num : raw;
190
+ }
191
+ return raw;
192
+ }
193
+ /** Convert one table row draft to its persisted record. */
194
+ function rowDraftToRecord(rowDraft, subFields) {
195
+ const row = {};
196
+ for (const [subKey, subField] of Object.entries(subFields)) {
197
+ if (subField.type === "boolean") {
198
+ const value = rowDraft.bool[subKey] === true;
199
+ if (rowDraft.boolOriginallyPresent[subKey] || rowDraft.boolTouched[subKey] || value || subField.required) row[subKey] = value;
200
+ continue;
201
+ }
202
+ const value = scalarDraftToValue(rowDraft.text[subKey], subField.type);
203
+ if (value !== void 0) row[subKey] = value;
204
+ }
205
+ return row;
206
+ }
207
+ /** Convert a full edit draft to the record to persist. */
208
+ function draftToRecord(state, schema) {
209
+ const record = {};
210
+ for (const [key, field] of Object.entries(schema.fields)) {
211
+ if (field.type === "derived" || field.type === "embed" || field.type === "toggle") continue;
212
+ if (field.type === "boolean") {
213
+ if (shouldEmitBoolean(state, key, field)) record[key] = state.bool[key] === true;
214
+ continue;
215
+ }
216
+ if (field.type === "table" && field.of) {
217
+ const subFields = field.of;
218
+ record[key] = (state.table[key] ?? []).map((rowDraft) => rowDraftToRecord(rowDraft, subFields));
219
+ continue;
220
+ }
221
+ const value = scalarDraftToValue(state.text[key], field.type);
222
+ if (value !== void 0) record[key] = value;
223
+ }
224
+ return record;
225
+ }
226
+ /** Normalise a raw inline-edit input (table-cell checkbox/select) to its
227
+ * persisted form. Mirrors `draftToRecord`'s boolean strictness; an empty
228
+ * enum selection (the placeholder option) clears the field via `undefined`. */
229
+ function coerceInlineValue(field, raw) {
230
+ if (field.type === "boolean") return raw === true;
231
+ return raw === "" ? void 0 : raw;
232
+ }
233
+ /** Build the full record to PUT for a single-cell inline edit, without
234
+ * mutating `item`. A `undefined` value omits the key (enum cleared),
235
+ * matching `draftToRecord`'s omission semantics. */
236
+ function buildUpdatedRecord(item, key, value) {
237
+ if (value === void 0) {
238
+ const { [key]: __omit, ...rest } = item;
239
+ return rest;
240
+ }
241
+ return {
242
+ ...item,
243
+ [key]: value
244
+ };
245
+ }
246
+ /** Empty for required-field validation — NOT a truthiness check (a
247
+ * numeric `0` is a filled value). */
248
+ function isMissingDraftValue(value) {
249
+ return value === void 0 || value === null || value === "";
250
+ }
251
+ /** Label of the first required table sub-field empty in any row, else null. */
252
+ function firstMissingTableSubField(field, rows) {
253
+ if (!field.of || !rows) return null;
254
+ for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
255
+ const row = rows[rowIdx];
256
+ for (const [subKey, subField] of Object.entries(field.of)) {
257
+ if (!subField.required || subField.type === "boolean") continue;
258
+ if (isMissingDraftValue(row.text[subKey])) return `${field.label} #${rowIdx + 1}: ${subField.label}`;
259
+ }
260
+ }
261
+ return null;
262
+ }
263
+ function validateOneField(key, field, draft, record) {
264
+ if (!fieldVisible(field, record)) return null;
265
+ if (field.type === "table" && field.of) {
266
+ const rows = draft.table[key];
267
+ if (field.required && (!rows || rows.length === 0)) return field.label;
268
+ return firstMissingTableSubField(field, rows);
269
+ }
270
+ if (!field.required) return null;
271
+ if (draft.mode === "create" && field.primary === true) return null;
272
+ if (field.type === "boolean" || field.type === "derived" || field.type === "embed" || field.type === "toggle") return null;
273
+ return isMissingDraftValue(draft.text[key]) ? field.label : null;
274
+ }
275
+ /** Human-readable label of the first missing required field, or null. */
276
+ function firstMissingRequiredField(draft, schema) {
277
+ const record = draftToRecord(draft, schema);
278
+ for (const [key, field] of Object.entries(schema.fields)) {
279
+ const missing = validateOneField(key, field, draft, record);
280
+ if (missing) return missing;
281
+ }
282
+ return null;
283
+ }
284
+ //#endregion
285
+ //#region src/core/itemLabel.ts
286
+ var LABEL_FIELD_TYPES = new Set([
287
+ "string",
288
+ "text",
289
+ "markdown",
290
+ "email"
291
+ ]);
292
+ /** Which field labels a record: the schema's explicit `displayField`, else the
293
+ * first non-primary text-like field (so a collection without a displayField
294
+ * shows e.g. the title rather than the opaque primaryKey), else null → the
295
+ * caller falls back to the primary-key value. */
296
+ function labelFieldFor(schema) {
297
+ if (schema.displayField) return schema.displayField;
298
+ for (const [key, spec] of Object.entries(schema.fields)) if (key !== schema.primaryKey && LABEL_FIELD_TYPES.has(spec.type)) return key;
299
+ return null;
300
+ }
301
+ /** A record's primary-key value as a string. */
302
+ function itemIdOf(item, schema) {
303
+ return String(item[schema.primaryKey] ?? "");
304
+ }
305
+ /** A record's display label: the resolved `labelField` value, else the
306
+ * primary key. Pass the result of `labelFieldFor(schema)` as `labelField`
307
+ * (compute it once per render rather than per item). */
308
+ function itemLabelOf(item, schema, labelField) {
309
+ if (labelField) {
310
+ const value = item[labelField];
311
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
312
+ const text = String(value);
313
+ if (text.length > 0) return text;
314
+ }
315
+ }
316
+ return itemIdOf(item, schema);
317
+ }
318
+ //#endregion
319
+ //#region src/core/calendarGrid.ts
320
+ var MS_PER_DAY = 864e5;
321
+ var ISO_DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
322
+ var TWO_DIGIT_RE = /^\d{2}$/;
323
+ var CLOCK_RE = /(\d{1,2}):(\d{2})/g;
324
+ var RANGE_SEP_RE = /[-–—~〜~]/;
325
+ /** Minutes in a full day — the timeline's vertical extent. */
326
+ var MINUTES_PER_DAY = 1440;
327
+ function pad2(value) {
328
+ return String(value).padStart(2, "0");
329
+ }
330
+ /** Canonical `YYYY-MM-DD` string for a civil date. */
331
+ function ymdKey(ymd) {
332
+ return `${String(ymd.year).padStart(4, "0")}-${pad2(ymd.month)}-${pad2(ymd.day)}`;
333
+ }
334
+ /** Strictly parse a `YYYY-MM-DD` string into a civil date, rejecting
335
+ * anything that isn't a real calendar day (e.g. `2026-02-30`, `2026-13-01`).
336
+ * Returns null for non-strings and malformed values so callers can route
337
+ * records with no usable date into the "no date" tray rather than crash. */
338
+ function parseIsoDate(value) {
339
+ if (typeof value !== "string") return null;
340
+ const match = ISO_DATE_RE.exec(value.trim());
341
+ if (!match) return null;
342
+ const year = Number(match[1]);
343
+ const month = Number(match[2]);
344
+ const day = Number(match[3]);
345
+ const probe = new Date(Date.UTC(year, month - 1, day));
346
+ if (probe.getUTCFullYear() !== year || probe.getUTCMonth() !== month - 1 || probe.getUTCDate() !== day) return null;
347
+ return {
348
+ year,
349
+ month,
350
+ day
351
+ };
352
+ }
353
+ /** Minutes-of-day for an `HH:MM` pair, or null when out of range. */
354
+ function clockToMinutes(hours, minutes) {
355
+ if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null;
356
+ return hours * 60 + minutes;
357
+ }
358
+ /** Strictly parse a `YYYY-MM-DDTHH:MM` (optional `:SS`) datetime into its
359
+ * civil date and minutes-of-day. Returns null for anything that isn't a real
360
+ * calendar day or a valid 24h clock. */
361
+ function parseIsoDateTime(value) {
362
+ if (typeof value !== "string") return null;
363
+ const trimmed = value.trim();
364
+ const tIndex = trimmed.indexOf("T");
365
+ if (tIndex === -1) return null;
366
+ const ymd = parseIsoDate(trimmed.slice(0, tIndex));
367
+ if (!ymd) return null;
368
+ const parts = trimmed.slice(tIndex + 1).split(":");
369
+ if (parts.length < 2 || parts.length > 3 || !parts.every((part) => TWO_DIGIT_RE.test(part))) return null;
370
+ const minutes = clockToMinutes(Number(parts[0]), Number(parts[1]));
371
+ if (minutes === null) return null;
372
+ return {
373
+ ymd,
374
+ minutes
375
+ };
376
+ }
377
+ /** Civil date from either a `YYYY-MM-DD` or a `YYYY-MM-DDTHH:MM` value, so the
378
+ * month grid buckets date-only and datetime anchors alike. */
379
+ function dateOf(value) {
380
+ return parseIsoDate(value) ?? parseIsoDateTime(value)?.ymd ?? null;
381
+ }
382
+ /** Minutes-of-day from a datetime value, or null for date-only / invalid. */
383
+ function timeOf(value) {
384
+ return parseIsoDateTime(value)?.minutes ?? null;
385
+ }
386
+ /** Parse a free-form time-string field into start/end minutes-of-day.
387
+ * Handles the common shapes in user data:
388
+ * "14:00-17:00" → { start: 840, end: 1020 } (range → block)
389
+ * "17:00-" → { start: 1020, end: null } (open end → single line)
390
+ * "16:30" → { start: 990, end: null } (point in time → single line)
391
+ * "終日" / "" → null (no clock → all-day)
392
+ * Returns null when no clock token is parseable. */
393
+ function parseTimeRange(value) {
394
+ if (typeof value !== "string") return null;
395
+ const text = value.trim();
396
+ if (!text) return null;
397
+ const tokens = [...text.matchAll(CLOCK_RE)];
398
+ if (tokens.length === 0) return null;
399
+ const minutesOf = (match) => clockToMinutes(Number(match[1]), Number(match[2]));
400
+ if (!RANGE_SEP_RE.test(text)) {
401
+ const startMin = minutesOf(tokens[0]);
402
+ return startMin === null ? null : {
403
+ startMin,
404
+ endMin: null
405
+ };
406
+ }
407
+ const sepIndex = text.search(RANGE_SEP_RE);
408
+ let startMin = null;
409
+ let endMin = null;
410
+ for (const token of tokens) if ((token.index ?? 0) < sepIndex) startMin = minutesOf(token);
411
+ else endMin = minutesOf(token);
412
+ if (startMin === null) return null;
413
+ return {
414
+ startMin,
415
+ endMin
416
+ };
417
+ }
418
+ function ymdToUtcMs(ymd) {
419
+ return Date.UTC(ymd.year, ymd.month - 1, ymd.day);
420
+ }
421
+ function utcMsToYmd(epochMs) {
422
+ const date = new Date(epochMs);
423
+ return {
424
+ year: date.getUTCFullYear(),
425
+ month: date.getUTCMonth() + 1,
426
+ day: date.getUTCDate()
427
+ };
428
+ }
429
+ /** Chronological comparison: negative if `left` precedes `right`, 0 if the
430
+ * same day, positive if after. */
431
+ function compareYmd(left, right) {
432
+ return ymdToUtcMs(left) - ymdToUtcMs(right);
433
+ }
434
+ /** True iff `day` falls within the inclusive span `[span.start, span.end]`. */
435
+ function spanCoversDay(span, day) {
436
+ return compareYmd(span.start, day) <= 0 && compareYmd(day, span.end) <= 0;
437
+ }
438
+ /** Build the 6×7 (42-cell) grid for the given month, including the
439
+ * leading/trailing days of the adjacent months so every week is full.
440
+ * `month` is 1-12. `weekStartsOn` is 0 (Sunday) … 6 (Saturday). */
441
+ function buildMonthGrid(year, month, weekStartsOn = 0) {
442
+ const lead = (new Date(Date.UTC(year, month - 1, 1)).getUTCDay() - weekStartsOn + 7) % 7;
443
+ const startMs = Date.UTC(year, month - 1, 1) - lead * MS_PER_DAY;
444
+ const cells = [];
445
+ for (let i = 0; i < 42; i++) {
446
+ const ymd = utcMsToYmd(startMs + i * MS_PER_DAY);
447
+ cells.push({
448
+ ymd,
449
+ inMonth: ymd.year === year && ymd.month === month,
450
+ key: ymdKey(ymd)
451
+ });
452
+ }
453
+ return cells;
454
+ }
455
+ /** Resolve a record's calendar span from its date/datetime fields. Returns
456
+ * null when the anchor date is missing/invalid (→ the caller's "no date"
457
+ * tray). An end date that is missing, invalid, or earlier than the start
458
+ * collapses to a single-day span — never an inverted range.
459
+ *
460
+ * Times for the day (time-allocation) view come from, in priority order:
461
+ * 1. the clock on a `datetime` anchor/end value, else
462
+ * 2. `timeField` — a separate free-form time-string column (e.g. "14:00-17:00").
463
+ * A record with no resolvable clock has `startMin === endMin === null`. */
464
+ function recordSpan(item, anchorField, endField, timeField) {
465
+ const startRaw = item[anchorField];
466
+ const start = dateOf(startRaw);
467
+ if (!start) return null;
468
+ let end = start;
469
+ let startMin = timeOf(startRaw);
470
+ let endMin = null;
471
+ if (endField) {
472
+ const endRaw = item[endField];
473
+ const parsedEnd = dateOf(endRaw);
474
+ if (parsedEnd && compareYmd(parsedEnd, start) >= 0) {
475
+ end = parsedEnd;
476
+ endMin = timeOf(endRaw);
477
+ }
478
+ }
479
+ if (timeField && startMin === null && endMin === null) {
480
+ const range = parseTimeRange(item[timeField]);
481
+ if (range) ({startMin, endMin} = range);
482
+ }
483
+ return {
484
+ item,
485
+ start,
486
+ end,
487
+ startMin,
488
+ endMin
489
+ };
490
+ }
491
+ /** Split records into those that land on the calendar (with their spans)
492
+ * and those with no usable anchor date (the "no date" tray). Spans are
493
+ * sorted by start day so same-day stacking is stable across renders. */
494
+ function bucketRecords(items, anchorField, endField, timeField) {
495
+ const spans = [];
496
+ const noDate = [];
497
+ for (const item of items) {
498
+ const span = recordSpan(item, anchorField, endField, timeField);
499
+ if (span) spans.push(span);
500
+ else noDate.push(item);
501
+ }
502
+ spans.sort((left, right) => compareYmd(left.start, right.start));
503
+ return {
504
+ spans,
505
+ noDate
506
+ };
507
+ }
508
+ /** Project a record's span onto a single day for the time-allocation view, or
509
+ * null when the span doesn't cover that day. */
510
+ function daySlice(span, day) {
511
+ if (!spanCoversDay(span, day)) return null;
512
+ const hasStart = span.startMin !== null;
513
+ const hasEnd = span.endMin !== null;
514
+ if (!hasStart && !hasEnd) return {
515
+ kind: "allDay",
516
+ startMin: 0,
517
+ endMin: MINUTES_PER_DAY,
518
+ bleedsBefore: false,
519
+ bleedsAfter: false
520
+ };
521
+ const singleDay = compareYmd(span.start, span.end) === 0;
522
+ const isStartDay = compareYmd(day, span.start) === 0;
523
+ const isEndDay = compareYmd(day, span.end) === 0;
524
+ if (singleDay && hasStart && !hasEnd) return {
525
+ kind: "line",
526
+ startMin: span.startMin,
527
+ endMin: span.startMin,
528
+ bleedsBefore: false,
529
+ bleedsAfter: false
530
+ };
531
+ const startMin = isStartDay && hasStart ? span.startMin : 0;
532
+ const endMin = isEndDay && hasEnd ? span.endMin : MINUTES_PER_DAY;
533
+ if (singleDay && endMin <= startMin) return {
534
+ kind: "line",
535
+ startMin,
536
+ endMin: startMin,
537
+ bleedsBefore: false,
538
+ bleedsAfter: false
539
+ };
540
+ return {
541
+ kind: "block",
542
+ startMin,
543
+ endMin,
544
+ bleedsBefore: !isStartDay,
545
+ bleedsAfter: !isEndDay
546
+ };
547
+ }
548
+ function assignLanes(blocks) {
549
+ const order = [...blocks.keys()].sort((left, right) => blocks[left].startMin - blocks[right].startMin || blocks[left].endMin - blocks[right].endMin);
550
+ const result = blocks.map(() => ({
551
+ lane: 0,
552
+ lanes: 1
553
+ }));
554
+ let cluster = [];
555
+ let clusterEnd = Number.NEGATIVE_INFINITY;
556
+ const laneEnds = [];
557
+ const flush = () => {
558
+ for (const index of cluster) result[index].lanes = laneEnds.length;
559
+ cluster = [];
560
+ laneEnds.length = 0;
561
+ clusterEnd = Number.NEGATIVE_INFINITY;
562
+ };
563
+ for (const index of order) {
564
+ const block = blocks[index];
565
+ if (cluster.length > 0 && block.startMin >= clusterEnd) flush();
566
+ let lane = laneEnds.findIndex((end) => end <= block.startMin);
567
+ if (lane === -1) {
568
+ lane = laneEnds.length;
569
+ laneEnds.push(block.endMin);
570
+ } else laneEnds[lane] = block.endMin;
571
+ result[index].lane = lane;
572
+ cluster.push(index);
573
+ clusterEnd = Math.max(clusterEnd, block.endMin);
574
+ }
575
+ flush();
576
+ return result;
577
+ }
578
+ /** Month label key inputs — returns the 1st of the month as a `Date` so the
579
+ * component can feed it to `Intl.DateTimeFormat(locale, …)` for a localized
580
+ * "April 2026" header without this module taking a locale dependency. */
581
+ function monthAnchorDate(year, month) {
582
+ return new Date(Date.UTC(year, month - 1, 1));
583
+ }
584
+ //#endregion
585
+ Object.defineProperty(exports, "ENUM_ALERT", {
586
+ enumerable: true,
587
+ get: function() {
588
+ return ENUM_ALERT;
589
+ }
590
+ });
591
+ Object.defineProperty(exports, "ENUM_NEUTRAL", {
592
+ enumerable: true,
593
+ get: function() {
594
+ return ENUM_NEUTRAL;
595
+ }
596
+ });
597
+ Object.defineProperty(exports, "ENUM_NUDGE", {
598
+ enumerable: true,
599
+ get: function() {
600
+ return ENUM_NUDGE;
601
+ }
602
+ });
603
+ Object.defineProperty(exports, "MINUTES_PER_DAY", {
604
+ enumerable: true,
605
+ get: function() {
606
+ return MINUTES_PER_DAY;
607
+ }
608
+ });
609
+ Object.defineProperty(exports, "actionVisible", {
610
+ enumerable: true,
611
+ get: function() {
612
+ return actionVisible;
613
+ }
614
+ });
615
+ Object.defineProperty(exports, "assignLanes", {
616
+ enumerable: true,
617
+ get: function() {
618
+ return assignLanes;
619
+ }
620
+ });
621
+ Object.defineProperty(exports, "bucketRecords", {
622
+ enumerable: true,
623
+ get: function() {
624
+ return bucketRecords;
625
+ }
626
+ });
627
+ Object.defineProperty(exports, "buildMonthGrid", {
628
+ enumerable: true,
629
+ get: function() {
630
+ return buildMonthGrid;
631
+ }
632
+ });
633
+ Object.defineProperty(exports, "buildUpdatedRecord", {
634
+ enumerable: true,
635
+ get: function() {
636
+ return buildUpdatedRecord;
637
+ }
638
+ });
639
+ Object.defineProperty(exports, "coerceInlineValue", {
640
+ enumerable: true,
641
+ get: function() {
642
+ return coerceInlineValue;
643
+ }
644
+ });
645
+ Object.defineProperty(exports, "compareYmd", {
646
+ enumerable: true,
647
+ get: function() {
648
+ return compareYmd;
649
+ }
650
+ });
651
+ Object.defineProperty(exports, "dateOf", {
652
+ enumerable: true,
653
+ get: function() {
654
+ return dateOf;
655
+ }
656
+ });
657
+ Object.defineProperty(exports, "daySlice", {
658
+ enumerable: true,
659
+ get: function() {
660
+ return daySlice;
661
+ }
662
+ });
663
+ Object.defineProperty(exports, "draftToRecord", {
664
+ enumerable: true,
665
+ get: function() {
666
+ return draftToRecord;
667
+ }
668
+ });
669
+ Object.defineProperty(exports, "emptyRow", {
670
+ enumerable: true,
671
+ get: function() {
672
+ return emptyRow;
673
+ }
674
+ });
675
+ Object.defineProperty(exports, "enumColorClasses", {
676
+ enumerable: true,
677
+ get: function() {
678
+ return enumColorClasses;
679
+ }
680
+ });
681
+ Object.defineProperty(exports, "enumValueIndex", {
682
+ enumerable: true,
683
+ get: function() {
684
+ return enumValueIndex;
685
+ }
686
+ });
687
+ Object.defineProperty(exports, "fieldVisible", {
688
+ enumerable: true,
689
+ get: function() {
690
+ return fieldVisible;
691
+ }
692
+ });
693
+ Object.defineProperty(exports, "firstMissingRequiredField", {
694
+ enumerable: true,
695
+ get: function() {
696
+ return firstMissingRequiredField;
697
+ }
698
+ });
699
+ Object.defineProperty(exports, "itemIdOf", {
700
+ enumerable: true,
701
+ get: function() {
702
+ return itemIdOf;
703
+ }
704
+ });
705
+ Object.defineProperty(exports, "itemLabelOf", {
706
+ enumerable: true,
707
+ get: function() {
708
+ return itemLabelOf;
709
+ }
710
+ });
711
+ Object.defineProperty(exports, "labelFieldFor", {
712
+ enumerable: true,
713
+ get: function() {
714
+ return labelFieldFor;
715
+ }
716
+ });
717
+ Object.defineProperty(exports, "monthAnchorDate", {
718
+ enumerable: true,
719
+ get: function() {
720
+ return monthAnchorDate;
721
+ }
722
+ });
723
+ Object.defineProperty(exports, "parseIsoDate", {
724
+ enumerable: true,
725
+ get: function() {
726
+ return parseIsoDate;
727
+ }
728
+ });
729
+ Object.defineProperty(exports, "parseIsoDateTime", {
730
+ enumerable: true,
731
+ get: function() {
732
+ return parseIsoDateTime;
733
+ }
734
+ });
735
+ Object.defineProperty(exports, "parseTimeRange", {
736
+ enumerable: true,
737
+ get: function() {
738
+ return parseTimeRange;
739
+ }
740
+ });
741
+ Object.defineProperty(exports, "recordSpan", {
742
+ enumerable: true,
743
+ get: function() {
744
+ return recordSpan;
745
+ }
746
+ });
747
+ Object.defineProperty(exports, "resolveEnumColor", {
748
+ enumerable: true,
749
+ get: function() {
750
+ return resolveEnumColor;
751
+ }
752
+ });
753
+ Object.defineProperty(exports, "rowFromItem", {
754
+ enumerable: true,
755
+ get: function() {
756
+ return rowFromItem;
757
+ }
758
+ });
759
+ Object.defineProperty(exports, "spanCoversDay", {
760
+ enumerable: true,
761
+ get: function() {
762
+ return spanCoversDay;
763
+ }
764
+ });
765
+ Object.defineProperty(exports, "whenMatches", {
766
+ enumerable: true,
767
+ get: function() {
768
+ return whenMatches;
769
+ }
770
+ });
771
+ Object.defineProperty(exports, "ymdKey", {
772
+ enumerable: true,
773
+ get: function() {
774
+ return ymdKey;
775
+ }
776
+ });
777
+
778
+ //# sourceMappingURL=calendarGrid-DZeLwBj9.cjs.map