@agent-cms/agentcms 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +143 -0
  2. package/README.md +1119 -2
  3. package/changelog.json +20 -0
  4. package/dist/admin/admin-files.json +48 -0
  5. package/dist/admin/assets/CodeEditor-DcFzGUt8.js +24 -0
  6. package/dist/admin/assets/flexweg-logo-CdtuqAFA.png +0 -0
  7. package/dist/admin/assets/index-CLZ1gQZl.js +5460 -0
  8. package/dist/admin/assets/index-DMcGogR2.css +1 -0
  9. package/dist/admin/assets/sql-wasm-C1U8OeUW.wasm +0 -0
  10. package/dist/admin/assets/sql-wasm-CYl3LXH_.js +2 -0
  11. package/dist/admin/assets/sql-wasm-V5jTDcrT.js +1 -0
  12. package/dist/admin/config.js +11 -0
  13. package/dist/admin/external.default.json +72 -0
  14. package/dist/admin/index.html +116 -0
  15. package/dist/admin/plugins/core-seo/bundle.js +55 -0
  16. package/dist/admin/plugins/core-seo/manifest.json +7 -0
  17. package/dist/admin/plugins/flexweg-archives/bundle.js +1414 -0
  18. package/dist/admin/plugins/flexweg-archives/manifest.json +7 -0
  19. package/dist/admin/plugins/flexweg-multilang/bundle.js +1187 -0
  20. package/dist/admin/plugins/flexweg-multilang/manifest.json +7 -0
  21. package/dist/admin/plugins/flexweg-rss/bundle.js +1354 -0
  22. package/dist/admin/plugins/flexweg-rss/manifest.json +7 -0
  23. package/dist/admin/plugins/flexweg-search/bundle.js +1243 -0
  24. package/dist/admin/plugins/flexweg-search/manifest.json +7 -0
  25. package/dist/admin/plugins/flexweg-sitemaps/bundle.js +1293 -0
  26. package/dist/admin/plugins/flexweg-sitemaps/manifest.json +7 -0
  27. package/dist/admin/runtime/cms-runtime.js +164 -0
  28. package/dist/admin/runtime/react-dom-client.js +13 -0
  29. package/dist/admin/runtime/react-dom.js +16 -0
  30. package/dist/admin/runtime/react-i18next.js +21 -0
  31. package/dist/admin/runtime/react-jsx-runtime.js +17 -0
  32. package/dist/admin/runtime/react.js +55 -0
  33. package/dist/admin/themes/corporate/bundle.js +14465 -0
  34. package/dist/admin/themes/corporate/manifest.json +7 -0
  35. package/dist/admin/themes/corporate/theme.css +2 -0
  36. package/dist/admin/themes/magazine/bundle.js +10229 -0
  37. package/dist/admin/themes/magazine/manifest.json +7 -0
  38. package/dist/admin/themes/magazine/theme.css +2 -0
  39. package/dist/admin/themes/marketplace-core/bundle.js +11213 -0
  40. package/dist/admin/themes/marketplace-core/manifest.json +7 -0
  41. package/dist/admin/themes/marketplace-core/theme.css +1613 -0
  42. package/dist/admin/themes/portfolio/bundle.js +9195 -0
  43. package/dist/admin/themes/portfolio/manifest.json +7 -0
  44. package/dist/admin/themes/portfolio/theme.css +2 -0
  45. package/dist/admin/themes/storefront/bundle.js +13172 -0
  46. package/dist/admin/themes/storefront/manifest.json +7 -0
  47. package/dist/admin/themes/storefront/theme.css +2 -0
  48. package/dist/agent/agentcms.mjs +1711 -0
  49. package/dist/agent/chunks/hosting-D_uj-Mt1.mjs +117 -0
  50. package/dist/agent/chunks/index-CrvEoyi0.mjs +3608 -0
  51. package/dist/agent/chunks/install-DkCCcKAO.mjs +577 -0
  52. package/dist/agent/chunks/plan-EYKegfhp.mjs +245 -0
  53. package/dist/agent/chunks/registry-lkfb_E9O.mjs +53517 -0
  54. package/dist/agent/chunks/siteAdd-BqcYt8nu.mjs +118 -0
  55. package/dist/agent/chunks/sql-wasm-GjJDiefO.mjs +10 -0
  56. package/dist/agent/chunks/sqljs-DjSCStJ0.mjs +20 -0
  57. package/dist/agent/vendor/sql-wasm.cjs +188 -0
  58. package/dist/theme-assets/corporate-menu.js +266 -0
  59. package/dist/theme-assets/corporate-posts.js +571 -0
  60. package/dist/theme-assets/corporate.css +2 -0
  61. package/dist/theme-assets/default-menu.js +266 -0
  62. package/dist/theme-assets/default-posts.js +378 -0
  63. package/dist/theme-assets/default.css +1 -0
  64. package/dist/theme-assets/magazine-menu.js +227 -0
  65. package/dist/theme-assets/magazine-posts.js +361 -0
  66. package/dist/theme-assets/magazine.css +2 -0
  67. package/dist/theme-assets/portfolio-filters.js +104 -0
  68. package/dist/theme-assets/portfolio-menu.js +240 -0
  69. package/dist/theme-assets/portfolio-posts.js +358 -0
  70. package/dist/theme-assets/portfolio.css +2 -0
  71. package/dist/theme-assets/storefront-catalog.js +504 -0
  72. package/dist/theme-assets/storefront-menu.js +239 -0
  73. package/dist/theme-assets/storefront-posts.js +404 -0
  74. package/dist/theme-assets/storefront.css +2 -0
  75. package/dist/version.json +9 -0
  76. package/package.json +101 -4
  77. package/plugin/.claude-plugin/plugin.json +16 -0
  78. package/plugin/skills/agentcms-install/SKILL.md +110 -0
  79. package/plugin/skills/agentcms-install/references/cloudflare.md +15 -0
  80. package/plugin/skills/agentcms-install/references/flexweg.md +11 -0
  81. package/plugin/skills/agentcms-install/references/github.md +67 -0
  82. package/plugin/skills/agentcms-install/references/gitlab.md +15 -0
  83. package/plugin/skills/agentcms-install/references/s3.md +81 -0
  84. package/plugin/skills/agentcms-manage/SKILL.md +92 -0
@@ -0,0 +1,571 @@
1
+ /**
2
+ * Corporate theme — runtime sidebar loader.
3
+ *
4
+ * Populates `[data-cms-related]` and `[data-cms-author-bio]` hosts
5
+ * from `/data/posts.json` and `/data/authors.json`. Same lifecycle as
6
+ * the magazine + default themes: fetched once on DOMContentLoaded;
7
+ * paint passes are independent and tolerate each other's failure.
8
+ *
9
+ * The injected DOM uses corporate-specific class names
10
+ * (`corporate-bio-*`, `corporate-related-*`) — matching CSS rules
11
+ * live in `theme.css` under `@layer components`.
12
+ *
13
+ * Failure is silent: missing data leaves the host hidden (related)
14
+ * or untouched (author-bio's host stays `hidden` from publish time).
15
+ */
16
+ (function () {
17
+ "use strict";
18
+
19
+ // ─── Base path ────────────────────────────────────────────────────
20
+ // Sites served from a sub-folder (e.g. GitHub Pages project sites)
21
+ // carry <meta name="x-cms-base" content="/repo/"> in <head>; no meta
22
+ // means the site lives at the domain root. URLs built at runtime from
23
+ // JSON data go through withBase() so they land under that folder.
24
+ var BASE = (function () {
25
+ var meta = document.querySelector('meta[name="x-cms-base"]');
26
+ var b = (meta && meta.getAttribute("content")) || "/";
27
+ if (b.charAt(0) !== "/") b = "/" + b;
28
+ if (b.charAt(b.length - 1) !== "/") b += "/";
29
+ return b;
30
+ })();
31
+
32
+ function withBase(url) {
33
+ if (BASE === "/" || !url) return url;
34
+ // Absolute URLs (http:, mailto:, data:, …), protocol-relative and
35
+ // fragment-only links pass through.
36
+ if (/^([a-z][a-z0-9+.-]*:|\/\/|#)/i.test(url)) return url;
37
+ if (url.indexOf(BASE) === 0 || url === BASE.slice(0, -1)) return url;
38
+ return BASE + (url.charAt(0) === "/" ? url.slice(1) : url);
39
+ }
40
+
41
+ // "/repo/fr/x.html" → "/fr/x.html" so path parsing ignores the base.
42
+ function stripBase(pathname) {
43
+ if (BASE === "/") return pathname;
44
+ if (pathname.indexOf(BASE) === 0) return "/" + pathname.slice(BASE.length);
45
+ if (pathname === BASE.slice(0, -1)) return "/";
46
+ return pathname;
47
+ }
48
+
49
+ // ─── Social icons ─────────────────────────────────────────────────
50
+ // Brand SVG paths + display labels for the curated SocialNetwork
51
+ // list. Mirror in core/socialIcons.tsx (and each other theme's
52
+ // posts-loader.js) when adding a network.
53
+ var SOCIAL_ICON_PATHS = {
54
+ twitter: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z",
55
+ linkedin: "M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z",
56
+ instagram: "M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z",
57
+ mastodon: "M23.193 7.88c0-5.207-3.411-6.733-3.411-6.733C18.062.357 15.108.025 12.041 0h-.076c-3.068.025-6.02.357-7.74 1.147 0 0-3.412 1.526-3.412 6.733 0 1.193-.023 2.619.015 4.13.124 5.092.934 10.11 5.641 11.355 2.17.574 4.034.695 5.535.612 2.722-.151 4.25-.972 4.25-.972l-.09-1.975s-1.945.613-4.129.539c-2.165-.074-4.449-.233-4.799-2.891a5.499 5.499 0 0 1-.048-.745s2.125.52 4.817.643c1.646.075 3.19-.097 4.758-.283 3.007-.359 5.625-2.212 5.954-3.905.52-2.667.477-6.508.477-6.508zm-4.024 6.709h-2.497V8.469c0-1.29-.543-1.944-1.628-1.944-1.2 0-1.802.776-1.802 2.312v3.349h-2.484v-3.35c0-1.536-.601-2.312-1.802-2.312-1.085 0-1.628.655-1.628 1.945v6.119H4.831V8.285c0-1.29.328-2.313.987-3.07.68-.756 1.57-1.144 2.674-1.144 1.278 0 2.246.491 2.886 1.474L12 6.585l.622-1.04c.64-.983 1.608-1.474 2.886-1.474 1.104 0 1.994.388 2.674 1.144.658.757.986 1.78.986 3.07v6.304z",
58
+ bluesky: "M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.137-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.952 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z",
59
+ github: "M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12",
60
+ website: "M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95a15.65 15.65 0 0 0-1.38-3.56A8.03 8.03 0 0 1 18.92 8zM12 4.04c.83 1.2 1.48 2.53 1.91 3.96h-3.82c.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2 0 .68.06 1.34.14 2H4.26zm.82 2h2.95c.32 1.25.78 2.45 1.38 3.56A7.987 7.987 0 0 1 5.08 16zm2.95-8H5.08a7.987 7.987 0 0 1 4.33-3.56A15.65 15.65 0 0 0 8.03 8zM12 19.96c-.83-1.2-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-.09-.66-.16-1.32-.16-2 0-.68.07-1.35.16-2h4.68c.09.65.16 1.32.16 2 0 .68-.07 1.34-.16 2zm.25 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95a8.03 8.03 0 0 1-4.33 3.56zM16.36 14c.08-.66.14-1.32.14-2 0-.68-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2h-3.38z",
61
+ };
62
+ var SOCIAL_ICON_LABELS = {
63
+ twitter: "Twitter / X",
64
+ linkedin: "LinkedIn",
65
+ instagram: "Instagram",
66
+ mastodon: "Mastodon",
67
+ bluesky: "Bluesky",
68
+ github: "GitHub",
69
+ website: "Website",
70
+ };
71
+ function socialIconSvg(network) {
72
+ var path = SOCIAL_ICON_PATHS[network];
73
+ if (!path) return "";
74
+ return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="' + path + '"/></svg>';
75
+ }
76
+ function socialIconLabel(network) {
77
+ return SOCIAL_ICON_LABELS[network] || network;
78
+ }
79
+
80
+ function fetchJson(path) {
81
+ return fetch(withBase(path), { credentials: "omit" })
82
+ .then(function (res) {
83
+ if (!res.ok) throw new Error("HTTP " + res.status);
84
+ return res.json();
85
+ })
86
+ .catch(function (err) {
87
+ console.warn("[cms] " + path + " load failed:", err);
88
+ return null;
89
+ });
90
+ }
91
+
92
+ // ─── Related / popular articles ───────────────────────────────────
93
+ //
94
+ // DOM emitted matches the corporate single-post mockup — a card with
95
+ // a heading and a list of items; each item is an <a> with a colored
96
+ // category eyebrow, line-clamped title, and a small date meta line.
97
+ //
98
+ // <div class="corporate-related-card">
99
+ // <h4 class="corporate-related-heading">Articles populaires</h4>
100
+ // <div class="corporate-related-list">
101
+ // <a class="corporate-related-item" href="/...">
102
+ // <p class="corporate-related-eyebrow">Category</p>
103
+ // <p class="corporate-related-title">Title</p>
104
+ // <p class="corporate-related-meta">Jan 12 • 5 min read</p>
105
+ // </a>
106
+ // …
107
+ // </div>
108
+ // </div>
109
+ //
110
+ // The card chrome (background/border/padding) lives in the host
111
+ // element itself via `.corporate-related-card` — see the
112
+ // SingleTemplate sidebar markup.
113
+
114
+ function renderRelatedItem(entry) {
115
+ var a = document.createElement("a");
116
+ a.href = withBase("/" + entry.url);
117
+ a.className = "corporate-related-item";
118
+ if (entry.category && entry.category.name) {
119
+ var eyebrow = document.createElement("p");
120
+ eyebrow.className = "corporate-related-eyebrow";
121
+ eyebrow.textContent = entry.category.name;
122
+ a.appendChild(eyebrow);
123
+ }
124
+ var title = document.createElement("p");
125
+ title.className = "corporate-related-title";
126
+ title.textContent = entry.title || "";
127
+ a.appendChild(title);
128
+ if (entry.dateLabel) {
129
+ var meta = document.createElement("p");
130
+ meta.className = "corporate-related-meta";
131
+ meta.textContent = entry.dateLabel;
132
+ a.appendChild(meta);
133
+ }
134
+ return a;
135
+ }
136
+
137
+ function renderRelated(host, posts) {
138
+ var termId = host.getAttribute("data-cms-term-id") || "";
139
+ var currentId = host.getAttribute("data-cms-current-id") || "";
140
+ var limit = parseInt(host.getAttribute("data-cms-limit") || "3", 10);
141
+ var label = host.getAttribute("data-cms-label") || "Popular articles";
142
+ var fallbackLabel =
143
+ host.getAttribute("data-cms-fallback-label") || "Latest articles";
144
+
145
+ var matching = [];
146
+ var heading = label;
147
+
148
+ if (termId) {
149
+ for (var i = 0; i < posts.length && matching.length < limit; i++) {
150
+ var p = posts[i];
151
+ if (p.id === currentId) continue;
152
+ if (p.type !== "post") continue;
153
+ if (p.primaryTermId !== termId) continue;
154
+ matching.push(p);
155
+ }
156
+ }
157
+
158
+ if (matching.length === 0) {
159
+ heading = fallbackLabel;
160
+ for (var j = 0; j < posts.length && matching.length < limit; j++) {
161
+ var pp = posts[j];
162
+ if (pp.id === currentId) continue;
163
+ if (pp.type !== "post") continue;
164
+ matching.push(pp);
165
+ }
166
+ }
167
+
168
+ if (matching.length === 0) {
169
+ host.setAttribute("hidden", "");
170
+ return;
171
+ }
172
+
173
+ host.removeAttribute("hidden");
174
+ host.innerHTML = "";
175
+ var headingEl = document.createElement("h4");
176
+ headingEl.className = "corporate-related-heading";
177
+ headingEl.textContent = heading;
178
+ host.appendChild(headingEl);
179
+ var list = document.createElement("div");
180
+ list.className = "corporate-related-list";
181
+ matching.forEach(function (entry) {
182
+ list.appendChild(renderRelatedItem(entry));
183
+ });
184
+ host.appendChild(list);
185
+ }
186
+
187
+ // ─── Author bio ───────────────────────────────────────────────────
188
+ //
189
+ // DOM emitted (matches the corporate single-post sidebar mockup —
190
+ // round avatar + name in headline weight + label-caps title +
191
+ // bio paragraph + circular social icon row):
192
+ //
193
+ // <h4 class="corporate-bio-eyebrow">Author</h4>
194
+ // <div class="corporate-bio-row">
195
+ // <img class="corporate-bio-avatar" />
196
+ // <div>
197
+ // <p class="corporate-bio-name">…</p>
198
+ // <p class="corporate-bio-title">Senior Strategist</p>
199
+ // </div>
200
+ // </div>
201
+ // <p class="corporate-bio-bio">…</p>
202
+ // <div class="corporate-bio-socials">
203
+ // <a href="…" class="corporate-bio-social"> [icon] </a>
204
+ // </div>
205
+
206
+ function authorAvatar(name, url) {
207
+ if (!url) {
208
+ // Fallback initials box — same color tokens as the avatar
209
+ // when the author hasn't uploaded an image.
210
+ var span = document.createElement("span");
211
+ span.className = "corporate-bio-avatar";
212
+ span.style.display = "inline-flex";
213
+ span.style.alignItems = "center";
214
+ span.style.justifyContent = "center";
215
+ span.style.background = "rgb(var(--color-secondary-fixed))";
216
+ span.style.color = "rgb(var(--color-secondary))";
217
+ span.style.fontWeight = "700";
218
+ span.setAttribute("aria-hidden", "true");
219
+ span.textContent = (function () {
220
+ if (!name) return "?";
221
+ var parts = name.trim().split(/\s+/);
222
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
223
+ return ((parts[0][0] || "") + (parts[parts.length - 1][0] || "")).toUpperCase();
224
+ })();
225
+ return span;
226
+ }
227
+ var img = document.createElement("img");
228
+ img.className = "corporate-bio-avatar";
229
+ img.src = withBase(url);
230
+ img.alt = name || "";
231
+ img.loading = "lazy";
232
+ return img;
233
+ }
234
+
235
+ function renderAuthorBio(host, authors) {
236
+ var authorId = host.getAttribute("data-cms-author-id") || "";
237
+ if (!authorId) return;
238
+ var entry = authors[authorId];
239
+ if (!entry) return;
240
+
241
+ host.innerHTML = "";
242
+
243
+ // Eyebrow label — "Author" or whatever the host configured. The
244
+ // host can override it via data-cms-bio-label for i18n.
245
+ var eyebrow = document.createElement("h4");
246
+ eyebrow.className = "corporate-bio-eyebrow";
247
+ eyebrow.textContent = host.getAttribute("data-cms-bio-label") || "Author";
248
+ host.appendChild(eyebrow);
249
+
250
+ var row = document.createElement("div");
251
+ row.className = "corporate-bio-row";
252
+ row.appendChild(authorAvatar(entry.displayName, entry.avatar));
253
+
254
+ var nameWrap = document.createElement("div");
255
+ var nameEl;
256
+ if (entry.url) {
257
+ nameEl = document.createElement("a");
258
+ nameEl.href = withBase("/" + entry.url);
259
+ } else {
260
+ nameEl = document.createElement("p");
261
+ }
262
+ nameEl.className = "corporate-bio-name";
263
+ nameEl.textContent = entry.displayName || "";
264
+ nameWrap.appendChild(nameEl);
265
+
266
+ if (entry.title) {
267
+ var role = document.createElement("p");
268
+ role.className = "corporate-bio-title";
269
+ role.textContent = entry.title;
270
+ nameWrap.appendChild(role);
271
+ }
272
+ row.appendChild(nameWrap);
273
+ host.appendChild(row);
274
+
275
+ if (entry.bio) {
276
+ var bio = document.createElement("p");
277
+ bio.className = "corporate-bio-bio";
278
+ bio.textContent = entry.bio;
279
+ host.appendChild(bio);
280
+ }
281
+
282
+ // Visible socials — already filtered server-side. Each entry
283
+ // produces an icon link to the user's external profile, opened in
284
+ // a new tab with rel=noopener noreferrer.
285
+ if (entry.socials && entry.socials.length) {
286
+ var socials = document.createElement("div");
287
+ socials.className = "corporate-bio-socials";
288
+ for (var i = 0; i < entry.socials.length; i++) {
289
+ var s = entry.socials[i];
290
+ var a = document.createElement("a");
291
+ a.className = "corporate-bio-social";
292
+ a.href = s.url;
293
+ a.target = "_blank";
294
+ a.rel = "noopener noreferrer";
295
+ a.setAttribute("aria-label", socialIconLabel(s.network));
296
+ a.title = socialIconLabel(s.network);
297
+ a.innerHTML = socialIconSvg(s.network);
298
+ socials.appendChild(a);
299
+ }
300
+ host.appendChild(socials);
301
+ }
302
+
303
+ host.removeAttribute("hidden");
304
+ }
305
+
306
+ // ─── Share buttons ────────────────────────────────────────────────
307
+ //
308
+ // Wires up `[data-cms-share]` buttons in the body. The single-post
309
+ // template emits two: `data-cms-share="native"` (Web Share API
310
+ // fallback to copy-link when unsupported) and `data-cms-share="copy"`
311
+ // (clipboard write of the page URL). Visual feedback is a brief
312
+ // text/title swap so the user knows the click registered without
313
+ // needing a toast library.
314
+
315
+ function showFlash(button, glyphName) {
316
+ var icon = button.querySelector(".material-symbols-outlined");
317
+ if (!icon) return;
318
+ var original = icon.textContent;
319
+ icon.textContent = glyphName;
320
+ setTimeout(function () {
321
+ icon.textContent = original;
322
+ }, 1400);
323
+ }
324
+
325
+ function copyLink(button) {
326
+ var url = window.location.href;
327
+ var done = function () { showFlash(button, "check"); };
328
+ if (navigator.clipboard && navigator.clipboard.writeText) {
329
+ navigator.clipboard.writeText(url).then(done, function () {
330
+ // Fallback when the clipboard write is denied (e.g. insecure
331
+ // context) — select-and-copy the URL via a temporary textarea.
332
+ legacyCopy(url);
333
+ done();
334
+ });
335
+ } else {
336
+ legacyCopy(url);
337
+ done();
338
+ }
339
+ }
340
+
341
+ function legacyCopy(text) {
342
+ try {
343
+ var ta = document.createElement("textarea");
344
+ ta.value = text;
345
+ ta.setAttribute("readonly", "");
346
+ ta.style.position = "absolute";
347
+ ta.style.left = "-9999px";
348
+ document.body.appendChild(ta);
349
+ ta.select();
350
+ document.execCommand("copy");
351
+ document.body.removeChild(ta);
352
+ } catch (e) {
353
+ console.warn("[cms] clipboard copy failed:", e);
354
+ }
355
+ }
356
+
357
+ function shareNative(button) {
358
+ var url = window.location.href;
359
+ var title = document.title || "";
360
+ if (navigator.share) {
361
+ navigator.share({ title: title, url: url }).catch(function () {
362
+ // User cancelled or the share sheet failed — silent.
363
+ });
364
+ } else {
365
+ copyLink(button);
366
+ }
367
+ }
368
+
369
+ function wireShareButtons() {
370
+ document.querySelectorAll("[data-cms-share]").forEach(function (button) {
371
+ var mode = button.getAttribute("data-cms-share");
372
+ button.addEventListener("click", function () {
373
+ if (mode === "copy") copyLink(button);
374
+ else shareNative(button);
375
+ });
376
+ });
377
+ }
378
+
379
+ // ─── Contact form ─────────────────────────────────────────────────
380
+ //
381
+ // Intercepts submit on every `[data-cms-form]` form. Two modes are
382
+ // surfaced in the contact-form block's inspector:
383
+ // - "endpoint" (default) → POST a JSON body to a third-party
384
+ // form service (Formspree, Web3Forms, etc.). Success / error
385
+ // messages from `[data-cms-form-success]` / `[data-cms-form-error]`
386
+ // toggle visibility based on the response status.
387
+ // - "mailto" → build a `mailto:` URL with the subject + body
388
+ // pre-filled, then `window.location.href` to it. No JS-only
389
+ // state to manage.
390
+ // When the form has neither a configured endpoint nor a mailto
391
+ // address (most likely during initial setup), submit is left to
392
+ // the browser's default behavior so the user notices the missing
393
+ // configuration.
394
+
395
+ function asUrlEncoded(formData) {
396
+ var pairs = [];
397
+ formData.forEach(function (value, key) {
398
+ pairs.push(encodeURIComponent(key) + "=" + encodeURIComponent(value));
399
+ });
400
+ return pairs.join("&");
401
+ }
402
+
403
+ function showFormStatus(form, kind) {
404
+ ["success", "error"].forEach(function (k) {
405
+ var el = form.querySelector("[data-cms-form-" + k + "]");
406
+ if (el) {
407
+ if (k === kind) el.removeAttribute("hidden");
408
+ else el.setAttribute("hidden", "");
409
+ }
410
+ });
411
+ }
412
+
413
+ function buildMailto(form, address) {
414
+ var data = new FormData(form);
415
+ var subject = data.get("subject") || "Contact form submission";
416
+ var lines = [];
417
+ data.forEach(function (value, key) {
418
+ if (key === "subject" || key === "consent") return;
419
+ lines.push(key + ": " + value);
420
+ });
421
+ return (
422
+ "mailto:" +
423
+ encodeURIComponent(address) +
424
+ "?subject=" +
425
+ encodeURIComponent(String(subject)) +
426
+ "&body=" +
427
+ encodeURIComponent(lines.join("\n\n"))
428
+ );
429
+ }
430
+
431
+ function handleFormSubmit(form, mode, endpoint, mailtoAddress, event) {
432
+ if (mode === "mailto") {
433
+ if (!mailtoAddress) return; // No config — let the browser handle.
434
+ event.preventDefault();
435
+ window.location.href = buildMailto(form, mailtoAddress);
436
+ return;
437
+ }
438
+ if (!endpoint) return; // No endpoint configured — fall through.
439
+ event.preventDefault();
440
+ var data = new FormData(form);
441
+ var submitButton = form.querySelector("button[type='submit']");
442
+ if (submitButton) submitButton.disabled = true;
443
+ fetch(withBase(endpoint), {
444
+ method: "POST",
445
+ headers: {
446
+ "Content-Type": "application/x-www-form-urlencoded",
447
+ Accept: "application/json",
448
+ },
449
+ body: asUrlEncoded(data),
450
+ })
451
+ .then(function (res) {
452
+ if (!res.ok) throw new Error("HTTP " + res.status);
453
+ showFormStatus(form, "success");
454
+ form.reset();
455
+ })
456
+ .catch(function (err) {
457
+ console.warn("[cms] contact form submit failed:", err);
458
+ showFormStatus(form, "error");
459
+ })
460
+ .finally(function () {
461
+ if (submitButton) submitButton.disabled = false;
462
+ });
463
+ }
464
+
465
+ // ─── Featured posts chevrons ──────────────────────────────────────
466
+ //
467
+ // The home's featured-posts section ships decorative prev/next
468
+ // chevrons (matching the mockup). On md+ the layout is a static
469
+ // 3-col grid — the chevrons scroll smoothly to the next/previous
470
+ // card slot. On mobile (single column with horizontal overflow if
471
+ // the grid is repurposed as a slider in a future iteration), they
472
+ // page through one card width at a time.
473
+ function wireFeaturedChevrons() {
474
+ var track = document.querySelector("[data-cms-featured-track]");
475
+ if (!track) return;
476
+ var prev = document.querySelector("[data-cms-featured-prev]");
477
+ var next = document.querySelector("[data-cms-featured-next]");
478
+ function step(dir) {
479
+ var cards = track.children;
480
+ if (!cards || cards.length === 0) return;
481
+ var card = cards[0];
482
+ var width = card.getBoundingClientRect().width;
483
+ // Read the actual computed gap from the flex container so the
484
+ // scroll step matches the visual rhythm. The `gap` shorthand
485
+ // resolves to a px value in the computed style (e.g. "32px"
486
+ // when Tailwind's gap-stack-lg = 2rem at 16px root font-size).
487
+ var styles = window.getComputedStyle(track);
488
+ var gap = parseFloat(styles.columnGap || styles.gap || "0") || 0;
489
+ track.scrollBy({ left: dir * (width + gap), behavior: "smooth" });
490
+ }
491
+ if (prev) prev.addEventListener("click", function () { step(-1); });
492
+ if (next) next.addEventListener("click", function () { step(1); });
493
+ }
494
+
495
+ function wireContactForms() {
496
+ document.querySelectorAll("[data-cms-form]").forEach(function (form) {
497
+ var mode = form.getAttribute("data-cms-form") || "endpoint";
498
+ var endpoint = form.getAttribute("data-cms-form-endpoint") || "";
499
+ var mailtoAddress = form.getAttribute("data-cms-form-mailto") || "";
500
+ form.addEventListener("submit", function (event) {
501
+ handleFormSubmit(form, mode, endpoint, mailtoAddress, event);
502
+ });
503
+ // Marks each input as filled so the floating-label CSS keeps
504
+ // the label in its raised position even when the field has a
505
+ // pre-filled value (browser autofill, back-button restore).
506
+ form.querySelectorAll("input, textarea, select").forEach(function (input) {
507
+ function update() {
508
+ var group = input.closest(".floating-label-group");
509
+ if (!group) return;
510
+ if (input.value && input.value.length > 0) group.classList.add("is-filled");
511
+ else group.classList.remove("is-filled");
512
+ }
513
+ input.addEventListener("input", update);
514
+ input.addEventListener("change", update);
515
+ update();
516
+ });
517
+ });
518
+ }
519
+
520
+ // ─── Paint passes ─────────────────────────────────────────────────
521
+
522
+ function paintRelated(postsBlob) {
523
+ if (!postsBlob || !Array.isArray(postsBlob.posts)) return;
524
+ document.querySelectorAll("[data-cms-related]").forEach(function (host) {
525
+ renderRelated(host, postsBlob.posts);
526
+ });
527
+ }
528
+
529
+ function paintAuthorBios(authorsBlob) {
530
+ if (!authorsBlob || !authorsBlob.authors) return;
531
+ document.querySelectorAll("[data-cms-author-bio]").forEach(function (host) {
532
+ renderAuthorBio(host, authorsBlob.authors);
533
+ });
534
+ }
535
+
536
+ function ready(fn) {
537
+ if (document.readyState !== "loading") fn();
538
+ else document.addEventListener("DOMContentLoaded", fn);
539
+ }
540
+
541
+ // Multi-language helper — same locale prefix detection as the
542
+ // other themes (see default/posts-loader.js for the rationale).
543
+ function detectLocalePrefix() {
544
+ var path = stripBase(window.location.pathname).replace(/^\/+/, "");
545
+ var first = path.split("/")[0];
546
+ if (!first) return null;
547
+ if (/^[a-z]{2}(-[a-z]{2})?$/.test(first.toLowerCase())) return first.toLowerCase();
548
+ return null;
549
+ }
550
+ function fetchWithLocaleFallback(filename) {
551
+ var locale = detectLocalePrefix();
552
+ if (!locale) return fetchJson("/data/" + filename);
553
+ return fetchJson("/" + locale + "/data/" + filename).then(function (data) {
554
+ if (data) return data;
555
+ return fetchJson("/data/" + filename);
556
+ });
557
+ }
558
+
559
+ ready(function () {
560
+ wireShareButtons();
561
+ wireContactForms();
562
+ wireFeaturedChevrons();
563
+ Promise.all([
564
+ fetchWithLocaleFallback("posts.json"),
565
+ fetchWithLocaleFallback("authors.json"),
566
+ ]).then(function (results) {
567
+ paintRelated(results[0]);
568
+ paintAuthorBios(results[1]);
569
+ });
570
+ });
571
+ })();