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