@declarion/react 0.1.79 → 0.1.80

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.
@@ -0,0 +1,405 @@
1
+ import { _ as e, g as t, h as n, i as r, l as i, m as a, t as o, v as s } from "./value-FIp50R1-.js";
2
+ import { useCallback as c, useEffect as l, useMemo as u, useRef as d, useState as f } from "react";
3
+ import { Fragment as p, jsx as m, jsxs as h } from "react/jsx-runtime";
4
+ import { DndContext as g, PointerSensor as _, closestCenter as v, useSensor as y, useSensors as ee } from "@dnd-kit/core";
5
+ import { SortableContext as b, arrayMove as te, rectSortingStrategy as x, useSortable as S } from "@dnd-kit/sortable";
6
+ import { CSS as C } from "@dnd-kit/utilities";
7
+ //#region src/components/file-widgets/Grid.tsx
8
+ function w(e, t) {
9
+ return i(e, t);
10
+ }
11
+ function T(t) {
12
+ let { field: i, fieldName: p, value: S, onChange: C, mode: w, disabled: T } = t, O = o(S), [k, A] = f([]), [j, M] = f(/* @__PURE__ */ new Set()), [N, P] = f(null), { register: F, unregister: I, isDraft: L } = r(), R = d(/* @__PURE__ */ new Set()), z = d([]);
13
+ z.current = k;
14
+ let B = d(O);
15
+ B.current = O, l(() => () => {
16
+ z.current.forEach((e) => e.abort());
17
+ }, []);
18
+ let V = ee(y(_, { activationConstraint: { distance: 5 } })), re = i.display?.placeholder || p, ie = (i.accepts ?? ["image/*"]).join(","), H = i.display?.reorderable !== !1, U = i.display?.listable_thumbnail, W = c(async (r) => {
19
+ let o = s(r, i.accepts, i.max_size_bytes), c = `tile-${Math.random().toString(36).slice(2)}-${Date.now()}`, l = new AbortController();
20
+ if (o) {
21
+ A((e) => [...e, {
22
+ localId: c,
23
+ filename: r.name,
24
+ progress: null,
25
+ error: o,
26
+ abort: () => {}
27
+ }]);
28
+ return;
29
+ }
30
+ A((e) => [...e, {
31
+ localId: c,
32
+ filename: r.name,
33
+ progress: {
34
+ loaded: 0,
35
+ total: r.size,
36
+ pct: 0
37
+ },
38
+ error: null,
39
+ abort: () => l.abort()
40
+ }]);
41
+ try {
42
+ let i = await e({
43
+ file: r,
44
+ entityCode: t.entityCode,
45
+ fieldCode: p,
46
+ signal: l.signal,
47
+ onProgress: (e) => A((t) => t.map((t) => t.localId === c ? {
48
+ ...t,
49
+ progress: e
50
+ } : t))
51
+ });
52
+ if (R.current.delete(c)) {
53
+ n(i.id).catch(() => {}), I(i.id);
54
+ return;
55
+ }
56
+ F(i.id, () => n(i.id));
57
+ let a = {
58
+ id: i.id,
59
+ filename: i.filename,
60
+ content_type: i.content_type,
61
+ size_bytes: i.size_bytes,
62
+ sha256: i.sha256,
63
+ width: i.width,
64
+ height: i.height,
65
+ url: i.url,
66
+ metadata: i.metadata
67
+ }, o = [...B.current, a];
68
+ B.current = o, C?.(o), A((e) => e.filter((e) => e.localId !== c));
69
+ } catch (e) {
70
+ if (e instanceof a && e.code === "ABORTED") {
71
+ A((e) => e.filter((e) => e.localId !== c));
72
+ return;
73
+ }
74
+ let t = e instanceof a ? {
75
+ code: e.code,
76
+ message: e.message
77
+ } : {
78
+ code: "UNKNOWN",
79
+ message: e instanceof Error ? e.message : "Upload failed"
80
+ };
81
+ A((e) => e.map((e) => e.localId === c ? {
82
+ ...e,
83
+ error: t,
84
+ progress: null,
85
+ abort: () => {}
86
+ } : e));
87
+ }
88
+ }, [
89
+ i.accepts,
90
+ i.max_size_bytes,
91
+ p,
92
+ C,
93
+ t.entityCode,
94
+ F,
95
+ I
96
+ ]), G = c((e) => {
97
+ Array.from(e).forEach((e) => void W(e));
98
+ }, [W]), K = d(null), q = c(() => {
99
+ T || K.current?.click();
100
+ }, [T]), J = c((e) => {
101
+ T || (e.preventDefault(), e.dataTransfer.dropEffect = "copy");
102
+ }, [T]), ae = c((e) => {
103
+ if (T) return;
104
+ e.preventDefault();
105
+ let t = e.dataTransfer.files;
106
+ t && t.length > 0 && G(t);
107
+ }, [T, G]), oe = c((e) => {
108
+ let t = e.target.files;
109
+ e.target.value = "", t && t.length > 0 && G(t);
110
+ }, [G]), se = c((e) => {
111
+ L(e) && (n(e).catch(() => {}), I(e));
112
+ let t = O.filter((t) => t.id !== e);
113
+ C?.(t), M((t) => {
114
+ let n = new Set(t);
115
+ return n.delete(e), n;
116
+ });
117
+ }, [
118
+ L,
119
+ O,
120
+ C,
121
+ I
122
+ ]), ce = c(() => {
123
+ j.size !== 0 && (Array.from(j).forEach((e) => {
124
+ L(e) && (n(e).catch(() => {}), I(e));
125
+ }), C?.(O.filter((e) => !j.has(e.id))), M(/* @__PURE__ */ new Set()));
126
+ }, [
127
+ L,
128
+ O,
129
+ j,
130
+ C,
131
+ I
132
+ ]), le = c((e) => {
133
+ let { active: t, over: n } = e;
134
+ if (!n || t.id === n.id) return;
135
+ let r = O.findIndex((e) => e.id === t.id), i = O.findIndex((e) => e.id === n.id);
136
+ r < 0 || i < 0 || C?.(te(O, r, i));
137
+ }, [O, C]), ue = c((e) => {
138
+ M((t) => {
139
+ let n = new Set(t);
140
+ return n.has(e) ? n.delete(e) : n.add(e), n;
141
+ });
142
+ }, []), Y = N == null ? null : O[N], X = c(() => {
143
+ P((e) => e == null ? e : Math.min(O.length - 1, e + 1));
144
+ }, [O.length]), Z = c(() => {
145
+ P((e) => e == null ? e : Math.max(0, e - 1));
146
+ }, []), Q = c(() => P(null), []);
147
+ l(() => {
148
+ if (N == null) return;
149
+ let e = (e) => {
150
+ e.key === "Escape" ? Q() : e.key === "ArrowRight" ? X() : e.key === "ArrowLeft" && Z();
151
+ };
152
+ return window.addEventListener("keydown", e), () => window.removeEventListener("keydown", e);
153
+ }, [
154
+ N,
155
+ Q,
156
+ X,
157
+ Z
158
+ ]);
159
+ let de = c((e) => {
160
+ T || (e.key === "Enter" || e.key === " ") && e.target === e.currentTarget && (e.preventDefault(), q());
161
+ }, [T, q]), $ = w === "display", fe = u(() => O.map((e) => e.id), [O]);
162
+ return /* @__PURE__ */ h("div", {
163
+ "data-testid": `file-widget-${p}`,
164
+ className: "flex flex-col gap-2",
165
+ children: [
166
+ !$ && j.size > 0 && /* @__PURE__ */ h("div", {
167
+ className: "flex items-center gap-2 rounded-md border border-border bg-surface-2 p-2 text-sm",
168
+ role: "toolbar",
169
+ "aria-label": "Bulk actions",
170
+ children: [
171
+ /* @__PURE__ */ h("span", {
172
+ className: "text-foreground",
173
+ children: [j.size, " selected"]
174
+ }),
175
+ /* @__PURE__ */ m("button", {
176
+ type: "button",
177
+ className: "rounded-sm border border-border bg-card px-2 py-1 text-xs text-destructive hover:bg-surface-2",
178
+ onClick: ce,
179
+ "data-testid": "bulk-delete",
180
+ children: "Delete"
181
+ }),
182
+ /* @__PURE__ */ m("button", {
183
+ type: "button",
184
+ className: "rounded-sm border border-border bg-card px-2 py-1 text-xs text-foreground hover:bg-surface-2",
185
+ onClick: () => M(/* @__PURE__ */ new Set()),
186
+ children: "Clear"
187
+ })
188
+ ]
189
+ }),
190
+ /* @__PURE__ */ m(g, {
191
+ sensors: V,
192
+ collisionDetection: v,
193
+ onDragEnd: le,
194
+ children: /* @__PURE__ */ m(b, {
195
+ items: fe,
196
+ strategy: x,
197
+ children: /* @__PURE__ */ h("div", {
198
+ className: "grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-3 rounded-md border border-dashed border-border bg-surface p-3 outline-none focus-visible:outline-2 focus-visible:outline-accent",
199
+ role: "grid",
200
+ "aria-label": re,
201
+ tabIndex: $ || T ? -1 : 0,
202
+ onDragOver: J,
203
+ onDrop: ae,
204
+ onKeyDown: de,
205
+ "data-testid": "grid-container",
206
+ children: [
207
+ O.map((e, t) => /* @__PURE__ */ m(ne, {
208
+ file: e,
209
+ selected: j.has(e.id),
210
+ disabled: T || $,
211
+ reorderable: H && !$,
212
+ preferredThumbnail: U,
213
+ onSelect: () => ue(e.id),
214
+ onRemove: () => se(e.id),
215
+ onPreview: () => P(t)
216
+ }, e.id)),
217
+ k.map((e) => /* @__PURE__ */ m(E, {
218
+ tile: e,
219
+ onCancel: () => {
220
+ e.abort(), R.current.add(e.localId), A((t) => t.filter((t) => t.localId !== e.localId));
221
+ },
222
+ onDismiss: () => A((t) => t.filter((t) => t.localId !== e.localId))
223
+ }, e.localId)),
224
+ O.length === 0 && k.length === 0 && /* @__PURE__ */ m("div", {
225
+ className: "col-span-full flex h-24 cursor-pointer items-center justify-center text-sm text-muted-foreground",
226
+ onClick: q,
227
+ "data-testid": "grid-empty",
228
+ children: i.display?.placeholder || "Drop files here or click to browse"
229
+ })
230
+ ]
231
+ })
232
+ })
233
+ }),
234
+ !$ && /* @__PURE__ */ m("input", {
235
+ ref: K,
236
+ type: "file",
237
+ accept: ie,
238
+ multiple: !0,
239
+ onChange: oe,
240
+ style: { display: "none" },
241
+ "data-testid": `file-input-${p}`
242
+ }),
243
+ Y && /* @__PURE__ */ m(D, {
244
+ file: Y,
245
+ hasPrev: N > 0,
246
+ hasNext: N < O.length - 1,
247
+ onPrev: Z,
248
+ onNext: X,
249
+ onClose: Q
250
+ })
251
+ ]
252
+ });
253
+ }
254
+ function ne({ file: e, selected: t, disabled: n, reorderable: r, preferredThumbnail: i, onSelect: a, onRemove: o, onPreview: s }) {
255
+ let { attributes: c, listeners: l, setNodeRef: u, transform: d, transition: f, isDragging: g } = S({
256
+ id: e.id,
257
+ disabled: !r
258
+ }), _ = {
259
+ transform: C.Transform.toString(d),
260
+ transition: f,
261
+ opacity: g ? .5 : 1
262
+ }, v = w(e, i);
263
+ return /* @__PURE__ */ h("div", {
264
+ ref: u,
265
+ style: _,
266
+ className: `group relative aspect-square overflow-hidden rounded-md border ${t ? "border-accent" : "border-border"} bg-surface-2`,
267
+ "data-testid": `grid-tile-${e.id}`,
268
+ "data-selected": t || void 0,
269
+ children: [v ? /* @__PURE__ */ m("img", {
270
+ src: v,
271
+ alt: e.filename,
272
+ className: "h-full w-full cursor-pointer object-cover",
273
+ loading: "lazy",
274
+ onClick: s
275
+ }) : /* @__PURE__ */ m("div", {
276
+ className: "flex h-full w-full cursor-pointer items-center justify-center bg-surface-3 text-xs text-muted-foreground",
277
+ onClick: s,
278
+ children: e.filename
279
+ }), !n && /* @__PURE__ */ h(p, { children: [
280
+ /* @__PURE__ */ m("input", {
281
+ type: "checkbox",
282
+ className: "absolute left-1 top-1 h-4 w-4 cursor-pointer",
283
+ checked: t,
284
+ onChange: a,
285
+ "aria-label": `Select ${e.filename}`,
286
+ "data-testid": `select-${e.id}`,
287
+ onClick: (e) => e.stopPropagation()
288
+ }),
289
+ r && /* @__PURE__ */ m("button", {
290
+ type: "button",
291
+ className: "absolute right-1 top-1 cursor-grab rounded-sm border border-border bg-card px-1 text-xs",
292
+ "aria-label": `Drag to reorder ${e.filename}`,
293
+ "data-testid": `drag-${e.id}`,
294
+ ...c,
295
+ ...l,
296
+ children: "⋮⋮"
297
+ }),
298
+ /* @__PURE__ */ h("div", {
299
+ className: "absolute inset-x-0 bottom-0 flex items-center justify-end gap-1 bg-black/50 p-1 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100",
300
+ children: [/* @__PURE__ */ m("button", {
301
+ type: "button",
302
+ className: "rounded-sm border border-border bg-card px-2 py-0.5 text-xs",
303
+ onClick: s,
304
+ children: "View"
305
+ }), /* @__PURE__ */ m("button", {
306
+ type: "button",
307
+ className: "rounded-sm border border-border bg-card px-2 py-0.5 text-xs text-destructive",
308
+ onClick: o,
309
+ "data-testid": `remove-${e.id}`,
310
+ children: "Delete"
311
+ })]
312
+ })
313
+ ] })]
314
+ });
315
+ }
316
+ function E({ tile: e, onCancel: t, onDismiss: n }) {
317
+ return /* @__PURE__ */ h("div", {
318
+ className: `relative flex aspect-square flex-col items-center justify-center rounded-md border ${e.error ? "border-destructive" : "border-border"} bg-surface-2 p-2 text-center text-xs`,
319
+ "data-testid": `pending-tile-${e.localId}`,
320
+ children: [
321
+ /* @__PURE__ */ m("span", {
322
+ className: "truncate",
323
+ title: e.filename,
324
+ children: e.filename
325
+ }),
326
+ e.progress && !e.error && /* @__PURE__ */ h("span", {
327
+ className: "mt-1 text-muted-foreground",
328
+ role: "progressbar",
329
+ "aria-valuenow": e.progress.pct,
330
+ "aria-valuemin": 0,
331
+ "aria-valuemax": 100,
332
+ children: [e.progress.pct, "%"]
333
+ }),
334
+ e.error && /* @__PURE__ */ m("div", {
335
+ role: "alert",
336
+ className: "mt-1 text-destructive",
337
+ children: e.error.message
338
+ }),
339
+ /* @__PURE__ */ m("button", {
340
+ type: "button",
341
+ className: "mt-1 rounded-sm border border-border bg-card px-2 py-0.5",
342
+ onClick: e.error ? n : t,
343
+ children: e.error ? "Dismiss" : "Cancel"
344
+ })
345
+ ]
346
+ });
347
+ }
348
+ function D({ file: e, hasPrev: n, hasNext: r, onPrev: i, onNext: a, onClose: o }) {
349
+ let s = e.presigned_url ?? e.url ?? w(e);
350
+ return /* @__PURE__ */ h("div", {
351
+ className: "fixed inset-0 z-50 flex items-center justify-center bg-black/80",
352
+ role: "dialog",
353
+ "aria-modal": "true",
354
+ "aria-label": e.filename,
355
+ "data-testid": "lightbox",
356
+ onClick: o,
357
+ children: [
358
+ /* @__PURE__ */ m("button", {
359
+ type: "button",
360
+ className: "absolute right-4 top-4 rounded-sm border border-border bg-card px-3 py-1 text-foreground",
361
+ onClick: (e) => {
362
+ e.stopPropagation(), o();
363
+ },
364
+ "data-testid": "lightbox-close",
365
+ children: "Close"
366
+ }),
367
+ n && /* @__PURE__ */ m("button", {
368
+ type: "button",
369
+ className: "absolute left-4 rounded-sm border border-border bg-card px-3 py-2 text-foreground",
370
+ onClick: (e) => {
371
+ e.stopPropagation(), i();
372
+ },
373
+ "data-testid": "lightbox-prev",
374
+ children: "‹"
375
+ }),
376
+ r && /* @__PURE__ */ m("button", {
377
+ type: "button",
378
+ className: "absolute right-4 bottom-4 rounded-sm border border-border bg-card px-3 py-2 text-foreground",
379
+ onClick: (e) => {
380
+ e.stopPropagation(), a();
381
+ },
382
+ "data-testid": "lightbox-next",
383
+ children: "›"
384
+ }),
385
+ s ? /* @__PURE__ */ m("img", {
386
+ src: s,
387
+ alt: e.filename,
388
+ className: "max-h-[90vh] max-w-[90vw] object-contain",
389
+ onClick: (e) => e.stopPropagation()
390
+ }) : /* @__PURE__ */ h("div", {
391
+ className: "rounded-md bg-card p-4 text-foreground",
392
+ children: [
393
+ e.filename,
394
+ " (",
395
+ t(e.size_bytes),
396
+ ")"
397
+ ]
398
+ })
399
+ ]
400
+ });
401
+ }
402
+ //#endregion
403
+ export { T as Grid };
404
+
405
+ //# sourceMappingURL=Grid-CseTLMLv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Grid-CseTLMLv.js","names":[],"sources":["../src/components/file-widgets/Grid.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport type { CSSProperties, DragEvent, KeyboardEvent } from \"react\";\nimport {\n DndContext,\n closestCenter,\n PointerSensor,\n useSensor,\n useSensors,\n type DragEndEvent,\n} from \"@dnd-kit/core\";\nimport {\n SortableContext,\n rectSortingStrategy,\n useSortable,\n arrayMove,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport {\n uploadFile,\n discardFile,\n validateClientSide,\n formatBytes,\n UploadError,\n} from \"./upload\";\nimport { useDraftRegistration } from \"./draft-scope\";\nimport { preferredListThumbnailURL } from \"./helpers\";\nimport { fileArrayFromValue } from \"./value\";\nimport type {\n FileWidgetProps,\n HydratedFile,\n UploadProgress,\n} from \"./types\";\n\n// Grid widget: thumbnail grid for `mode: array` files. Per einstein §6.4.\n//\n// Behaviors:\n// - drag-drop multi-upload (each file gets its own progress tile)\n// - per-item hover overlay (View / Metadata / Delete)\n// - drag-handle reorder via @dnd-kit\n// - multi-select toolbar with Bulk Delete\n// - click → lightbox with prev/next + keyboard nav\n// - IntersectionObserver lazy thumbnail loading\n// - per-tile thumbnail prefers the schema-selected list derivation, then\n// falls back to built-in thumbs or the primary file URL\n//\n// Value contract: array of HydratedFile. onChange emits the full new array\n// (callers persist it via the entity's $files payload on save).\n\ninterface InFlightTile {\n // Local-only tiles representing uploads in progress. Once finished they\n // are appended to the value array and removed from this list.\n localId: string;\n filename: string;\n progress: UploadProgress | null;\n error: { code: string; message: string } | null;\n abort: () => void;\n}\n\nfunction thumbUrl(f: HydratedFile, preferredDerivationKey?: string): string | undefined {\n return preferredListThumbnailURL(f, preferredDerivationKey);\n}\n\nexport function Grid(props: FileWidgetProps) {\n const { field, fieldName, value, onChange, mode, disabled } = props;\n const items = fileArrayFromValue(value);\n const [tiles, setTiles] = useState<InFlightTile[]>([]);\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [lightbox, setLightbox] = useState<number | null>(null);\n const { register, unregister, isDraft } = useDraftRegistration();\n const cancelledUploadsRef = useRef<Set<string>>(new Set());\n const tilesRef = useRef<InFlightTile[]>([]);\n tilesRef.current = tiles;\n // valueRef snapshots the current value array so parallel uploads append\n // against the most recent state, not the closure they were created with.\n const valueRef = useRef<HydratedFile[]>(items);\n valueRef.current = items;\n\n // Cleanup all in-flight uploads on unmount.\n useEffect(() => {\n return () => {\n tilesRef.current.forEach((t) => t.abort());\n };\n }, []);\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n );\n\n const ariaLabel = field.display?.placeholder || fieldName;\n const accept = (field.accepts ?? [\"image/*\"]).join(\",\");\n const reorderable = field.display?.reorderable !== false;\n const preferredThumbnail = field.display?.listable_thumbnail;\n\n const startUpload = useCallback(\n async (raw: File) => {\n const v = validateClientSide(raw, field.accepts, field.max_size_bytes);\n const localId = `tile-${Math.random().toString(36).slice(2)}-${Date.now()}`;\n const ctrl = new AbortController();\n\n if (v) {\n setTiles((prev) => [\n ...prev,\n {\n localId,\n filename: raw.name,\n progress: null,\n error: v,\n abort: () => {},\n },\n ]);\n return;\n }\n\n setTiles((prev) => [\n ...prev,\n {\n localId,\n filename: raw.name,\n progress: { loaded: 0, total: raw.size, pct: 0 },\n error: null,\n abort: () => ctrl.abort(),\n },\n ]);\n\n try {\n const result = await uploadFile({\n file: raw,\n entityCode: props.entityCode,\n fieldCode: fieldName,\n signal: ctrl.signal,\n onProgress: (p) =>\n setTiles((prev) =>\n prev.map((t) =>\n t.localId === localId ? { ...t, progress: p } : t,\n ),\n ),\n });\n if (cancelledUploadsRef.current.delete(localId)) {\n void discardFile(result.id).catch(() => {});\n unregister(result.id);\n return;\n }\n register(result.id, () => discardFile(result.id));\n const next: HydratedFile = {\n id: result.id,\n filename: result.filename,\n content_type: result.content_type,\n size_bytes: result.size_bytes,\n sha256: result.sha256,\n width: result.width,\n height: result.height,\n url: result.url,\n metadata: result.metadata,\n };\n const merged = [...valueRef.current, next];\n valueRef.current = merged;\n onChange?.(merged);\n setTiles((prev) => prev.filter((t) => t.localId !== localId));\n } catch (err) {\n if (err instanceof UploadError && err.code === \"ABORTED\") {\n setTiles((prev) => prev.filter((t) => t.localId !== localId));\n return;\n }\n const e =\n err instanceof UploadError\n ? { code: err.code, message: err.message }\n : {\n code: \"UNKNOWN\",\n message: err instanceof Error ? err.message : \"Upload failed\",\n };\n setTiles((prev) =>\n prev.map((t) =>\n t.localId === localId\n ? { ...t, error: e, progress: null, abort: () => {} }\n : t,\n ),\n );\n }\n },\n [field.accepts, field.max_size_bytes, fieldName, onChange, props.entityCode, register, unregister],\n );\n\n const handleFiles = useCallback(\n (files: FileList | File[]) => {\n const arr = Array.from(files);\n arr.forEach((f) => void startUpload(f));\n },\n [startUpload],\n );\n\n const fileInputRef = useRef<HTMLInputElement | null>(null);\n const onPick = useCallback(() => {\n if (disabled) return;\n fileInputRef.current?.click();\n }, [disabled]);\n\n const onDragOver = useCallback(\n (e: DragEvent<HTMLDivElement>) => {\n if (disabled) return;\n e.preventDefault();\n e.dataTransfer.dropEffect = \"copy\";\n },\n [disabled],\n );\n\n const onDrop = useCallback(\n (e: DragEvent<HTMLDivElement>) => {\n if (disabled) return;\n e.preventDefault();\n const files = e.dataTransfer.files;\n if (files && files.length > 0) handleFiles(files);\n },\n [disabled, handleFiles],\n );\n\n const onChangeFile = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const files = e.target.files;\n e.target.value = \"\";\n if (files && files.length > 0) handleFiles(files);\n },\n [handleFiles],\n );\n\n const handleRemove = useCallback(\n (id: string) => {\n if (isDraft(id)) {\n void discardFile(id).catch(() => {});\n unregister(id);\n }\n const next = items.filter((f) => f.id !== id);\n onChange?.(next);\n setSelected((prev) => {\n const n = new Set(prev);\n n.delete(id);\n return n;\n });\n },\n [isDraft, items, onChange, unregister],\n );\n\n const handleBulkDelete = useCallback(() => {\n if (selected.size === 0) return;\n const ids = Array.from(selected);\n ids.forEach((id) => {\n if (isDraft(id)) {\n void discardFile(id).catch(() => {});\n unregister(id);\n }\n });\n onChange?.(items.filter((f) => !selected.has(f.id)));\n setSelected(new Set());\n }, [isDraft, items, selected, onChange, unregister]);\n\n const handleDragEnd = useCallback(\n (event: DragEndEvent) => {\n const { active, over } = event;\n if (!over || active.id === over.id) return;\n const oldIdx = items.findIndex((f) => f.id === active.id);\n const newIdx = items.findIndex((f) => f.id === over.id);\n if (oldIdx < 0 || newIdx < 0) return;\n onChange?.(arrayMove(items, oldIdx, newIdx));\n },\n [items, onChange],\n );\n\n const toggleSelect = useCallback((id: string) => {\n setSelected((prev) => {\n const n = new Set(prev);\n if (n.has(id)) n.delete(id);\n else n.add(id);\n return n;\n });\n }, []);\n\n const lightboxFile = lightbox != null ? items[lightbox] : null;\n const lightboxNext = useCallback(() => {\n setLightbox((cur) =>\n cur == null ? cur : Math.min(items.length - 1, cur + 1),\n );\n }, [items.length]);\n const lightboxPrev = useCallback(() => {\n setLightbox((cur) => (cur == null ? cur : Math.max(0, cur - 1)));\n }, []);\n const lightboxClose = useCallback(() => setLightbox(null), []);\n\n useEffect(() => {\n if (lightbox == null) return;\n const handler = (e: globalThis.KeyboardEvent) => {\n if (e.key === \"Escape\") lightboxClose();\n else if (e.key === \"ArrowRight\") lightboxNext();\n else if (e.key === \"ArrowLeft\") lightboxPrev();\n };\n window.addEventListener(\"keydown\", handler);\n return () => window.removeEventListener(\"keydown\", handler);\n }, [lightbox, lightboxClose, lightboxNext, lightboxPrev]);\n\n const onGridKeyDown = useCallback(\n (e: KeyboardEvent<HTMLDivElement>) => {\n if (disabled) return;\n if (e.key === \"Enter\" || e.key === \" \") {\n if (e.target === e.currentTarget) {\n e.preventDefault();\n onPick();\n }\n }\n },\n [disabled, onPick],\n );\n\n const isDisplay = mode === \"display\";\n\n const sortableIds = useMemo(() => items.map((f) => f.id), [items]);\n\n return (\n <div data-testid={`file-widget-${fieldName}`} className=\"flex flex-col gap-2\">\n {!isDisplay && selected.size > 0 && (\n <div\n className=\"flex items-center gap-2 rounded-md border border-border bg-surface-2 p-2 text-sm\"\n role=\"toolbar\"\n aria-label=\"Bulk actions\"\n >\n <span className=\"text-foreground\">{selected.size} selected</span>\n <button\n type=\"button\"\n className=\"rounded-sm border border-border bg-card px-2 py-1 text-xs text-destructive hover:bg-surface-2\"\n onClick={handleBulkDelete}\n data-testid=\"bulk-delete\"\n >\n Delete\n </button>\n <button\n type=\"button\"\n className=\"rounded-sm border border-border bg-card px-2 py-1 text-xs text-foreground hover:bg-surface-2\"\n onClick={() => setSelected(new Set())}\n >\n Clear\n </button>\n </div>\n )}\n\n <DndContext\n sensors={sensors}\n collisionDetection={closestCenter}\n onDragEnd={handleDragEnd}\n >\n <SortableContext items={sortableIds} strategy={rectSortingStrategy}>\n <div\n className=\"grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-3 rounded-md border border-dashed border-border bg-surface p-3 outline-none focus-visible:outline-2 focus-visible:outline-accent\"\n role=\"grid\"\n aria-label={ariaLabel}\n tabIndex={isDisplay || disabled ? -1 : 0}\n onDragOver={onDragOver}\n onDrop={onDrop}\n onKeyDown={onGridKeyDown}\n data-testid=\"grid-container\"\n >\n {items.map((f, idx) => (\n <SortableTile\n key={f.id}\n file={f}\n selected={selected.has(f.id)}\n disabled={disabled || isDisplay}\n reorderable={reorderable && !isDisplay}\n preferredThumbnail={preferredThumbnail}\n onSelect={() => toggleSelect(f.id)}\n onRemove={() => handleRemove(f.id)}\n onPreview={() => setLightbox(idx)}\n />\n ))}\n {tiles.map((t) => (\n <PendingTile\n key={t.localId}\n tile={t}\n onCancel={() => {\n t.abort();\n cancelledUploadsRef.current.add(t.localId);\n setTiles((prev) =>\n prev.filter((x) => x.localId !== t.localId),\n );\n }}\n onDismiss={() =>\n setTiles((prev) =>\n prev.filter((x) => x.localId !== t.localId),\n )\n }\n />\n ))}\n {items.length === 0 && tiles.length === 0 && (\n <div\n className=\"col-span-full flex h-24 cursor-pointer items-center justify-center text-sm text-muted-foreground\"\n onClick={onPick}\n data-testid=\"grid-empty\"\n >\n {field.display?.placeholder || \"Drop files here or click to browse\"}\n </div>\n )}\n </div>\n </SortableContext>\n </DndContext>\n\n {!isDisplay && (\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept}\n multiple\n onChange={onChangeFile}\n style={{ display: \"none\" }}\n data-testid={`file-input-${fieldName}`}\n />\n )}\n\n {lightboxFile && (\n <Lightbox\n file={lightboxFile}\n hasPrev={lightbox! > 0}\n hasNext={lightbox! < items.length - 1}\n onPrev={lightboxPrev}\n onNext={lightboxNext}\n onClose={lightboxClose}\n />\n )}\n </div>\n );\n}\n\nfunction SortableTile({\n file,\n selected,\n disabled,\n reorderable,\n preferredThumbnail,\n onSelect,\n onRemove,\n onPreview,\n}: {\n file: HydratedFile;\n selected: boolean;\n disabled?: boolean;\n reorderable: boolean;\n preferredThumbnail?: string;\n onSelect: () => void;\n onRemove: () => void;\n onPreview: () => void;\n}) {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: file.id, disabled: !reorderable });\n const style: CSSProperties = {\n transform: CSS.Transform.toString(transform),\n transition,\n opacity: isDragging ? 0.5 : 1,\n };\n const url = thumbUrl(file, preferredThumbnail);\n return (\n <div\n ref={setNodeRef}\n style={style}\n className={`group relative aspect-square overflow-hidden rounded-md border ${\n selected ? \"border-accent\" : \"border-border\"\n } bg-surface-2`}\n data-testid={`grid-tile-${file.id}`}\n data-selected={selected || undefined}\n >\n {/* Lazy thumb: native loading=\"lazy\" approximates IntersectionObserver\n and is what the rest of the SDK uses (see ImageWidget). */}\n {url ? (\n <img\n src={url}\n alt={file.filename}\n className=\"h-full w-full cursor-pointer object-cover\"\n loading=\"lazy\"\n onClick={onPreview}\n />\n ) : (\n <div\n className=\"flex h-full w-full cursor-pointer items-center justify-center bg-surface-3 text-xs text-muted-foreground\"\n onClick={onPreview}\n >\n {file.filename}\n </div>\n )}\n {!disabled && (\n <>\n <input\n type=\"checkbox\"\n className=\"absolute left-1 top-1 h-4 w-4 cursor-pointer\"\n checked={selected}\n onChange={onSelect}\n aria-label={`Select ${file.filename}`}\n data-testid={`select-${file.id}`}\n onClick={(e) => e.stopPropagation()}\n />\n {reorderable && (\n <button\n type=\"button\"\n className=\"absolute right-1 top-1 cursor-grab rounded-sm border border-border bg-card px-1 text-xs\"\n aria-label={`Drag to reorder ${file.filename}`}\n data-testid={`drag-${file.id}`}\n {...attributes}\n {...listeners}\n >\n ⋮⋮\n </button>\n )}\n <div className=\"absolute inset-x-0 bottom-0 flex items-center justify-end gap-1 bg-black/50 p-1 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100\">\n <button\n type=\"button\"\n className=\"rounded-sm border border-border bg-card px-2 py-0.5 text-xs\"\n onClick={onPreview}\n >\n View\n </button>\n <button\n type=\"button\"\n className=\"rounded-sm border border-border bg-card px-2 py-0.5 text-xs text-destructive\"\n onClick={onRemove}\n data-testid={`remove-${file.id}`}\n >\n Delete\n </button>\n </div>\n </>\n )}\n </div>\n );\n}\n\nfunction PendingTile({\n tile,\n onCancel,\n onDismiss,\n}: {\n tile: InFlightTile;\n onCancel: () => void;\n onDismiss: () => void;\n}) {\n return (\n <div\n className={`relative flex aspect-square flex-col items-center justify-center rounded-md border ${\n tile.error ? \"border-destructive\" : \"border-border\"\n } bg-surface-2 p-2 text-center text-xs`}\n data-testid={`pending-tile-${tile.localId}`}\n >\n <span className=\"truncate\" title={tile.filename}>\n {tile.filename}\n </span>\n {tile.progress && !tile.error && (\n <span\n className=\"mt-1 text-muted-foreground\"\n role=\"progressbar\"\n aria-valuenow={tile.progress.pct}\n aria-valuemin={0}\n aria-valuemax={100}\n >\n {tile.progress.pct}%\n </span>\n )}\n {tile.error && (\n <div role=\"alert\" className=\"mt-1 text-destructive\">\n {tile.error.message}\n </div>\n )}\n <button\n type=\"button\"\n className=\"mt-1 rounded-sm border border-border bg-card px-2 py-0.5\"\n onClick={tile.error ? onDismiss : onCancel}\n >\n {tile.error ? \"Dismiss\" : \"Cancel\"}\n </button>\n </div>\n );\n}\n\nfunction Lightbox({\n file,\n hasPrev,\n hasNext,\n onPrev,\n onNext,\n onClose,\n}: {\n file: HydratedFile;\n hasPrev: boolean;\n hasNext: boolean;\n onPrev: () => void;\n onNext: () => void;\n onClose: () => void;\n}) {\n const url = file.presigned_url ?? file.url ?? thumbUrl(file);\n return (\n <div\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/80\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={file.filename}\n data-testid=\"lightbox\"\n onClick={onClose}\n >\n <button\n type=\"button\"\n className=\"absolute right-4 top-4 rounded-sm border border-border bg-card px-3 py-1 text-foreground\"\n onClick={(e) => {\n e.stopPropagation();\n onClose();\n }}\n data-testid=\"lightbox-close\"\n >\n Close\n </button>\n {hasPrev && (\n <button\n type=\"button\"\n className=\"absolute left-4 rounded-sm border border-border bg-card px-3 py-2 text-foreground\"\n onClick={(e) => {\n e.stopPropagation();\n onPrev();\n }}\n data-testid=\"lightbox-prev\"\n >\n ‹\n </button>\n )}\n {hasNext && (\n <button\n type=\"button\"\n className=\"absolute right-4 bottom-4 rounded-sm border border-border bg-card px-3 py-2 text-foreground\"\n onClick={(e) => {\n e.stopPropagation();\n onNext();\n }}\n data-testid=\"lightbox-next\"\n >\n ›\n </button>\n )}\n {url ? (\n <img\n src={url}\n alt={file.filename}\n className=\"max-h-[90vh] max-w-[90vw] object-contain\"\n onClick={(e) => e.stopPropagation()}\n />\n ) : (\n <div className=\"rounded-md bg-card p-4 text-foreground\">\n {file.filename} ({formatBytes(file.size_bytes)})\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -62,4 +62,4 @@ function o({ value: o, onChange: s, readOnly: c }) {
62
62
  //#endregion
63
63
  export { o as JsonEditor };
64
64
 
65
- //# sourceMappingURL=JsonEditor-CWueGEgA.js.map
65
+ //# sourceMappingURL=JsonEditor-Tew1oNqZ.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"JsonEditor-CWueGEgA.js","names":[],"sources":["../src/components/fields/JsonEditor.tsx"],"sourcesContent":["import { useRef, useEffect } from \"react\";\nimport {\n createJSONEditor,\n Mode,\n type Content,\n type OnChangeStatus,\n type JSONEditorPropsOptional,\n} from \"vanilla-jsoneditor\";\n// Dark theme CSS vars for vanilla-jsoneditor (sets --jse-* custom properties)\nimport \"vanilla-jsoneditor/themes/jse-theme-dark.css\";\n\ninterface JsonEditorProps {\n value: unknown;\n onChange?: (value: unknown) => void;\n readOnly?: boolean;\n // Future: JSON Schema for in-editor validation (Ajv). Accept now, wire later.\n schema?: Record<string, unknown>;\n}\n\nfunction toContent(v: unknown): Content {\n if (v == null) return { text: \"\" };\n if (typeof v === \"string\") {\n try {\n return { json: JSON.parse(v) };\n } catch {\n return { text: v };\n }\n }\n return { json: v };\n}\n\nexport function JsonEditor({ value, onChange, readOnly }: JsonEditorProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const editorRef = useRef<ReturnType<typeof createJSONEditor> | null>(null);\n const onChangeRef = useRef(onChange);\n useEffect(() => { onChangeRef.current = onChange; }, [onChange]);\n\n // Create / recreate editor when readOnly changes\n useEffect(() => {\n if (!containerRef.current) return;\n\n const props: JSONEditorPropsOptional = {\n content: toContent(value),\n readOnly: readOnly ?? false,\n mode: readOnly ? Mode.tree : Mode.text,\n mainMenuBar: !readOnly,\n navigationBar: false,\n statusBar: false,\n onChange: (content: Content, _prev: Content, status: OnChangeStatus) => {\n if (readOnly || !onChangeRef.current) return;\n if (status.contentErrors) return;\n if (\"json\" in content) {\n onChangeRef.current(content.json);\n } else if (\"text\" in content) {\n try {\n onChangeRef.current(JSON.parse(content.text));\n } catch {\n onChangeRef.current(content.text);\n }\n }\n },\n };\n\n const syncDarkClass = () => {\n const isDark = document.documentElement.dataset.theme === \"dark\";\n containerRef.current?.classList.toggle(\"jse-theme-dark\", isDark);\n };\n syncDarkClass();\n\n // Watch for theme attribute changes on <html>.\n const observer = new MutationObserver(syncDarkClass);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"data-theme\"],\n });\n\n const editor = createJSONEditor({\n target: containerRef.current,\n props,\n });\n editorRef.current = editor;\n\n return () => {\n observer.disconnect();\n editor.destroy();\n editorRef.current = null;\n };\n }, [readOnly]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Update content when value changes externally (read-only mode only)\n useEffect(() => {\n if (!editorRef.current || !readOnly) return;\n editorRef.current.updateProps({ content: toContent(value) });\n }, [value, readOnly]);\n\n return (\n <div\n ref={containerRef}\n className={readOnly ? \"json-editor-readonly\" : \"json-editor-editable\"}\n />\n );\n}\n"],"mappings":";;;;;AAmBA,SAAS,EAAU,GAAqB;AACtC,KAAI,KAAK,KAAM,QAAO,EAAE,MAAM,IAAI;AAClC,KAAI,OAAO,KAAM,SACf,KAAI;AACF,SAAO,EAAE,MAAM,KAAK,MAAM,EAAE,EAAE;SACxB;AACN,SAAO,EAAE,MAAM,GAAG;;AAGtB,QAAO,EAAE,MAAM,GAAG;;AAGpB,SAAgB,EAAW,EAAE,UAAO,aAAU,eAA6B;CACzE,IAAM,IAAe,EAAuB,KAAK,EAC3C,IAAY,EAAmD,KAAK,EACpE,IAAc,EAAO,EAAS;AA6DpC,QA5DA,QAAgB;AAAE,IAAY,UAAU;IAAa,CAAC,EAAS,CAAC,EAGhE,QAAgB;AACd,MAAI,CAAC,EAAa,QAAS;EAE3B,IAAM,IAAiC;GACrC,SAAS,EAAU,EAAM;GACzB,UAAU,KAAY;GACtB,MAAM,IAAW,EAAK,OAAO,EAAK;GAClC,aAAa,CAAC;GACd,eAAe;GACf,WAAW;GACX,WAAW,GAAkB,GAAgB,MAA2B;AAClE,eAAY,CAAC,EAAY,YACzB,GAAO,eACX;SAAI,UAAU,EACZ,GAAY,QAAQ,EAAQ,KAAK;cACxB,UAAU,EACnB,KAAI;AACF,QAAY,QAAQ,KAAK,MAAM,EAAQ,KAAK,CAAC;aACvC;AACN,QAAY,QAAQ,EAAQ,KAAK;;;;GAIxC,EAEK,UAAsB;GAC1B,IAAM,IAAS,SAAS,gBAAgB,QAAQ,UAAU;AAC1D,KAAa,SAAS,UAAU,OAAO,kBAAkB,EAAO;;AAElE,KAAe;EAGf,IAAM,IAAW,IAAI,iBAAiB,EAAc;AACpD,IAAS,QAAQ,SAAS,iBAAiB;GACzC,YAAY;GACZ,iBAAiB,CAAC,aAAa;GAChC,CAAC;EAEF,IAAM,IAAS,EAAiB;GAC9B,QAAQ,EAAa;GACrB;GACD,CAAC;AAGF,SAFA,EAAU,UAAU,SAEP;AAGX,GAFA,EAAS,YAAY,EACrB,EAAO,SAAS,EAChB,EAAU,UAAU;;IAErB,CAAC,EAAS,CAAC,EAGd,QAAgB;AACV,GAAC,EAAU,WAAW,CAAC,KAC3B,EAAU,QAAQ,YAAY,EAAE,SAAS,EAAU,EAAM,EAAE,CAAC;IAC3D,CAAC,GAAO,EAAS,CAAC,EAGnB,kBAAC,OAAD;EACE,KAAK;EACL,WAAW,IAAW,yBAAyB;EAC/C,CAAA"}
1
+ {"version":3,"file":"JsonEditor-Tew1oNqZ.js","names":[],"sources":["../src/components/fields/JsonEditor.tsx"],"sourcesContent":["import { useRef, useEffect } from \"react\";\nimport {\n createJSONEditor,\n Mode,\n type Content,\n type OnChangeStatus,\n type JSONEditorPropsOptional,\n} from \"vanilla-jsoneditor\";\n// Dark theme CSS vars for vanilla-jsoneditor (sets --jse-* custom properties)\nimport \"vanilla-jsoneditor/themes/jse-theme-dark.css\";\n\ninterface JsonEditorProps {\n value: unknown;\n onChange?: (value: unknown) => void;\n readOnly?: boolean;\n // Future: JSON Schema for in-editor validation (Ajv). Accept now, wire later.\n schema?: Record<string, unknown>;\n}\n\nfunction toContent(v: unknown): Content {\n if (v == null) return { text: \"\" };\n if (typeof v === \"string\") {\n try {\n return { json: JSON.parse(v) };\n } catch {\n return { text: v };\n }\n }\n return { json: v };\n}\n\nexport function JsonEditor({ value, onChange, readOnly }: JsonEditorProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const editorRef = useRef<ReturnType<typeof createJSONEditor> | null>(null);\n const onChangeRef = useRef(onChange);\n useEffect(() => { onChangeRef.current = onChange; }, [onChange]);\n\n // Create / recreate editor when readOnly changes\n useEffect(() => {\n if (!containerRef.current) return;\n\n const props: JSONEditorPropsOptional = {\n content: toContent(value),\n readOnly: readOnly ?? false,\n mode: readOnly ? Mode.tree : Mode.text,\n mainMenuBar: !readOnly,\n navigationBar: false,\n statusBar: false,\n onChange: (content: Content, _prev: Content, status: OnChangeStatus) => {\n if (readOnly || !onChangeRef.current) return;\n if (status.contentErrors) return;\n if (\"json\" in content) {\n onChangeRef.current(content.json);\n } else if (\"text\" in content) {\n try {\n onChangeRef.current(JSON.parse(content.text));\n } catch {\n onChangeRef.current(content.text);\n }\n }\n },\n };\n\n const syncDarkClass = () => {\n const isDark = document.documentElement.dataset.theme === \"dark\";\n containerRef.current?.classList.toggle(\"jse-theme-dark\", isDark);\n };\n syncDarkClass();\n\n // Watch for theme attribute changes on <html>.\n const observer = new MutationObserver(syncDarkClass);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"data-theme\"],\n });\n\n const editor = createJSONEditor({\n target: containerRef.current,\n props,\n });\n editorRef.current = editor;\n\n return () => {\n observer.disconnect();\n editor.destroy();\n editorRef.current = null;\n };\n }, [readOnly]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Update content when value changes externally (read-only mode only)\n useEffect(() => {\n if (!editorRef.current || !readOnly) return;\n editorRef.current.updateProps({ content: toContent(value) });\n }, [value, readOnly]);\n\n return (\n <div\n ref={containerRef}\n className={readOnly ? \"json-editor-readonly\" : \"json-editor-editable\"}\n />\n );\n}\n"],"mappings":";;;;;AAmBA,SAAS,EAAU,GAAqB;AACtC,KAAI,KAAK,KAAM,QAAO,EAAE,MAAM,IAAI;AAClC,KAAI,OAAO,KAAM,SACf,KAAI;AACF,SAAO,EAAE,MAAM,KAAK,MAAM,EAAE,EAAE;SACxB;AACN,SAAO,EAAE,MAAM,GAAG;;AAGtB,QAAO,EAAE,MAAM,GAAG;;AAGpB,SAAgB,EAAW,EAAE,UAAO,aAAU,eAA6B;CACzE,IAAM,IAAe,EAAuB,KAAK,EAC3C,IAAY,EAAmD,KAAK,EACpE,IAAc,EAAO,EAAS;AA6DpC,QA5DA,QAAgB;AAAE,IAAY,UAAU;IAAa,CAAC,EAAS,CAAC,EAGhE,QAAgB;AACd,MAAI,CAAC,EAAa,QAAS;EAE3B,IAAM,IAAiC;GACrC,SAAS,EAAU,EAAM;GACzB,UAAU,KAAY;GACtB,MAAM,IAAW,EAAK,OAAO,EAAK;GAClC,aAAa,CAAC;GACd,eAAe;GACf,WAAW;GACX,WAAW,GAAkB,GAAgB,MAA2B;AAClE,eAAY,CAAC,EAAY,YACzB,GAAO,eACX;SAAI,UAAU,EACZ,GAAY,QAAQ,EAAQ,KAAK;cACxB,UAAU,EACnB,KAAI;AACF,QAAY,QAAQ,KAAK,MAAM,EAAQ,KAAK,CAAC;aACvC;AACN,QAAY,QAAQ,EAAQ,KAAK;;;;GAIxC,EAEK,UAAsB;GAC1B,IAAM,IAAS,SAAS,gBAAgB,QAAQ,UAAU;AAC1D,KAAa,SAAS,UAAU,OAAO,kBAAkB,EAAO;;AAElE,KAAe;EAGf,IAAM,IAAW,IAAI,iBAAiB,EAAc;AACpD,IAAS,QAAQ,SAAS,iBAAiB;GACzC,YAAY;GACZ,iBAAiB,CAAC,aAAa;GAChC,CAAC;EAEF,IAAM,IAAS,EAAiB;GAC9B,QAAQ,EAAa;GACrB;GACD,CAAC;AAGF,SAFA,EAAU,UAAU,SAEP;AAGX,GAFA,EAAS,YAAY,EACrB,EAAO,SAAS,EAChB,EAAU,UAAU;;IAErB,CAAC,EAAS,CAAC,EAGd,QAAgB;AACV,GAAC,EAAU,WAAW,CAAC,KAC3B,EAAU,QAAQ,YAAY,EAAE,SAAS,EAAU,EAAM,EAAE,CAAC;IAC3D,CAAC,GAAO,EAAS,CAAC,EAGnB,kBAAC,OAAD;EACE,KAAK;EACL,WAAW,IAAW,yBAAyB;EAC/C,CAAA"}
@@ -0,0 +1,72 @@
1
+ import { n as e } from "./rich-text-CaQ6-8Gx.js";
2
+ import { jsx as t, jsxs as n } from "react/jsx-runtime";
3
+ import r, { defaultUrlTransform as i } from "react-markdown";
4
+ import a from "remark-breaks";
5
+ import o from "remark-gfm";
6
+ //#region src/components/shared/MarkdownRenderer.tsx
7
+ var s = "prose prose-sm dark:prose-invert max-w-none break-words overflow-hidden", c = [a], l = [o];
8
+ function u(e) {
9
+ return e.startsWith("declarion://file/") ? e : i(e);
10
+ }
11
+ function d(r) {
12
+ return {
13
+ img({ src: i, alt: a }) {
14
+ let o = e(i);
15
+ if (!o) return /* @__PURE__ */ t("img", {
16
+ src: i,
17
+ alt: a ?? ""
18
+ });
19
+ let s = r[o], c = s?.presigned_url ?? s?.url;
20
+ return c ? /* @__PURE__ */ t("img", {
21
+ src: c,
22
+ alt: a ?? s.filename ?? ""
23
+ }) : /* @__PURE__ */ n("span", {
24
+ style: {
25
+ color: "var(--warn)",
26
+ fontSize: 12
27
+ },
28
+ children: ["Missing image ", o]
29
+ });
30
+ },
31
+ a({ href: i, children: a }) {
32
+ let o = e(i);
33
+ if (!o) return /* @__PURE__ */ t("a", {
34
+ href: i,
35
+ children: a
36
+ });
37
+ let s = r[o], c = s?.presigned_url ?? s?.url;
38
+ return c ? /* @__PURE__ */ t("a", {
39
+ href: c,
40
+ target: "_blank",
41
+ rel: "noreferrer",
42
+ children: a
43
+ }) : /* @__PURE__ */ n("span", {
44
+ style: {
45
+ color: "var(--warn)",
46
+ fontSize: 12
47
+ },
48
+ children: ["Missing file ", o]
49
+ });
50
+ }
51
+ };
52
+ }
53
+ function f({ content: e, plugins: n = "breaks", files: i, components: a, skipHtml: o = !0, prose: f = !0 }) {
54
+ let p = /* @__PURE__ */ t(r, {
55
+ skipHtml: o,
56
+ remarkPlugins: n === "gfm" ? l : c,
57
+ components: i ? {
58
+ ...d(i),
59
+ ...a
60
+ } : a,
61
+ urlTransform: i ? u : void 0,
62
+ children: e
63
+ });
64
+ return f ? /* @__PURE__ */ t("div", {
65
+ className: s,
66
+ children: p
67
+ }) : p;
68
+ }
69
+ //#endregion
70
+ export { f as MarkdownRenderer };
71
+
72
+ //# sourceMappingURL=MarkdownRenderer-Dl3GDuY9.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MarkdownRenderer-Dl3GDuY9.js","names":[],"sources":["../src/components/shared/MarkdownRenderer.tsx"],"sourcesContent":["// MarkdownRenderer: the heavy markdown rendering path. This module owns ALL\n// react-markdown / remark imports, so the renderer and the remark/micromark\n// parser (~120 kB) land in their own lazy chunk and never the eager SDK\n// bundle. Reached only through the lazy `Markdown` wrapper (Markdown.tsx) -\n// never import this file directly from feature code.\nimport type { ReactNode } from \"react\";\nimport ReactMarkdown, { defaultUrlTransform, type Components } from \"react-markdown\";\nimport remarkBreaks from \"remark-breaks\";\nimport remarkGfm from \"remark-gfm\";\nimport type { PluggableList } from \"unified\";\nimport type { HydratedFile } from \"@/types/files\";\nimport { extractDeclarionFileId } from \"@/lib/rich-text\";\n\n// Standard prose container. Before this consolidation TextField, RichTextField\n// and the markdown cell factories each repeated this exact class string.\nconst PROSE_CLASS =\n \"prose prose-sm dark:prose-invert max-w-none break-words overflow-hidden\";\n\nconst REMARK_BREAKS: PluggableList = [remarkBreaks];\nconst REMARK_GFM: PluggableList = [remarkGfm];\n\nexport interface MarkdownRendererProps {\n content: string;\n // Plugin set: \"breaks\" (remark-breaks, default) or \"gfm\" (remark-gfm: tables,\n // strikethrough, autolinks). One enum, not a plugin array, so callers never\n // import remark packages and pull them back into the eager bundle.\n plugins?: \"breaks\" | \"gfm\";\n // Inline-file map. When provided, declarion://file/<id> refs in links and\n // images resolve to the file URL; unresolved refs render a warning.\n files?: Record<string, HydratedFile>;\n // Component overrides merged over the defaults (e.g. chat code styling).\n components?: Components;\n // Strip raw HTML. Default true; pass false only with security review.\n skipHtml?: boolean;\n // Wrap output in the standard prose container. Default true.\n prose?: boolean;\n}\n\n// declarion://file/<id> refs must survive urlTransform so the file-aware\n// img/a components can resolve them; everything else goes through\n// react-markdown's defaultUrlTransform (which strips javascript: URIs).\nfunction fileUrlTransform(url: string): string {\n if (url.startsWith(\"declarion://file/\")) return url;\n return defaultUrlTransform(url);\n}\n\nfunction fileAwareComponents(files: Record<string, HydratedFile>): Components {\n return {\n img({ src, alt }: { src?: string; alt?: string }) {\n const fileId = extractDeclarionFileId(src);\n if (!fileId) return <img src={src} alt={alt ?? \"\"} />;\n const file = files[fileId];\n const url = file?.presigned_url ?? file?.url;\n if (!url) {\n return <span style={{ color: \"var(--warn)\", fontSize: 12 }}>Missing image {fileId}</span>;\n }\n return <img src={url} alt={alt ?? file.filename ?? \"\"} />;\n },\n a({ href, children }: { href?: string; children?: ReactNode }) {\n const fileId = extractDeclarionFileId(href);\n if (!fileId) return <a href={href}>{children}</a>;\n const file = files[fileId];\n const url = file?.presigned_url ?? file?.url;\n if (!url) {\n return <span style={{ color: \"var(--warn)\", fontSize: 12 }}>Missing file {fileId}</span>;\n }\n return (\n <a href={url} target=\"_blank\" rel=\"noreferrer\">\n {children}\n </a>\n );\n },\n };\n}\n\nexport function MarkdownRenderer({\n content,\n plugins = \"breaks\",\n files,\n components,\n skipHtml = true,\n prose = true,\n}: MarkdownRendererProps) {\n const remarkPlugins = plugins === \"gfm\" ? REMARK_GFM : REMARK_BREAKS;\n const merged: Components | undefined = files\n ? { ...fileAwareComponents(files), ...components }\n : components;\n const rendered = (\n <ReactMarkdown\n skipHtml={skipHtml}\n remarkPlugins={remarkPlugins}\n components={merged}\n urlTransform={files ? fileUrlTransform : undefined}\n >\n {content}\n </ReactMarkdown>\n );\n return prose ? <div className={PROSE_CLASS}>{rendered}</div> : rendered;\n}\n"],"mappings":";;;;;;AAeA,IAAM,IACJ,2EAEI,IAA+B,CAAC,EAAa,EAC7C,IAA4B,CAAC,EAAU;AAsB7C,SAAS,EAAiB,GAAqB;AAE7C,QADI,EAAI,WAAW,oBAAoB,GAAS,IACzC,EAAoB,EAAI;;AAGjC,SAAS,EAAoB,GAAiD;AAC5E,QAAO;EACL,IAAI,EAAE,QAAK,UAAuC;GAChD,IAAM,IAAS,EAAuB,EAAI;AAC1C,OAAI,CAAC,EAAQ,QAAO,kBAAC,OAAD;IAAU;IAAK,KAAK,KAAO;IAAM,CAAA;GACrD,IAAM,IAAO,EAAM,IACb,IAAM,GAAM,iBAAiB,GAAM;AAIzC,UAHK,IAGE,kBAAC,OAAD;IAAK,KAAK;IAAK,KAAK,KAAO,EAAK,YAAY;IAAM,CAAA,GAFhD,kBAAC,QAAD;IAAM,OAAO;KAAE,OAAO;KAAe,UAAU;KAAI;cAAnD,CAAqD,kBAAe,EAAc;;;EAI7F,EAAE,EAAE,SAAM,eAAqD;GAC7D,IAAM,IAAS,EAAuB,EAAK;AAC3C,OAAI,CAAC,EAAQ,QAAO,kBAAC,KAAD;IAAS;IAAO;IAAa,CAAA;GACjD,IAAM,IAAO,EAAM,IACb,IAAM,GAAM,iBAAiB,GAAM;AAIzC,UAHK,IAIH,kBAAC,KAAD;IAAG,MAAM;IAAK,QAAO;IAAS,KAAI;IAC/B;IACC,CAAA,GALG,kBAAC,QAAD;IAAM,OAAO;KAAE,OAAO;KAAe,UAAU;KAAI;cAAnD,CAAqD,iBAAc,EAAc;;;EAQ7F;;AAGH,SAAgB,EAAiB,EAC/B,YACA,aAAU,UACV,UACA,eACA,cAAW,IACX,WAAQ,MACgB;CAKxB,IAAM,IACJ,kBAAC,GAAD;EACY;EACK,eAPG,MAAY,QAAQ,IAAa;EAQnD,YAPmC,IACnC;GAAE,GAAG,EAAoB,EAAM;GAAE,GAAG;GAAY,GAChD;EAMA,cAAc,IAAQ,IAAmB,KAAA;YAExC;EACa,CAAA;AAElB,QAAO,IAAQ,kBAAC,OAAD;EAAK,WAAW;YAAc;EAAe,CAAA,GAAG"}
@@ -7,7 +7,6 @@ export { FieldsBlock } from "./FieldsBlock";
7
7
  export { ChildrenBlock, type ChildDisplayProps } from "./ChildrenBlock";
8
8
  export { ChildrenTable } from "./ChildrenTable";
9
9
  export { EditableChildrenTable } from "./EditableChildrenTable";
10
- export { DraggableChildrenTable } from "./DraggableChildrenTable";
11
10
  export { ChildrenList } from "./ChildrenList";
12
11
  export { ChildrenCards } from "./ChildrenCards";
13
12
  export { ChildrenTimeline } from "./ChildrenTimeline";
@@ -1,16 +1,9 @@
1
+ export interface MarkdownPreview {
2
+ text: string;
3
+ isPlain: boolean;
4
+ }
1
5
  /**
2
- * Convert markdown source to a single-line plain-text preview. Whitespace is
3
- * collapsed; block boundaries (paragraphs, list items, table cells) become
4
- * single spaces. No length cap — server-side `list_preview` already bounds
5
- * the wire payload, and the calling cell's CSS ellipsis bounds the visual.
6
+ * Convert markdown source to a single-line plain-text preview plus an
7
+ * `isPlain` signal. One synchronous pass, no parser.
6
8
  */
7
- export declare function markdownToPlainText(src: string): string;
8
- /**
9
- * Returns true when the parsed AST contains ONLY plain types
10
- * (root / paragraph / text / break). In that case the popover would show a
11
- * format that is visually equivalent to the in-cell span, so callers should
12
- * skip the HoverCard and use a native title tooltip alone — no popover
13
- * noise on prose that just happens to contain parens, asterisks-as-stars,
14
- * or other characters that look markdown-ish but parse as text.
15
- */
16
- export declare function looksLikePlainText(src: string): boolean;
9
+ export declare function markdownPreview(src: string): MarkdownPreview;
@@ -0,0 +1,2 @@
1
+ import type { FileWidgetProps } from "./types";
2
+ export declare function GridLazy(props: FileWidgetProps): import("react/jsx-runtime").JSX.Element;
@@ -1,7 +1,7 @@
1
1
  export { SquareImage } from "./SquareImage";
2
2
  export { CircleImage } from "./CircleImage";
3
3
  export { WideBanner } from "./WideBanner";
4
- export { Grid } from "./Grid";
4
+ export { GridLazy as Grid } from "./GridLazy";
5
5
  export { DocumentCard } from "./DocumentCard";
6
6
  export { FileList } from "./FileList";
7
7
  export { LabeledSlots } from "./LabeledSlots";
@@ -0,0 +1,3 @@
1
+ import type { MarkdownRendererProps } from "./MarkdownRenderer";
2
+ export declare function Markdown(props: MarkdownRendererProps): import("react/jsx-runtime").JSX.Element;
3
+ export type { MarkdownRendererProps } from "./MarkdownRenderer";
@@ -0,0 +1,11 @@
1
+ import { type Components } from "react-markdown";
2
+ import type { HydratedFile } from "../../types/files";
3
+ export interface MarkdownRendererProps {
4
+ content: string;
5
+ plugins?: "breaks" | "gfm";
6
+ files?: Record<string, HydratedFile>;
7
+ components?: Components;
8
+ skipHtml?: boolean;
9
+ prose?: boolean;
10
+ }
11
+ export declare function MarkdownRenderer({ content, plugins, files, components, skipHtml, prose, }: MarkdownRendererProps): import("react/jsx-runtime").JSX.Element;