@scalemule/news-publication 0.0.66 → 0.0.68

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';
2
+ import { articlePresentation, articleFormatLabel, parseDocumentBlocks, formatArtworkCaption } from './chunk-DN3TBZLK.js';
3
3
  import { useState, useRef, useEffect } from 'react';
4
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
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));
@@ -329,10 +329,12 @@ function StoryReview({
329
329
  publicationFooter,
330
330
  readerAccountUrl,
331
331
  renderSignIn,
332
- feedbackEmail
332
+ feedbackEmail,
333
+ initialReview,
334
+ onReviewLoaded
333
335
  }) {
334
336
  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("");
337
+ 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
338
  const [shareUrl, setShareUrl] = useState(""), [canShare, setCanShare] = useState(false);
337
339
  const [contributorName, setContributorName] = useState(""), [contributorEmail, setContributorEmail] = useState("");
338
340
  const [identityStep, setIdentityStep] = useState("name"), [requestedPanel, setRequestedPanel] = useState("comment");
@@ -387,6 +389,9 @@ function StoryReview({
387
389
  const responseRef = useRef(null);
388
390
  const thanksRef = useRef(null);
389
391
  const [moreQuestions, setMoreQuestions] = useState(false);
392
+ useEffect(() => {
393
+ if (review) onReviewLoaded?.(review);
394
+ }, [review, onReviewLoaded]);
390
395
  const [replacementPhoto, setReplacementPhoto] = useState(null);
391
396
  const photoToReplace = pendingPhoto.photo?.replacement || replacementPhoto;
392
397
  function openMedia(photo = null) {
@@ -507,7 +512,7 @@ function StoryReview({
507
512
  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
513
  let exchange = {};
509
514
  try {
510
- exchange = await request("/session", token ? { token } : {});
515
+ exchange = await request("/session", { ...token ? { token } : {}, include_review: true });
511
516
  } catch (e) {
512
517
  if (token) throw e;
513
518
  }
@@ -524,7 +529,7 @@ function StoryReview({
524
529
  }
525
530
  }
526
531
  }
527
- const data = await request();
532
+ const data = exchange.review ?? await request();
528
533
  if (active) {
529
534
  if (data.verification_required) {
530
535
  setVerification(true);
@@ -691,10 +696,33 @@ function StoryReview({
691
696
  /* @__PURE__ */ jsx("a", { href: emailLink, children: "Email the newsroom" }),
692
697
  ". Your email app will open; send your message there."
693
698
  ] });
699
+ const presentation = articlePresentation(review?.presentation);
700
+ const formatLabel = articleFormatLabel(presentation.format);
701
+ const sourceHero = review?.revision.document.media.filter((m) => m.position === "hero").map((m) => /* @__PURE__ */ jsx(
702
+ PrivatePhoto,
703
+ {
704
+ media: m,
705
+ base,
706
+ onChange: can("UPLOAD_MEDIA") ? () => openMedia(m) : void 0,
707
+ onSuggestCorrection: can("REQUEST_CHANGES") ? () => openCorrection(m) : void 0,
708
+ disabled: busy
709
+ },
710
+ m.file_id
711
+ ));
712
+ const generated = review?.generated_visual;
713
+ const hero = sourceHero?.length ? sourceHero : generated && /^https:\/\//.test(generated.url) ? /* @__PURE__ */ jsxs("figure", { children: [
714
+ /* @__PURE__ */ jsx("img", { src: generated.url, alt: generated.alt || review?.revision.document.title || "Article illustration" }),
715
+ /* @__PURE__ */ jsxs("figcaption", { children: [
716
+ generated.caption,
717
+ " ",
718
+ generated.label
719
+ ] })
720
+ ] }) : null;
721
+ const heroBeforeHeadline = presentation.layout.headline_position === "after_hero";
694
722
  return /* @__PURE__ */ jsxs("div", { className: `sm-review-frame${publicationHeader ? " sm-review-publication-frame" : ""}`, children: [
695
723
  /* @__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.` })
724
+ /* @__PURE__ */ jsx("strong", { children: review?.article_published || review?.published ? "Private review" : "Private preview \xB7 Not published" }),
725
+ /* @__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
726
  ] }) }),
699
727
  publicationHeader ?? /* @__PURE__ */ jsx("div", { className: "sm-review-brand-wrap", children: /* @__PURE__ */ jsxs("header", { className: "sm-review-masthead", children: [
700
728
  nameplateUrl && /* @__PURE__ */ jsx("img", { src: nameplateUrl, alt: "", width: 80, height: 80 }),
@@ -841,117 +869,118 @@ function StoryReview({
841
869
  "newsroom for final editorial review. It has not been published yet."
842
870
  ] })
843
871
  ] }),
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);
872
+ /* @__PURE__ */ jsxs(
873
+ "article",
874
+ {
875
+ className: "sm-review-article",
876
+ "data-article-layout": presentation.layout.renderer,
877
+ "data-wide-body": presentation.layout.wide_body,
878
+ "data-hero-aspect": presentation.layout.hero_aspect,
879
+ children: [
880
+ heroBeforeHeadline && /* @__PURE__ */ jsx("div", { className: "sm-review-hero", children: hero }),
881
+ /* @__PURE__ */ jsxs("p", { className: "sm-review-category", children: [
882
+ /* @__PURE__ */ jsx("span", { children: review.revision.document.section }),
883
+ formatLabel && /* @__PURE__ */ jsx("span", { children: formatLabel }),
884
+ celebration && /* @__PURE__ */ jsx("span", { children: human(review.celebration_type || "") })
885
+ ] }),
886
+ /* @__PURE__ */ jsx("h1", { children: review.revision.document.title }),
887
+ /* @__PURE__ */ jsx("p", { className: "sm-review-dek", children: review.revision.document.subtitle }),
888
+ /* @__PURE__ */ jsx(EditorialNotices, { editorial: review.revision.editorial }),
889
+ !heroBeforeHeadline && /* @__PURE__ */ jsx("div", { className: "sm-review-hero", children: hero }),
890
+ celebration && !review.revision.document.media.some(
891
+ (m) => m.position === "hero"
892
+ ) && can("UPLOAD_MEDIA") && /* @__PURE__ */ jsxs(
893
+ "aside",
894
+ {
895
+ className: "sm-review-photo-invitation",
896
+ "aria-label": "Family photograph",
897
+ children: [
898
+ /* @__PURE__ */ jsxs("div", { children: [
899
+ /* @__PURE__ */ jsx("strong", { children: "A photograph makes it yours." }),
900
+ /* @__PURE__ */ jsx("p", { children: "Family photograph may be added before publication." })
901
+ ] }),
902
+ /* @__PURE__ */ jsx("button", { onClick: () => openMedia(), children: "+ Add a photograph" })
903
+ ]
912
904
  }
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
- );
905
+ ),
906
+ (() => {
907
+ const blocks = parseDocumentBlocks({
908
+ content_markdown: review.revision.document.content_markdown,
909
+ media: review.revision.document.media,
910
+ blocks: review.revision.document.blocks
911
+ });
912
+ const mediaMap = /* @__PURE__ */ new Map();
913
+ for (const m of review.revision.document.media) {
914
+ mediaMap.set(m.file_id, m);
915
+ if (m.contribution_id) mediaMap.set(m.contribution_id, m);
937
916
  }
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
- ] }),
917
+ 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) => {
918
+ switch (block.type) {
919
+ case "paragraph":
920
+ return /* @__PURE__ */ jsx(
921
+ "p",
922
+ {
923
+ dangerouslySetInnerHTML: { __html: block.html }
924
+ },
925
+ idx
926
+ );
927
+ case "heading": {
928
+ if (block.level === 1)
929
+ return /* @__PURE__ */ jsx("h1", { dangerouslySetInnerHTML: { __html: block.html } }, idx);
930
+ if (block.level === 2)
931
+ return /* @__PURE__ */ jsx("h2", { dangerouslySetInnerHTML: { __html: block.html } }, idx);
932
+ if (block.level === 3)
933
+ return /* @__PURE__ */ jsx("h3", { dangerouslySetInnerHTML: { __html: block.html } }, idx);
934
+ if (block.level === 4)
935
+ return /* @__PURE__ */ jsx("h4", { dangerouslySetInnerHTML: { __html: block.html } }, idx);
936
+ if (block.level === 5)
937
+ return /* @__PURE__ */ jsx("h5", { dangerouslySetInnerHTML: { __html: block.html } }, idx);
938
+ return /* @__PURE__ */ jsx("h6", { dangerouslySetInnerHTML: { __html: block.html } }, idx);
939
+ }
940
+ case "quote":
941
+ return /* @__PURE__ */ jsxs("blockquote", { children: [
942
+ /* @__PURE__ */ jsx("p", { dangerouslySetInnerHTML: { __html: block.html } }),
943
+ block.attribution && /* @__PURE__ */ jsx("cite", { children: block.attribution })
944
+ ] }, idx);
945
+ case "list":
946
+ 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);
947
+ case "divider":
948
+ return /* @__PURE__ */ jsx("hr", {}, idx);
949
+ case "inline_media": {
950
+ const m = mediaMap.get(block.media_id);
951
+ if (!m) return null;
952
+ return /* @__PURE__ */ jsx(
953
+ PrivatePhoto,
954
+ {
955
+ media: m,
956
+ base,
957
+ onChange: m.kind !== "video" && can("UPLOAD_MEDIA") ? () => openMedia(m) : void 0,
958
+ onSuggestCorrection: can("REQUEST_CHANGES") ? () => openCorrection(m) : void 0,
959
+ disabled: busy,
960
+ isInline: true
961
+ },
962
+ m.file_id
963
+ );
964
+ }
965
+ default:
966
+ return null;
967
+ }
968
+ }) });
969
+ })(),
970
+ review.revision.document.media.filter((m) => m.position === "gallery").map((m) => /* @__PURE__ */ jsx(
971
+ PrivatePhoto,
972
+ {
973
+ media: m,
974
+ base,
975
+ onChange: m.kind !== "video" && can("UPLOAD_MEDIA") ? () => openMedia(m) : void 0,
976
+ onSuggestCorrection: can("REQUEST_CHANGES") ? () => openCorrection(m) : void 0,
977
+ disabled: busy
978
+ },
979
+ m.file_id
980
+ ))
981
+ ]
982
+ }
983
+ ),
955
984
  celebration && /* @__PURE__ */ jsxs("section", { id: "review-options", className: "sm-review-welcome", children: [
956
985
  /* @__PURE__ */ jsx("h2", { children: "Help us make this just right" }),
957
986
  /* @__PURE__ */ jsxs("p", { children: [
@@ -100,6 +100,11 @@ type NewsItem = {
100
100
  title: string;
101
101
  excerpt: string | null;
102
102
  cover_image_url: string | null;
103
+ /** Versioned canonical pixel identity supplied by the publication owner. */
104
+ visual_identity?: {
105
+ version: number;
106
+ key: string;
107
+ } | null;
103
108
  published_at: string;
104
109
  updated_at: string;
105
110
  reading_time_minutes: number;
@@ -100,6 +100,11 @@ type NewsItem = {
100
100
  title: string;
101
101
  excerpt: string | null;
102
102
  cover_image_url: string | null;
103
+ /** Versioned canonical pixel identity supplied by the publication owner. */
104
+ visual_identity?: {
105
+ version: number;
106
+ key: string;
107
+ } | null;
103
108
  published_at: string;
104
109
  updated_at: string;
105
110
  reading_time_minutes: number;
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { V as VerifiedCard } from './types-DBi49N8m.cjs';
2
+ import { V as VerifiedCard } from './types-22lEf_UU.cjs';
3
3
 
4
4
  /**
5
5
  * Validate that an action link is safe for readers.
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { V as VerifiedCard } from './types-DBi49N8m.js';
2
+ import { V as VerifiedCard } from './types-22lEf_UU.js';
3
3
 
4
4
  /**
5
5
  * Validate that an action link is safe for readers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/news-publication",
3
- "version": "0.0.66",
3
+ "version": "0.0.68",
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; }