@hanzo/ui 8.0.24 → 8.0.25

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 (60) hide show
  1. package/README.md +59 -0
  2. package/dist/chunk-3NG33DV2.cjs +607 -0
  3. package/dist/chunk-3NG33DV2.cjs.map +1 -0
  4. package/dist/chunk-5CZOYONP.js +343 -0
  5. package/dist/chunk-5CZOYONP.js.map +1 -0
  6. package/dist/{chunk-P2P6ST6J.js → chunk-7ZNK6JSS.js} +9 -394
  7. package/dist/chunk-7ZNK6JSS.js.map +1 -0
  8. package/dist/chunk-GVCOAJLO.js +397 -0
  9. package/dist/chunk-GVCOAJLO.js.map +1 -0
  10. package/dist/chunk-GWNNLYX4.cjs +353 -0
  11. package/dist/chunk-GWNNLYX4.cjs.map +1 -0
  12. package/dist/{chunk-NBZDIMP3.cjs → chunk-ONVERVFG.cjs} +33 -438
  13. package/dist/chunk-ONVERVFG.cjs.map +1 -0
  14. package/dist/chunk-P2QU5MN5.js +555 -0
  15. package/dist/chunk-P2QU5MN5.js.map +1 -0
  16. package/dist/chunk-WHJR5S4L.cjs +418 -0
  17. package/dist/chunk-WHJR5S4L.cjs.map +1 -0
  18. package/dist/framework/CollectionBuilder.d.ts +12 -0
  19. package/dist/framework/CollectionsBrowser.d.ts +26 -19
  20. package/dist/framework/DocTypeRecords.d.ts +31 -11
  21. package/dist/framework/MediaGrid.d.ts +42 -0
  22. package/dist/framework/RecordCards.d.ts +35 -0
  23. package/dist/framework/builder-logic.d.ts +76 -0
  24. package/dist/framework/client.d.ts +31 -44
  25. package/dist/framework/core.cjs +213 -0
  26. package/dist/framework/core.cjs.map +1 -0
  27. package/dist/framework/core.d.ts +6 -0
  28. package/dist/framework/core.js +4 -0
  29. package/dist/framework/core.js.map +1 -0
  30. package/dist/framework/data.d.ts +2 -2
  31. package/dist/framework/fields.d.ts +47 -4
  32. package/dist/framework/index.cjs +1340 -0
  33. package/dist/framework/index.cjs.map +1 -0
  34. package/dist/framework/index.d.ts +9 -23
  35. package/dist/framework/index.js +1119 -0
  36. package/dist/framework/index.js.map +1 -0
  37. package/dist/framework/media.d.ts +53 -0
  38. package/dist/framework/parts.d.ts +74 -0
  39. package/dist/framework/responsive.d.ts +41 -0
  40. package/dist/framework/types.d.ts +13 -7
  41. package/dist/product/SelectMenu.d.ts +13 -1
  42. package/dist/product/index.cjs +2061 -336
  43. package/dist/product/index.cjs.map +1 -1
  44. package/dist/product/index.js +1909 -3
  45. package/dist/product/index.js.map +1 -1
  46. package/dist/product/social/index.cjs +17 -16
  47. package/dist/product/social/index.js +3 -2
  48. package/package.json +11 -5
  49. package/dist/chunk-2BM42AZR.cjs +0 -2301
  50. package/dist/chunk-2BM42AZR.cjs.map +0 -1
  51. package/dist/chunk-7P7R7FC5.js +0 -2231
  52. package/dist/chunk-7P7R7FC5.js.map +0 -1
  53. package/dist/chunk-NBZDIMP3.cjs.map +0 -1
  54. package/dist/chunk-P2P6ST6J.js.map +0 -1
  55. package/dist/framework/Loader.d.ts +0 -4
  56. package/dist/framework.cjs +0 -760
  57. package/dist/framework.cjs.map +0 -1
  58. package/dist/framework.d.ts +0 -1
  59. package/dist/framework.js +0 -730
  60. package/dist/framework.js.map +0 -1
package/README.md CHANGED
@@ -34,6 +34,65 @@ Both layers are **presentational + data-injected**: the host supplies rows and p
34
34
 
35
35
  > Each layer owns a `DataTable` — the product one is a generic typed `<T>` list; the data one is the field-driven record grid. Keeping the record layer on the `./data` subpath keeps each name unambiguous.
36
36
 
37
+ ## The DocType renderer — one UI for every business app
38
+
39
+ `@hanzo/ui/framework` renders the **Hanzo Framework** DocType engine (`hanzoai/cloud
40
+ clients/framework`, live at `/v1/framework/*`; also published as
41
+ [`hanzoai/framework`](https://github.com/hanzoai/framework) over
42
+ [`hanzoai/doctype`](https://github.com/hanzoai/doctype)). The engine is
43
+ metadata-driven: a CMS Page, an ERP Sales Order, a Helpdesk Ticket and a CRM
44
+ Company are the same kind of thing — a DocType with typed fields. So they get the
45
+ same renderer. An **app lane is a `module` filter** over the DocType registry plus
46
+ its own copy; there is no per-lane list, form, detail or builder to drift.
47
+
48
+ ```tsx
49
+ import { createFrameworkClient } from '@hanzo/ui/framework/core' // transport only
50
+ import { CollectionsBrowser, DocTypeRecords, DocTypeDetail } from '@hanzo/ui/framework'
51
+
52
+ // The HOST owns transport: paths are relative to the framework root, so the app
53
+ // decides the origin + the credential. Nothing in the library picks a URL.
54
+ const client = createFrameworkClient({
55
+ get: (p) => api.get(`/v1/framework/${p}`),
56
+ post: (p, b) => api.post(`/v1/framework/${p}`, b),
57
+ put: (p, b) => api.put(`/v1/framework/${p}`, b),
58
+ del: (p) => api.del(`/v1/framework/${p}`),
59
+ })
60
+
61
+ <CollectionsBrowser client={client} module="erp" label="ERP" … onOpen={openCollection} />
62
+ <DocTypeRecords client={client} doctype="erp-sales" onOpen={…} onCreate={…} />
63
+ <DocTypeDetail client={client} doctype="erp-sales" name={name} onBack={…} onView={…} />
64
+ ```
65
+
66
+ **Mobile first — a rule, not a fallback.** The layout is decided from the
67
+ CONTAINER's measured width (`onLayout`, so a narrow pane on a wide screen is still
68
+ a phone), and the answer to "not measured yet" is PHONE. So the first paint —
69
+ SSR included — is a stacked **card** per record; the `@hanzo/data` table is the
70
+ enhancement applied once the box proves it can hold one. Every control meets the
71
+ 44px tap floor (WCAG 2.5.5) at phone width, and the table honors the DocType's own
72
+ `inListView` projection so a twelve-field document does not become a horizontal
73
+ scroll wall.
74
+
75
+ **Two entries, because they are two different things.** `./framework/core` is the
76
+ contract — wire types, the client, the metadata↔render mapping, the builder
77
+ projection, the media model. It imports no React and no `@hanzo/gui`, so a data
78
+ layer, a server route or a plain node test can bind the engine without loading a
79
+ component tree. `./framework` re-exports all of it plus the views.
80
+
81
+ **Ports, not bindings.** The client takes a `FrameworkTransport`; the DAM takes a
82
+ `MediaStore` (ensure-bucket / presign / put / delete) and a **bucket parameter**,
83
+ so ERP attachments never land in a bucket named after the CMS. Routing is
84
+ `onOpen`/`onCreate`/`onBack`/`onView` callbacks — no router import anywhere.
85
+
86
+ **One entry, not two.** 8.0.24 shipped a first `./framework` lift with the same
87
+ `createFrameworkClient(Transport)` shape but no builder, no DAM and no mobile
88
+ layout, injecting `renderBuilder`/`renderMedia` instead. 8.0.25 is the convergence:
89
+ the same entry, a superset implementation, and every 8.0.24 name still resolves —
90
+ `Transport` aliases `FrameworkTransport`, `Loader` aliases the in-flow `Loading`,
91
+ `setupDescription`/`setupBullets` stay optional (they now fall back to copy DERIVED
92
+ FROM THE LANE, which is the actual fix — the old default was one lane's words), and
93
+ `renderBuilder`/`renderMedia` still win when supplied. A host that passes neither
94
+ now gets the real built-in `CollectionBuilder`/`MediaGrid` instead of nothing.
95
+
37
96
  ## Motion
38
97
 
39
98
  The calm motion vocabulary (`FadeIn`, drawer/backdrop transitions, skeleton shimmer, live pulse) ships as one stylesheet. Import it once at the app root:
@@ -0,0 +1,607 @@
1
+ 'use client';
2
+ 'use strict';
3
+
4
+ // src/framework/types.ts
5
+ var DRAFT = 0;
6
+ var SUBMITTED = 1;
7
+ var CANCELLED = 2;
8
+ var REDACTED_MARKER = "__set__";
9
+
10
+ // src/framework/client.ts
11
+ var enc = encodeURIComponent;
12
+ var asRecord = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
13
+ function rows(payload) {
14
+ if (Array.isArray(payload)) {
15
+ return payload.filter((x) => x && typeof x === "object");
16
+ }
17
+ const o = asRecord(payload);
18
+ for (const k of ["data", "items", "rows"]) {
19
+ if (Array.isArray(o[k])) {
20
+ return o[k].filter((x) => x && typeof x === "object");
21
+ }
22
+ }
23
+ return [];
24
+ }
25
+ var asDoc = (v) => asRecord(v);
26
+ var asDocType = (v) => {
27
+ const r = asRecord(v);
28
+ return {
29
+ ...r,
30
+ name: String(r.name ?? ""),
31
+ fields: Array.isArray(r.fields) ? r.fields : []
32
+ };
33
+ };
34
+ function listQuery(q) {
35
+ if (!q) return "";
36
+ const p = new URLSearchParams();
37
+ if (q.filters && Object.keys(q.filters).length) p.set("filters", JSON.stringify(q.filters));
38
+ if (q.fields && q.fields.length) p.set("fields", q.fields.join(","));
39
+ if (q.orderBy) p.set("order_by", q.orderBy);
40
+ if (q.limit) p.set("limit", String(q.limit));
41
+ const s = p.toString();
42
+ return s ? `?${s}` : "";
43
+ }
44
+ function createFrameworkClient(t) {
45
+ return {
46
+ doctypes: {
47
+ list: () => t.get("doctypes").then((r) => rows(r).map(asDocType)),
48
+ get: (name) => t.get(`doctypes/${enc(name)}`).then(asDocType),
49
+ create: (dt) => t.post("doctypes", dt).then(asDocType),
50
+ update: (name, dt) => t.put(`doctypes/${enc(name)}`, dt).then(asDocType),
51
+ remove: (name) => t.del(`doctypes/${enc(name)}`)
52
+ },
53
+ records: {
54
+ list: (doctype, q) => t.get(`${enc(doctype)}${listQuery(q)}`).then((r) => rows(r).map(asDoc)),
55
+ get: (doctype, name) => t.get(`${enc(doctype)}/${enc(name)}`).then(asDoc),
56
+ create: (doctype, data) => t.post(enc(doctype), data).then(asDoc),
57
+ update: (doctype, name, data) => t.put(`${enc(doctype)}/${enc(name)}`, data).then(asDoc),
58
+ remove: (doctype, name) => t.del(`${enc(doctype)}/${enc(name)}`),
59
+ submit: (doctype, name) => t.post(`${enc(doctype)}/${enc(name)}/submit`).then(asDoc),
60
+ cancel: (doctype, name) => t.post(`${enc(doctype)}/${enc(name)}/cancel`).then(asDoc)
61
+ },
62
+ modules: {
63
+ list: () => t.get("modules").then(
64
+ (r) => rows(r).map((m) => ({
65
+ module: String(m.module ?? ""),
66
+ doctypes: Array.isArray(m.doctypes) ? m.doctypes : []
67
+ }))
68
+ ),
69
+ get: (module) => t.get(`modules/${enc(module)}`).then((v) => {
70
+ const r = asRecord(v);
71
+ return {
72
+ module: String(r.module ?? module),
73
+ doctypes: Array.isArray(r.doctypes) ? r.doctypes : [],
74
+ installed: Array.isArray(r.installed) ? r.installed : []
75
+ };
76
+ }),
77
+ install: (module) => t.post(`modules/${enc(module)}/install`).then((v) => {
78
+ const r = asRecord(v);
79
+ return {
80
+ module: String(r.module ?? module),
81
+ created: Array.isArray(r.created) ? r.created : [],
82
+ existing: Array.isArray(r.existing) ? r.existing : []
83
+ };
84
+ })
85
+ },
86
+ roles: {
87
+ list: () => t.get("roles").then(
88
+ (r) => rows(r).map((x) => ({ user: String(x.user ?? ""), role: String(x.role ?? "") }))
89
+ ),
90
+ assign: (user, role) => t.post("roles", { user, role }).then(() => void 0),
91
+ revoke: (user, role) => t.del(`roles/${enc(user)}/${enc(role)}`)
92
+ },
93
+ summary: () => t.get("summary").then(asRecord)
94
+ };
95
+ }
96
+
97
+ // src/framework/fields.ts
98
+ var TYPE_MAP = {
99
+ Data: "text",
100
+ SmallText: "text",
101
+ Text: "longText",
102
+ LongText: "longText",
103
+ RichText: "richText",
104
+ Int: "number",
105
+ Float: "number",
106
+ Currency: "currency",
107
+ Check: "boolean",
108
+ Date: "date",
109
+ Datetime: "dateTime",
110
+ Select: "select",
111
+ Link: "relation",
112
+ Table: "json",
113
+ Attach: "url",
114
+ JSON: "json",
115
+ Password: "text"
116
+ };
117
+ var ACRONYMS = /* @__PURE__ */ new Set(["id", "url", "seo", "api", "ip"]);
118
+ function humanize(name) {
119
+ return name.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim().split(/\s+/).filter(Boolean).map((w) => ACRONYMS.has(w.toLowerCase()) ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
120
+ }
121
+ function selectOptions(options) {
122
+ return (options ?? "").split("\n").map((s) => s.replace(/\r$/, "").trim()).filter(Boolean).map((v) => ({ value: v, label: v }));
123
+ }
124
+ function metadataFor(f) {
125
+ switch (f.fieldtype) {
126
+ case "Select":
127
+ return { options: selectOptions(f.options) };
128
+ case "Currency":
129
+ return { currencyCode: "USD" };
130
+ case "Int":
131
+ return { decimals: 0 };
132
+ case "Link":
133
+ return { objectName: f.options };
134
+ default:
135
+ return void 0;
136
+ }
137
+ }
138
+ function autonameSource(dt) {
139
+ const a = (dt.autoname ?? "").trim();
140
+ return a.startsWith("field:") ? a.slice("field:".length).trim() : "";
141
+ }
142
+ function docTypeToFields(dt, opts = {}) {
143
+ const src = autonameSource(dt);
144
+ return (dt.fields ?? []).filter((f) => !f.hidden).map((f) => {
145
+ const readOnly = f.readOnly || opts.editing && f.fieldname === src || void 0;
146
+ const metadata = metadataFor(f);
147
+ return {
148
+ name: f.fieldname,
149
+ label: f.label || humanize(f.fieldname),
150
+ type: TYPE_MAP[f.fieldtype] ?? "text",
151
+ ...metadata ? { metadata } : {},
152
+ ...readOnly ? { readOnly: true } : {}
153
+ };
154
+ });
155
+ }
156
+ function statusField(dt) {
157
+ const f = (dt.fields ?? []).find(
158
+ (x) => x.fieldtype === "Select" && x.fieldname === "status" && (x.options ?? "").includes("Published")
159
+ );
160
+ return f ? f.fieldname : "";
161
+ }
162
+ function listHiddenFields(dt) {
163
+ const anyListed = (dt.fields ?? []).some((f) => f.inListView);
164
+ if (!anyListed) return [];
165
+ return (dt.fields ?? []).filter((f) => !f.inListView && !f.hidden).map((f) => f.fieldname);
166
+ }
167
+ function cardFields(dt, fields, max = 4) {
168
+ const title = dt.titleField ?? "";
169
+ const listed = new Set(listedFieldNames(dt));
170
+ const pick = listed.size ? fields.filter((f) => listed.has(f.name)) : fields;
171
+ return pick.filter((f) => f.name !== title).slice(0, max);
172
+ }
173
+ function listedFieldNames(dt) {
174
+ return (dt.fields ?? []).filter((f) => f.inListView && !f.hidden).map((f) => f.fieldname);
175
+ }
176
+ function isMediaDoctype(dt) {
177
+ return (dt.fields ?? []).some((f) => f.fieldtype === "Attach" && f.reqd);
178
+ }
179
+ function mediaFileField(dt) {
180
+ const f = (dt.fields ?? []).find((x) => x.fieldtype === "Attach" && x.reqd) ?? (dt.fields ?? []).find((x) => x.fieldtype === "Attach");
181
+ return f ? f.fieldname : "";
182
+ }
183
+ function mediaDocPayload(dt, facts) {
184
+ const declared = new Set((dt.fields ?? []).map((f) => f.fieldname));
185
+ const out = {};
186
+ const fileField = mediaFileField(dt);
187
+ if (fileField) out[fileField] = facts.fileRef;
188
+ const titleF = dt.titleField && declared.has(dt.titleField) ? dt.titleField : declared.has("title") ? "title" : "";
189
+ if (titleF && titleF !== fileField) out[titleF] = facts.filename;
190
+ const optional = [
191
+ ["mime", facts.mime],
192
+ ["size", facts.size],
193
+ ["width", facts.width],
194
+ ["height", facts.height]
195
+ ];
196
+ for (const [k, v] of optional) if (declared.has(k) && k !== fileField && k !== titleF) out[k] = v;
197
+ return out;
198
+ }
199
+ function moduleDoctypes(dts, module) {
200
+ return dts.filter((dt) => (dt.module ?? "") === module);
201
+ }
202
+ var PROJECT_FIELD = "project";
203
+ function hasProjectField(dt) {
204
+ return (dt.fields ?? []).some((f) => f.fieldname === PROJECT_FIELD);
205
+ }
206
+ function isDraft(record) {
207
+ return Number(record?.docstatus ?? DRAFT) === DRAFT;
208
+ }
209
+ function titleOf(doc, dt) {
210
+ if (dt.titleField && typeof doc[dt.titleField] === "string" && doc[dt.titleField]) {
211
+ return String(doc[dt.titleField]);
212
+ }
213
+ return typeof doc.name === "string" && doc.name ? doc.name : "(untitled)";
214
+ }
215
+ function slugify(s) {
216
+ return String(s ?? "").normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
217
+ }
218
+ function isValidDoctypeName(name) {
219
+ return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name.trim());
220
+ }
221
+ var secToMs = (v) => {
222
+ const n = Number(v);
223
+ return Number.isFinite(n) && n > 0 ? n * 1e3 : void 0;
224
+ };
225
+ var numOrNull = (v) => {
226
+ const n = Number(v);
227
+ return Number.isFinite(n) ? n : null;
228
+ };
229
+ function coerceForDisplay(f, v) {
230
+ switch (f.fieldtype) {
231
+ case "Currency":
232
+ return { amount: numOrNull(v), currencyCode: "USD" };
233
+ case "Check":
234
+ return v === 1 || v === true || v === "1";
235
+ case "Datetime":
236
+ return typeof v === "string" ? v.replace(" ", "T") : v;
237
+ case "Password":
238
+ return v === REDACTED_MARKER ? "" : v;
239
+ // never surface the marker
240
+ default:
241
+ return v;
242
+ }
243
+ }
244
+ function toRecord(doc, dt) {
245
+ const rec = {
246
+ id: doc.name,
247
+ name: doc.name,
248
+ doctype: doc.doctype,
249
+ docstatus: doc.docstatus,
250
+ createdAt: secToMs(doc.createdAt),
251
+ updatedAt: secToMs(doc.updatedAt)
252
+ };
253
+ for (const f of dt.fields ?? []) {
254
+ if (!(f.fieldname in doc)) continue;
255
+ rec[f.fieldname] = coerceForDisplay(f, doc[f.fieldname]);
256
+ }
257
+ return rec;
258
+ }
259
+ function coerceForWrite(f, v) {
260
+ switch (f.fieldtype) {
261
+ case "Currency":
262
+ return v && typeof v === "object" && "amount" in v ? Number(v.amount) : Number(v);
263
+ case "Check":
264
+ return Boolean(v);
265
+ case "Int":
266
+ case "Float":
267
+ return Number(v);
268
+ case "Datetime":
269
+ if (v instanceof Date) return v.toISOString();
270
+ if (typeof v === "number" && Number.isFinite(v)) return new Date(v).toISOString();
271
+ return v;
272
+ case "Link":
273
+ if (v == null || v === "") return "";
274
+ if (typeof v === "object") {
275
+ return String(v.id ?? v.value ?? "");
276
+ }
277
+ return v;
278
+ default:
279
+ return v;
280
+ }
281
+ }
282
+ function enrichLinks(record, dt, optionsByField) {
283
+ const out = { ...record };
284
+ for (const f of dt.fields ?? []) {
285
+ if (f.fieldtype !== "Link") continue;
286
+ const raw = out[f.fieldname];
287
+ if (typeof raw === "string" && raw) {
288
+ const opt = (optionsByField[f.fieldname] ?? []).find((o) => o.value === raw);
289
+ out[f.fieldname] = { id: raw, label: opt?.label ?? raw };
290
+ }
291
+ }
292
+ return out;
293
+ }
294
+ function linkFields(dt) {
295
+ return (dt.fields ?? []).filter((f) => f.fieldtype === "Link");
296
+ }
297
+ function newDraft(dt) {
298
+ const out = {};
299
+ for (const f of dt.fields ?? []) {
300
+ if (f.default === void 0 || f.default === "") continue;
301
+ out[f.fieldname] = f.fieldtype === "Check" ? f.default === "1" || f.default === "true" : f.default;
302
+ }
303
+ return out;
304
+ }
305
+ function savePayload(draft, dt) {
306
+ const src = autonameSource(dt);
307
+ const out = {};
308
+ for (const f of dt.fields ?? []) {
309
+ if (f.readOnly) continue;
310
+ let v = draft[f.fieldname];
311
+ if (v === void 0) continue;
312
+ v = coerceForWrite(f, v);
313
+ if (f.fieldname === src && typeof v === "string") v = slugify(v);
314
+ out[f.fieldname] = v;
315
+ }
316
+ return out;
317
+ }
318
+
319
+ // src/framework/builder-logic.ts
320
+ var BUILDER_FIELD_TYPES = [
321
+ { type: "Data", label: "Text", hint: "Single line of text" },
322
+ { type: "RichText", label: "Rich text", hint: "Formatted body (WYSIWYG)" },
323
+ { type: "LongText", label: "Long text", hint: "Multi-line plain text" },
324
+ { type: "Int", label: "Number", hint: "Whole number" },
325
+ { type: "Float", label: "Decimal", hint: "Decimal number" },
326
+ { type: "Currency", label: "Currency", hint: "Money amount" },
327
+ { type: "Check", label: "Checkbox", hint: "True / false" },
328
+ { type: "Date", label: "Date", hint: "Calendar date" },
329
+ { type: "Datetime", label: "Date & time", hint: "Date with time" },
330
+ { type: "Select", label: "Select", hint: "One of a list of options" },
331
+ { type: "Link", label: "Relation", hint: "Reference another collection" },
332
+ { type: "Attach", label: "Attachment", hint: "A file / image URL" },
333
+ { type: "Table", label: "Table", hint: "Repeatable child rows (JSON)" },
334
+ { type: "JSON", label: "JSON", hint: "Arbitrary JSON" }
335
+ ];
336
+ var LABELS = Object.fromEntries(
337
+ BUILDER_FIELD_TYPES.map((t) => [t.type, t.label])
338
+ );
339
+ function fieldTypeLabel(t) {
340
+ return LABELS[t] ?? t;
341
+ }
342
+ function fieldNeedsOptions(t) {
343
+ return t === "Select" || t === "Link" || t === "Table";
344
+ }
345
+ var seq = 0;
346
+ function newKey() {
347
+ seq += 1;
348
+ return `f${seq}_${Math.random().toString(36).slice(2, 8)}`;
349
+ }
350
+ function blankField(type = "Data") {
351
+ return { key: newKey(), label: "", type, required: false, inListView: false, options: "" };
352
+ }
353
+ function starterFields() {
354
+ return [
355
+ { key: newKey(), label: "Title", type: "Data", required: true, inListView: true, options: "" },
356
+ { key: newKey(), label: "Slug", type: "Data", required: true, inListView: true, options: "" },
357
+ { key: newKey(), label: "Body", type: "RichText", required: false, inListView: false, options: "" },
358
+ { key: newKey(), label: "Status", type: "Select", required: false, inListView: true, options: "Draft\nPublished" },
359
+ // Optional project scope — lets a host's org→project switcher filter this collection.
360
+ { key: newKey(), label: "Project", type: "Data", required: false, inListView: false, options: "" }
361
+ ];
362
+ }
363
+ function fieldsFromDocType(dt) {
364
+ return (dt.fields ?? []).map((f) => ({
365
+ key: newKey(),
366
+ label: f.label || f.fieldname,
367
+ type: f.fieldtype,
368
+ required: Boolean(f.reqd),
369
+ inListView: Boolean(f.inListView),
370
+ options: f.options ?? ""
371
+ }));
372
+ }
373
+ function fieldNameFromLabel(label) {
374
+ const slug = slugify(label).replace(/-/g, "_");
375
+ return /^[0-9]/.test(slug) ? `f_${slug}` : slug;
376
+ }
377
+ function validateBuilder(name, fields) {
378
+ const fieldErrors = {};
379
+ let formError = null;
380
+ if (!isValidDoctypeName(name.trim())) {
381
+ formError = "Enter a collection name with letters, digits, dashes or underscores (no spaces).";
382
+ } else if (fields.length === 0) {
383
+ formError = "Add at least one field.";
384
+ }
385
+ const seen = /* @__PURE__ */ new Set();
386
+ for (const f of fields) {
387
+ const label = f.label.trim();
388
+ if (!label) {
389
+ fieldErrors[f.key] = "Name this field.";
390
+ continue;
391
+ }
392
+ const fname = fieldNameFromLabel(label);
393
+ if (!fname) {
394
+ fieldErrors[f.key] = "Use letters or digits in the field name.";
395
+ continue;
396
+ }
397
+ if (seen.has(fname)) {
398
+ fieldErrors[f.key] = `Duplicate field "${fname}".`;
399
+ continue;
400
+ }
401
+ seen.add(fname);
402
+ if ((f.type === "Link" || f.type === "Table") && !f.options.trim()) {
403
+ fieldErrors[f.key] = f.type === "Link" ? "Choose the collection to relate to." : "Name the child collection.";
404
+ } else if (f.type === "Select" && !f.options.trim()) {
405
+ fieldErrors[f.key] = "Add at least one option.";
406
+ }
407
+ }
408
+ return { ok: !formError && Object.keys(fieldErrors).length === 0, fieldErrors, formError };
409
+ }
410
+ function toDocField(f) {
411
+ const fieldname = fieldNameFromLabel(f.label);
412
+ const field = {
413
+ fieldname,
414
+ fieldtype: f.type,
415
+ label: f.label.trim()
416
+ };
417
+ if (f.required) field.reqd = true;
418
+ if (f.inListView) field.inListView = true;
419
+ if (fieldNeedsOptions(f.type) && f.options.trim()) field.options = f.options.trim();
420
+ return field;
421
+ }
422
+ function moveField(fields, key, dir) {
423
+ const i = fields.findIndex((f) => f.key === key);
424
+ const j = i + dir;
425
+ if (i < 0 || j < 0 || j >= fields.length) return fields;
426
+ const next = [...fields];
427
+ [next[i], next[j]] = [next[j], next[i]];
428
+ return next;
429
+ }
430
+ function toDocType(name, module, fields) {
431
+ const docFields = fields.map(toDocField);
432
+ const byName = new Set(docFields.map((f) => f.fieldname));
433
+ const dt = { name: name.trim(), module, fields: docFields };
434
+ if (byName.has("slug")) dt.autoname = "field:slug";
435
+ if (byName.has("title")) dt.titleField = "title";
436
+ return dt;
437
+ }
438
+
439
+ // src/framework/media.ts
440
+ var REF_PREFIX = "s3://";
441
+ function mediaRef(bucket, key) {
442
+ return `${REF_PREFIX}${bucket}/${key}`;
443
+ }
444
+ function isRef(value) {
445
+ return typeof value === "string" && value.startsWith(REF_PREFIX);
446
+ }
447
+ function parseRef(ref) {
448
+ if (!ref.startsWith(REF_PREFIX)) return null;
449
+ const rest = ref.slice(REF_PREFIX.length);
450
+ const slash = rest.indexOf("/");
451
+ if (slash <= 0) return null;
452
+ return { bucket: rest.slice(0, slash), key: rest.slice(slash + 1) };
453
+ }
454
+ function joinKey(...parts) {
455
+ return parts.filter(Boolean).join("/");
456
+ }
457
+ function mediaKey(filename, folder = "") {
458
+ const dot = filename.lastIndexOf(".");
459
+ const ext = dot > 0 ? filename.slice(dot + 1).toLowerCase().replace(/[^a-z0-9]/g, "") : "";
460
+ const base = slugify(dot > 0 ? filename.slice(0, dot) : filename) || "file";
461
+ const stamp = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
462
+ const name = ext ? `${base}-${stamp}.${ext}` : `${base}-${stamp}`;
463
+ return folder ? joinKey(slugify(folder), name) : name;
464
+ }
465
+ function looksLikeImage(value) {
466
+ return /\.(png|jpe?g|gif|webp|svg|avif)(\?|#|$)/i.test(value) || value.startsWith("data:image/");
467
+ }
468
+ function createMediaUploader(store, bucket) {
469
+ return {
470
+ bucket,
471
+ async upload(file, folder = "") {
472
+ await store.ensureBucket(bucket).catch(() => void 0);
473
+ const key = mediaKey(file.name, folder);
474
+ const presigned = await store.presignUpload(bucket, key);
475
+ if (!presigned || (presigned.method ?? "PUT") !== "PUT" || !presigned.url) {
476
+ throw new Error("Could not start the upload (object storage not configured).");
477
+ }
478
+ await store.put(presigned.url, file);
479
+ const { width, height } = await imageDimensions(file);
480
+ return {
481
+ fileRef: mediaRef(bucket, key),
482
+ filename: file.name,
483
+ mime: file.type || "application/octet-stream",
484
+ size: file.size,
485
+ width,
486
+ height
487
+ };
488
+ },
489
+ async resolveUrl(value) {
490
+ if (typeof value !== "string" || value === "") return "";
491
+ if (!isRef(value)) return value;
492
+ const parsed = parseRef(value);
493
+ if (!parsed) return "";
494
+ const presigned = await store.presignDownload(parsed.bucket, parsed.key).catch(() => null);
495
+ return presigned?.url ?? "";
496
+ },
497
+ async remove(value) {
498
+ if (!isRef(value)) return;
499
+ const parsed = parseRef(value);
500
+ if (!parsed) return;
501
+ await store.deleteObject(parsed.bucket, parsed.key).catch(() => void 0);
502
+ }
503
+ };
504
+ }
505
+ function imageDimensions(file) {
506
+ return new Promise((resolve) => {
507
+ if (typeof window === "undefined" || typeof Image === "undefined" || !file.type.startsWith("image/")) {
508
+ resolve({ width: 0, height: 0 });
509
+ return;
510
+ }
511
+ const url = URL.createObjectURL(file);
512
+ const img = new Image();
513
+ img.onload = () => {
514
+ resolve({ width: img.naturalWidth, height: img.naturalHeight });
515
+ URL.revokeObjectURL(url);
516
+ };
517
+ img.onerror = () => {
518
+ resolve({ width: 0, height: 0 });
519
+ URL.revokeObjectURL(url);
520
+ };
521
+ img.src = url;
522
+ });
523
+ }
524
+
525
+ // src/framework/data.ts
526
+ async function loadLinkOptions(client, dt) {
527
+ const links = linkFields(dt);
528
+ const out = {};
529
+ await Promise.all(
530
+ links.map(async (f) => {
531
+ const target = f.options;
532
+ if (!target) return;
533
+ const [tdt, docs] = await Promise.all([
534
+ client.doctypes.get(target).catch(() => null),
535
+ client.records.list(target, { limit: 200 }).catch(() => [])
536
+ ]);
537
+ out[f.fieldname] = docs.map((d) => ({
538
+ value: String(d.name),
539
+ label: tdt ? titleOf(d, tdt) : String(d.name)
540
+ }));
541
+ })
542
+ );
543
+ return out;
544
+ }
545
+ function makeFieldOptions(linkOptions) {
546
+ return (field) => {
547
+ if (field.type === "relation") return linkOptions[field.name] ?? [];
548
+ if (field.type === "select" || field.type === "multiSelect") {
549
+ return field.metadata?.options;
550
+ }
551
+ return void 0;
552
+ };
553
+ }
554
+
555
+ exports.BUILDER_FIELD_TYPES = BUILDER_FIELD_TYPES;
556
+ exports.CANCELLED = CANCELLED;
557
+ exports.DRAFT = DRAFT;
558
+ exports.PROJECT_FIELD = PROJECT_FIELD;
559
+ exports.REDACTED_MARKER = REDACTED_MARKER;
560
+ exports.SUBMITTED = SUBMITTED;
561
+ exports.autonameSource = autonameSource;
562
+ exports.blankField = blankField;
563
+ exports.cardFields = cardFields;
564
+ exports.createFrameworkClient = createFrameworkClient;
565
+ exports.createMediaUploader = createMediaUploader;
566
+ exports.docTypeToFields = docTypeToFields;
567
+ exports.enrichLinks = enrichLinks;
568
+ exports.fieldNameFromLabel = fieldNameFromLabel;
569
+ exports.fieldNeedsOptions = fieldNeedsOptions;
570
+ exports.fieldTypeLabel = fieldTypeLabel;
571
+ exports.fieldsFromDocType = fieldsFromDocType;
572
+ exports.hasProjectField = hasProjectField;
573
+ exports.humanize = humanize;
574
+ exports.imageDimensions = imageDimensions;
575
+ exports.isDraft = isDraft;
576
+ exports.isMediaDoctype = isMediaDoctype;
577
+ exports.isRef = isRef;
578
+ exports.isValidDoctypeName = isValidDoctypeName;
579
+ exports.joinKey = joinKey;
580
+ exports.linkFields = linkFields;
581
+ exports.listHiddenFields = listHiddenFields;
582
+ exports.listQuery = listQuery;
583
+ exports.listedFieldNames = listedFieldNames;
584
+ exports.loadLinkOptions = loadLinkOptions;
585
+ exports.looksLikeImage = looksLikeImage;
586
+ exports.makeFieldOptions = makeFieldOptions;
587
+ exports.mediaDocPayload = mediaDocPayload;
588
+ exports.mediaFileField = mediaFileField;
589
+ exports.mediaKey = mediaKey;
590
+ exports.mediaRef = mediaRef;
591
+ exports.moduleDoctypes = moduleDoctypes;
592
+ exports.moveField = moveField;
593
+ exports.newDraft = newDraft;
594
+ exports.newKey = newKey;
595
+ exports.parseRef = parseRef;
596
+ exports.rows = rows;
597
+ exports.savePayload = savePayload;
598
+ exports.slugify = slugify;
599
+ exports.starterFields = starterFields;
600
+ exports.statusField = statusField;
601
+ exports.titleOf = titleOf;
602
+ exports.toDocField = toDocField;
603
+ exports.toDocType = toDocType;
604
+ exports.toRecord = toRecord;
605
+ exports.validateBuilder = validateBuilder;
606
+ //# sourceMappingURL=chunk-3NG33DV2.cjs.map
607
+ //# sourceMappingURL=chunk-3NG33DV2.cjs.map