@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
@@ -0,0 +1,1119 @@
1
+ 'use client';
2
+ import { SelectMenu } from '../chunk-5CZOYONP.js';
3
+ import { PrimaryButton, classifyBackend, FieldSwitch, PageHeader, BackendStateCard, EmptyState } from '../chunk-GVCOAJLO.js';
4
+ import '../chunk-FVQXROBK.js';
5
+ import { BUILDER_FIELD_TYPES, fieldsFromDocType, starterFields, validateBuilder, blankField, moveField, toDocType, fieldNameFromLabel, fieldNeedsOptions, moduleDoctypes, mediaFileField, mediaDocPayload, titleOf, looksLikeImage, loadLinkOptions, hasProjectField, PROJECT_FIELD, enrichLinks, toRecord, docTypeToFields, makeFieldOptions, listHiddenFields, cardFields, savePayload, isMediaDoctype, newDraft, isDraft, statusField, SUBMITTED, CANCELLED } from '../chunk-P2QU5MN5.js';
6
+ export { BUILDER_FIELD_TYPES, CANCELLED, DRAFT, PROJECT_FIELD, REDACTED_MARKER, SUBMITTED, autonameSource, blankField, cardFields, createFrameworkClient, createMediaUploader, docTypeToFields, enrichLinks, fieldNameFromLabel, fieldNeedsOptions, fieldTypeLabel, fieldsFromDocType, hasProjectField, humanize, imageDimensions, isDraft, isMediaDoctype, isRef, isValidDoctypeName, joinKey, linkFields, listHiddenFields, listQuery, listedFieldNames, loadLinkOptions, looksLikeImage, makeFieldOptions, mediaDocPayload, mediaFileField, mediaKey, mediaRef, moduleDoctypes, moveField, newDraft, newKey, parseRef, rows, savePayload, slugify, starterFields, statusField, titleOf, toDocField, toDocType, toRecord, validateBuilder } from '../chunk-P2QU5MN5.js';
7
+ import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
8
+ import { Button, XStack, Card, Text, Spinner, YStack, Input } from '@hanzo/gui';
9
+ import { TriangleAlert, GripVertical, ArrowDown, Plus, Boxes, Image, Upload, Trash2, Search, ChevronRight, X, Save, Ban, Send, PenOff, Globe, Pencil, ArrowUp, ArrowLeft, RefreshCw } from '@hanzogui/lucide-icons-2';
10
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
11
+ import { searchRecords, FieldDisplay, RecordsView, RecordForm, RecordDetail } from '@hanzo/data';
12
+
13
+ var TABLE_MIN = 720;
14
+ var GRID_MIN = 560;
15
+ var TAP = 44;
16
+ function layoutFor(width, tableMin = TABLE_MIN) {
17
+ return width > 0 && width >= tableMin ? "desktop" : "phone";
18
+ }
19
+ function useContainerLayout(tableMin = TABLE_MIN) {
20
+ const [width, setWidth] = useState(0);
21
+ const onLayout = useCallback((e) => {
22
+ const w = Math.round(e?.nativeEvent?.layout?.width ?? 0);
23
+ setWidth((prev) => w > 0 && w !== prev ? w : prev);
24
+ }, []);
25
+ const layout = layoutFor(width, tableMin);
26
+ const phone = layout === "phone";
27
+ return {
28
+ layout,
29
+ phone,
30
+ wideEnoughForGrid: width >= GRID_MIN,
31
+ width,
32
+ onLayout
33
+ };
34
+ }
35
+ function Action({ children, icon, onPress, disabled, primary, danger, phone, label }) {
36
+ const Cmp = primary ? PrimaryButton : Button;
37
+ return /* @__PURE__ */ jsx(
38
+ Cmp,
39
+ {
40
+ size: phone ? "$4" : "$2",
41
+ minH: phone ? TAP : void 0,
42
+ grow: phone ? 1 : 0,
43
+ flexBasis: phone ? 0 : "auto",
44
+ minW: phone ? 140 : void 0,
45
+ icon,
46
+ disabled,
47
+ onPress,
48
+ "aria-label": label,
49
+ ...danger ? { theme: "red" } : {},
50
+ children
51
+ }
52
+ );
53
+ }
54
+ function Actions({ children, phone }) {
55
+ return /* @__PURE__ */ jsx(XStack, { gap: "$2", items: "center", flexWrap: "wrap", width: phone ? "100%" : void 0, children });
56
+ }
57
+ function ErrorBar({ message }) {
58
+ return /* @__PURE__ */ jsx(Card, { borderWidth: 1, borderColor: "$red7", bg: "$red2", p: "$3", maxW: 620, children: /* @__PURE__ */ jsxs(XStack, { gap: "$2", items: "flex-start", children: [
59
+ /* @__PURE__ */ jsx(TriangleAlert, { size: 15 }),
60
+ /* @__PURE__ */ jsx(Text, { fontSize: "$3", color: "$red11", flex: 1, children: message })
61
+ ] }) });
62
+ }
63
+ function Loading({ label, size = "small" }) {
64
+ return /* @__PURE__ */ jsxs(XStack, { items: "center", justify: "center", gap: "$3", py: "$8", children: [
65
+ /* @__PURE__ */ jsx(Spinner, { size }),
66
+ label ? /* @__PURE__ */ jsx(Text, { fontSize: "$3", color: "$color11", children: label }) : null
67
+ ] });
68
+ }
69
+ function Meta({ children }) {
70
+ return /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$color10", numberOfLines: 1, children });
71
+ }
72
+ var WRAP_ANYWHERE = { overflowWrap: "anywhere", wordBreak: "break-word" };
73
+ function Panel({ children, phone }) {
74
+ return /* @__PURE__ */ jsx(
75
+ Card,
76
+ {
77
+ borderWidth: 1,
78
+ borderColor: "$borderColor",
79
+ p: phone ? "$3" : "$4",
80
+ maxW: 760,
81
+ width: "100%",
82
+ style: WRAP_ANYWHERE,
83
+ children: /* @__PURE__ */ jsx(YStack, { gap: "$3", children })
84
+ }
85
+ );
86
+ }
87
+ var TYPE_OPTIONS = BUILDER_FIELD_TYPES.map((t) => ({ key: t.type, label: t.label }));
88
+ function CollectionBuilder({ client, module, existing, onSaved, onCancel }) {
89
+ const editing = Boolean(existing);
90
+ const [name, setName] = useState(existing?.name ?? "");
91
+ const [fields, setFields] = useState(
92
+ () => existing ? fieldsFromDocType(existing) : starterFields()
93
+ );
94
+ const [busy, setBusy] = useState(false);
95
+ const [saveError, setSaveError] = useState(null);
96
+ const [showErrors, setShowErrors] = useState(false);
97
+ const { phone, onLayout } = useContainerLayout();
98
+ const validation = useMemo(() => validateBuilder(name, fields), [name, fields]);
99
+ const patch = useCallback((key, next) => {
100
+ setFields((fs) => fs.map((f) => f.key === key ? { ...f, ...next } : f));
101
+ }, []);
102
+ const addField = useCallback(() => setFields((fs) => [...fs, blankField()]), []);
103
+ const removeField = useCallback((key) => setFields((fs) => fs.filter((f) => f.key !== key)), []);
104
+ const move = useCallback((key, dir) => setFields((fs) => moveField(fs, key, dir)), []);
105
+ const save = useCallback(async () => {
106
+ setShowErrors(true);
107
+ if (!validation.ok) return;
108
+ setBusy(true);
109
+ setSaveError(null);
110
+ try {
111
+ const dt = toDocType(name, module, fields);
112
+ if (editing) await client.doctypes.update(name, dt);
113
+ else await client.doctypes.create(dt);
114
+ onSaved(name);
115
+ } catch (e) {
116
+ setSaveError(classifyBackend(e).message);
117
+ } finally {
118
+ setBusy(false);
119
+ }
120
+ }, [client, editing, fields, module, name, onSaved, validation.ok]);
121
+ const iconBtn = (icon, label, onPress, disabled, danger) => /* @__PURE__ */ jsx(
122
+ Button,
123
+ {
124
+ size: phone ? "$3" : "$1",
125
+ circular: true,
126
+ minW: phone ? TAP : void 0,
127
+ minH: phone ? TAP : void 0,
128
+ "aria-label": label,
129
+ icon,
130
+ disabled,
131
+ onPress,
132
+ ...danger ? { theme: "red" } : {}
133
+ }
134
+ );
135
+ return /* @__PURE__ */ jsxs(Card, { onLayout, borderWidth: 1, borderColor: "$borderColor", p: phone ? "$3" : "$4", gap: "$4", maxW: 820, width: "100%", children: [
136
+ /* @__PURE__ */ jsxs(YStack, { gap: "$2", children: [
137
+ /* @__PURE__ */ jsx(Text, { fontSize: "$5", fontWeight: "800", children: editing ? `Edit ${name}` : "New collection" }),
138
+ /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$color10", children: "A content type on the Hanzo Framework \u2014 name it and define its fields. Every field type is available; a rich-text field gives a full WYSIWYG body." })
139
+ ] }),
140
+ /* @__PURE__ */ jsxs(YStack, { gap: "$1.5", width: "100%", children: [
141
+ /* @__PURE__ */ jsx(Text, { fontSize: "$1", color: "$color11", textTransform: "uppercase", letterSpacing: 0.4, children: "Collection" }),
142
+ /* @__PURE__ */ jsx(
143
+ Input,
144
+ {
145
+ width: "100%",
146
+ minH: phone ? TAP : void 0,
147
+ size: phone ? "$4" : "$3",
148
+ placeholder: "e.g. Recipe or LandingPage",
149
+ value: name,
150
+ onChangeText: setName,
151
+ disabled: busy || editing,
152
+ autoCapitalize: "none"
153
+ }
154
+ ),
155
+ editing ? /* @__PURE__ */ jsx(Text, { fontSize: "$1", color: "$color9", children: "A collection\u2019s name is its identity and can\u2019t change." }) : null
156
+ ] }),
157
+ /* @__PURE__ */ jsxs(YStack, { gap: "$2", width: "100%", children: [
158
+ /* @__PURE__ */ jsxs(XStack, { items: "center", justify: "space-between", children: [
159
+ /* @__PURE__ */ jsx(Text, { fontSize: "$3", fontWeight: "700", color: "$color11", children: "Fields" }),
160
+ /* @__PURE__ */ jsxs(Text, { fontSize: "$1", color: "$color9", children: [
161
+ fields.length,
162
+ " field",
163
+ fields.length === 1 ? "" : "s"
164
+ ] })
165
+ ] }),
166
+ /* @__PURE__ */ jsx(YStack, { gap: "$2", width: "100%", children: fields.map((f, i) => {
167
+ const err = showErrors ? validation.fieldErrors[f.key] : void 0;
168
+ const fname = f.label.trim() ? fieldNameFromLabel(f.label) : "";
169
+ return /* @__PURE__ */ jsxs(
170
+ YStack,
171
+ {
172
+ borderWidth: 1,
173
+ borderColor: err ? "$red7" : "$borderColor",
174
+ rounded: "$4",
175
+ p: "$3",
176
+ gap: "$2",
177
+ bg: "$color1",
178
+ width: "100%",
179
+ children: [
180
+ /* @__PURE__ */ jsxs(XStack, { gap: "$2", items: "center", width: "100%", children: [
181
+ phone ? null : /* @__PURE__ */ jsx(GripVertical, { size: 15, opacity: 0.5 }),
182
+ /* @__PURE__ */ jsx(
183
+ Input,
184
+ {
185
+ flex: 1,
186
+ minW: 0,
187
+ minH: phone ? TAP : void 0,
188
+ size: phone ? "$4" : "$3",
189
+ placeholder: "Field name (e.g. Title)",
190
+ value: f.label,
191
+ onChangeText: (v) => patch(f.key, { label: v }),
192
+ disabled: busy,
193
+ autoCapitalize: "none"
194
+ }
195
+ ),
196
+ phone ? null : /* @__PURE__ */ jsx(YStack, { minW: 150, children: /* @__PURE__ */ jsx(
197
+ SelectMenu,
198
+ {
199
+ required: true,
200
+ ariaLabel: "Field type",
201
+ options: TYPE_OPTIONS,
202
+ value: f.type,
203
+ onChange: (v) => v && patch(f.key, { type: v }),
204
+ disabled: busy,
205
+ minWidth: 150
206
+ }
207
+ ) })
208
+ ] }),
209
+ phone ? /* @__PURE__ */ jsx(YStack, { width: "100%", children: /* @__PURE__ */ jsx(
210
+ SelectMenu,
211
+ {
212
+ required: true,
213
+ ariaLabel: "Field type",
214
+ options: TYPE_OPTIONS,
215
+ value: f.type,
216
+ onChange: (v) => v && patch(f.key, { type: v }),
217
+ disabled: busy,
218
+ minWidth: 0,
219
+ minHeight: TAP
220
+ }
221
+ ) }) : null,
222
+ /* @__PURE__ */ jsxs(XStack, { gap: "$4", items: "center", justify: "space-between", flexWrap: "wrap", width: "100%", children: [
223
+ /* @__PURE__ */ jsxs(XStack, { gap: "$4", items: "center", children: [
224
+ /* @__PURE__ */ jsxs(XStack, { gap: "$2", items: "center", minH: phone ? TAP : void 0, children: [
225
+ /* @__PURE__ */ jsx(FieldSwitch, { checked: f.required, onChange: (v) => patch(f.key, { required: v }), disabled: busy }),
226
+ /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$color10", children: "Required" })
227
+ ] }),
228
+ /* @__PURE__ */ jsxs(XStack, { gap: "$2", items: "center", minH: phone ? TAP : void 0, children: [
229
+ /* @__PURE__ */ jsx(FieldSwitch, { checked: f.inListView, onChange: (v) => patch(f.key, { inListView: v }), disabled: busy }),
230
+ /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$color10", children: "In list" })
231
+ ] })
232
+ ] }),
233
+ /* @__PURE__ */ jsxs(XStack, { gap: "$2", children: [
234
+ iconBtn(/* @__PURE__ */ jsx(ArrowUp, { size: 14 }), "Move field up", () => move(f.key, -1), busy || i === 0),
235
+ iconBtn(/* @__PURE__ */ jsx(ArrowDown, { size: 14 }), "Move field down", () => move(f.key, 1), busy || i === fields.length - 1),
236
+ iconBtn(/* @__PURE__ */ jsx(Trash2, { size: 14 }), "Remove field", () => removeField(f.key), busy, true)
237
+ ] })
238
+ ] }),
239
+ fieldNeedsOptions(f.type) ? /* @__PURE__ */ jsxs(YStack, { gap: "$1.5", width: "100%", children: [
240
+ /* @__PURE__ */ jsx(Text, { fontSize: "$1", color: "$color10", textTransform: "uppercase", letterSpacing: 0.4, children: f.type === "Select" ? "Options" : f.type === "Link" ? "Relates to" : "Child type" }),
241
+ /* @__PURE__ */ jsx(
242
+ Input,
243
+ {
244
+ width: "100%",
245
+ minH: phone ? TAP : void 0,
246
+ size: phone ? "$4" : "$2",
247
+ placeholder: f.type === "Select" ? "One per line \u2014 Draft / Published" : "Collection name (e.g. Author)",
248
+ value: f.options,
249
+ onChangeText: (v) => patch(f.key, { options: v }),
250
+ multiline: f.type === "Select",
251
+ numberOfLines: f.type === "Select" ? 3 : 1,
252
+ disabled: busy,
253
+ autoCapitalize: "none"
254
+ }
255
+ )
256
+ ] }) : null,
257
+ /* @__PURE__ */ jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", flexWrap: "wrap", children: [
258
+ fname ? /* @__PURE__ */ jsxs(Text, { fontSize: "$1", color: "$color8", children: [
259
+ "field: ",
260
+ fname
261
+ ] }) : /* @__PURE__ */ jsx(YStack, {}),
262
+ err ? /* @__PURE__ */ jsxs(XStack, { gap: "$1", items: "center", children: [
263
+ /* @__PURE__ */ jsx(TriangleAlert, { size: 12 }),
264
+ /* @__PURE__ */ jsx(Text, { fontSize: "$1", color: "$red11", children: err })
265
+ ] }) : null
266
+ ] })
267
+ ]
268
+ },
269
+ f.key
270
+ );
271
+ }) }),
272
+ /* @__PURE__ */ jsx(XStack, { width: phone ? "100%" : void 0, children: /* @__PURE__ */ jsx(Action, { phone, icon: /* @__PURE__ */ jsx(Plus, { size: 14 }), onPress: addField, disabled: busy, children: "Add field" }) })
273
+ ] }),
274
+ showErrors && validation.formError ? /* @__PURE__ */ jsx(ErrorBar, { message: validation.formError }) : null,
275
+ saveError ? /* @__PURE__ */ jsx(ErrorBar, { message: saveError }) : null,
276
+ /* @__PURE__ */ jsxs(Actions, { phone, children: [
277
+ /* @__PURE__ */ jsx(Action, { phone, disabled: busy, onPress: onCancel, children: "Cancel" }),
278
+ /* @__PURE__ */ jsx(Action, { phone, primary: true, disabled: busy, onPress: save, children: busy ? "Saving\u2026" : editing ? "Save collection" : "Create collection" })
279
+ ] })
280
+ ] });
281
+ }
282
+ function CollectionsBrowser({
283
+ client,
284
+ module,
285
+ label,
286
+ subtitle,
287
+ onOpen,
288
+ setupDescription,
289
+ setupBullets,
290
+ renderBuilder,
291
+ icon = Boxes
292
+ }) {
293
+ const [state, setState] = useState({ phase: "loading" });
294
+ const [busy, setBusy] = useState(false);
295
+ const [creating, setCreating] = useState(false);
296
+ const [actionError, setActionError] = useState(null);
297
+ const { phone, onLayout } = useContainerLayout();
298
+ const load = useCallback(
299
+ async (signal) => {
300
+ setState({ phase: "loading" });
301
+ try {
302
+ const [dts, mod] = await Promise.all([
303
+ client.doctypes.list(),
304
+ client.modules.get(module).catch(() => null)
305
+ // lane may not be registered on an older engine
306
+ ]);
307
+ if (signal.cancelled) return;
308
+ setState({
309
+ phase: "ready",
310
+ collections: moduleDoctypes(dts, module),
311
+ registered: Boolean(mod && mod.doctypes.length)
312
+ });
313
+ } catch (e) {
314
+ if (!signal.cancelled) setState({ phase: "error", error: classifyBackend(e) });
315
+ }
316
+ },
317
+ [client, module]
318
+ );
319
+ useEffect(() => {
320
+ const signal = { cancelled: false };
321
+ void load(signal);
322
+ return () => {
323
+ signal.cancelled = true;
324
+ };
325
+ }, [load]);
326
+ const reload = useCallback(() => void load({ cancelled: false }), [load]);
327
+ const install = useCallback(async () => {
328
+ setBusy(true);
329
+ setActionError(null);
330
+ try {
331
+ await client.modules.install(module);
332
+ reload();
333
+ } catch (e) {
334
+ setActionError(classifyBackend(e).message);
335
+ } finally {
336
+ setBusy(false);
337
+ }
338
+ }, [client, module, reload]);
339
+ if (state.phase === "loading") {
340
+ return /* @__PURE__ */ jsx(YStack, { onLayout, width: "100%", children: /* @__PURE__ */ jsx(Loading, { label: `Loading ${label}\u2026` }) });
341
+ }
342
+ if (state.phase === "error") {
343
+ return /* @__PURE__ */ jsxs(YStack, { onLayout, gap: "$3", width: "100%", children: [
344
+ /* @__PURE__ */ jsx(PageHeader, { title: label, subtitle }),
345
+ /* @__PURE__ */ jsx(BackendStateCard, { state: state.error, onRetry: reload, hint: "framework \xB7 GET /v1/framework/doctypes" })
346
+ ] });
347
+ }
348
+ const { collections } = state;
349
+ return /* @__PURE__ */ jsxs(YStack, { onLayout, gap: "$3", width: "100%", children: [
350
+ /* @__PURE__ */ jsx(
351
+ PageHeader,
352
+ {
353
+ title: label,
354
+ subtitle,
355
+ actions: collections.length && !creating ? /* @__PURE__ */ jsx(
356
+ Action,
357
+ {
358
+ phone,
359
+ primary: true,
360
+ icon: /* @__PURE__ */ jsx(Plus, { size: 15 }),
361
+ onPress: () => {
362
+ setCreating(true);
363
+ setActionError(null);
364
+ },
365
+ children: "New collection"
366
+ }
367
+ ) : void 0
368
+ }
369
+ ),
370
+ actionError ? /* @__PURE__ */ jsx(ErrorBar, { message: actionError }) : null,
371
+ creating ? (renderBuilder ?? ((p) => /* @__PURE__ */ jsx(CollectionBuilder, { client, module, ...p })))({
372
+ onSaved: (name) => {
373
+ setCreating(false);
374
+ onOpen(name);
375
+ },
376
+ onCancel: () => setCreating(false)
377
+ }) : null,
378
+ collections.length === 0 && !creating ? /* @__PURE__ */ jsx(
379
+ EmptyState,
380
+ {
381
+ icon,
382
+ title: `Set up ${label}`,
383
+ description: setupDescription ?? `${label} is a set of collections on the Hanzo Framework \u2014 typed documents, per organization.`,
384
+ bullets: setupBullets ?? [
385
+ `Installs the default ${label} collections into your organization`,
386
+ "Every record is a document on the framework \u2014 versioned, permissioned, per-org",
387
+ "Add your own collections and fields any time"
388
+ ],
389
+ primary: state.registered ? { label: busy ? "Setting up\u2026" : `Set up ${label}`, onPress: install } : void 0,
390
+ secondary: { label: "New collection", onPress: () => setCreating(true) }
391
+ }
392
+ ) : !creating ? (
393
+ // A fixed 240px card is a 240px column on a 390px phone with dead space
394
+ // beside it. `flexBasis` + `flexGrow` makes each card fill the row on a
395
+ // phone and settle back to a 240px tile once the box can hold several.
396
+ /* @__PURE__ */ jsx(XStack, { gap: "$3", flexWrap: "wrap", width: "100%", children: collections.map((dt) => /* @__PURE__ */ jsxs(
397
+ YStack,
398
+ {
399
+ onPress: () => onOpen(dt.name),
400
+ hoverStyle: { borderColor: "$color8" },
401
+ pressStyle: { bg: "$color3" },
402
+ cursor: "pointer",
403
+ borderWidth: 1,
404
+ borderColor: "$borderColor",
405
+ rounded: "$4",
406
+ p: "$4",
407
+ gap: "$2",
408
+ minH: 88,
409
+ grow: phone ? 1 : 0,
410
+ flexBasis: phone ? "100%" : 240,
411
+ maxW: phone ? void 0 : 240,
412
+ children: [
413
+ /* @__PURE__ */ jsxs(XStack, { gap: "$2", items: "center", children: [
414
+ /* @__PURE__ */ jsx(Boxes, { size: 16 }),
415
+ /* @__PURE__ */ jsx(Text, { fontSize: "$4", fontWeight: "700", flex: 1, minW: 0, numberOfLines: 1, children: dt.name })
416
+ ] }),
417
+ /* @__PURE__ */ jsxs(Meta, { children: [
418
+ dt.fields?.length ?? 0,
419
+ " field",
420
+ (dt.fields?.length ?? 0) === 1 ? "" : "s",
421
+ dt.isSubmittable ? " \xB7 submittable" : ""
422
+ ] })
423
+ ]
424
+ },
425
+ dt.name
426
+ )) })
427
+ ) : null
428
+ ] });
429
+ }
430
+ function MediaGrid({
431
+ client,
432
+ dt,
433
+ docs,
434
+ media,
435
+ onOpen,
436
+ onChanged,
437
+ toolbarExtra,
438
+ accept = "image/*,application/pdf,video/*,audio/*"
439
+ }) {
440
+ const fileField = mediaFileField(dt);
441
+ const inputRef = useRef(null);
442
+ const [busy, setBusy] = useState(false);
443
+ const [progress, setProgress] = useState(null);
444
+ const [error, setError] = useState(null);
445
+ const { phone, onLayout } = useContainerLayout();
446
+ const upload = useCallback(
447
+ async (files) => {
448
+ const list = Array.from(files);
449
+ if (list.length === 0) return;
450
+ setBusy(true);
451
+ setError(null);
452
+ setProgress({ done: 0, total: list.length });
453
+ try {
454
+ for (let i = 0; i < list.length; i += 1) {
455
+ const facts = await media.upload(list[i]);
456
+ await client.records.create(dt.name, mediaDocPayload(dt, facts));
457
+ setProgress({ done: i + 1, total: list.length });
458
+ }
459
+ onChanged();
460
+ } catch (e) {
461
+ setError(classifyBackend(e).message);
462
+ } finally {
463
+ setBusy(false);
464
+ setProgress(null);
465
+ }
466
+ },
467
+ [client, dt, media, onChanged]
468
+ );
469
+ const pick = () => inputRef.current?.click();
470
+ const onInput = (e) => {
471
+ if (e.target.files) void upload(e.target.files);
472
+ e.target.value = "";
473
+ };
474
+ const HiddenInput = /* @__PURE__ */ jsx("input", { ref: inputRef, type: "file", multiple: true, accept, style: { display: "none" }, onChange: onInput });
475
+ const uploadLabel = busy ? progress ? `Uploading ${progress.done}/${progress.total}\u2026` : "Uploading\u2026" : "Upload";
476
+ if (docs.length === 0) {
477
+ return /* @__PURE__ */ jsxs(YStack, { onLayout, gap: "$3", width: "100%", children: [
478
+ HiddenInput,
479
+ toolbarExtra ? /* @__PURE__ */ jsx(XStack, { justify: phone ? "flex-start" : "flex-end", gap: "$2", width: "100%", children: toolbarExtra }) : null,
480
+ error ? /* @__PURE__ */ jsx(ErrorBar, { message: error }) : null,
481
+ /* @__PURE__ */ jsx(DropZone, { busy, onDrop: upload, children: /* @__PURE__ */ jsx(
482
+ EmptyState,
483
+ {
484
+ icon: Image,
485
+ title: "No media yet",
486
+ description: "Drag files here, or upload \u2014 images and files go to your organization's object storage and become reusable media you can attach to any record.",
487
+ primary: { label: uploadLabel, onPress: pick }
488
+ }
489
+ ) })
490
+ ] });
491
+ }
492
+ return /* @__PURE__ */ jsxs(YStack, { onLayout, gap: "$3", width: "100%", children: [
493
+ HiddenInput,
494
+ /* @__PURE__ */ jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", justify: phone ? "flex-start" : "flex-end", width: "100%", children: [
495
+ toolbarExtra,
496
+ /* @__PURE__ */ jsx(Action, { phone, primary: true, icon: /* @__PURE__ */ jsx(Upload, { size: 15 }), disabled: busy, onPress: pick, children: uploadLabel })
497
+ ] }),
498
+ error ? /* @__PURE__ */ jsx(ErrorBar, { message: error }) : null,
499
+ /* @__PURE__ */ jsx(DropZone, { busy, onDrop: upload, children: /* @__PURE__ */ jsx(XStack, { gap: "$3", flexWrap: "wrap", width: "100%", children: docs.map((d) => /* @__PURE__ */ jsx(
500
+ MediaCard,
501
+ {
502
+ doc: d,
503
+ dt,
504
+ fileField,
505
+ media,
506
+ phone,
507
+ onOpen: () => onOpen(String(d.name)),
508
+ onDelete: async () => {
509
+ setBusy(true);
510
+ setError(null);
511
+ try {
512
+ await media.remove(d[fileField]);
513
+ await client.records.remove(dt.name, String(d.name));
514
+ onChanged();
515
+ } catch (e) {
516
+ setError(classifyBackend(e).message);
517
+ } finally {
518
+ setBusy(false);
519
+ }
520
+ },
521
+ disabled: busy
522
+ },
523
+ String(d.name)
524
+ )) }) })
525
+ ] });
526
+ }
527
+ function MediaCard({
528
+ doc,
529
+ dt,
530
+ fileField,
531
+ media,
532
+ phone,
533
+ onOpen,
534
+ onDelete,
535
+ disabled
536
+ }) {
537
+ const [url, setUrl] = useState("");
538
+ const [confirming, setConfirming] = useState(false);
539
+ const raw = String(doc[fileField] ?? "");
540
+ const title = titleOf(doc, dt);
541
+ useEffect(() => {
542
+ let cancelled = false;
543
+ void media.resolveUrl(raw).then((u) => {
544
+ if (!cancelled) setUrl(u);
545
+ });
546
+ return () => {
547
+ cancelled = true;
548
+ };
549
+ }, [raw, media]);
550
+ return /* @__PURE__ */ jsxs(
551
+ YStack,
552
+ {
553
+ borderWidth: 1,
554
+ borderColor: "$borderColor",
555
+ rounded: "$4",
556
+ grow: 0,
557
+ flexBasis: phone ? "47%" : 200,
558
+ maxW: phone ? void 0 : 200,
559
+ minW: 140,
560
+ overflow: "hidden",
561
+ hoverStyle: { borderColor: "$color8" },
562
+ children: [
563
+ /* @__PURE__ */ jsx(
564
+ YStack,
565
+ {
566
+ height: phone ? 110 : 130,
567
+ bg: "$color3",
568
+ items: "center",
569
+ justify: "center",
570
+ overflow: "hidden",
571
+ cursor: "pointer",
572
+ pressStyle: { opacity: 0.8 },
573
+ onPress: onOpen,
574
+ children: url && (looksLikeImage(raw) || looksLikeImage(url)) ? (
575
+ // eslint-disable-next-line @next/next/no-img-element
576
+ /* @__PURE__ */ jsx("img", { src: url, alt: String(doc.alt ?? title), style: { width: "100%", height: "100%", objectFit: "cover" } })
577
+ ) : /* @__PURE__ */ jsx(Image, { size: 28 })
578
+ }
579
+ ),
580
+ /* @__PURE__ */ jsxs(YStack, { p: "$3", gap: "$1", children: [
581
+ /* @__PURE__ */ jsx(Text, { fontSize: "$3", fontWeight: "700", numberOfLines: 1, children: title }),
582
+ /* @__PURE__ */ jsxs(XStack, { justify: "space-between", items: "center", gap: "$2", children: [
583
+ /* @__PURE__ */ jsx(Meta, { children: String(doc.mime ?? "file") }),
584
+ confirming ? /* @__PURE__ */ jsxs(XStack, { gap: "$1", children: [
585
+ /* @__PURE__ */ jsx(
586
+ Button,
587
+ {
588
+ size: phone ? "$3" : "$1",
589
+ minH: phone ? TAP : void 0,
590
+ theme: "red",
591
+ disabled,
592
+ onPress: () => {
593
+ setConfirming(false);
594
+ void onDelete();
595
+ },
596
+ children: "Delete"
597
+ }
598
+ ),
599
+ /* @__PURE__ */ jsx(Button, { size: phone ? "$3" : "$1", minH: phone ? TAP : void 0, disabled, onPress: () => setConfirming(false), children: "No" })
600
+ ] }) : /* @__PURE__ */ jsx(
601
+ Button,
602
+ {
603
+ size: phone ? "$3" : "$1",
604
+ circular: true,
605
+ chromeless: true,
606
+ minW: phone ? TAP : void 0,
607
+ minH: phone ? TAP : void 0,
608
+ "aria-label": `Delete ${title}`,
609
+ icon: /* @__PURE__ */ jsx(Trash2, { size: phone ? 16 : 13 }),
610
+ disabled,
611
+ onPress: () => setConfirming(true)
612
+ }
613
+ )
614
+ ] })
615
+ ] })
616
+ ]
617
+ }
618
+ );
619
+ }
620
+ function DropZone({
621
+ children,
622
+ busy,
623
+ onDrop
624
+ }) {
625
+ const [over, setOver] = useState(false);
626
+ return /* @__PURE__ */ jsx(
627
+ YStack,
628
+ {
629
+ width: "100%",
630
+ onDragOver: (e) => {
631
+ e.preventDefault();
632
+ if (!busy) setOver(true);
633
+ },
634
+ onDragLeave: () => setOver(false),
635
+ onDrop: (e) => {
636
+ e.preventDefault();
637
+ setOver(false);
638
+ if (!busy && e.dataTransfer?.files?.length) void onDrop(e.dataTransfer.files);
639
+ },
640
+ rounded: "$4",
641
+ p: over ? "$3" : 0,
642
+ borderWidth: over ? 2 : 0,
643
+ borderColor: "$color8",
644
+ style: { borderStyle: "dashed" },
645
+ children
646
+ }
647
+ );
648
+ }
649
+ function headline(record, titleField) {
650
+ const t = titleField ? record[titleField] : void 0;
651
+ if (typeof t === "string" && t.trim()) return t;
652
+ if (typeof t === "number") return String(t);
653
+ const name = record.name ?? record.id;
654
+ return typeof name === "string" && name ? name : "Untitled";
655
+ }
656
+ function RecordCards({
657
+ titleField,
658
+ fields,
659
+ cardFields: cardFields2,
660
+ records,
661
+ loading,
662
+ empty,
663
+ onOpen,
664
+ onCreate,
665
+ createLabel = "New record",
666
+ toolbarExtra
667
+ }) {
668
+ const [query, setQuery] = useState("");
669
+ const visible = useMemo(() => searchRecords(records, query, fields), [records, query, fields]);
670
+ return /* @__PURE__ */ jsxs(YStack, { testID: "doctype-cards", gap: "$3", width: "100%", children: [
671
+ /* @__PURE__ */ jsxs(YStack, { gap: "$2", width: "100%", children: [
672
+ /* @__PURE__ */ jsxs(
673
+ XStack,
674
+ {
675
+ items: "center",
676
+ gap: "$2",
677
+ px: "$3",
678
+ minH: 44,
679
+ rounded: "$3",
680
+ borderWidth: 1,
681
+ borderColor: "$borderColor",
682
+ width: "100%",
683
+ children: [
684
+ /* @__PURE__ */ jsx(Search, { size: 15, opacity: 0.6 }),
685
+ /* @__PURE__ */ jsx(
686
+ Input,
687
+ {
688
+ unstyled: true,
689
+ flex: 1,
690
+ minW: 0,
691
+ height: 42,
692
+ fontSize: "$3",
693
+ color: "$color12",
694
+ placeholder: "Search",
695
+ value: query,
696
+ onChangeText: setQuery,
697
+ autoCapitalize: "none"
698
+ }
699
+ )
700
+ ]
701
+ }
702
+ ),
703
+ /* @__PURE__ */ jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", width: "100%", children: [
704
+ onCreate ? /* @__PURE__ */ jsx(Action, { phone: true, primary: true, onPress: onCreate, children: createLabel }) : null,
705
+ toolbarExtra
706
+ ] })
707
+ ] }),
708
+ loading ? /* @__PURE__ */ jsx(Loading, { label: "Loading records\u2026" }) : null,
709
+ !loading && visible.length === 0 ? /* @__PURE__ */ jsx(Card, { borderWidth: 1, borderColor: "$borderColor", p: "$4", width: "100%", children: /* @__PURE__ */ jsx(Text, { fontSize: "$3", color: "$color10", children: query ? `No records match \u201C${query}\u201D.` : empty ?? "No records yet." }) }) : null,
710
+ /* @__PURE__ */ jsx(YStack, { gap: "$2", width: "100%", children: visible.map((rec, i) => {
711
+ const key = String(rec.name ?? rec.id ?? i);
712
+ return /* @__PURE__ */ jsx(
713
+ Card,
714
+ {
715
+ borderWidth: 1,
716
+ borderColor: "$borderColor",
717
+ rounded: "$3",
718
+ p: "$3",
719
+ width: "100%",
720
+ cursor: onOpen ? "pointer" : void 0,
721
+ pressStyle: onOpen ? { bg: "$color3" } : void 0,
722
+ hoverStyle: onOpen ? { borderColor: "$color8" } : void 0,
723
+ onPress: onOpen ? () => onOpen(rec) : void 0,
724
+ style: WRAP_ANYWHERE,
725
+ children: /* @__PURE__ */ jsxs(YStack, { gap: "$2", width: "100%", children: [
726
+ /* @__PURE__ */ jsxs(XStack, { items: "center", gap: "$2", width: "100%", children: [
727
+ /* @__PURE__ */ jsx(Text, { fontSize: "$4", fontWeight: "700", flex: 1, minW: 0, numberOfLines: 2, children: headline(rec, titleField) }),
728
+ onOpen ? /* @__PURE__ */ jsx(ChevronRight, { size: 16, opacity: 0.5 }) : null
729
+ ] }),
730
+ cardFields2.map((f) => (
731
+ // Label above value: a two-column row forces the value into a
732
+ // sliver at 390px. Stacked, every field type gets the full width.
733
+ /* @__PURE__ */ jsxs(YStack, { gap: "$0.5", width: "100%", children: [
734
+ /* @__PURE__ */ jsx(Text, { fontSize: "$1", color: "$color10", textTransform: "uppercase", letterSpacing: 0.4, children: f.label }),
735
+ /* @__PURE__ */ jsx(XStack, { width: "100%", flexWrap: "wrap", children: /* @__PURE__ */ jsx(FieldDisplay, { field: f, value: rec[f.name] }) })
736
+ ] }, f.name)
737
+ ))
738
+ ] })
739
+ },
740
+ key
741
+ );
742
+ }) }),
743
+ visible.length ? /* @__PURE__ */ jsxs(Meta, { children: [
744
+ visible.length,
745
+ " of ",
746
+ records.length,
747
+ " record",
748
+ records.length === 1 ? "" : "s"
749
+ ] }) : null
750
+ ] });
751
+ }
752
+ function DocTypeRecords({
753
+ client,
754
+ doctype,
755
+ onOpen,
756
+ onCreate,
757
+ project,
758
+ title,
759
+ media,
760
+ renderMedia,
761
+ limit = 200
762
+ }) {
763
+ const [state, setState] = useState({ phase: "loading" });
764
+ const [mutationError, setMutationError] = useState(null);
765
+ const { phone, onLayout } = useContainerLayout();
766
+ const load = useCallback(
767
+ async (signal) => {
768
+ setState({ phase: "loading" });
769
+ try {
770
+ const dt = await client.doctypes.get(doctype);
771
+ const linkOptions = await loadLinkOptions(client, dt);
772
+ const filters = project && hasProjectField(dt) ? { [PROJECT_FIELD]: project } : void 0;
773
+ const docs = await client.records.list(doctype, { limit, ...filters ? { filters } : {} });
774
+ if (signal.cancelled) return;
775
+ const records = docs.map((d) => enrichLinks(toRecord(d, dt), dt, linkOptions));
776
+ setState({ phase: "ready", dt, fields: docTypeToFields(dt), docs, records, linkOptions });
777
+ } catch (e) {
778
+ if (!signal.cancelled) setState({ phase: "error", error: classifyBackend(e) });
779
+ }
780
+ },
781
+ [client, doctype, project, limit]
782
+ );
783
+ useEffect(() => {
784
+ const signal = { cancelled: false };
785
+ void load(signal);
786
+ return () => {
787
+ signal.cancelled = true;
788
+ };
789
+ }, [load]);
790
+ const reload = useCallback(() => void load({ cancelled: false }), [load]);
791
+ const ready = state.phase === "ready" ? state : void 0;
792
+ const fieldOptions = useMemo(
793
+ () => ready ? makeFieldOptions(ready.linkOptions) : void 0,
794
+ [ready]
795
+ );
796
+ const [view, setView] = useState(void 0);
797
+ const dtName = ready?.dt.name;
798
+ useEffect(() => {
799
+ if (!ready) return;
800
+ setView({
801
+ id: `doctype:${ready.dt.name}`,
802
+ name: ready.dt.name,
803
+ kind: "table",
804
+ filters: [],
805
+ sorts: [],
806
+ hiddenFields: listHiddenFields(ready.dt)
807
+ });
808
+ }, [dtName]);
809
+ const cards = useMemo(
810
+ () => ready ? cardFields(ready.dt, ready.fields) : [],
811
+ [ready]
812
+ );
813
+ const onEditCommit = useCallback(
814
+ async (record, field, value) => {
815
+ if (state.phase !== "ready") return;
816
+ const name = typeof record.name === "string" ? record.name : "";
817
+ if (!name) return;
818
+ setMutationError(null);
819
+ try {
820
+ const body = savePayload({ ...record, [field.name]: value }, state.dt);
821
+ const saved = await client.records.update(doctype, name, body);
822
+ const next = enrichLinks(toRecord(saved, state.dt), state.dt, state.linkOptions);
823
+ setState((s) => s.phase === "ready" ? { ...s, records: s.records.map((r) => r.name === name ? next : r) } : s);
824
+ } catch (e) {
825
+ setMutationError(classifyBackend(e).message);
826
+ throw e;
827
+ }
828
+ },
829
+ [client, doctype, state]
830
+ );
831
+ if (state.phase === "error") {
832
+ return /* @__PURE__ */ jsx(YStack, { onLayout, width: "100%", children: /* @__PURE__ */ jsx(BackendStateCard, { state: state.error, onRetry: reload, hint: `framework \xB7 GET /v1/framework/${doctype}` }) });
833
+ }
834
+ const refresh = phone ? /* @__PURE__ */ jsx(Action, { phone: true, icon: /* @__PURE__ */ jsx(RefreshCw, { size: 15 }), onPress: reload, children: "Refresh" }) : /* @__PURE__ */ jsx(Button, { size: "$2", minH: TAP - 12, icon: /* @__PURE__ */ jsx(RefreshCw, { size: 15 }), onPress: reload, children: "Refresh" });
835
+ if (ready && (media || renderMedia) && isMediaDoctype(ready.dt)) {
836
+ return /* @__PURE__ */ jsx(YStack, { onLayout, width: "100%", children: renderMedia ? renderMedia({ dt: ready.dt, docs: ready.docs, onOpen, onChanged: reload, toolbarExtra: refresh }) : /* @__PURE__ */ jsx(
837
+ MediaGrid,
838
+ {
839
+ client,
840
+ dt: ready.dt,
841
+ docs: ready.docs,
842
+ media,
843
+ onOpen,
844
+ onChanged: reload,
845
+ toolbarExtra: refresh
846
+ }
847
+ ) });
848
+ }
849
+ const openByKey = (r) => onOpen(String(r.name ?? r.id ?? ""));
850
+ return /* @__PURE__ */ jsxs(YStack, { onLayout, gap: "$3", width: "100%", children: [
851
+ mutationError ? /* @__PURE__ */ jsx(ErrorBar, { message: mutationError }) : null,
852
+ phone ? /* @__PURE__ */ jsx(
853
+ RecordCards,
854
+ {
855
+ titleField: ready?.dt.titleField,
856
+ fields: ready?.fields ?? [],
857
+ cardFields: cards,
858
+ records: ready?.records ?? [],
859
+ loading: state.phase === "loading",
860
+ empty: `No records in ${doctype} yet.`,
861
+ onOpen: openByKey,
862
+ onCreate,
863
+ toolbarExtra: refresh
864
+ }
865
+ ) : /* @__PURE__ */ jsx(YStack, { testID: "doctype-table", width: "100%", children: /* @__PURE__ */ jsx(
866
+ RecordsView,
867
+ {
868
+ title,
869
+ fields: ready?.fields ?? [],
870
+ records: ready?.records ?? [],
871
+ loading: state.phase === "loading",
872
+ view,
873
+ onViewChange: setView,
874
+ onOpen: openByKey,
875
+ onCreate,
876
+ createLabel: "New record",
877
+ onEditCommit,
878
+ fieldOptions,
879
+ toolbarExtra: refresh,
880
+ titleField: ready?.dt.titleField,
881
+ empty: `No records in ${doctype} yet.`
882
+ }
883
+ ) })
884
+ ] });
885
+ }
886
+ function DocTypeDetail({ client, doctype, name, project, onBack, onView }) {
887
+ const isNew = name === "new";
888
+ const [state, setState] = useState({ phase: "loading" });
889
+ const [editing, setEditing] = useState(isNew);
890
+ const [draft, setDraft] = useState({});
891
+ const [busy, setBusy] = useState(false);
892
+ const [saveError, setSaveError] = useState(null);
893
+ const [confirmingDelete, setConfirmingDelete] = useState(false);
894
+ const { phone, onLayout } = useContainerLayout();
895
+ const load = useCallback(
896
+ async (signal) => {
897
+ setState({ phase: "loading" });
898
+ try {
899
+ const dt2 = await client.doctypes.get(doctype);
900
+ const linkOptions = await loadLinkOptions(client, dt2);
901
+ const doc = isNew ? null : await client.records.get(doctype, name);
902
+ if (signal.cancelled) return;
903
+ const record2 = doc ? enrichLinks(toRecord(doc, dt2), dt2, linkOptions) : null;
904
+ setState({ phase: "ready", dt: dt2, record: record2, linkOptions });
905
+ const seed = newDraft(dt2);
906
+ if (record2 == null && project && hasProjectField(dt2)) seed[PROJECT_FIELD] = project;
907
+ setDraft(record2 ? { ...record2 } : seed);
908
+ setEditing(isNew);
909
+ setSaveError(null);
910
+ setConfirmingDelete(false);
911
+ } catch (e) {
912
+ if (!signal.cancelled) setState({ phase: "error", error: classifyBackend(e) });
913
+ }
914
+ },
915
+ [client, doctype, name, isNew, project]
916
+ );
917
+ useEffect(() => {
918
+ const signal = { cancelled: false };
919
+ void load(signal);
920
+ return () => {
921
+ signal.cancelled = true;
922
+ };
923
+ }, [load]);
924
+ const reload = useCallback(() => void load({ cancelled: false }), [load]);
925
+ const ready = state.phase === "ready" ? state : void 0;
926
+ const dt = ready?.dt;
927
+ const record = ready?.record ?? null;
928
+ const fields = useMemo(() => dt ? docTypeToFields(dt, { editing: !isNew }) : [], [dt, isNew]);
929
+ const fieldOptions = useMemo(() => ready ? makeFieldOptions(ready.linkOptions) : void 0, [ready]);
930
+ const title = dt ? isNew ? `New ${doctype}` : titleOf(record ?? {}, dt) : doctype;
931
+ const onChange = useCallback((field, value) => {
932
+ setDraft((d) => ({ ...d, [field]: value }));
933
+ }, []);
934
+ const reflect = useCallback(
935
+ (saved) => {
936
+ if (!dt || !ready) return;
937
+ const next = enrichLinks(toRecord(saved, dt), dt, ready.linkOptions);
938
+ setState({ phase: "ready", dt, record: next, linkOptions: ready.linkOptions });
939
+ setDraft({ ...next });
940
+ },
941
+ [dt, ready]
942
+ );
943
+ const save = useCallback(async () => {
944
+ if (!dt) return;
945
+ setBusy(true);
946
+ setSaveError(null);
947
+ try {
948
+ const body = savePayload(draft, dt);
949
+ if (isNew) {
950
+ const created = await client.records.create(doctype, body);
951
+ if (created.name) onView(String(created.name));
952
+ else onBack();
953
+ return;
954
+ }
955
+ if (!record?.name) throw new Error("This record has no name to update.");
956
+ reflect(await client.records.update(doctype, String(record.name), body));
957
+ setEditing(false);
958
+ } catch (e) {
959
+ setSaveError(classifyBackend(e).message);
960
+ } finally {
961
+ setBusy(false);
962
+ }
963
+ }, [client, doctype, draft, dt, isNew, onBack, onView, record, reflect]);
964
+ const run = useCallback(
965
+ async (op) => {
966
+ setBusy(true);
967
+ setSaveError(null);
968
+ try {
969
+ const saved = await op();
970
+ if (saved) reflect(saved);
971
+ } catch (e) {
972
+ setSaveError(classifyBackend(e).message);
973
+ } finally {
974
+ setBusy(false);
975
+ }
976
+ },
977
+ [reflect]
978
+ );
979
+ const remove = useCallback(async () => {
980
+ if (!record?.name) return;
981
+ setBusy(true);
982
+ setSaveError(null);
983
+ try {
984
+ await client.records.remove(doctype, String(record.name));
985
+ onBack();
986
+ } catch (e) {
987
+ setSaveError(classifyBackend(e).message);
988
+ setBusy(false);
989
+ setConfirmingDelete(false);
990
+ }
991
+ }, [client, doctype, onBack, record]);
992
+ const backButton = /* @__PURE__ */ jsx(Action, { phone, icon: /* @__PURE__ */ jsx(ArrowLeft, { size: 15 }), onPress: onBack, children: "Back" });
993
+ if (state.phase === "loading") {
994
+ return /* @__PURE__ */ jsx(YStack, { onLayout, width: "100%", children: /* @__PURE__ */ jsx(Loading, { label: `Loading ${doctype}\u2026` }) });
995
+ }
996
+ if (state.phase === "error") {
997
+ return /* @__PURE__ */ jsxs(YStack, { onLayout, gap: "$3", width: "100%", children: [
998
+ /* @__PURE__ */ jsx(XStack, { children: backButton }),
999
+ /* @__PURE__ */ jsx(BackendStateCard, { state: state.error, onRetry: reload, hint: `framework \xB7 ${doctype}/${name}` })
1000
+ ] });
1001
+ }
1002
+ if (!dt) {
1003
+ return /* @__PURE__ */ jsx(YStack, { onLayout, gap: "$3", width: "100%", children: /* @__PURE__ */ jsx(XStack, { children: backButton }) });
1004
+ }
1005
+ const draftDoc = isDraft(record);
1006
+ const statusF = statusField(dt);
1007
+ const status = record ? String(record[statusF] ?? "") : "";
1008
+ const docstatus = record ? Number(record.docstatus ?? 0) : 0;
1009
+ const canEdit = !editing && !isNew && draftDoc;
1010
+ const canDelete = !editing && !isNew && draftDoc;
1011
+ const canPublish = canEdit && Boolean(statusF);
1012
+ const canSubmit = !editing && !isNew && Boolean(dt.isSubmittable) && draftDoc;
1013
+ const canCancel = !editing && !isNew && Boolean(dt.isSubmittable) && docstatus === SUBMITTED;
1014
+ const lifecycle = docstatus === SUBMITTED ? " \xB7 submitted" : docstatus === CANCELLED ? " \xB7 cancelled" : "";
1015
+ return /* @__PURE__ */ jsxs(YStack, { onLayout, gap: "$3", width: "100%", children: [
1016
+ /* @__PURE__ */ jsxs(YStack, { gap: "$3", width: "100%", children: [
1017
+ /* @__PURE__ */ jsxs(XStack, { items: "center", gap: "$3", width: "100%", flexWrap: "wrap", children: [
1018
+ phone ? null : backButton,
1019
+ /* @__PURE__ */ jsxs(YStack, { flex: 1, minW: 200, children: [
1020
+ /* @__PURE__ */ jsx(Text, { fontSize: "$6", fontWeight: "800", numberOfLines: 2, children: title }),
1021
+ /* @__PURE__ */ jsxs(Text, { fontSize: "$2", color: "$color10", numberOfLines: 1, children: [
1022
+ doctype,
1023
+ status ? ` \xB7 ${status}` : "",
1024
+ lifecycle
1025
+ ] })
1026
+ ] })
1027
+ ] }),
1028
+ /* @__PURE__ */ jsxs(Actions, { phone, children: [
1029
+ phone ? backButton : null,
1030
+ editing ? /* @__PURE__ */ jsxs(Fragment, { children: [
1031
+ !isNew ? /* @__PURE__ */ jsx(
1032
+ Action,
1033
+ {
1034
+ phone,
1035
+ icon: /* @__PURE__ */ jsx(X, { size: 15 }),
1036
+ disabled: busy,
1037
+ onPress: () => {
1038
+ setDraft(record ? { ...record } : {});
1039
+ setEditing(false);
1040
+ setSaveError(null);
1041
+ },
1042
+ children: "Cancel"
1043
+ }
1044
+ ) : null,
1045
+ /* @__PURE__ */ jsx(Action, { phone, primary: true, icon: /* @__PURE__ */ jsx(Save, { size: 15 }), disabled: busy, onPress: save, children: busy ? "Saving\u2026" : isNew ? "Create" : "Save" })
1046
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1047
+ canDelete ? /* @__PURE__ */ jsx(Action, { phone, danger: true, icon: /* @__PURE__ */ jsx(Trash2, { size: 15 }), disabled: busy, onPress: () => setConfirmingDelete(true), children: "Delete" }) : null,
1048
+ canCancel ? /* @__PURE__ */ jsx(
1049
+ Action,
1050
+ {
1051
+ phone,
1052
+ icon: /* @__PURE__ */ jsx(Ban, { size: 15 }),
1053
+ disabled: busy,
1054
+ onPress: () => run(() => client.records.cancel(doctype, String(record.name))),
1055
+ children: "Cancel doc"
1056
+ }
1057
+ ) : null,
1058
+ canSubmit ? /* @__PURE__ */ jsx(
1059
+ Action,
1060
+ {
1061
+ phone,
1062
+ icon: /* @__PURE__ */ jsx(Send, { size: 15 }),
1063
+ disabled: busy,
1064
+ onPress: () => run(() => client.records.submit(doctype, String(record.name))),
1065
+ children: "Submit"
1066
+ }
1067
+ ) : null,
1068
+ canPublish ? status === "Published" ? /* @__PURE__ */ jsx(
1069
+ Action,
1070
+ {
1071
+ phone,
1072
+ icon: /* @__PURE__ */ jsx(PenOff, { size: 15 }),
1073
+ disabled: busy,
1074
+ onPress: () => run(
1075
+ async () => client.records.update(doctype, String(record.name), savePayload({ ...record, [statusF]: "Draft" }, dt))
1076
+ ),
1077
+ children: "Unpublish"
1078
+ }
1079
+ ) : /* @__PURE__ */ jsx(
1080
+ Action,
1081
+ {
1082
+ phone,
1083
+ primary: true,
1084
+ icon: /* @__PURE__ */ jsx(Globe, { size: 15 }),
1085
+ disabled: busy,
1086
+ onPress: () => run(
1087
+ async () => client.records.update(
1088
+ doctype,
1089
+ String(record.name),
1090
+ savePayload({ ...record, [statusF]: "Published" }, dt)
1091
+ )
1092
+ ),
1093
+ children: "Publish"
1094
+ }
1095
+ ) : null,
1096
+ canEdit ? /* @__PURE__ */ jsx(Action, { phone, primary: true, icon: /* @__PURE__ */ jsx(Pencil, { size: 15 }), onPress: () => setEditing(true), children: "Edit" }) : null
1097
+ ] })
1098
+ ] })
1099
+ ] }),
1100
+ !editing && !isNew && !draftDoc ? /* @__PURE__ */ jsx(Card, { borderWidth: 1, borderColor: "$borderColor", bg: "$color2", p: "$3", maxW: 620, children: /* @__PURE__ */ jsx(Text, { fontSize: "$2", color: "$color11", children: docstatus === SUBMITTED ? "This document is submitted. Cancel it to make further changes \u2014 the engine refuses edits and deletes on a submitted document." : "This document is cancelled. It is kept as a record and can no longer be edited or deleted." }) }) : null,
1101
+ confirmingDelete ? /* @__PURE__ */ jsxs(Card, { borderWidth: 1, borderColor: "$red7", bg: "$red2", p: "$3", gap: "$3", maxW: 620, children: [
1102
+ /* @__PURE__ */ jsxs(Text, { fontSize: "$3", fontWeight: "700", children: [
1103
+ "Delete \u201C",
1104
+ title,
1105
+ "\u201D? This cannot be undone."
1106
+ ] }),
1107
+ /* @__PURE__ */ jsxs(Actions, { phone, children: [
1108
+ /* @__PURE__ */ jsx(Action, { phone, danger: true, disabled: busy, onPress: remove, children: busy ? "Deleting\u2026" : "Confirm delete" }),
1109
+ /* @__PURE__ */ jsx(Action, { phone, disabled: busy, onPress: () => setConfirmingDelete(false), children: "Keep" })
1110
+ ] })
1111
+ ] }) : null,
1112
+ saveError ? /* @__PURE__ */ jsx(ErrorBar, { message: saveError }) : null,
1113
+ /* @__PURE__ */ jsx(Panel, { phone, children: editing ? /* @__PURE__ */ jsx(RecordForm, { fields, values: draft, onChange, fieldOptions }) : record ? /* @__PURE__ */ jsx(RecordDetail, { title, fields, record, fieldOptions }) : null })
1114
+ ] });
1115
+ }
1116
+
1117
+ export { Action, Actions, CollectionBuilder, CollectionsBrowser, DocTypeDetail, DocTypeRecords, ErrorBar, GRID_MIN, Loading, MediaGrid, Meta, Panel, RecordCards, TABLE_MIN, TAP, layoutFor, useContainerLayout };
1118
+ //# sourceMappingURL=index.js.map
1119
+ //# sourceMappingURL=index.js.map