@scalemule/news-publication 0.0.20 → 0.0.22

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/README.md CHANGED
@@ -85,3 +85,18 @@ import { ArticleProvenance } from '@scalemule/news-publication'
85
85
  ```
86
86
 
87
87
  The component inherits the publication's typography. Optional styling hooks are `article-provenance` and `article-provenance__label`. `readerSources()` is available when an existing publication component owns source-list rendering.
88
+
89
+ ### Verified Community Cards
90
+
91
+ `VerifiedCardSection` and `VerifiedCardComponent` render broadsheet-styled fact sheets for verified entities (`PLACE`, `ACTIVITY`, `EVENT`, `ORGANIZATION`, `PRODUCT`). Verified cards display confirmed operating hours, venues, contact details, dates, and primary action links (tickets, registration, maps) validated by newsroom staff.
92
+
93
+ ```tsx
94
+ import { VerifiedCardSection, getArticleCards } from '@scalemule/news-publication'
95
+
96
+ const cards = getArticleCards(article.metadata?.cards)
97
+
98
+ <VerifiedCardSection cards={cards} />
99
+ ```
100
+
101
+ Styles adhere to the publication theme (`--pub-paper`, `--pub-ink`, `--pub-accent`, `--pub-rule`, `--pub-soft`) and are included in `@scalemule/news-publication/tokens.css` or available standalone via `@scalemule/news-publication/verified-card.css`.
102
+
@@ -0,0 +1,227 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+
3
+ // src/verified-card.tsx
4
+ var ALLOWED_CARD_TYPES = /* @__PURE__ */ new Set([
5
+ "PLACE",
6
+ "ACTIVITY",
7
+ "EVENT",
8
+ "ORGANIZATION",
9
+ "PRODUCT"
10
+ ]);
11
+ function clip(val, maxChars) {
12
+ if (typeof val !== "string") return void 0;
13
+ const trimmed = val.trim();
14
+ if (!trimmed) return void 0;
15
+ return Array.from(trimmed).slice(0, maxChars).join("");
16
+ }
17
+ function isSafeActionUrl(raw) {
18
+ if (typeof raw !== "string") return false;
19
+ const trimmed = raw.trim();
20
+ if (!trimmed || trimmed.length > 2048) return false;
21
+ if (trimmed.startsWith("tel:") || trimmed.startsWith("mailto:")) {
22
+ return /^tel:\+?[\d\-().\s]+$/.test(trimmed) || /^mailto:[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed);
23
+ }
24
+ try {
25
+ const url = new URL(trimmed);
26
+ if (url.protocol !== "https:" && url.protocol !== "http:") return false;
27
+ if (url.username || url.password) return false;
28
+ const hostname = url.hostname.toLowerCase();
29
+ if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".internal") || hostname.endsWith(".local") || hostname === "127.0.0.1" || hostname === "169.254.169.254") {
30
+ return false;
31
+ }
32
+ for (const [key] of url.searchParams.entries()) {
33
+ const cleanKey = key.toLowerCase().replace(/[^a-z0-9]/g, "");
34
+ if (cleanKey === "token" || cleanKey === "apikey" || cleanKey === "accesstoken" || cleanKey === "secret" || cleanKey === "clientsecret" || cleanKey === "authorization" || cleanKey === "signature" || cleanKey === "password" || cleanKey === "xamzsignature" || cleanKey === "xamzsecuritytoken") {
35
+ return false;
36
+ }
37
+ }
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+ function getArticleCards(rawCards) {
44
+ if (!Array.isArray(rawCards) || rawCards.length === 0) return [];
45
+ const cards = [];
46
+ for (const item of rawCards) {
47
+ if (!item || typeof item !== "object") continue;
48
+ const c = item;
49
+ const id = clip(c.id, 64);
50
+ const title = clip(c.title, 300);
51
+ if (!id || !title) continue;
52
+ const rawType = typeof c.card_type === "string" ? c.card_type : c.cardType;
53
+ const cardType = ALLOWED_CARD_TYPES.has(rawType) ? rawType : "PLACE";
54
+ const kicker = clip(c.kicker, 120) || (cardType === "PLACE" ? "LOCAL VENUE \xB7 VERIFIED DIRECTORY" : cardType === "EVENT" ? "COMMUNITY EVENT \xB7 VERIFIED FACTSHEET" : cardType === "ACTIVITY" ? "COMMUNITY ACTIVITY \xB7 VERIFIED FACTSHEET" : cardType === "ORGANIZATION" ? "COMMUNITY ORGANIZATION \xB7 DIRECTORY" : "VERIFIED FACTSHEET");
55
+ const subtitle = clip(c.subtitle, 300);
56
+ const summary = clip(c.summary, 1e3);
57
+ const badge = clip(c.badge, 64);
58
+ const facts = [];
59
+ if (Array.isArray(c.facts)) {
60
+ for (const f of c.facts) {
61
+ if (!f || typeof f !== "object") continue;
62
+ const label = clip(f.label, 80);
63
+ const value = clip(f.value, 500);
64
+ if (!label || !value) continue;
65
+ let href = void 0;
66
+ const rawHref = f.href;
67
+ if (typeof rawHref === "string" && isSafeActionUrl(rawHref)) {
68
+ href = rawHref;
69
+ }
70
+ facts.push({ label, value, href });
71
+ }
72
+ } else {
73
+ const timeframe = clip(c.timeframe, 200);
74
+ if (timeframe) facts.push({ label: "Timeframe", value: timeframe });
75
+ const address = clip(c.address, 300);
76
+ if (address) {
77
+ facts.push({
78
+ label: "Address",
79
+ value: address,
80
+ href: `https://maps.google.com/?q=${encodeURIComponent(address)}`
81
+ });
82
+ }
83
+ const hours = clip(c.hours, 300);
84
+ if (hours) facts.push({ label: "Hours", value: hours });
85
+ const phone = clip(c.phone, 80);
86
+ if (phone) {
87
+ facts.push({
88
+ label: "Phone",
89
+ value: phone,
90
+ href: `tel:${phone.replace(/[^+\d]/g, "")}`
91
+ });
92
+ }
93
+ const cost = clip(c.cost, 120);
94
+ if (cost) facts.push({ label: "Cost", value: cost });
95
+ }
96
+ const actions = [];
97
+ if (Array.isArray(c.actions)) {
98
+ for (const act of c.actions) {
99
+ if (!act || typeof act !== "object") continue;
100
+ const actObj = act;
101
+ const label = clip(actObj.label, 80);
102
+ const url = typeof actObj.url === "string" ? actObj.url.trim() : "";
103
+ if (!label || !url || !isSafeActionUrl(url)) continue;
104
+ actions.push({
105
+ label,
106
+ url,
107
+ primary: Boolean(actObj.primary)
108
+ });
109
+ if (actions.length >= 5) break;
110
+ }
111
+ }
112
+ let image = void 0;
113
+ if (c.image && typeof c.image === "object") {
114
+ const img = c.image;
115
+ const masterUrl = typeof img.master_url === "string" && isSafeActionUrl(img.master_url) ? img.master_url : void 0;
116
+ const url = typeof img.url === "string" && isSafeActionUrl(img.url) ? img.url : masterUrl;
117
+ if (url) {
118
+ image = {
119
+ file_id: clip(img.file_id, 64),
120
+ master_url: masterUrl,
121
+ url,
122
+ width: typeof img.width === "number" ? img.width : void 0,
123
+ height: typeof img.height === "number" ? img.height : void 0,
124
+ alt: clip(img.alt, 500),
125
+ caption: clip(img.caption, 1e3),
126
+ attribution: clip(img.attribution, 500),
127
+ label: clip(img.label, 100),
128
+ ai_generated: Boolean(img.ai_generated)
129
+ };
130
+ }
131
+ }
132
+ const verificationNote = clip(c.verification_note || c.verificationNote, 300) || "Verified by publication reporting staff";
133
+ const verifiedAt = clip(c.verified_at || c.verifiedAt, 100) || "October 2026";
134
+ let rating = void 0;
135
+ if (typeof c.rating === "number" || typeof c.rating === "string") {
136
+ rating = c.rating;
137
+ }
138
+ cards.push({
139
+ id,
140
+ cardType,
141
+ kicker,
142
+ title,
143
+ subtitle,
144
+ summary,
145
+ badge,
146
+ facts,
147
+ actions,
148
+ verificationNote,
149
+ verifiedAt,
150
+ image,
151
+ rating
152
+ });
153
+ if (cards.length >= 10) break;
154
+ }
155
+ return cards;
156
+ }
157
+ function VerifiedCardComponent({
158
+ card,
159
+ className
160
+ }) {
161
+ return /* @__PURE__ */ jsxs(
162
+ "aside",
163
+ {
164
+ className: `article-verified-card ${className || ""}`.trim(),
165
+ "aria-label": `Verified Factsheet: ${card.title}`,
166
+ children: [
167
+ /* @__PURE__ */ jsxs("div", { className: "article-verified-card__header", children: [
168
+ /* @__PURE__ */ jsx("span", { className: "article-verified-card__kicker", children: card.kicker }),
169
+ card.badge && /* @__PURE__ */ jsx("span", { className: "article-verified-card__badge", children: card.badge })
170
+ ] }),
171
+ /* @__PURE__ */ jsx("h2", { className: "article-verified-card__title", children: card.title }),
172
+ card.subtitle && /* @__PURE__ */ jsx("p", { className: "article-verified-card__subtitle", children: card.subtitle }),
173
+ card.summary && /* @__PURE__ */ jsx("p", { className: "article-verified-card__summary", children: card.summary }),
174
+ card.facts.length > 0 && /* @__PURE__ */ jsx("dl", { className: "article-verified-card__fact-grid", children: card.facts.map((fact, i) => /* @__PURE__ */ jsxs("div", { className: "article-verified-card__fact-row", children: [
175
+ /* @__PURE__ */ jsx("dt", { className: "article-verified-card__fact-label", children: fact.label }),
176
+ /* @__PURE__ */ jsx("dd", { className: "article-verified-card__fact-value", children: fact.href ? /* @__PURE__ */ jsx(
177
+ "a",
178
+ {
179
+ className: "article-verified-card__fact-link",
180
+ href: fact.href,
181
+ rel: "noopener noreferrer",
182
+ target: fact.href.startsWith("http") ? "_blank" : void 0,
183
+ children: fact.value
184
+ }
185
+ ) : fact.value })
186
+ ] }, i)) }),
187
+ card.actions.length > 0 && /* @__PURE__ */ jsx("div", { className: "article-verified-card__actions", children: card.actions.map((act, i) => /* @__PURE__ */ jsxs(
188
+ "a",
189
+ {
190
+ className: `article-verified-card__action ${act.primary ? "article-verified-card__action--primary" : "article-verified-card__action--secondary"}`,
191
+ href: act.url,
192
+ target: "_blank",
193
+ rel: "noopener noreferrer",
194
+ children: [
195
+ /* @__PURE__ */ jsx("span", { children: act.label }),
196
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "article-verified-card__action-icon", children: "\u2197" })
197
+ ]
198
+ },
199
+ i
200
+ )) }),
201
+ /* @__PURE__ */ jsxs("footer", { className: "article-verified-card__footer", children: [
202
+ /* @__PURE__ */ jsxs("div", { className: "article-verified-card__seal", children: [
203
+ /* @__PURE__ */ jsx("span", { className: "article-verified-card__seal-check", "aria-hidden": "true", children: "\u2713" }),
204
+ /* @__PURE__ */ jsx("span", { children: card.verificationNote })
205
+ ] }),
206
+ /* @__PURE__ */ jsx("span", { className: "article-verified-card__date", children: card.verifiedAt })
207
+ ] })
208
+ ]
209
+ }
210
+ );
211
+ }
212
+ function VerifiedCardSection({
213
+ cards,
214
+ className
215
+ }) {
216
+ if (!cards || cards.length === 0) return null;
217
+ return /* @__PURE__ */ jsx(
218
+ "section",
219
+ {
220
+ className: `article-verified-card-section ${className || ""}`.trim(),
221
+ "aria-label": "Verified Community Information",
222
+ children: cards.map((card) => /* @__PURE__ */ jsx(VerifiedCardComponent, { card }, card.id))
223
+ }
224
+ );
225
+ }
226
+
227
+ export { VerifiedCardComponent, VerifiedCardSection, getArticleCards, isSafeActionUrl };
package/dist/index.cjs CHANGED
@@ -1836,6 +1836,279 @@ function readerSources(input) {
1836
1836
  return output;
1837
1837
  }
1838
1838
 
1839
+ // src/listening.ts
1840
+ function publicationListeningTrack(article, publicationId, publicationName, origin) {
1841
+ const articleUrl2 = new URL(`/news/${encodeURIComponent(article.slug)}`, origin).href;
1842
+ let storyId = articleUrl2;
1843
+ if (article.canonical_url) {
1844
+ try {
1845
+ const canonical = new URL(article.canonical_url);
1846
+ if (canonical.protocol === "https:" && !canonical.username && !canonical.password && /^\/news\/[^/]+\/?$/.test(canonical.pathname) && localPublications.some((item) => new URL(item.url).hostname === canonical.hostname.replace(/^www\./, ""))) {
1847
+ canonical.search = "";
1848
+ canonical.hash = "";
1849
+ canonical.hostname = canonical.hostname.replace(/^www\./, "");
1850
+ canonical.pathname = canonical.pathname.replace(/\/$/, "");
1851
+ storyId = canonical.href;
1852
+ }
1853
+ } catch {
1854
+ }
1855
+ }
1856
+ const revision = article.editorial?.modified_at;
1857
+ return {
1858
+ id: article.id,
1859
+ publicationId,
1860
+ publicationName,
1861
+ title: article.title,
1862
+ articleUrl: articleUrl2,
1863
+ storyId,
1864
+ ...revision && Number.isFinite(Date.parse(revision)) ? { revision } : {},
1865
+ ...article.audio?.duration_ms && article.audio.duration_ms > 0 ? { durationSeconds: article.audio.duration_ms / 1e3 } : {},
1866
+ ...article.section ? { section: article.section } : {},
1867
+ ...article.published_at ? { publishedAt: article.published_at } : {}
1868
+ };
1869
+ }
1870
+ async function publicationListeningCandidates(items, read, publicationId, publicationName, origin) {
1871
+ const output = [];
1872
+ const recent = items.filter((item) => !item.editorial?.duplicate_url).slice(0, 30);
1873
+ let failures = 0;
1874
+ for (let i = 0; i < recent.length; i += 4) {
1875
+ const batch = await Promise.allSettled(recent.slice(i, i + 4).map((item) => read(item.slug)));
1876
+ for (const result of batch) {
1877
+ if (result.status === "rejected") {
1878
+ failures++;
1879
+ continue;
1880
+ }
1881
+ const article = result.value;
1882
+ if (!article.editorial?.duplicate_url && article.audio?.url && article.audio.duration_ms && article.audio.duration_ms > 0)
1883
+ output.push(publicationListeningTrack(article, publicationId, publicationName, origin));
1884
+ }
1885
+ }
1886
+ if (!output.length && failures) throw new Error("Audio inventory could not be loaded");
1887
+ return output.sort((a, b) => (b.publishedAt ?? "").localeCompare(a.publishedAt ?? ""));
1888
+ }
1889
+ var ALLOWED_CARD_TYPES = /* @__PURE__ */ new Set([
1890
+ "PLACE",
1891
+ "ACTIVITY",
1892
+ "EVENT",
1893
+ "ORGANIZATION",
1894
+ "PRODUCT"
1895
+ ]);
1896
+ function clip(val, maxChars) {
1897
+ if (typeof val !== "string") return void 0;
1898
+ const trimmed = val.trim();
1899
+ if (!trimmed) return void 0;
1900
+ return Array.from(trimmed).slice(0, maxChars).join("");
1901
+ }
1902
+ function isSafeActionUrl(raw) {
1903
+ if (typeof raw !== "string") return false;
1904
+ const trimmed = raw.trim();
1905
+ if (!trimmed || trimmed.length > 2048) return false;
1906
+ if (trimmed.startsWith("tel:") || trimmed.startsWith("mailto:")) {
1907
+ return /^tel:\+?[\d\-().\s]+$/.test(trimmed) || /^mailto:[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed);
1908
+ }
1909
+ try {
1910
+ const url = new URL(trimmed);
1911
+ if (url.protocol !== "https:" && url.protocol !== "http:") return false;
1912
+ if (url.username || url.password) return false;
1913
+ const hostname = url.hostname.toLowerCase();
1914
+ if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".internal") || hostname.endsWith(".local") || hostname === "127.0.0.1" || hostname === "169.254.169.254") {
1915
+ return false;
1916
+ }
1917
+ for (const [key] of url.searchParams.entries()) {
1918
+ const cleanKey = key.toLowerCase().replace(/[^a-z0-9]/g, "");
1919
+ if (cleanKey === "token" || cleanKey === "apikey" || cleanKey === "accesstoken" || cleanKey === "secret" || cleanKey === "clientsecret" || cleanKey === "authorization" || cleanKey === "signature" || cleanKey === "password" || cleanKey === "xamzsignature" || cleanKey === "xamzsecuritytoken") {
1920
+ return false;
1921
+ }
1922
+ }
1923
+ return true;
1924
+ } catch {
1925
+ return false;
1926
+ }
1927
+ }
1928
+ function getArticleCards(rawCards) {
1929
+ if (!Array.isArray(rawCards) || rawCards.length === 0) return [];
1930
+ const cards = [];
1931
+ for (const item of rawCards) {
1932
+ if (!item || typeof item !== "object") continue;
1933
+ const c = item;
1934
+ const id = clip(c.id, 64);
1935
+ const title = clip(c.title, 300);
1936
+ if (!id || !title) continue;
1937
+ const rawType = typeof c.card_type === "string" ? c.card_type : c.cardType;
1938
+ const cardType = ALLOWED_CARD_TYPES.has(rawType) ? rawType : "PLACE";
1939
+ const kicker = clip(c.kicker, 120) || (cardType === "PLACE" ? "LOCAL VENUE \xB7 VERIFIED DIRECTORY" : cardType === "EVENT" ? "COMMUNITY EVENT \xB7 VERIFIED FACTSHEET" : cardType === "ACTIVITY" ? "COMMUNITY ACTIVITY \xB7 VERIFIED FACTSHEET" : cardType === "ORGANIZATION" ? "COMMUNITY ORGANIZATION \xB7 DIRECTORY" : "VERIFIED FACTSHEET");
1940
+ const subtitle = clip(c.subtitle, 300);
1941
+ const summary = clip(c.summary, 1e3);
1942
+ const badge = clip(c.badge, 64);
1943
+ const facts = [];
1944
+ if (Array.isArray(c.facts)) {
1945
+ for (const f of c.facts) {
1946
+ if (!f || typeof f !== "object") continue;
1947
+ const label = clip(f.label, 80);
1948
+ const value = clip(f.value, 500);
1949
+ if (!label || !value) continue;
1950
+ let href = void 0;
1951
+ const rawHref = f.href;
1952
+ if (typeof rawHref === "string" && isSafeActionUrl(rawHref)) {
1953
+ href = rawHref;
1954
+ }
1955
+ facts.push({ label, value, href });
1956
+ }
1957
+ } else {
1958
+ const timeframe = clip(c.timeframe, 200);
1959
+ if (timeframe) facts.push({ label: "Timeframe", value: timeframe });
1960
+ const address = clip(c.address, 300);
1961
+ if (address) {
1962
+ facts.push({
1963
+ label: "Address",
1964
+ value: address,
1965
+ href: `https://maps.google.com/?q=${encodeURIComponent(address)}`
1966
+ });
1967
+ }
1968
+ const hours = clip(c.hours, 300);
1969
+ if (hours) facts.push({ label: "Hours", value: hours });
1970
+ const phone = clip(c.phone, 80);
1971
+ if (phone) {
1972
+ facts.push({
1973
+ label: "Phone",
1974
+ value: phone,
1975
+ href: `tel:${phone.replace(/[^+\d]/g, "")}`
1976
+ });
1977
+ }
1978
+ const cost = clip(c.cost, 120);
1979
+ if (cost) facts.push({ label: "Cost", value: cost });
1980
+ }
1981
+ const actions = [];
1982
+ if (Array.isArray(c.actions)) {
1983
+ for (const act of c.actions) {
1984
+ if (!act || typeof act !== "object") continue;
1985
+ const actObj = act;
1986
+ const label = clip(actObj.label, 80);
1987
+ const url = typeof actObj.url === "string" ? actObj.url.trim() : "";
1988
+ if (!label || !url || !isSafeActionUrl(url)) continue;
1989
+ actions.push({
1990
+ label,
1991
+ url,
1992
+ primary: Boolean(actObj.primary)
1993
+ });
1994
+ if (actions.length >= 5) break;
1995
+ }
1996
+ }
1997
+ let image = void 0;
1998
+ if (c.image && typeof c.image === "object") {
1999
+ const img = c.image;
2000
+ const masterUrl = typeof img.master_url === "string" && isSafeActionUrl(img.master_url) ? img.master_url : void 0;
2001
+ const url = typeof img.url === "string" && isSafeActionUrl(img.url) ? img.url : masterUrl;
2002
+ if (url) {
2003
+ image = {
2004
+ file_id: clip(img.file_id, 64),
2005
+ master_url: masterUrl,
2006
+ url,
2007
+ width: typeof img.width === "number" ? img.width : void 0,
2008
+ height: typeof img.height === "number" ? img.height : void 0,
2009
+ alt: clip(img.alt, 500),
2010
+ caption: clip(img.caption, 1e3),
2011
+ attribution: clip(img.attribution, 500),
2012
+ label: clip(img.label, 100),
2013
+ ai_generated: Boolean(img.ai_generated)
2014
+ };
2015
+ }
2016
+ }
2017
+ const verificationNote = clip(c.verification_note || c.verificationNote, 300) || "Verified by publication reporting staff";
2018
+ const verifiedAt = clip(c.verified_at || c.verifiedAt, 100) || "October 2026";
2019
+ let rating = void 0;
2020
+ if (typeof c.rating === "number" || typeof c.rating === "string") {
2021
+ rating = c.rating;
2022
+ }
2023
+ cards.push({
2024
+ id,
2025
+ cardType,
2026
+ kicker,
2027
+ title,
2028
+ subtitle,
2029
+ summary,
2030
+ badge,
2031
+ facts,
2032
+ actions,
2033
+ verificationNote,
2034
+ verifiedAt,
2035
+ image,
2036
+ rating
2037
+ });
2038
+ if (cards.length >= 10) break;
2039
+ }
2040
+ return cards;
2041
+ }
2042
+ function VerifiedCardComponent({
2043
+ card,
2044
+ className
2045
+ }) {
2046
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2047
+ "aside",
2048
+ {
2049
+ className: `article-verified-card ${className || ""}`.trim(),
2050
+ "aria-label": `Verified Factsheet: ${card.title}`,
2051
+ children: [
2052
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "article-verified-card__header", children: [
2053
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "article-verified-card__kicker", children: card.kicker }),
2054
+ card.badge && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "article-verified-card__badge", children: card.badge })
2055
+ ] }),
2056
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "article-verified-card__title", children: card.title }),
2057
+ card.subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "article-verified-card__subtitle", children: card.subtitle }),
2058
+ card.summary && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "article-verified-card__summary", children: card.summary }),
2059
+ card.facts.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("dl", { className: "article-verified-card__fact-grid", children: card.facts.map((fact, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "article-verified-card__fact-row", children: [
2060
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "article-verified-card__fact-label", children: fact.label }),
2061
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "article-verified-card__fact-value", children: fact.href ? /* @__PURE__ */ jsxRuntime.jsx(
2062
+ "a",
2063
+ {
2064
+ className: "article-verified-card__fact-link",
2065
+ href: fact.href,
2066
+ rel: "noopener noreferrer",
2067
+ target: fact.href.startsWith("http") ? "_blank" : void 0,
2068
+ children: fact.value
2069
+ }
2070
+ ) : fact.value })
2071
+ ] }, i)) }),
2072
+ card.actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "article-verified-card__actions", children: card.actions.map((act, i) => /* @__PURE__ */ jsxRuntime.jsxs(
2073
+ "a",
2074
+ {
2075
+ className: `article-verified-card__action ${act.primary ? "article-verified-card__action--primary" : "article-verified-card__action--secondary"}`,
2076
+ href: act.url,
2077
+ target: "_blank",
2078
+ rel: "noopener noreferrer",
2079
+ children: [
2080
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: act.label }),
2081
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "article-verified-card__action-icon", children: "\u2197" })
2082
+ ]
2083
+ },
2084
+ i
2085
+ )) }),
2086
+ /* @__PURE__ */ jsxRuntime.jsxs("footer", { className: "article-verified-card__footer", children: [
2087
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "article-verified-card__seal", children: [
2088
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "article-verified-card__seal-check", "aria-hidden": "true", children: "\u2713" }),
2089
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: card.verificationNote })
2090
+ ] }),
2091
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "article-verified-card__date", children: card.verifiedAt })
2092
+ ] })
2093
+ ]
2094
+ }
2095
+ );
2096
+ }
2097
+ function VerifiedCardSection({
2098
+ cards,
2099
+ className
2100
+ }) {
2101
+ if (!cards || cards.length === 0) return null;
2102
+ return /* @__PURE__ */ jsxRuntime.jsx(
2103
+ "section",
2104
+ {
2105
+ className: `article-verified-card-section ${className || ""}`.trim(),
2106
+ "aria-label": "Verified Community Information",
2107
+ children: cards.map((card) => /* @__PURE__ */ jsxRuntime.jsx(VerifiedCardComponent, { card }, card.id))
2108
+ }
2109
+ );
2110
+ }
2111
+
1839
2112
  Object.defineProperty(exports, "COOKIE_CONSENT_BACKUP_KEY", {
1840
2113
  enumerable: true,
1841
2114
  get: function () { return compliance.COOKIE_CONSENT_BACKUP_KEY; }
@@ -1945,6 +2218,8 @@ exports.THEME_STORAGE_KEY = THEME_STORAGE_KEY;
1945
2218
  exports.ThemeToggle = ThemeToggle;
1946
2219
  exports.UTM_FIELDS = UTM_FIELDS;
1947
2220
  exports.UUID = UUID;
2221
+ exports.VerifiedCardComponent = VerifiedCardComponent;
2222
+ exports.VerifiedCardSection = VerifiedCardSection;
1948
2223
  exports.acceptRemoteRecord = acceptRemoteRecord;
1949
2224
  exports.acceptSnapshot = acceptSnapshot;
1950
2225
  exports.activeNearby = activeNearby;
@@ -1979,6 +2254,7 @@ exports.feedCacheOptions = feedCacheOptions;
1979
2254
  exports.filterNews = filterNews;
1980
2255
  exports.findPublicationByHost = findPublicationByHost;
1981
2256
  exports.findPublicationBySlug = findPublicationBySlug;
2257
+ exports.getArticleCards = getArticleCards;
1982
2258
  exports.getNetworkPublicationUrl = getNetworkPublicationUrl;
1983
2259
  exports.getStoredTheme = getStoredTheme;
1984
2260
  exports.getSystemTheme = getSystemTheme;
@@ -1986,6 +2262,7 @@ exports.inventoryCacheOptions = inventoryCacheOptions;
1986
2262
  exports.isAdult = isAdult;
1987
2263
  exports.isClassifiedCategory = isClassifiedCategory;
1988
2264
  exports.isEmploymentType = isEmploymentType;
2265
+ exports.isSafeActionUrl = isSafeActionUrl;
1989
2266
  exports.jobEmployerBody = jobEmployerBody;
1990
2267
  exports.jobOpeningBody = jobOpeningBody;
1991
2268
  exports.jobPostingBody = jobPostingBody;
@@ -2007,6 +2284,8 @@ exports.publicJobsPath = publicJobsPath;
2007
2284
  exports.publicUrl = publicUrl;
2008
2285
  exports.publicationFromSnapshot = publicationFromSnapshot;
2009
2286
  exports.publicationJsonLd = publicationJsonLd;
2287
+ exports.publicationListeningCandidates = publicationListeningCandidates;
2288
+ exports.publicationListeningTrack = publicationListeningTrack;
2010
2289
  exports.publicationMeta = publicationMeta;
2011
2290
  exports.publicationRecord = publicationRecord;
2012
2291
  exports.readerSources = readerSources;
package/dist/index.d.cts CHANGED
@@ -1,7 +1,8 @@
1
- import { b as PublicationTheme, c as PublicationInput, N as NewsPublication, F as FeatureName, H as HomepageSelection, d as NearbyUpdate, e as NewsItem, E as EditorialSection, f as NewsArticle, C as CouponOffer } from './snapshot-OZFP3-Up.cjs';
2
- export { A as ArticleCitation, g as CategoryRef, h as CouponSettings, i as CoverImageMetadata, j as FEATURE_NAMES, k as FeatureFlags, l as NewsFeed, m as NewsLayoutId, n as PublicationModules, o as PublicationRecord, P as PublicationSnapshot, R as ReferralSettings, S as SnapshotHooks, p as StoryDisplay, q as SyncSource, a as SyncedPublication, T as TagRef, r as acceptRemoteRecord, s as acceptSnapshot, t as parseSnapshot, u as publicationFromSnapshot, v as publicationRecord, w as snapshotFromRecord, x as syncPublication } from './snapshot-OZFP3-Up.cjs';
1
+ import { a as PublicationTheme, P as PublicationInput, N as NewsPublication, F as FeatureName, H as HomepageSelection, b as NearbyUpdate, c as NewsItem, E as EditorialSection, d as NewsArticle, C as CouponOffer } from './types-DFDCZHbS.cjs';
2
+ export { A as ArticleCitation, e as CardType, f as CategoryRef, g as CouponSettings, h as CoverImageMetadata, i as FEATURE_NAMES, j as FeatureFlags, k as NewsFeed, l as NewsLayoutId, m as PublicationModules, R as ReferralSettings, S as StoryDisplay, T as TagRef, V as VerifiedCard, n as VerifiedCardAction, o as VerifiedCardFact, p as VerifiedCardImage } from './types-DFDCZHbS.cjs';
3
3
  export { M as MEET_ACTIVITIES, b as MeetActivity, c as MeetupInput, a as PreparedMeetup, P as PublicPlace, d as activityLabel, e as canHostMeetup, i as isAdult, p as prepareMeetup, s as screenMessage } from './meet-RWmfj3I3.cjs';
4
4
  export { b as CLASSIFIED_CATEGORIES, c as ClassifiedCategory, C as ClassifiedListing, E as EmploymentType, J as JOB_EMPLOYMENT_TYPES, d as PreparedClassified, e as PreparedJob, P as PublicDeal, f as PublicDealOffer, a as PublicJob, g as categoryLabel, h as classifiedPath, i as classifiedSearchPath, j as clipChars, k as dealDollars, l as dealGoPath, m as dealsFeedPath, n as employmentLabel, o as isClassifiedCategory, p as isEmploymentType, q as jobEmployerBody, r as jobOpeningBody, s as jobPostingBody, t as prepareClassified, u as prepareJobPost, v as publicJobPath, w as publicJobsPath } from './marketplace-29i5w4LS.cjs';
5
+ export { b as PublicationRecord, P as PublicationSnapshot, S as SnapshotHooks, c as SyncSource, a as SyncedPublication, d as acceptRemoteRecord, e as acceptSnapshot, p as parseSnapshot, f as publicationFromSnapshot, g as publicationRecord, s as snapshotFromRecord, h as syncPublication } from './snapshot-C0IGpt-B.cjs';
5
6
  import { CookieCategoryDefinition, CookieConsentRecord } from '@scalemule/compliance';
6
7
  export { COOKIE_CONSENT_BACKUP_KEY, COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, COOKIE_CONSENT_MAX_AGE_SECONDS, CookieCategory, CookieCategoryDefinition, CookieConsentRecord, CookieDisclosure, CookiePreferences, DEFAULT_CONSENT_HUB_URL, DEFAULT_PREFERENCES_ACCEPTED, DEFAULT_PREFERENCES_REJECTED, EU_EEA_UK_COUNTRIES, NETWORK_CONSENT_STORAGE_KEY, acceptAllCookies, checkUrlConsentBridge, clearCookieConsent, decorateNetworkUrl, getCookieConsent, hasConsented, isCategoryAllowed, isGeoConsentRequired, queryNetworkConsentHub, rejectOptionalCookies, setCookieConsent, setNetworkConsentHub } from '@scalemule/compliance';
7
8
  export { CookieConsentBanner, CookieConsentBannerProps } from './cookie-consent-banner.cjs';
@@ -10,6 +11,7 @@ export { ThemeToggle, ThemeToggleProps } from './theme-toggle.cjs';
10
11
  export { PublicationNetworkItem, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, listedPublications, localPublications } from './network.cjs';
11
12
  export { LocalNetworkBar, LocalNetworkBarProps } from './network-bar.cjs';
12
13
  import * as react from 'react';
14
+ export { VerifiedCardComponent, VerifiedCardSection, getArticleCards, isSafeActionUrl } from './verified-card.cjs';
13
15
 
14
16
  /** Walnut Creek Times broadsheet colors. A paper overrides only what it changes. */
15
17
  declare const BROADSHEET_THEME: PublicationTheme;
@@ -311,4 +313,47 @@ declare function readerSources(input: unknown): {
311
313
  publisher?: string;
312
314
  }[];
313
315
 
314
- export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, type Attribution, BROADSHEET_THEME, COOKIE_CATEGORIES, CouponOffer, EDITORIAL_SECTION_ORDER, EVENTS, EditorialSection, type EffectiveTheme, FeatureName, HomepageSelection, LAYOUT_COOKIE, NEWS_CACHE, NearbyUpdate, NewsArticle, NewsError, NewsItem, NewsPublication, PublicationInput, PublicationTheme, SESSION_IDLE_MS, THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, type ThemeMode, UTM_FIELDS, UUID, activeNearby, applyTheme, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, clearTheme, composeEdition, defaultRevalidateTargets, definePublication, displayHeadline, edgeCacheControl, editorialSections, featureEnabled, feedCacheKey, feedCacheOptions, filterNews, getStoredTheme, getSystemTheme, inventoryCacheOptions, llmsText, matchesStoryTag, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationMeta, readerSources, recordConsentToCompliance, referralCode, resolveCoupon, resolveEffectiveTheme, resolveLayout, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, setTheme, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, subscribeTheme, themeInitScript, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
316
+ interface ListeningArticle {
317
+ id: string;
318
+ slug: string;
319
+ title: string;
320
+ canonical_url?: string | null;
321
+ section?: string | null;
322
+ published_at?: string;
323
+ editorial?: {
324
+ duplicate_url?: string;
325
+ modified_at?: string;
326
+ } | null;
327
+ audio?: {
328
+ url?: string | null;
329
+ duration_ms?: number | null;
330
+ };
331
+ }
332
+ /** Public metadata only. Never cache signed recording URLs in reader history. */
333
+ declare function publicationListeningTrack(article: ListeningArticle, publicationId: string, publicationName: string, origin: string): {
334
+ publishedAt?: string | undefined;
335
+ section?: string | undefined;
336
+ durationSeconds?: number | undefined;
337
+ revision?: string | undefined;
338
+ id: string;
339
+ publicationId: string;
340
+ publicationName: string;
341
+ title: string;
342
+ articleUrl: string;
343
+ storyId: string;
344
+ };
345
+ /** Bounded public read fan-out, initiated only when a reader requests a catch-up queue. */
346
+ declare function publicationListeningCandidates<T extends ListeningArticle>(items: T[], read: (slug: string) => Promise<ListeningArticle>, publicationId: string, publicationName: string, origin: string): Promise<{
347
+ publishedAt?: string | undefined;
348
+ section?: string | undefined;
349
+ durationSeconds?: number | undefined;
350
+ revision?: string | undefined;
351
+ id: string;
352
+ publicationId: string;
353
+ publicationName: string;
354
+ title: string;
355
+ articleUrl: string;
356
+ storyId: string;
357
+ }[]>;
358
+
359
+ export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, type Attribution, BROADSHEET_THEME, COOKIE_CATEGORIES, CouponOffer, EDITORIAL_SECTION_ORDER, EVENTS, EditorialSection, type EffectiveTheme, FeatureName, HomepageSelection, LAYOUT_COOKIE, type ListeningArticle, NEWS_CACHE, NearbyUpdate, NewsArticle, NewsError, NewsItem, NewsPublication, PublicationInput, PublicationTheme, SESSION_IDLE_MS, THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, type ThemeMode, UTM_FIELDS, UUID, activeNearby, applyTheme, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, clearTheme, composeEdition, defaultRevalidateTargets, definePublication, displayHeadline, edgeCacheControl, editorialSections, featureEnabled, feedCacheKey, feedCacheOptions, filterNews, getStoredTheme, getSystemTheme, inventoryCacheOptions, llmsText, matchesStoryTag, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, recordConsentToCompliance, referralCode, resolveCoupon, resolveEffectiveTheme, resolveLayout, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, setTheme, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, subscribeTheme, themeInitScript, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };