@thebes/cadmea 1.4.0 → 1.6.0

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,12 +1,12 @@
1
- import { className, createComponent, delegateEvents, effect, insert, memo, setAttribute, template } from "solid-js/web";
1
+ import { className, createComponent, delegateEvents, effect, insert, memo, setAttribute, template, use } from "solid-js/web";
2
2
  import { createMutation, createQuery, useQueryClient } from "@tanstack/solid-query";
3
- import { For, Index, Show, Suspense, createEffect, createSignal, lazy, onCleanup } from "solid-js";
3
+ import { For, Index, Show, Suspense, createEffect, createSignal, lazy, onCleanup, onMount } from "solid-js";
4
4
  import { createForm } from "@tanstack/solid-form";
5
- import { validateDocument } from "@thebes/cadmus/cms";
5
+ import { PREVIEW_VALUES_MESSAGE, VISUAL_EDIT_MESSAGE, validateDocument } from "@thebes/cadmus/cms";
6
6
  import { Link, useBlocker } from "@tanstack/solid-router";
7
7
  import { createSolidTable, flexRender, getCoreRowModel } from "@tanstack/solid-table";
8
8
  //#region src/CollectionEdit.tsx
9
- 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$1 = /*#__PURE__*/ template(`<button type=button class="btn flex-1">`), _tmpl$4$1 = /*#__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(`<span class="text-base-content/60 self-center px-1 text-xs"aria-live=polite>`), _tmpl$7$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$8$1 = /*#__PURE__*/ template(`<fieldset class="border-base-300 rounded-box border p-4"><legend class="px-2 text-sm font-semibold">`), _tmpl$9$1 = /*#__PURE__*/ template(`<div class="grid grid-cols-1 gap-4 md:grid-cols-2">`), _tmpl$0$1 = /*#__PURE__*/ template(`<span class=text-error> *`), _tmpl$1$1 = /*#__PURE__*/ template(`<p class="text-base-content/60 mb-1 text-xs">`), _tmpl$10$1 = /*#__PURE__*/ template(`<p class="text-error mt-1 text-sm"role=alert>`), _tmpl$11$1 = /*#__PURE__*/ template(`<div><label class=label>`), _tmpl$12$1 = /*#__PURE__*/ template(`<input class=input type=text>`), _tmpl$13$1 = /*#__PURE__*/ template(`<select class=select>`), _tmpl$14 = /*#__PURE__*/ template(`<option>`), _tmpl$15 = /*#__PURE__*/ template(`<input class=input type=number>`), _tmpl$16 = /*#__PURE__*/ template(`<input class=input type=text readonly>`), _tmpl$17 = /*#__PURE__*/ template(`<input class=checkbox type=checkbox>`), _tmpl$18 = /*#__PURE__*/ template(`<p class="text-sm opacity-70 break-all">`), _tmpl$19 = /*#__PURE__*/ template(`<p class="text-sm text-error">`), _tmpl$20 = /*#__PURE__*/ template(`<div class="flex flex-col gap-2"><input class=file-input type=file>`), _tmpl$21 = /*#__PURE__*/ template(`<div class="mb-1 flex flex-wrap gap-1">`), _tmpl$22 = /*#__PURE__*/ template(`<button type=button aria-label=Clear class="absolute top-2 right-2 cursor-pointer opacity-60">×`), _tmpl$23 = /*#__PURE__*/ template(`<div role=listbox class="bg-base-100 border-base-300 rounded-box absolute z-10 mt-1 flex max-h-56 w-full flex-col overflow-auto border p-1 shadow">`), _tmpl$24 = /*#__PURE__*/ template(`<div class=relative><input type=text role=combobox autocomplete=off class=input>`), _tmpl$25 = /*#__PURE__*/ template(`<span class="badge badge-primary gap-1"><button type=button class=cursor-pointer>×`), _tmpl$26 = /*#__PURE__*/ template(`<button type=button role=option class="rounded px-3 py-2 text-left">`), _tmpl$27 = /*#__PURE__*/ template(`<div role=menu class="bg-base-100 border-base-300 rounded-box absolute z-10 mt-1 flex flex-col border p-1 shadow">`), _tmpl$28 = /*#__PURE__*/ template(`<div class="relative self-start"><button type=button class="btn btn-outline btn-sm"aria-haspopup=menu>Add block`), _tmpl$29 = /*#__PURE__*/ template(`<div class="form-control md:col-span-2"><div class="label font-medium"></div><div class="flex flex-col gap-3">`), _tmpl$30 = /*#__PURE__*/ template(`<span class="text-base-content/60 truncate text-sm">`), _tmpl$31 = /*#__PURE__*/ template(`<div class="flex flex-col gap-2">`), _tmpl$32 = /*#__PURE__*/ template(`<div class="card bg-base-200 flex flex-col gap-2 p-3"><div class="flex items-center gap-2"><button type=button class="btn btn-ghost btn-sm gap-2"><span aria-hidden=true></span><span class=font-semibold></span></button><div class="ml-auto flex gap-1"><button type=button class="btn btn-ghost btn-xs"aria-label="Move up">↑</button><button type=button class="btn btn-ghost btn-xs"aria-label="Move down">↓</button><button type=button class="btn btn-ghost btn-xs"aria-label=Duplicate>⧉</button><button type=button class="btn btn-ghost btn-xs text-error"aria-label=Remove>Remove`), _tmpl$33 = /*#__PURE__*/ template(`<button type=button class="btn btn-outline btn-sm self-start">Add `), _tmpl$34 = /*#__PURE__*/ template(`<i aria-hidden=true>`), _tmpl$35 = /*#__PURE__*/ template(`<button type=button role=menuitem class="flex items-center gap-2 rounded px-3 py-2 text-left">`);
9
+ var _tmpl$$5 = /*#__PURE__*/ template(`<p class="text-sm text-error"role=alert>`), _tmpl$2$3 = /*#__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(`<span class="text-base-content/60 self-center px-1 text-xs"aria-live=polite>`), _tmpl$7$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$8$1 = /*#__PURE__*/ template(`<fieldset class="border-base-300 rounded-box border p-4"><legend class="px-2 text-sm font-semibold">`), _tmpl$9$1 = /*#__PURE__*/ template(`<div class="grid grid-cols-1 gap-4 md:grid-cols-2">`), _tmpl$0$1 = /*#__PURE__*/ template(`<span class=text-error> *`), _tmpl$1$1 = /*#__PURE__*/ template(`<p class="text-base-content/60 mb-1 text-xs">`), _tmpl$10$1 = /*#__PURE__*/ template(`<p class="text-error mt-1 text-sm"role=alert>`), _tmpl$11$1 = /*#__PURE__*/ template(`<div><label class=label>`), _tmpl$12$1 = /*#__PURE__*/ template(`<input class=input type=text>`), _tmpl$13$1 = /*#__PURE__*/ template(`<select class=select>`), _tmpl$14 = /*#__PURE__*/ template(`<option>`), _tmpl$15 = /*#__PURE__*/ template(`<input class=input type=number>`), _tmpl$16 = /*#__PURE__*/ template(`<input class=input type=text readonly>`), _tmpl$17 = /*#__PURE__*/ template(`<input class=checkbox type=checkbox>`), _tmpl$18 = /*#__PURE__*/ template(`<p class="text-sm opacity-70 break-all">`), _tmpl$19 = /*#__PURE__*/ template(`<p class="text-sm text-error">`), _tmpl$20 = /*#__PURE__*/ template(`<div class="flex flex-col gap-2"><input class=file-input type=file>`), _tmpl$21 = /*#__PURE__*/ template(`<div class="mb-1 flex flex-wrap gap-1">`), _tmpl$22 = /*#__PURE__*/ template(`<button type=button aria-label=Clear class="absolute top-2 right-2 cursor-pointer opacity-60">×`), _tmpl$23 = /*#__PURE__*/ template(`<div role=listbox class="bg-base-100 border-base-300 rounded-box absolute z-10 mt-1 flex max-h-56 w-full flex-col overflow-auto border p-1 shadow">`), _tmpl$24 = /*#__PURE__*/ template(`<div class=relative><input type=text role=combobox autocomplete=off class=input>`), _tmpl$25 = /*#__PURE__*/ template(`<span class="badge badge-primary gap-1"><button type=button class=cursor-pointer>×`), _tmpl$26 = /*#__PURE__*/ template(`<button type=button role=option class="rounded px-3 py-2 text-left">`), _tmpl$27 = /*#__PURE__*/ template(`<div role=menu class="bg-base-100 border-base-300 rounded-box absolute z-10 mt-1 flex flex-col border p-1 shadow">`), _tmpl$28 = /*#__PURE__*/ template(`<div class="relative self-start"><button type=button class="btn btn-outline btn-sm"aria-haspopup=menu>Add block`), _tmpl$29 = /*#__PURE__*/ template(`<div class="form-control md:col-span-2"><div class="label font-medium"></div><div class="flex flex-col gap-3">`), _tmpl$30 = /*#__PURE__*/ template(`<span class="text-base-content/60 truncate text-sm">`), _tmpl$31 = /*#__PURE__*/ template(`<div class="flex flex-col gap-2">`), _tmpl$32 = /*#__PURE__*/ template(`<div class="card bg-base-200 flex flex-col gap-2 p-3"><div class="flex items-center gap-2"><button type=button class="btn btn-ghost btn-sm gap-2"><span aria-hidden=true></span><span class=font-semibold></span></button><div class="ml-auto flex gap-1"><button type=button class="btn btn-ghost btn-xs"aria-label="Move up">↑</button><button type=button class="btn btn-ghost btn-xs"aria-label="Move down">↓</button><button type=button class="btn btn-ghost btn-xs"aria-label=Duplicate>⧉</button><button type=button class="btn btn-ghost btn-xs text-error"aria-label=Remove>Remove`), _tmpl$33 = /*#__PURE__*/ template(`<button type=button class="btn btn-outline btn-sm self-start">Add `), _tmpl$34 = /*#__PURE__*/ template(`<i aria-hidden=true>`), _tmpl$35 = /*#__PURE__*/ template(`<button type=button role=menuitem class="flex items-center gap-2 rounded px-3 py-2 text-left">`);
10
10
  const RichTextEditor = lazy(() => import("../RichTextEditor-ComcBFfl.js").then((mod) => ({ default: mod.RichTextEditor })));
11
11
  function editableFields(config) {
12
12
  return Object.entries(config.fields).filter(([key]) => key !== "id");
@@ -54,6 +54,7 @@ function CollectionEdit(props) {
54
54
  const isDefaultValue = form.useStore((s) => s.isDefaultValue);
55
55
  createEffect(() => props.onDirtyChange?.(!isDefaultValue()));
56
56
  const formValues = form.useStore((s) => s.values);
57
+ createEffect(() => props.onValuesChange?.(editablePayload(formValues())));
57
58
  function editablePayload(value) {
58
59
  return Object.fromEntries(Object.entries(value).filter(([key]) => props.config.fields[key]?.type !== "date"));
59
60
  }
@@ -103,7 +104,7 @@ function CollectionEdit(props) {
103
104
  return props.error;
104
105
  },
105
106
  get children() {
106
- var _el$2 = _tmpl$$4();
107
+ var _el$2 = _tmpl$$5();
107
108
  insert(_el$2, () => props.error);
108
109
  return _el$2;
109
110
  }
@@ -149,7 +150,7 @@ function CollectionEdit(props) {
149
150
  return props.capabilities?.canUpdate !== false;
150
151
  },
151
152
  get children() {
152
- var _el$11 = _tmpl$4$1();
153
+ var _el$11 = _tmpl$4$2();
153
154
  _el$11.$$click = () => void form.handleSubmit();
154
155
  insert(_el$11, createComponent(Show, {
155
156
  get when() {
@@ -170,7 +171,7 @@ function CollectionEdit(props) {
170
171
  get children() {
171
172
  return [
172
173
  (() => {
173
- var _el$4 = _tmpl$3$1();
174
+ var _el$4 = _tmpl$3$2();
174
175
  _el$4.$$click = () => void props.draftActions?.onSaveDraft(editablePayload(formValues()));
175
176
  insert(_el$4, createComponent(Show, {
176
177
  get when() {
@@ -187,7 +188,7 @@ function CollectionEdit(props) {
187
188
  return _el$4;
188
189
  })(),
189
190
  (() => {
190
- var _el$6 = _tmpl$4$1();
191
+ var _el$6 = _tmpl$4$2();
191
192
  _el$6.$$click = () => void props.draftActions?.onPublish?.();
192
193
  insert(_el$6, createComponent(Show, {
193
194
  get when() {
@@ -849,7 +850,7 @@ delegateEvents([
849
850
  ]);
850
851
  //#endregion
851
852
  //#region src/tanstack-start/create.tsx
852
- var _tmpl$$3 = /*#__PURE__*/ template(`<div class="flex flex-col gap-4"><h1 class="text-xl font-semibold">`);
853
+ var _tmpl$$4 = /*#__PURE__*/ template(`<div class="flex flex-col gap-4"><h1 class="text-xl font-semibold">`);
853
854
  /**
854
855
  * Builds a create-page component for a collection. See
855
856
  * `createCollectionListPage`'s doc comment for the same rationale on
@@ -868,7 +869,7 @@ function createCollectionCreatePage(options) {
868
869
  onError: (e) => setError(e.message)
869
870
  }));
870
871
  return (() => {
871
- var _el$ = _tmpl$$3(), _el$2 = _el$.firstChild;
872
+ var _el$ = _tmpl$$4(), _el$2 = _el$.firstChild;
872
873
  insert(_el$2, () => options.label ?? `New ${options.collection.slug}`);
873
874
  insert(_el$, createComponent(CollectionEdit, {
874
875
  get config() {
@@ -893,8 +894,63 @@ function createCollectionCreatePage(options) {
893
894
  };
894
895
  }
895
896
  //#endregion
897
+ //#region src/VisualEditingPane.tsx
898
+ var _tmpl$$3 = /*#__PURE__*/ template(`<iframe>`);
899
+ function originOf(url) {
900
+ try {
901
+ return new URL(url).origin;
902
+ } catch {
903
+ return;
904
+ }
905
+ }
906
+ function VisualEditingPane(props) {
907
+ let iframe;
908
+ const targetOrigin = () => props.allowedOrigin ?? originOf(props.src) ?? "*";
909
+ onMount(() => {
910
+ const expected = props.allowedOrigin ?? originOf(props.src);
911
+ const handler = (event) => {
912
+ if (expected && event.origin !== expected) return;
913
+ const data = event.data;
914
+ if (data?.type === VISUAL_EDIT_MESSAGE && data.ref) props.onEdit?.(data.ref);
915
+ };
916
+ window.addEventListener("message", handler);
917
+ onCleanup(() => window.removeEventListener("message", handler));
918
+ });
919
+ createEffect(() => {
920
+ const values = props.previewValues;
921
+ const target = props.previewTarget;
922
+ const win = iframe?.contentWindow;
923
+ if (!values || !target || !win) return;
924
+ const message = {
925
+ type: PREVIEW_VALUES_MESSAGE,
926
+ collection: target.collection,
927
+ id: target.id,
928
+ values
929
+ };
930
+ win.postMessage(message, targetOrigin());
931
+ });
932
+ return (() => {
933
+ var _el$ = _tmpl$$3();
934
+ use((el) => {
935
+ iframe = el;
936
+ }, _el$);
937
+ effect((_p$) => {
938
+ var _v$ = props.src, _v$2 = props.title ?? "Preview", _v$3 = props.class ?? "h-full w-full border-0";
939
+ _v$ !== _p$.e && setAttribute(_el$, "src", _p$.e = _v$);
940
+ _v$2 !== _p$.t && setAttribute(_el$, "title", _p$.t = _v$2);
941
+ _v$3 !== _p$.a && className(_el$, _p$.a = _v$3);
942
+ return _p$;
943
+ }, {
944
+ e: void 0,
945
+ t: void 0,
946
+ a: void 0
947
+ });
948
+ return _el$;
949
+ })();
950
+ }
951
+ //#endregion
896
952
  //#region src/tanstack-start/edit.tsx
897
- var _tmpl$$2 = /*#__PURE__*/ template(`<button type=button class="btn btn-error btn-outline btn-sm self-start">`), _tmpl$2$2 = /*#__PURE__*/ template(`<div class="flex flex-col gap-4"><h1 class="text-xl font-semibold">`);
953
+ var _tmpl$$2 = /*#__PURE__*/ template(`<button type=button class="btn btn-error btn-outline btn-sm self-start">`), _tmpl$2$2 = /*#__PURE__*/ template(`<div class="flex flex-col gap-4"><h1 class="text-xl font-semibold">`), _tmpl$3$1 = /*#__PURE__*/ template(`<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">`), _tmpl$4$1 = /*#__PURE__*/ template(`<div class="lg:sticky lg:top-4 lg:h-[calc(100vh-2rem)]">`);
898
954
  /**
899
955
  * Builds an edit-page component for a collection — fetch, update, and
900
956
  * delete, all wired together, plus a router-level unsaved-changes guard
@@ -908,6 +964,7 @@ function createCollectionEditPage(options) {
908
964
  const [error, setError] = createSignal();
909
965
  const [dirty, setDirty] = createSignal(false);
910
966
  const [latestDraftId, setLatestDraftId] = createSignal();
967
+ const [previewValues, setPreviewValues] = createSignal({});
911
968
  useBlocker({
912
969
  shouldBlockFn: () => dirty(),
913
970
  enableBeforeUnload: () => dirty()
@@ -964,7 +1021,7 @@ function createCollectionEditPage(options) {
964
1021
  },
965
1022
  onError: (e) => setError(e.message)
966
1023
  }));
967
- return (() => {
1024
+ const EditorPane = () => (() => {
968
1025
  var _el$ = _tmpl$2$2(), _el$2 = _el$.firstChild;
969
1026
  insert(_el$2, () => options.label ?? `Edit ${options.collection.slug}`);
970
1027
  insert(_el$, createComponent(Show, {
@@ -996,6 +1053,7 @@ function createCollectionEditPage(options) {
996
1053
  return options.fieldWidgets;
997
1054
  },
998
1055
  onDirtyChange: setDirty,
1056
+ onValuesChange: setPreviewValues,
999
1057
  get capabilities() {
1000
1058
  return options.capabilities?.();
1001
1059
  },
@@ -1011,7 +1069,8 @@ function createCollectionEditPage(options) {
1011
1069
  canPreview: latestDraftId() !== void 0,
1012
1070
  saveDraftLabel: options.draftActions.saveDraftLabel,
1013
1071
  publishLabel: options.draftActions.publishLabel,
1014
- previewLabel: options.draftActions.previewLabel
1072
+ previewLabel: options.draftActions.previewLabel,
1073
+ autosave: options.draftActions.autosave
1015
1074
  };
1016
1075
  }
1017
1076
  });
@@ -1030,6 +1089,47 @@ function createCollectionEditPage(options) {
1030
1089
  }), null);
1031
1090
  return _el$;
1032
1091
  })();
1092
+ return createComponent(Show, {
1093
+ get when() {
1094
+ return options.preview;
1095
+ },
1096
+ get fallback() {
1097
+ return createComponent(EditorPane, {});
1098
+ },
1099
+ get children() {
1100
+ var _el$4 = _tmpl$3$1();
1101
+ insert(_el$4, createComponent(EditorPane, {}), null);
1102
+ insert(_el$4, createComponent(Show, {
1103
+ get when() {
1104
+ return options.preview?.url();
1105
+ },
1106
+ children: (url) => (() => {
1107
+ var _el$5 = _tmpl$4$1();
1108
+ insert(_el$5, createComponent(VisualEditingPane, {
1109
+ get src() {
1110
+ return url();
1111
+ },
1112
+ get allowedOrigin() {
1113
+ return options.preview?.allowedOrigin?.();
1114
+ },
1115
+ get previewValues() {
1116
+ return previewValues();
1117
+ },
1118
+ get previewTarget() {
1119
+ return {
1120
+ collection: options.collection.slug,
1121
+ id: Number(row.data?.id)
1122
+ };
1123
+ },
1124
+ "class": "border-base-300 rounded-box h-full w-full border",
1125
+ title: "Live preview"
1126
+ }));
1127
+ return _el$5;
1128
+ })()
1129
+ }), null);
1130
+ return _el$4;
1131
+ }
1132
+ });
1033
1133
  };
1034
1134
  }
1035
1135
  delegateEvents(["click"]);