@hanzo/ui 8.0.24 → 8.0.26

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