@thebes/cadmea 1.0.0 → 1.1.1

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.
@@ -1,5 +1,6 @@
1
1
  import { JSX } from "solid-js";
2
- import { CollectionConfig } from "@thebes/cadmus/cms";
2
+ import { CollectionConfig, EditRef } from "@thebes/cadmus/cms";
3
+ import { ImageCrop, ImageHotspot } from "@thebes/cadmus/storage";
3
4
 
4
5
  //#region src/capabilities.d.ts
5
6
  /**
@@ -16,7 +17,45 @@ interface CollectionCapabilities {
16
17
  canDelete?: boolean;
17
18
  }
18
19
  //#endregion
20
+ //#region src/ImageHotspotField.d.ts
21
+ /**
22
+ * Image hotspot/crop editor widget (issue #17). A custom field widget for
23
+ * `upload` image fields: upload an image, then click it to set the focal
24
+ * point (hotspot) and optionally enter a crop region. Stores the value as a
25
+ * JSON string `{ url, hotspot?, crop? }` in the same column (back-compatible
26
+ * — a plain URL string still parses). Pair with `ImageService.render`'s
27
+ * `hotspot`/`crop` args on the read side (see @thebes/cadmus-cloudflare-images).
28
+ *
29
+ * Register it via `createCollectionEditPage`/`CollectionEdit`'s `fieldWidgets`
30
+ * option, keyed by the field name.
31
+ */
32
+ interface ImageWithHotspot {
33
+ url: string;
34
+ hotspot?: ImageHotspot;
35
+ crop?: ImageCrop;
36
+ }
37
+ /**
38
+ * Parse an upload-field value into `{ url, hotspot?, crop? }`. Accepts the
39
+ * JSON object this widget writes, an already-parsed object, or a bare URL
40
+ * string (legacy / non-hotspot uploads). Returns null for empty values.
41
+ */
42
+ declare function parseImageHotspotValue(value: unknown): ImageWithHotspot | null;
43
+ /** Serialize an {@link ImageWithHotspot} for storage in an upload field. */
44
+ declare function serializeImageHotspotValue(value: ImageWithHotspot): string;
45
+ /** Props every `fieldWidgets` widget receives from CollectionEdit. */
46
+ interface FieldWidgetProps {
47
+ fieldKey: string;
48
+ value: unknown;
49
+ setValue: (value: unknown) => void;
50
+ onUploadFile?: (file: File) => Promise<{
51
+ url: string;
52
+ }>;
53
+ }
54
+ declare function ImageHotspotField(props: FieldWidgetProps): import("solid-js").JSX.Element;
55
+ //#endregion
19
56
  //#region src/CollectionEdit.d.ts
57
+ /** A custom per-field editor, registered via `fieldWidgets`. */
58
+ type FieldWidget = (props: FieldWidgetProps) => JSX.Element;
20
59
  interface RelationshipOption {
21
60
  id: number;
22
61
  label: string;
@@ -81,6 +120,13 @@ interface CollectionEditProps {
81
120
  * consuming route) since `CollectionEdit` has no router access itself.
82
121
  */
83
122
  onDirtyChange?: (dirty: boolean) => void;
123
+ /**
124
+ * Per-field custom editor widgets (issue #17), keyed by field name. When a
125
+ * field has a widget here, it's rendered instead of the generic input for
126
+ * that field's type — e.g. `{ heroImage: ImageHotspotField }`. The widget
127
+ * receives the field value, a setter, and `onUploadFile`.
128
+ */
129
+ fieldWidgets?: Record<string, FieldWidget>;
84
130
  /** Only rendered when `config.versions?.drafts` is also true. */
85
131
  draftActions?: DraftActions;
86
132
  /**
@@ -91,7 +137,7 @@ interface CollectionEditProps {
91
137
  */
92
138
  capabilities?: CollectionCapabilities;
93
139
  }
94
- declare function CollectionEdit(props: CollectionEditProps): import("solid-js").JSX.Element;
140
+ declare function CollectionEdit(props: CollectionEditProps): JSX.Element;
95
141
  //#endregion
96
142
  //#region src/CollectionList.d.ts
97
143
  interface CollectionListProps {
@@ -143,5 +189,33 @@ interface SearchPaletteProps {
143
189
  */
144
190
  declare function SearchPalette(props: SearchPaletteProps): JSX.Element;
145
191
  //#endregion
146
- export { type CollectionCapabilities, CollectionEdit, type CollectionEditProps, CollectionList, type CollectionListProps, SearchPalette, type SearchPaletteProps, type SearchPaletteResult };
192
+ //#region src/VisualEditingPane.d.ts
193
+ /**
194
+ * Visual-editing preview pane (issue #15, studio side). Embeds the site's
195
+ * preview in an iframe and listens for the click-to-edit `postMessage` that
196
+ * `@thebes/cadmus/cms`'s `mountVisualEditing` posts from inside the preview.
197
+ * On a click, it calls `onEdit(ref)` so the studio can navigate to that
198
+ * field's editor (e.g. `/admin/<collection>/<id>`).
199
+ *
200
+ * The preview page must (a) tag editable regions with `editAttr(...)` and
201
+ * (b) call `mountVisualEditing()` client-side. This component is the parent
202
+ * half of that handshake.
203
+ */
204
+ interface VisualEditingPaneProps {
205
+ /** URL of the preview route to embed. */
206
+ src: string;
207
+ /** Called when an editable region in the preview is clicked. */
208
+ onEdit?: (ref: EditRef) => void;
209
+ /**
210
+ * Origin the preview is served from — messages from any other origin are
211
+ * ignored (postMessage security). Defaults to `src`'s origin.
212
+ */
213
+ allowedOrigin?: string;
214
+ /** Class for the iframe (size it via the consumer's layout). */
215
+ class?: string;
216
+ title?: string;
217
+ }
218
+ declare function VisualEditingPane(props: VisualEditingPaneProps): import("solid-js").JSX.Element;
219
+ //#endregion
220
+ export { type CollectionCapabilities, CollectionEdit, type CollectionEditProps, CollectionList, type CollectionListProps, type FieldWidget, type FieldWidgetProps, ImageHotspotField, type ImageWithHotspot, SearchPalette, type SearchPaletteProps, type SearchPaletteResult, VisualEditingPane, type VisualEditingPaneProps, parseImageHotspotValue, serializeImageHotspotValue };
147
221
  //# sourceMappingURL=index.d.ts.map
@@ -1,7 +1,8 @@
1
- import { className, createComponent, delegateEvents, effect, insert, memo, setAttribute, template, use } from "solid-js/web";
2
- import { For, Show, Suspense, createEffect, createSignal, lazy, onCleanup } from "solid-js";
1
+ import { className, createComponent, delegateEvents, effect, insert, memo, setAttribute, setStyleProperty, template, use } from "solid-js/web";
2
+ import { For, Show, Suspense, createEffect, createMemo, createSignal, lazy, onCleanup, onMount } from "solid-js";
3
+ import { VISUAL_EDIT_MESSAGE } from "@thebes/cadmus/cms";
3
4
  //#region src/CollectionEdit.tsx
4
- var _tmpl$$2 = /*#__PURE__*/ template(`<p class="text-sm text-error"role=alert>`), _tmpl$2$2 = /*#__PURE__*/ template(`<span class="loading loading-spinner loading-sm">`), _tmpl$3$2 = /*#__PURE__*/ template(`<button type=button class="btn flex-1">`), _tmpl$4$2 = /*#__PURE__*/ template(`<button type=button class="btn btn-primary flex-1">`), _tmpl$5$1 = /*#__PURE__*/ template(`<button type=button class="btn btn-outline flex-1">`), _tmpl$6$1 = /*#__PURE__*/ template(`<form class="flex flex-col gap-4"><div class="bg-base-100 sticky bottom-0 flex gap-2 border-t py-3">`), _tmpl$7$1 = /*#__PURE__*/ template(`<span class=text-error> *`), _tmpl$8$1 = /*#__PURE__*/ template(`<div class=form-control><label class=label>`), _tmpl$9$1 = /*#__PURE__*/ template(`<button type=submit class="btn btn-primary flex-1">`), _tmpl$0$1 = /*#__PURE__*/ template(`<input class=input type=text>`), _tmpl$1$1 = /*#__PURE__*/ template(`<select class=select>`), _tmpl$10$1 = /*#__PURE__*/ template(`<option>`), _tmpl$11$1 = /*#__PURE__*/ template(`<input class=input type=number>`), _tmpl$12$1 = /*#__PURE__*/ template(`<input class=input type=text readonly>`), _tmpl$13$1 = /*#__PURE__*/ template(`<input class=checkbox type=checkbox>`), _tmpl$14 = /*#__PURE__*/ template(`<p class="text-sm opacity-70 break-all">`), _tmpl$15 = /*#__PURE__*/ template(`<p class="text-sm text-error">`), _tmpl$16 = /*#__PURE__*/ template(`<div class="flex flex-col gap-2"><input class=file-input type=file>`), _tmpl$17 = /*#__PURE__*/ template(`<select class=select><option value>—`), _tmpl$18 = /*#__PURE__*/ template(`<div class="flex flex-col gap-3"><button type=button class="btn btn-outline btn-sm self-start">Add `), _tmpl$19 = /*#__PURE__*/ template(`<div class="card bg-base-200 flex flex-col gap-2 p-3"><button type=button class="btn btn-error btn-outline btn-sm self-start">Remove`);
5
+ var _tmpl$$4 = /*#__PURE__*/ template(`<p class="text-sm text-error"role=alert>`), _tmpl$2$3 = /*#__PURE__*/ template(`<span class="loading loading-spinner loading-sm">`), _tmpl$3$3 = /*#__PURE__*/ template(`<button type=button class="btn flex-1">`), _tmpl$4$3 = /*#__PURE__*/ template(`<button type=button class="btn btn-primary flex-1">`), _tmpl$5$2 = /*#__PURE__*/ template(`<button type=button class="btn btn-outline flex-1">`), _tmpl$6$2 = /*#__PURE__*/ template(`<form class="flex flex-col gap-4"><div class="bg-base-100 sticky bottom-0 flex gap-2 border-t py-3">`), _tmpl$7$1 = /*#__PURE__*/ template(`<span class=text-error> *`), _tmpl$8$1 = /*#__PURE__*/ template(`<div class=form-control><label class=label>`), _tmpl$9$1 = /*#__PURE__*/ template(`<button type=submit class="btn btn-primary flex-1">`), _tmpl$0$1 = /*#__PURE__*/ template(`<input class=input type=text>`), _tmpl$1$1 = /*#__PURE__*/ template(`<select class=select>`), _tmpl$10$1 = /*#__PURE__*/ template(`<option>`), _tmpl$11$1 = /*#__PURE__*/ template(`<input class=input type=number>`), _tmpl$12$1 = /*#__PURE__*/ template(`<input class=input type=text readonly>`), _tmpl$13$1 = /*#__PURE__*/ template(`<input class=checkbox type=checkbox>`), _tmpl$14 = /*#__PURE__*/ template(`<p class="text-sm opacity-70 break-all">`), _tmpl$15 = /*#__PURE__*/ template(`<p class="text-sm text-error">`), _tmpl$16 = /*#__PURE__*/ template(`<div class="flex flex-col gap-2"><input class=file-input type=file>`), _tmpl$17 = /*#__PURE__*/ template(`<select class=select><option value>—`), _tmpl$18 = /*#__PURE__*/ template(`<div class="flex flex-col gap-3"><button type=button class="btn btn-outline btn-sm self-start">Add `), _tmpl$19 = /*#__PURE__*/ template(`<div class="card bg-base-200 flex flex-col gap-2 p-3"><button type=button class="btn btn-error btn-outline btn-sm self-start">Remove`);
5
6
  const RichTextEditor = lazy(() => import("../RichTextEditor-BPilh7Pw.js").then((mod) => ({ default: mod.RichTextEditor })));
6
7
  function editableFields(config) {
7
8
  return Object.entries(config.fields).filter(([key]) => key !== "id");
@@ -27,18 +28,19 @@ function CollectionEdit(props) {
27
28
  }
28
29
  const ctx = {
29
30
  onUploadFile: props.onUploadFile,
30
- relationshipOptions: props.relationshipOptions
31
+ relationshipOptions: props.relationshipOptions,
32
+ fieldWidgets: props.fieldWidgets
31
33
  };
32
34
  const versioned = () => props.config.versions?.drafts && props.draftActions;
33
35
  return (() => {
34
- var _el$ = _tmpl$6$1(), _el$3 = _el$.firstChild;
36
+ var _el$ = _tmpl$6$2(), _el$3 = _el$.firstChild;
35
37
  _el$.addEventListener("submit", handleSubmit);
36
38
  insert(_el$, createComponent(Show, {
37
39
  get when() {
38
40
  return props.error;
39
41
  },
40
42
  get children() {
41
- var _el$2 = _tmpl$$2();
43
+ var _el$2 = _tmpl$$4();
42
44
  insert(_el$2, () => props.error);
43
45
  return _el$2;
44
46
  }
@@ -82,7 +84,7 @@ function CollectionEdit(props) {
82
84
  return props.submitLabel ?? "Save";
83
85
  },
84
86
  get children() {
85
- return _tmpl$2$2();
87
+ return _tmpl$2$3();
86
88
  }
87
89
  }));
88
90
  effect(() => _el$11.disabled = props.saving);
@@ -93,7 +95,7 @@ function CollectionEdit(props) {
93
95
  get children() {
94
96
  return [
95
97
  (() => {
96
- var _el$4 = _tmpl$3$2();
98
+ var _el$4 = _tmpl$3$3();
97
99
  _el$4.$$click = () => void props.draftActions?.onSaveDraft(editablePayload());
98
100
  insert(_el$4, createComponent(Show, {
99
101
  get when() {
@@ -103,14 +105,14 @@ function CollectionEdit(props) {
103
105
  return props.draftActions?.saveDraftLabel ?? "Save draft";
104
106
  },
105
107
  get children() {
106
- return _tmpl$2$2();
108
+ return _tmpl$2$3();
107
109
  }
108
110
  }));
109
111
  effect(() => _el$4.disabled = props.draftActions?.saving);
110
112
  return _el$4;
111
113
  })(),
112
114
  (() => {
113
- var _el$6 = _tmpl$4$2();
115
+ var _el$6 = _tmpl$4$3();
114
116
  _el$6.$$click = () => void props.draftActions?.onPublish?.();
115
117
  insert(_el$6, createComponent(Show, {
116
118
  get when() {
@@ -120,7 +122,7 @@ function CollectionEdit(props) {
120
122
  return props.draftActions?.publishLabel ?? "Publish";
121
123
  },
122
124
  get children() {
123
- return _tmpl$2$2();
125
+ return _tmpl$2$3();
124
126
  }
125
127
  }));
126
128
  effect(() => _el$6.disabled = !props.draftActions?.canPublish || props.draftActions?.publishing);
@@ -131,7 +133,7 @@ function CollectionEdit(props) {
131
133
  return props.draftActions?.onPreview;
132
134
  },
133
135
  get children() {
134
- var _el$8 = _tmpl$5$1();
136
+ var _el$8 = _tmpl$5$2();
135
137
  _el$8.$$click = () => void props.draftActions?.onPreview?.();
136
138
  insert(_el$8, createComponent(Show, {
137
139
  get when() {
@@ -141,7 +143,7 @@ function CollectionEdit(props) {
141
143
  return props.draftActions?.previewLabel ?? "Preview";
142
144
  },
143
145
  get children() {
144
- return _tmpl$2$2();
146
+ return _tmpl$2$3();
145
147
  }
146
148
  }));
147
149
  effect(() => _el$8.disabled = !props.draftActions?.canPreview || props.draftActions?.previewing);
@@ -155,6 +157,15 @@ function CollectionEdit(props) {
155
157
  })();
156
158
  }
157
159
  function renderInput(key, field, value, setField, ctx) {
160
+ const Widget = ctx.fieldWidgets?.[key] ?? ctx.fieldWidgets?.[key.slice(key.lastIndexOf(".") + 1)];
161
+ if (Widget) return createComponent(Widget, {
162
+ fieldKey: key,
163
+ value,
164
+ setValue: (v) => setField(key, v),
165
+ get onUploadFile() {
166
+ return ctx.onUploadFile;
167
+ }
168
+ });
158
169
  switch (field.type) {
159
170
  case "text": return (() => {
160
171
  var _el$13 = _tmpl$0$1();
@@ -209,7 +220,7 @@ function renderInput(key, field, value, setField, ctx) {
209
220
  case "array": return renderArrayInput(key, field, value, setField, ctx);
210
221
  case "richText": return createComponent(Suspense, {
211
222
  get fallback() {
212
- return _tmpl$2$2();
223
+ return _tmpl$2$3();
213
224
  },
214
225
  get children() {
215
226
  return createComponent(RichTextEditor, {
@@ -260,7 +271,7 @@ function renderUploadInput(key, field, value, setField, ctx) {
260
271
  return uploading();
261
272
  },
262
273
  get children() {
263
- return _tmpl$2$2();
274
+ return _tmpl$2$3();
264
275
  }
265
276
  }), null);
266
277
  insert(_el$20, createComponent(Show, {
@@ -380,7 +391,7 @@ function formatDateValue(value) {
380
391
  delegateEvents(["click", "input"]);
381
392
  //#endregion
382
393
  //#region src/CollectionList.tsx
383
- var _tmpl$$1 = /*#__PURE__*/ template(`<button type=button class="btn btn-outline btn-sm">`), _tmpl$2$1 = /*#__PURE__*/ template(`<div class=join><select aria-label="Sort by"class="select select-sm join-item"></select><select aria-label="Sort direction"class="select select-sm join-item"><option value=asc>Ascending</option><option value=desc>Descending`), _tmpl$3$1 = /*#__PURE__*/ template(`<th>`), _tmpl$4$1 = /*#__PURE__*/ template(`<table class="table hidden md:table"><thead><tr></tr></thead><tbody>`), _tmpl$5 = /*#__PURE__*/ template(`<div class="flex flex-col gap-2 md:hidden">`), _tmpl$6 = /*#__PURE__*/ template(`<div class="bg-base-100 sticky bottom-0 flex items-center justify-between gap-2 border-t py-2"><button type=button class="btn btn-sm">Prev</button><span class="text-sm opacity-70">Page </span><button type=button class="btn btn-sm">Next`), _tmpl$7 = /*#__PURE__*/ template(`<div class="flex flex-col gap-3"><div class="flex flex-wrap items-center justify-between gap-2">`), _tmpl$8 = /*#__PURE__*/ template(`<option>`), _tmpl$9 = /*#__PURE__*/ template(`<p class="text-sm opacity-70">No <!> yet.`), _tmpl$0 = /*#__PURE__*/ template(`<td><input type=checkbox class="checkbox checkbox-sm">`), _tmpl$1 = /*#__PURE__*/ template(`<tr>`), _tmpl$10 = /*#__PURE__*/ template(`<td>`), _tmpl$11 = /*#__PURE__*/ template(`<input type=checkbox class="checkbox checkbox-sm mt-1">`), _tmpl$12 = /*#__PURE__*/ template(`<div class="card bg-base-200 cursor-pointer p-3"role=button tabindex=0><div class="flex items-start gap-3"><div class="flex flex-1 flex-col gap-1">`), _tmpl$13 = /*#__PURE__*/ template(`<div class="flex justify-between gap-2 text-sm"><span class=opacity-60></span><span class=text-right>`);
394
+ var _tmpl$$3 = /*#__PURE__*/ template(`<button type=button class="btn btn-outline btn-sm">`), _tmpl$2$2 = /*#__PURE__*/ template(`<div class=join><select aria-label="Sort by"class="select select-sm join-item"></select><select aria-label="Sort direction"class="select select-sm join-item"><option value=asc>Ascending</option><option value=desc>Descending`), _tmpl$3$2 = /*#__PURE__*/ template(`<th>`), _tmpl$4$2 = /*#__PURE__*/ template(`<table class="table hidden md:table"><thead><tr></tr></thead><tbody>`), _tmpl$5$1 = /*#__PURE__*/ template(`<div class="flex flex-col gap-2 md:hidden">`), _tmpl$6$1 = /*#__PURE__*/ template(`<div class="bg-base-100 sticky bottom-0 flex items-center justify-between gap-2 border-t py-2"><button type=button class="btn btn-sm">Prev</button><span class="text-sm opacity-70">Page </span><button type=button class="btn btn-sm">Next`), _tmpl$7 = /*#__PURE__*/ template(`<div class="flex flex-col gap-3"><div class="flex flex-wrap items-center justify-between gap-2">`), _tmpl$8 = /*#__PURE__*/ template(`<option>`), _tmpl$9 = /*#__PURE__*/ template(`<p class="text-sm opacity-70">No <!> yet.`), _tmpl$0 = /*#__PURE__*/ template(`<td><input type=checkbox class="checkbox checkbox-sm">`), _tmpl$1 = /*#__PURE__*/ template(`<tr>`), _tmpl$10 = /*#__PURE__*/ template(`<td>`), _tmpl$11 = /*#__PURE__*/ template(`<input type=checkbox class="checkbox checkbox-sm mt-1">`), _tmpl$12 = /*#__PURE__*/ template(`<div class="card bg-base-200 cursor-pointer p-3"role=button tabindex=0><div class="flex items-start gap-3"><div class="flex flex-1 flex-col gap-1">`), _tmpl$13 = /*#__PURE__*/ template(`<div class="flex justify-between gap-2 text-sm"><span class=opacity-60></span><span class=text-right>`);
384
395
  function listableFields(config) {
385
396
  const excluded = new Set([
386
397
  "richText",
@@ -421,7 +432,7 @@ function CollectionList(props) {
421
432
  return props.selectable;
422
433
  },
423
434
  get children() {
424
- var _el$3 = _tmpl$$1();
435
+ var _el$3 = _tmpl$$3();
425
436
  _el$3.$$click = () => setSelectMode((v) => !v);
426
437
  insert(_el$3, () => selectMode() ? "Done" : "Select");
427
438
  return _el$3;
@@ -432,7 +443,7 @@ function CollectionList(props) {
432
443
  return props.onSortChange;
433
444
  },
434
445
  get children() {
435
- var _el$4 = _tmpl$2$1(), _el$5 = _el$4.firstChild, _el$6 = _el$5.nextSibling;
446
+ var _el$4 = _tmpl$2$2(), _el$5 = _el$4.firstChild, _el$6 = _el$5.nextSibling;
436
447
  _el$5.addEventListener("change", (e) => props.onSortChange?.(e.currentTarget.value, props.sortDirection ?? "asc"));
437
448
  insert(_el$5, createComponent(For, {
438
449
  get each() {
@@ -465,13 +476,13 @@ function CollectionList(props) {
465
476
  },
466
477
  get children() {
467
478
  return [(() => {
468
- var _el$7 = _tmpl$4$1(), _el$8 = _el$7.firstChild, _el$9 = _el$8.firstChild, _el$1 = _el$8.nextSibling;
479
+ var _el$7 = _tmpl$4$2(), _el$8 = _el$7.firstChild, _el$9 = _el$8.firstChild, _el$1 = _el$8.nextSibling;
469
480
  insert(_el$9, createComponent(Show, {
470
481
  get when() {
471
482
  return selectMode();
472
483
  },
473
484
  get children() {
474
- return _tmpl$3$1();
485
+ return _tmpl$3$2();
475
486
  }
476
487
  }), null);
477
488
  insert(_el$9, createComponent(For, {
@@ -479,7 +490,7 @@ function CollectionList(props) {
479
490
  return columns();
480
491
  },
481
492
  children: ([key]) => (() => {
482
- var _el$21 = _tmpl$3$1();
493
+ var _el$21 = _tmpl$3$2();
483
494
  insert(_el$21, key);
484
495
  return _el$21;
485
496
  })()
@@ -522,7 +533,7 @@ function CollectionList(props) {
522
533
  }));
523
534
  return _el$7;
524
535
  })(), (() => {
525
- var _el$10 = _tmpl$5();
536
+ var _el$10 = _tmpl$5$1();
526
537
  insert(_el$10, createComponent(For, {
527
538
  get each() {
528
539
  return props.rows;
@@ -574,7 +585,7 @@ function CollectionList(props) {
574
585
  return memo(() => props.page !== void 0)() && props.pageSize !== void 0;
575
586
  },
576
587
  get children() {
577
- var _el$11 = _tmpl$6(), _el$12 = _el$11.firstChild, _el$13 = _el$12.nextSibling;
588
+ var _el$11 = _tmpl$6$1(), _el$12 = _el$11.firstChild, _el$13 = _el$12.nextSibling;
578
589
  _el$13.firstChild;
579
590
  var _el$15 = _el$13.nextSibling;
580
591
  _el$12.$$click = () => props.onPageChange?.((props.page ?? 1) - 1);
@@ -597,8 +608,160 @@ function CollectionList(props) {
597
608
  }
598
609
  delegateEvents(["click", "keydown"]);
599
610
  //#endregion
611
+ //#region src/ImageHotspotField.tsx
612
+ var _tmpl$$2 = /*#__PURE__*/ template(`<span class="loading loading-spinner loading-sm">`), _tmpl$2$1 = /*#__PURE__*/ template(`<p class="text-sm text-error">`), _tmpl$3$1 = /*#__PURE__*/ template(`<div class="flex flex-col gap-3"><input class=file-input type=file accept=image/*>`), _tmpl$4$1 = /*#__PURE__*/ template(`<div class="flex flex-col gap-2"><p class="text-xs opacity-60">Click the image to set the focal point.</p><div class="relative inline-block max-w-md"><img alt="Set focal point"class="block w-full cursor-crosshair rounded"></div><details class=text-sm><summary class="cursor-pointer opacity-70">Crop (advanced)</summary><div class="mt-2 grid grid-cols-4 gap-2"></div></details><p class="break-all text-xs opacity-50">`), _tmpl$5 = /*#__PURE__*/ template(`<span class="pointer-events-none absolute h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-[var(--accent,#56c6be)] shadow">`), _tmpl$6 = /*#__PURE__*/ template(`<label class="flex flex-col gap-1 text-xs"><span class="capitalize opacity-70"></span><input class="input input-sm"type=number min=0 max=1 step=0.05>`);
613
+ const round2 = (n) => Math.round(n * 100) / 100;
614
+ const clamp01 = (n) => Math.min(1, Math.max(0, n));
615
+ /**
616
+ * Parse an upload-field value into `{ url, hotspot?, crop? }`. Accepts the
617
+ * JSON object this widget writes, an already-parsed object, or a bare URL
618
+ * string (legacy / non-hotspot uploads). Returns null for empty values.
619
+ */
620
+ function parseImageHotspotValue(value) {
621
+ if (!value) return null;
622
+ if (typeof value === "object") return value;
623
+ if (typeof value === "string") {
624
+ const trimmed = value.trim();
625
+ if (trimmed.startsWith("{")) try {
626
+ return JSON.parse(trimmed);
627
+ } catch {}
628
+ return trimmed ? { url: trimmed } : null;
629
+ }
630
+ return null;
631
+ }
632
+ /** Serialize an {@link ImageWithHotspot} for storage in an upload field. */
633
+ function serializeImageHotspotValue(value) {
634
+ return JSON.stringify(value);
635
+ }
636
+ function ImageHotspotField(props) {
637
+ const parsed = createMemo(() => parseImageHotspotValue(props.value));
638
+ const [uploading, setUploading] = createSignal(false);
639
+ const [error, setError] = createSignal();
640
+ const patch = (next) => {
641
+ const current = parsed() ?? { url: "" };
642
+ props.setValue(serializeImageHotspotValue({
643
+ ...current,
644
+ ...next
645
+ }));
646
+ };
647
+ async function handleFile(e) {
648
+ const file = e.currentTarget.files?.[0];
649
+ if (!file) return;
650
+ if (!props.onUploadFile) {
651
+ setError("No upload handler configured for this form.");
652
+ return;
653
+ }
654
+ setUploading(true);
655
+ setError(void 0);
656
+ try {
657
+ const { url } = await props.onUploadFile(file);
658
+ patch({ url });
659
+ } catch (err) {
660
+ setError(err instanceof Error ? err.message : "Upload failed");
661
+ } finally {
662
+ setUploading(false);
663
+ }
664
+ }
665
+ function handleImageClick(e) {
666
+ const rect = e.currentTarget.getBoundingClientRect();
667
+ const x = clamp01((e.clientX - rect.left) / rect.width);
668
+ const y = clamp01((e.clientY - rect.top) / rect.height);
669
+ patch({ hotspot: {
670
+ x: round2(x),
671
+ y: round2(y)
672
+ } });
673
+ }
674
+ const crop = () => parsed()?.crop ?? {
675
+ top: 0,
676
+ right: 0,
677
+ bottom: 0,
678
+ left: 0
679
+ };
680
+ const setCrop = (edge, raw) => {
681
+ patch({ crop: {
682
+ ...crop(),
683
+ [edge]: clamp01(Number(raw) || 0)
684
+ } });
685
+ };
686
+ return (() => {
687
+ var _el$ = _tmpl$3$1(), _el$2 = _el$.firstChild;
688
+ _el$2.addEventListener("change", handleFile);
689
+ insert(_el$, createComponent(Show, {
690
+ get when() {
691
+ return uploading();
692
+ },
693
+ get children() {
694
+ return _tmpl$$2();
695
+ }
696
+ }), null);
697
+ insert(_el$, createComponent(Show, {
698
+ get when() {
699
+ return error();
700
+ },
701
+ get children() {
702
+ var _el$4 = _tmpl$2$1();
703
+ insert(_el$4, error);
704
+ return _el$4;
705
+ }
706
+ }), null);
707
+ insert(_el$, createComponent(Show, {
708
+ get when() {
709
+ return parsed()?.url;
710
+ },
711
+ children: (url) => (() => {
712
+ var _el$5 = _tmpl$4$1(), _el$7 = _el$5.firstChild.nextSibling, _el$8 = _el$7.firstChild, _el$9 = _el$7.nextSibling, _el$1 = _el$9.firstChild.nextSibling, _el$10 = _el$9.nextSibling;
713
+ _el$8.$$click = handleImageClick;
714
+ insert(_el$7, createComponent(Show, {
715
+ get when() {
716
+ return parsed()?.hotspot;
717
+ },
718
+ children: (hs) => (() => {
719
+ var _el$11 = _tmpl$5();
720
+ effect((_p$) => {
721
+ var _v$3 = `${hs().x * 100}%`, _v$4 = `${hs().y * 100}%`;
722
+ _v$3 !== _p$.e && setStyleProperty(_el$11, "left", _p$.e = _v$3);
723
+ _v$4 !== _p$.t && setStyleProperty(_el$11, "top", _p$.t = _v$4);
724
+ return _p$;
725
+ }, {
726
+ e: void 0,
727
+ t: void 0
728
+ });
729
+ return _el$11;
730
+ })()
731
+ }), null);
732
+ insert(_el$1, () => [
733
+ "top",
734
+ "right",
735
+ "bottom",
736
+ "left"
737
+ ].map((edge) => (() => {
738
+ var _el$12 = _tmpl$6(), _el$13 = _el$12.firstChild, _el$14 = _el$13.nextSibling;
739
+ insert(_el$13, edge);
740
+ _el$14.$$input = (e) => setCrop(edge, e.currentTarget.value);
741
+ effect(() => _el$14.value = crop()[edge]);
742
+ return _el$12;
743
+ })()));
744
+ insert(_el$10, url);
745
+ effect(() => setAttribute(_el$8, "src", url()));
746
+ return _el$5;
747
+ })()
748
+ }), null);
749
+ effect((_p$) => {
750
+ var _v$ = props.fieldKey, _v$2 = uploading();
751
+ _v$ !== _p$.e && setAttribute(_el$2, "id", _p$.e = _v$);
752
+ _v$2 !== _p$.t && (_el$2.disabled = _p$.t = _v$2);
753
+ return _p$;
754
+ }, {
755
+ e: void 0,
756
+ t: void 0
757
+ });
758
+ return _el$;
759
+ })();
760
+ }
761
+ delegateEvents(["click", "input"]);
762
+ //#endregion
600
763
  //#region src/SearchPalette.tsx
601
- var _tmpl$ = /*#__PURE__*/ template(`<ul class="max-h-80 overflow-y-auto py-2">`), _tmpl$2 = /*#__PURE__*/ template(`<div aria-hidden=true class="fixed inset-0 z-50 flex items-start justify-center bg-[var(--color-backdrop)] px-4 pt-[15vh]"><div role=dialog aria-modal=true aria-label=Search class="w-full max-w-lg overflow-hidden rounded-2xl border border-[var(--line)] bg-[var(--surface-strong)] shadow-2xl"><div class="flex items-center gap-2 border-b border-[var(--line)] px-4 py-3"><i class="ph ph-magnifying-glass text-lg text-[var(--sea-ink-soft)]"aria-hidden=true></i><input type=text placeholder=Search… aria-label=Search class="flex-1 bg-transparent text-base text-[var(--sea-ink)] outline-none placeholder:text-[var(--sea-ink-soft)]"><kbd class="rounded border border-[var(--chip-line)] bg-[var(--chip-bg)] px-1.5 py-0.5 text-xs text-[var(--sea-ink-soft)]">Esc`), _tmpl$3 = /*#__PURE__*/ template(`<p class="px-4 py-6 text-center text-sm text-[var(--sea-ink-soft)]">`), _tmpl$4 = /*#__PURE__*/ template(`<li><button type=button class="flex w-full items-center justify-between gap-3 px-4 py-2 text-left text-sm text-[var(--sea-ink)] hover:bg-[var(--link-bg-hover)]"><span class=truncate></span><span class="shrink-0 text-xs text-[var(--sea-ink-soft)]">`);
764
+ var _tmpl$$1 = /*#__PURE__*/ template(`<ul class="max-h-80 overflow-y-auto py-2">`), _tmpl$2 = /*#__PURE__*/ template(`<div aria-hidden=true class="fixed inset-0 z-50 flex items-start justify-center bg-[var(--color-backdrop)] px-4 pt-[15vh]"><div role=dialog aria-modal=true aria-label=Search class="w-full max-w-lg overflow-hidden rounded-2xl border border-[var(--line)] bg-[var(--surface-strong)] shadow-2xl"><div class="flex items-center gap-2 border-b border-[var(--line)] px-4 py-3"><i class="ph ph-magnifying-glass text-lg text-[var(--sea-ink-soft)]"aria-hidden=true></i><input type=text placeholder=Search… aria-label=Search class="flex-1 bg-transparent text-base text-[var(--sea-ink)] outline-none placeholder:text-[var(--sea-ink-soft)]"><kbd class="rounded border border-[var(--chip-line)] bg-[var(--chip-bg)] px-1.5 py-0.5 text-xs text-[var(--sea-ink-soft)]">Esc`), _tmpl$3 = /*#__PURE__*/ template(`<p class="px-4 py-6 text-center text-sm text-[var(--sea-ink-soft)]">`), _tmpl$4 = /*#__PURE__*/ template(`<li><button type=button class="flex w-full items-center justify-between gap-3 px-4 py-2 text-left text-sm text-[var(--sea-ink)] hover:bg-[var(--link-bg-hover)]"><span class=truncate></span><span class="shrink-0 text-xs text-[var(--sea-ink-soft)]">`);
602
765
  const DEBOUNCE_MS = 200;
603
766
  function capitalize(value) {
604
767
  return value.length === 0 ? value : value[0].toUpperCase() + value.slice(1);
@@ -706,7 +869,7 @@ function SearchPalette(props) {
706
869
  })();
707
870
  },
708
871
  get children() {
709
- var _el$6 = _tmpl$();
872
+ var _el$6 = _tmpl$$1();
710
873
  insert(_el$6, createComponent(For, {
711
874
  get each() {
712
875
  return results();
@@ -735,6 +898,43 @@ delegateEvents([
735
898
  "input"
736
899
  ]);
737
900
  //#endregion
738
- export { CollectionEdit, CollectionList, SearchPalette };
901
+ //#region src/VisualEditingPane.tsx
902
+ var _tmpl$ = /*#__PURE__*/ template(`<iframe>`);
903
+ function originOf(url) {
904
+ try {
905
+ return new URL(url).origin;
906
+ } catch {
907
+ return;
908
+ }
909
+ }
910
+ function VisualEditingPane(props) {
911
+ onMount(() => {
912
+ const expected = props.allowedOrigin ?? originOf(props.src);
913
+ const handler = (event) => {
914
+ if (expected && event.origin !== expected) return;
915
+ const data = event.data;
916
+ if (data?.type === VISUAL_EDIT_MESSAGE && data.ref) props.onEdit?.(data.ref);
917
+ };
918
+ window.addEventListener("message", handler);
919
+ onCleanup(() => window.removeEventListener("message", handler));
920
+ });
921
+ return (() => {
922
+ var _el$ = _tmpl$();
923
+ effect((_p$) => {
924
+ var _v$ = props.src, _v$2 = props.title ?? "Preview", _v$3 = props.class ?? "h-full w-full border-0";
925
+ _v$ !== _p$.e && setAttribute(_el$, "src", _p$.e = _v$);
926
+ _v$2 !== _p$.t && setAttribute(_el$, "title", _p$.t = _v$2);
927
+ _v$3 !== _p$.a && className(_el$, _p$.a = _v$3);
928
+ return _p$;
929
+ }, {
930
+ e: void 0,
931
+ t: void 0,
932
+ a: void 0
933
+ });
934
+ return _el$;
935
+ })();
936
+ }
937
+ //#endregion
938
+ export { CollectionEdit, CollectionList, ImageHotspotField, SearchPalette, VisualEditingPane, parseImageHotspotValue, serializeImageHotspotValue };
739
939
 
740
940
  //# sourceMappingURL=index.js.map