@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.
- package/dist/index/index.d.ts +77 -3
- package/dist/index/index.js +226 -26
- package/dist/index/index.js.map +1 -1
- package/dist/index/server.js +146 -30
- package/dist/index/server.js.map +1 -1
- package/dist/tanstack-start/index.d.ts +116 -16
- package/dist/tanstack-start/index.js +17 -1
- package/dist/tanstack-start/index.js.map +1 -1
- package/dist/tanstack-start/server.js +17 -1
- package/dist/tanstack-start/server.js.map +1 -1
- package/package.json +3 -3
package/dist/index/index.d.ts
CHANGED
|
@@ -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):
|
|
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
|
-
|
|
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
|
package/dist/index/index.js
CHANGED
|
@@ -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$$
|
|
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$
|
|
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$$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$$
|
|
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$$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
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
|
-
|
|
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
|