@scalemule/news-publication 0.0.67 → 0.0.69

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,7 +1,7 @@
1
1
  "use client";
2
- import { parseDocumentBlocks, formatArtworkCaption } from './chunk-OISLCYER.js';
3
- import { useState, useRef, useEffect } from 'react';
4
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
+ import { articlePresentation, articleFormatLabel, parseDocumentBlocks, formatArtworkCaption } from './chunk-DN3TBZLK.js';
3
+ import { createContext, useState, useRef, useEffect, useContext } from 'react';
4
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
 
6
6
  var stable = (value) => Array.isArray(value) ? value.map(stable) : value && typeof value === "object" ? Object.fromEntries(Object.keys(value).sort().map((k) => [k, stable(value[k])])) : value;
7
7
  var same = (a, b) => JSON.stringify(stable(a)) === JSON.stringify(stable(b));
@@ -320,6 +320,7 @@ function EditorialNotices({ editorial }) {
320
320
  ] }, i))
321
321
  ] }) : null;
322
322
  }
323
+ var ReviewMediaSession = createContext(false);
323
324
  var human = (s) => s.toLowerCase().replaceAll("_", " ");
324
325
  function StoryReview({
325
326
  sessionId,
@@ -329,10 +330,14 @@ function StoryReview({
329
330
  publicationFooter,
330
331
  readerAccountUrl,
331
332
  renderSignIn,
332
- feedbackEmail
333
+ feedbackEmail,
334
+ initialReview,
335
+ onReviewLoaded,
336
+ formatImageCaption = (value) => value || ""
333
337
  }) {
334
338
  const base = `/api/news/review/${encodeURIComponent(sessionId)}`;
335
- const [review, setReview] = useState(null), [error, setError] = useState(""), [notice, setNotice] = useState(""), [busy, setBusy] = useState(false), [panel, setPanelState] = useState("article"), [verification, setVerification] = useState(false), [code, setCode] = useState("");
339
+ const [mediaSessionReady, setMediaSessionReady] = useState(false);
340
+ const [review, setReview] = useState(initialReview ?? null), [error, setError] = useState(""), [notice, setNotice] = useState(""), [busy, setBusy] = useState(false), [panel, setPanelState] = useState("article"), [verification, setVerification] = useState(false), [code, setCode] = useState("");
336
341
  const [shareUrl, setShareUrl] = useState(""), [canShare, setCanShare] = useState(false);
337
342
  const [contributorName, setContributorName] = useState(""), [contributorEmail, setContributorEmail] = useState("");
338
343
  const [identityStep, setIdentityStep] = useState("name"), [requestedPanel, setRequestedPanel] = useState("comment");
@@ -387,6 +392,9 @@ function StoryReview({
387
392
  const responseRef = useRef(null);
388
393
  const thanksRef = useRef(null);
389
394
  const [moreQuestions, setMoreQuestions] = useState(false);
395
+ useEffect(() => {
396
+ if (review) onReviewLoaded?.(review);
397
+ }, [review, onReviewLoaded]);
390
398
  const [replacementPhoto, setReplacementPhoto] = useState(null);
391
399
  const photoToReplace = pendingPhoto.photo?.replacement || replacementPhoto;
392
400
  function openMedia(photo = null) {
@@ -466,6 +474,7 @@ function StoryReview({
466
474
  setReview(null);
467
475
  } else {
468
476
  setVerification(false);
477
+ setMediaSessionReady(true);
469
478
  setReview(data);
470
479
  }
471
480
  }
@@ -507,7 +516,7 @@ function StoryReview({
507
516
  const token = new URL(window.location.href).searchParams.get("key") || (/^[a-f0-9]{64}$/.test(window.location.hash.slice(1)) ? window.location.hash.slice(1) : void 0);
508
517
  let exchange = {};
509
518
  try {
510
- exchange = await request("/session", token ? { token } : {});
519
+ exchange = await request("/session", { ...token ? { token } : {}, include_review: true });
511
520
  } catch (e) {
512
521
  if (token) throw e;
513
522
  }
@@ -524,13 +533,14 @@ function StoryReview({
524
533
  }
525
534
  }
526
535
  }
527
- const data = await request();
536
+ const data = exchange.review ?? await request();
528
537
  if (active) {
529
538
  if (data.verification_required) {
530
539
  setVerification(true);
531
540
  setReview(null);
532
541
  } else {
533
542
  setVerification(false);
543
+ setMediaSessionReady(true);
534
544
  setReview(data);
535
545
  }
536
546
  }
@@ -691,10 +701,29 @@ function StoryReview({
691
701
  /* @__PURE__ */ jsx("a", { href: emailLink, children: "Email the newsroom" }),
692
702
  ". Your email app will open; send your message there."
693
703
  ] });
694
- return /* @__PURE__ */ jsxs("div", { className: `sm-review-frame${publicationHeader ? " sm-review-publication-frame" : ""}`, children: [
704
+ const presentation = articlePresentation(review?.presentation);
705
+ const formatLabel = articleFormatLabel(presentation.format);
706
+ const sourceHero = review?.revision.document.media.filter((m) => m.position === "hero").map((m) => /* @__PURE__ */ jsx(
707
+ PrivatePhoto,
708
+ {
709
+ media: m,
710
+ base,
711
+ onChange: can("UPLOAD_MEDIA") ? () => openMedia(m) : void 0,
712
+ onSuggestCorrection: can("REQUEST_CHANGES") ? () => openCorrection(m) : void 0,
713
+ disabled: busy
714
+ },
715
+ m.file_id
716
+ ));
717
+ const generated = review?.generated_visual;
718
+ const hero = sourceHero?.length ? sourceHero : generated && /^https:\/\//.test(generated.url) ? /* @__PURE__ */ jsxs("figure", { children: [
719
+ /* @__PURE__ */ jsx("img", { src: generated.url, alt: formatImageCaption(generated.alt) || review?.revision.document.title || "Article illustration" }),
720
+ formatImageCaption(generated.caption) && /* @__PURE__ */ jsx("figcaption", { children: formatImageCaption(generated.caption) })
721
+ ] }) : null;
722
+ const heroBeforeHeadline = presentation.layout.headline_position === "after_hero";
723
+ return /* @__PURE__ */ jsx(ReviewMediaSession.Provider, { value: mediaSessionReady, children: /* @__PURE__ */ jsxs("div", { className: `sm-review-frame${publicationHeader ? " sm-review-publication-frame" : ""}`, children: [
695
724
  /* @__PURE__ */ jsx("div", { className: "sm-review-banner-wrap", children: /* @__PURE__ */ jsxs("div", { className: "sm-review-private", children: [
696
- /* @__PURE__ */ jsx("strong", { children: review?.published ? "Private review" : "Private preview \xB7 Not published" }),
697
- /* @__PURE__ */ jsx("span", { children: review?.published ? "This revision has been published. This review link remains private." : `Prepared by ${publicationName} for ${celebration ? "family " : ""}review.` })
725
+ /* @__PURE__ */ jsx("strong", { children: review?.article_published || review?.published ? "Private review" : "Private preview \xB7 Not published" }),
726
+ /* @__PURE__ */ jsx("span", { children: review?.published ? "This revision has been published. This review link remains private." : review?.article_published ? "The article is published. This private link preserves the version you were invited to review." : `Prepared by ${publicationName} for ${celebration ? "family " : ""}review.` })
698
727
  ] }) }),
699
728
  publicationHeader ?? /* @__PURE__ */ jsx("div", { className: "sm-review-brand-wrap", children: /* @__PURE__ */ jsxs("header", { className: "sm-review-masthead", children: [
700
729
  nameplateUrl && /* @__PURE__ */ jsx("img", { src: nameplateUrl, alt: "", width: 80, height: 80 }),
@@ -841,117 +870,118 @@ function StoryReview({
841
870
  "newsroom for final editorial review. It has not been published yet."
842
871
  ] })
843
872
  ] }),
844
- /* @__PURE__ */ jsxs("article", { className: "sm-review-article", children: [
845
- /* @__PURE__ */ jsxs("p", { className: "sm-review-category", children: [
846
- /* @__PURE__ */ jsx("span", { children: review.revision.document.section }),
847
- celebration && /* @__PURE__ */ jsx("span", { children: human(review.celebration_type || "") })
848
- ] }),
849
- /* @__PURE__ */ jsx("h1", { children: review.revision.document.title }),
850
- /* @__PURE__ */ jsx("p", { className: "sm-review-dek", children: review.revision.document.subtitle }),
851
- /* @__PURE__ */ jsx(EditorialNotices, { editorial: review.revision.editorial }),
852
- review.revision.document.media.filter((m) => m.position === "hero").map((m) => /* @__PURE__ */ jsx(
853
- PrivatePhoto,
854
- {
855
- media: m,
856
- base,
857
- onChange: can("UPLOAD_MEDIA") ? () => openMedia(m) : void 0,
858
- onSuggestCorrection: can("REQUEST_CHANGES") ? () => openCorrection(m) : void 0,
859
- disabled: busy
860
- },
861
- m.file_id
862
- )),
863
- celebration && !review.revision.document.media.some(
864
- (m) => m.position === "hero"
865
- ) && can("UPLOAD_MEDIA") && /* @__PURE__ */ jsxs(
866
- "aside",
867
- {
868
- className: "sm-review-photo-invitation",
869
- "aria-label": "Family photograph",
870
- children: [
871
- /* @__PURE__ */ jsxs("div", { children: [
872
- /* @__PURE__ */ jsx("strong", { children: "A photograph makes it yours." }),
873
- /* @__PURE__ */ jsx("p", { children: "Family photograph may be added before publication." })
874
- ] }),
875
- /* @__PURE__ */ jsx("button", { onClick: () => openMedia(), children: "+ Add a photograph" })
876
- ]
877
- }
878
- ),
879
- (() => {
880
- const blocks = parseDocumentBlocks({
881
- content_markdown: review.revision.document.content_markdown,
882
- media: review.revision.document.media,
883
- blocks: review.revision.document.blocks
884
- });
885
- const mediaMap = /* @__PURE__ */ new Map();
886
- for (const m of review.revision.document.media) {
887
- mediaMap.set(m.file_id, m);
888
- if (m.contribution_id) mediaMap.set(m.contribution_id, m);
889
- }
890
- return /* @__PURE__ */ jsx("div", { className: "sm-review-prose", children: blocks.length === 0 && review.revision.content_html ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: review.revision.content_html } }) : blocks.map((block, idx) => {
891
- switch (block.type) {
892
- case "paragraph":
893
- return /* @__PURE__ */ jsx(
894
- "p",
895
- {
896
- dangerouslySetInnerHTML: { __html: block.html }
897
- },
898
- idx
899
- );
900
- case "heading": {
901
- if (block.level === 1)
902
- return /* @__PURE__ */ jsx("h1", { dangerouslySetInnerHTML: { __html: block.html } }, idx);
903
- if (block.level === 2)
904
- return /* @__PURE__ */ jsx("h2", { dangerouslySetInnerHTML: { __html: block.html } }, idx);
905
- if (block.level === 3)
906
- return /* @__PURE__ */ jsx("h3", { dangerouslySetInnerHTML: { __html: block.html } }, idx);
907
- if (block.level === 4)
908
- return /* @__PURE__ */ jsx("h4", { dangerouslySetInnerHTML: { __html: block.html } }, idx);
909
- if (block.level === 5)
910
- return /* @__PURE__ */ jsx("h5", { dangerouslySetInnerHTML: { __html: block.html } }, idx);
911
- return /* @__PURE__ */ jsx("h6", { dangerouslySetInnerHTML: { __html: block.html } }, idx);
873
+ /* @__PURE__ */ jsxs(
874
+ "article",
875
+ {
876
+ className: "sm-review-article",
877
+ "data-article-layout": presentation.layout.renderer,
878
+ "data-wide-body": presentation.layout.wide_body,
879
+ "data-hero-aspect": presentation.layout.hero_aspect,
880
+ children: [
881
+ heroBeforeHeadline && /* @__PURE__ */ jsx("div", { className: "sm-review-hero", children: hero }),
882
+ /* @__PURE__ */ jsxs("p", { className: "sm-review-category", children: [
883
+ /* @__PURE__ */ jsx("span", { children: review.revision.document.section }),
884
+ formatLabel && /* @__PURE__ */ jsx("span", { children: formatLabel }),
885
+ celebration && /* @__PURE__ */ jsx("span", { children: human(review.celebration_type || "") })
886
+ ] }),
887
+ /* @__PURE__ */ jsx("h1", { children: review.revision.document.title }),
888
+ /* @__PURE__ */ jsx("p", { className: "sm-review-dek", children: review.revision.document.subtitle }),
889
+ /* @__PURE__ */ jsx(EditorialNotices, { editorial: review.revision.editorial }),
890
+ !heroBeforeHeadline && /* @__PURE__ */ jsx("div", { className: "sm-review-hero", children: hero }),
891
+ celebration && !review.revision.document.media.some(
892
+ (m) => m.position === "hero"
893
+ ) && can("UPLOAD_MEDIA") && /* @__PURE__ */ jsxs(
894
+ "aside",
895
+ {
896
+ className: "sm-review-photo-invitation",
897
+ "aria-label": "Family photograph",
898
+ children: [
899
+ /* @__PURE__ */ jsxs("div", { children: [
900
+ /* @__PURE__ */ jsx("strong", { children: "A photograph makes it yours." }),
901
+ /* @__PURE__ */ jsx("p", { children: "Family photograph may be added before publication." })
902
+ ] }),
903
+ /* @__PURE__ */ jsx("button", { onClick: () => openMedia(), children: "+ Add a photograph" })
904
+ ]
912
905
  }
913
- case "quote":
914
- return /* @__PURE__ */ jsxs("blockquote", { children: [
915
- /* @__PURE__ */ jsx("p", { dangerouslySetInnerHTML: { __html: block.html } }),
916
- block.attribution && /* @__PURE__ */ jsx("cite", { children: block.attribution })
917
- ] }, idx);
918
- case "list":
919
- return block.ordered ? /* @__PURE__ */ jsx("ol", { children: block.items.map((it, i) => /* @__PURE__ */ jsx("li", { dangerouslySetInnerHTML: { __html: it } }, i)) }, idx) : /* @__PURE__ */ jsx("ul", { children: block.items.map((it, i) => /* @__PURE__ */ jsx("li", { dangerouslySetInnerHTML: { __html: it } }, i)) }, idx);
920
- case "divider":
921
- return /* @__PURE__ */ jsx("hr", {}, idx);
922
- case "inline_media": {
923
- const m = mediaMap.get(block.media_id);
924
- if (!m) return null;
925
- return /* @__PURE__ */ jsx(
926
- PrivatePhoto,
927
- {
928
- media: m,
929
- base,
930
- onChange: m.kind !== "video" && can("UPLOAD_MEDIA") ? () => openMedia(m) : void 0,
931
- onSuggestCorrection: can("REQUEST_CHANGES") ? () => openCorrection(m) : void 0,
932
- disabled: busy,
933
- isInline: true
934
- },
935
- m.file_id
936
- );
906
+ ),
907
+ (() => {
908
+ const blocks = parseDocumentBlocks({
909
+ content_markdown: review.revision.document.content_markdown,
910
+ media: review.revision.document.media,
911
+ blocks: review.revision.document.blocks
912
+ });
913
+ const mediaMap = /* @__PURE__ */ new Map();
914
+ for (const m of review.revision.document.media) {
915
+ mediaMap.set(m.file_id, m);
916
+ if (m.contribution_id) mediaMap.set(m.contribution_id, m);
937
917
  }
938
- default:
939
- return null;
940
- }
941
- }) });
942
- })(),
943
- review.revision.document.media.filter((m) => m.position === "gallery").map((m) => /* @__PURE__ */ jsx(
944
- PrivatePhoto,
945
- {
946
- media: m,
947
- base,
948
- onChange: m.kind !== "video" && can("UPLOAD_MEDIA") ? () => openMedia(m) : void 0,
949
- onSuggestCorrection: can("REQUEST_CHANGES") ? () => openCorrection(m) : void 0,
950
- disabled: busy
951
- },
952
- m.file_id
953
- ))
954
- ] }),
918
+ return /* @__PURE__ */ jsx("div", { className: "sm-review-prose", children: blocks.length === 0 && review.revision.content_html ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: review.revision.content_html } }) : blocks.map((block, idx) => {
919
+ switch (block.type) {
920
+ case "paragraph":
921
+ return /* @__PURE__ */ jsx(
922
+ "p",
923
+ {
924
+ dangerouslySetInnerHTML: { __html: block.html }
925
+ },
926
+ idx
927
+ );
928
+ case "heading": {
929
+ if (block.level === 1)
930
+ return /* @__PURE__ */ jsx("h1", { dangerouslySetInnerHTML: { __html: block.html } }, idx);
931
+ if (block.level === 2)
932
+ return /* @__PURE__ */ jsx("h2", { dangerouslySetInnerHTML: { __html: block.html } }, idx);
933
+ if (block.level === 3)
934
+ return /* @__PURE__ */ jsx("h3", { dangerouslySetInnerHTML: { __html: block.html } }, idx);
935
+ if (block.level === 4)
936
+ return /* @__PURE__ */ jsx("h4", { dangerouslySetInnerHTML: { __html: block.html } }, idx);
937
+ if (block.level === 5)
938
+ return /* @__PURE__ */ jsx("h5", { dangerouslySetInnerHTML: { __html: block.html } }, idx);
939
+ return /* @__PURE__ */ jsx("h6", { dangerouslySetInnerHTML: { __html: block.html } }, idx);
940
+ }
941
+ case "quote":
942
+ return /* @__PURE__ */ jsxs("blockquote", { children: [
943
+ /* @__PURE__ */ jsx("p", { dangerouslySetInnerHTML: { __html: block.html } }),
944
+ block.attribution && /* @__PURE__ */ jsx("cite", { children: block.attribution })
945
+ ] }, idx);
946
+ case "list":
947
+ return block.ordered ? /* @__PURE__ */ jsx("ol", { children: block.items.map((it, i) => /* @__PURE__ */ jsx("li", { dangerouslySetInnerHTML: { __html: it } }, i)) }, idx) : /* @__PURE__ */ jsx("ul", { children: block.items.map((it, i) => /* @__PURE__ */ jsx("li", { dangerouslySetInnerHTML: { __html: it } }, i)) }, idx);
948
+ case "divider":
949
+ return /* @__PURE__ */ jsx("hr", {}, idx);
950
+ case "inline_media": {
951
+ const m = mediaMap.get(block.media_id);
952
+ if (!m) return null;
953
+ return /* @__PURE__ */ jsx(
954
+ PrivatePhoto,
955
+ {
956
+ media: m,
957
+ base,
958
+ onChange: m.kind !== "video" && can("UPLOAD_MEDIA") ? () => openMedia(m) : void 0,
959
+ onSuggestCorrection: can("REQUEST_CHANGES") ? () => openCorrection(m) : void 0,
960
+ disabled: busy,
961
+ isInline: true
962
+ },
963
+ m.file_id
964
+ );
965
+ }
966
+ default:
967
+ return null;
968
+ }
969
+ }) });
970
+ })(),
971
+ review.revision.document.media.filter((m) => m.position === "gallery").map((m) => /* @__PURE__ */ jsx(
972
+ PrivatePhoto,
973
+ {
974
+ media: m,
975
+ base,
976
+ onChange: m.kind !== "video" && can("UPLOAD_MEDIA") ? () => openMedia(m) : void 0,
977
+ onSuggestCorrection: can("REQUEST_CHANGES") ? () => openCorrection(m) : void 0,
978
+ disabled: busy
979
+ },
980
+ m.file_id
981
+ ))
982
+ ]
983
+ }
984
+ ),
955
985
  celebration && /* @__PURE__ */ jsxs("section", { id: "review-options", className: "sm-review-welcome", children: [
956
986
  /* @__PURE__ */ jsx("h2", { children: "Help us make this just right" }),
957
987
  /* @__PURE__ */ jsxs("p", { children: [
@@ -1652,7 +1682,7 @@ function StoryReview({
1652
1682
  ] })
1653
1683
  ] }),
1654
1684
  publicationFooter
1655
- ] });
1685
+ ] }) });
1656
1686
  }
1657
1687
  function PrivatePhoto({
1658
1688
  base,
@@ -1662,9 +1692,11 @@ function PrivatePhoto({
1662
1692
  disabled,
1663
1693
  isInline
1664
1694
  }) {
1695
+ const sessionReady = useContext(ReviewMediaSession);
1665
1696
  const [url, setUrl] = useState("");
1666
1697
  const [video, setVideo] = useState(false);
1667
1698
  useEffect(() => {
1699
+ if (!sessionReady) return;
1668
1700
  let active = true, object = "";
1669
1701
  void fetch(`${base}/media/${encodeURIComponent(media.file_id)}`, {
1670
1702
  cache: "no-store",
@@ -1683,7 +1715,7 @@ function PrivatePhoto({
1683
1715
  active = false;
1684
1716
  if (object) URL.revokeObjectURL(object);
1685
1717
  };
1686
- }, [base, media.file_id]);
1718
+ }, [base, media.file_id, sessionReady]);
1687
1719
  const captionInfo = formatArtworkCaption(media);
1688
1720
  return /* @__PURE__ */ jsxs(
1689
1721
  "figure",
@@ -1707,7 +1739,7 @@ function PrivatePhoto({
1707
1739
  alt: media.alt || captionInfo.title || "Story photograph",
1708
1740
  referrerPolicy: "no-referrer"
1709
1741
  }
1710
- ) : /* @__PURE__ */ jsx("p", { children: "Media unavailable." }),
1742
+ ) : /* @__PURE__ */ jsx("p", { children: sessionReady ? "Media unavailable." : "Loading photograph\u2026" }),
1711
1743
  /* @__PURE__ */ jsxs("figcaption", { className: "sm-review-photo-details", children: [
1712
1744
  /* @__PURE__ */ jsxs("div", { className: "sm-review-caption-text", children: [
1713
1745
  captionInfo.title && /* @__PURE__ */ jsx("div", { className: "sm-review-artwork-title", children: /* @__PURE__ */ jsxs("strong", { children: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/news-publication",
3
- "version": "0.0.67",
3
+ "version": "0.0.69",
4
4
  "description": "Shared server framework for ScaleMule local news publications. Walnut Creek Times is the broadsheet default; each paper supplies its own identity and feature flags.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -179,7 +179,12 @@
179
179
  "import": "./dist/toys-server.js",
180
180
  "require": "./dist/toys-server.cjs"
181
181
  },
182
- "./toys.css": "./toys.css"
182
+ "./toys.css": "./toys.css",
183
+ "./article-workflow": {
184
+ "types": "./dist/article-workflow-client.d.ts",
185
+ "import": "./dist/article-workflow-client.js",
186
+ "require": "./dist/article-workflow-client.cjs"
187
+ }
183
188
  },
184
189
  "files": [
185
190
  "dist",
package/story-review.css CHANGED
@@ -809,3 +809,8 @@
809
809
  padding: 20px 16px max(20px, env(safe-area-inset-bottom));
810
810
  }
811
811
  }
812
+
813
+ /* The same finite layout vocabulary is used by public and private readers. */
814
+ .sm-review-article[data-wide-body="true"] { max-width: 900px; }
815
+ .sm-review-article[data-hero-aspect="portrait"] .sm-review-hero img { aspect-ratio: 4 / 5; object-fit: contain; max-height: 80vh; }
816
+ .sm-review-article[data-hero-aspect="square"] .sm-review-hero img { aspect-ratio: 1; object-fit: contain; }