@scalemule/news-publication 0.0.21 → 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
@@ -1886,6 +1886,228 @@ async function publicationListeningCandidates(items, read, publicationId, public
1886
1886
  if (!output.length && failures) throw new Error("Audio inventory could not be loaded");
1887
1887
  return output.sort((a, b) => (b.publishedAt ?? "").localeCompare(a.publishedAt ?? ""));
1888
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
+ }
1889
2111
 
1890
2112
  Object.defineProperty(exports, "COOKIE_CONSENT_BACKUP_KEY", {
1891
2113
  enumerable: true,
@@ -1996,6 +2218,8 @@ exports.THEME_STORAGE_KEY = THEME_STORAGE_KEY;
1996
2218
  exports.ThemeToggle = ThemeToggle;
1997
2219
  exports.UTM_FIELDS = UTM_FIELDS;
1998
2220
  exports.UUID = UUID;
2221
+ exports.VerifiedCardComponent = VerifiedCardComponent;
2222
+ exports.VerifiedCardSection = VerifiedCardSection;
1999
2223
  exports.acceptRemoteRecord = acceptRemoteRecord;
2000
2224
  exports.acceptSnapshot = acceptSnapshot;
2001
2225
  exports.activeNearby = activeNearby;
@@ -2030,6 +2254,7 @@ exports.feedCacheOptions = feedCacheOptions;
2030
2254
  exports.filterNews = filterNews;
2031
2255
  exports.findPublicationByHost = findPublicationByHost;
2032
2256
  exports.findPublicationBySlug = findPublicationBySlug;
2257
+ exports.getArticleCards = getArticleCards;
2033
2258
  exports.getNetworkPublicationUrl = getNetworkPublicationUrl;
2034
2259
  exports.getStoredTheme = getStoredTheme;
2035
2260
  exports.getSystemTheme = getSystemTheme;
@@ -2037,6 +2262,7 @@ exports.inventoryCacheOptions = inventoryCacheOptions;
2037
2262
  exports.isAdult = isAdult;
2038
2263
  exports.isClassifiedCategory = isClassifiedCategory;
2039
2264
  exports.isEmploymentType = isEmploymentType;
2265
+ exports.isSafeActionUrl = isSafeActionUrl;
2040
2266
  exports.jobEmployerBody = jobEmployerBody;
2041
2267
  exports.jobOpeningBody = jobOpeningBody;
2042
2268
  exports.jobPostingBody = jobPostingBody;
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;
package/dist/index.d.ts 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.js';
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.js';
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.js';
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.js';
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.js';
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.js';
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-CxuyMF39.js';
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.js';
@@ -10,6 +11,7 @@ export { ThemeToggle, ThemeToggleProps } from './theme-toggle.js';
10
11
  export { PublicationNetworkItem, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, listedPublications, localPublications } from './network.js';
11
12
  export { LocalNetworkBar, LocalNetworkBarProps } from './network-bar.js';
12
13
  import * as react from 'react';
14
+ export { VerifiedCardComponent, VerifiedCardSection, getArticleCards, isSafeActionUrl } from './verified-card.js';
13
15
 
14
16
  /** Walnut Creek Times broadsheet colors. A paper overrides only what it changes. */
15
17
  declare const BROADSHEET_THEME: PublicationTheme;
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, ThemeToggle, applyTheme, clearTheme, getStoredTheme, getSystemTheme, resolveEffectiveTheme, setTheme, subscribeTheme, themeInitScript } from './chunk-NQD3M7ZY.js';
2
2
  export { LocalNetworkBar } from './chunk-QBT66OVE.js';
3
+ export { VerifiedCardComponent, VerifiedCardSection, getArticleCards, isSafeActionUrl } from './chunk-I3ZTQCV3.js';
3
4
  import { definePublication } from './chunk-C7JTEHTH.js';
4
5
  export { BROADSHEET_THEME, FEATURE_NAMES, LAYOUT_COOKIE, acceptRemoteRecord, acceptSnapshot, definePublication, featureEnabled, parseSnapshot, publicationFromSnapshot, publicationRecord, resolveLayout, snapshotFromRecord, syncPublication } from './chunk-C7JTEHTH.js';
5
6
  export { CLASSIFIED_CATEGORIES, JOB_EMPLOYMENT_TYPES, categoryLabel, classifiedPath, classifiedSearchPath, clipChars, dealDollars, dealGoPath, dealsFeedPath, employmentLabel, isClassifiedCategory, isEmploymentType, jobEmployerBody, jobOpeningBody, jobPostingBody, prepareClassified, prepareJobPost, publicJobPath, publicJobsPath } from './chunk-C7ZPLH2K.js';
@@ -0,0 +1,48 @@
1
+ import { P as PublicationInput, N as NewsPublication } from './types-DFDCZHbS.cjs';
2
+
3
+ /**
4
+ * Serializable application config. Code hooks (`matchTag`, `coupons.resolve`)
5
+ * stay in the site and are attached after the snapshot loads.
6
+ */
7
+ type PublicationRecord = Omit<PublicationInput, "matchTag" | "coupons"> & {
8
+ coupons?: Omit<NonNullable<PublicationInput["coupons"]>, "resolve">;
9
+ };
10
+ /** Last config the application accepted. Age never invalidates it. */
11
+ type PublicationSnapshot = {
12
+ version: 1;
13
+ syncedAt: string;
14
+ record: PublicationRecord;
15
+ };
16
+ type SnapshotHooks = {
17
+ matchTag?: PublicationInput["matchTag"];
18
+ resolveCoupon?: NonNullable<PublicationInput["coupons"]>["resolve"];
19
+ };
20
+ type SyncSource = "fresh" | "retained";
21
+ type SyncedPublication = {
22
+ publication: NewsPublication;
23
+ snapshot: PublicationSnapshot;
24
+ source: SyncSource;
25
+ /** How long this snapshot has been the one on disk. Zero for a fresh sync. */
26
+ ageMs: number;
27
+ };
28
+ declare function publicationRecord(publication: NewsPublication): PublicationRecord;
29
+ declare function snapshotFromRecord(record: PublicationRecord, syncedAt?: string): PublicationSnapshot;
30
+ declare function parseSnapshot(raw: string): PublicationSnapshot | null;
31
+ declare function acceptSnapshot(value: unknown): PublicationSnapshot | null;
32
+ /** A remote payload replaces the snapshot only when it is a complete record for this paper. */
33
+ declare function acceptRemoteRecord(value: unknown, expectedSlug?: string): PublicationRecord | null;
34
+ declare function publicationFromSnapshot(snapshot: PublicationSnapshot, hooks?: SnapshotHooks): NewsPublication;
35
+ /**
36
+ * Fetch the application config. Any failure, timeout, or malformed body keeps
37
+ * the previous snapshot. Nothing here expires a snapshot because the API was
38
+ * unreachable for hours or days.
39
+ */
40
+ declare function syncPublication(input: {
41
+ previous: PublicationSnapshot | null;
42
+ fetchRecord: () => Promise<unknown>;
43
+ expectedSlug?: string;
44
+ now?: number;
45
+ hooks?: SnapshotHooks;
46
+ }): Promise<SyncedPublication>;
47
+
48
+ export { type PublicationSnapshot as P, type SnapshotHooks as S, type SyncedPublication as a, type PublicationRecord as b, type SyncSource as c, acceptRemoteRecord as d, acceptSnapshot as e, publicationFromSnapshot as f, publicationRecord as g, syncPublication as h, parseSnapshot as p, snapshotFromRecord as s };
@@ -0,0 +1,48 @@
1
+ import { P as PublicationInput, N as NewsPublication } from './types-DFDCZHbS.js';
2
+
3
+ /**
4
+ * Serializable application config. Code hooks (`matchTag`, `coupons.resolve`)
5
+ * stay in the site and are attached after the snapshot loads.
6
+ */
7
+ type PublicationRecord = Omit<PublicationInput, "matchTag" | "coupons"> & {
8
+ coupons?: Omit<NonNullable<PublicationInput["coupons"]>, "resolve">;
9
+ };
10
+ /** Last config the application accepted. Age never invalidates it. */
11
+ type PublicationSnapshot = {
12
+ version: 1;
13
+ syncedAt: string;
14
+ record: PublicationRecord;
15
+ };
16
+ type SnapshotHooks = {
17
+ matchTag?: PublicationInput["matchTag"];
18
+ resolveCoupon?: NonNullable<PublicationInput["coupons"]>["resolve"];
19
+ };
20
+ type SyncSource = "fresh" | "retained";
21
+ type SyncedPublication = {
22
+ publication: NewsPublication;
23
+ snapshot: PublicationSnapshot;
24
+ source: SyncSource;
25
+ /** How long this snapshot has been the one on disk. Zero for a fresh sync. */
26
+ ageMs: number;
27
+ };
28
+ declare function publicationRecord(publication: NewsPublication): PublicationRecord;
29
+ declare function snapshotFromRecord(record: PublicationRecord, syncedAt?: string): PublicationSnapshot;
30
+ declare function parseSnapshot(raw: string): PublicationSnapshot | null;
31
+ declare function acceptSnapshot(value: unknown): PublicationSnapshot | null;
32
+ /** A remote payload replaces the snapshot only when it is a complete record for this paper. */
33
+ declare function acceptRemoteRecord(value: unknown, expectedSlug?: string): PublicationRecord | null;
34
+ declare function publicationFromSnapshot(snapshot: PublicationSnapshot, hooks?: SnapshotHooks): NewsPublication;
35
+ /**
36
+ * Fetch the application config. Any failure, timeout, or malformed body keeps
37
+ * the previous snapshot. Nothing here expires a snapshot because the API was
38
+ * unreachable for hours or days.
39
+ */
40
+ declare function syncPublication(input: {
41
+ previous: PublicationSnapshot | null;
42
+ fetchRecord: () => Promise<unknown>;
43
+ expectedSlug?: string;
44
+ now?: number;
45
+ hooks?: SnapshotHooks;
46
+ }): Promise<SyncedPublication>;
47
+
48
+ export { type PublicationSnapshot as P, type SnapshotHooks as S, type SyncedPublication as a, type PublicationRecord as b, type SyncSource as c, acceptRemoteRecord as d, acceptSnapshot as e, publicationFromSnapshot as f, publicationRecord as g, syncPublication as h, parseSnapshot as p, snapshotFromRecord as s };
@@ -1,4 +1,5 @@
1
- import { P as PublicationSnapshot, S as SnapshotHooks, a as SyncedPublication } from './snapshot-OZFP3-Up.cjs';
1
+ import { P as PublicationSnapshot, S as SnapshotHooks, a as SyncedPublication } from './snapshot-C0IGpt-B.cjs';
2
+ import './types-DFDCZHbS.cjs';
2
3
 
3
4
  declare function readSnapshotFile(file: string): Promise<PublicationSnapshot | null>;
4
5
  /**