@tokenoftrust/storefront-runner 2.0.2 → 2.0.4

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 (254) hide show
  1. package/apps/storefront/.astro/content.d.ts +159 -0
  2. package/apps/storefront/.astro/integrations/_astrojs_cloudflare/cloudflare.d.ts +1 -0
  3. package/apps/storefront/.astro/types.d.ts +3 -0
  4. package/apps/storefront/node_modules/.bin/acorn +43 -0
  5. package/apps/storefront/node_modules/.bin/astro +43 -0
  6. package/apps/storefront/node_modules/.bin/astro-check +43 -0
  7. package/apps/storefront/node_modules/.bin/browserslist +43 -0
  8. package/apps/storefront/node_modules/.bin/cf-wrangler +43 -0
  9. package/apps/storefront/node_modules/.bin/depcruise +43 -0
  10. package/apps/storefront/node_modules/.bin/depcruise-baseline +43 -0
  11. package/apps/storefront/node_modules/.bin/depcruise-fmt +43 -0
  12. package/apps/storefront/node_modules/.bin/depcruise-wrap-stream-in-html +43 -0
  13. package/apps/storefront/node_modules/.bin/dependency-cruise +43 -0
  14. package/apps/storefront/node_modules/.bin/dependency-cruiser +43 -0
  15. package/apps/storefront/node_modules/.bin/drizzle-kit +43 -0
  16. package/apps/storefront/node_modules/.bin/esbuild +43 -0
  17. package/apps/storefront/node_modules/.bin/jiti +43 -0
  18. package/apps/storefront/node_modules/.bin/playwright +43 -0
  19. package/apps/storefront/node_modules/.bin/playwright-core +43 -0
  20. package/apps/storefront/node_modules/.bin/prettier +43 -0
  21. package/apps/storefront/node_modules/.bin/rollup +43 -0
  22. package/apps/storefront/node_modules/.bin/tsc +43 -0
  23. package/apps/storefront/node_modules/.bin/tsserver +43 -0
  24. package/apps/storefront/node_modules/.bin/tsx +43 -0
  25. package/apps/storefront/node_modules/.bin/vite +43 -0
  26. package/apps/storefront/node_modules/.bin/vitest +43 -0
  27. package/apps/storefront/node_modules/.bin/workerd +43 -0
  28. package/apps/storefront/node_modules/.bin/wrangler +43 -0
  29. package/apps/storefront/node_modules/.bin/wrangler2 +43 -0
  30. package/apps/storefront/node_modules/.bin/yaml +43 -0
  31. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_client__js.js +58 -0
  32. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_client__js.js.map +1 -0
  33. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_n_@preact_signals.js +741 -0
  34. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_n_@preact_signals.js.map +1 -0
  35. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_server__js.js +2549 -0
  36. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_server__js.js.map +1 -0
  37. package/apps/storefront/node_modules/.vite/deps/_metadata.json +150 -0
  38. package/apps/storefront/node_modules/.vite/deps/astro-BIHuWFxm.js +434 -0
  39. package/apps/storefront/node_modules/.vite/deps/astro-BIHuWFxm.js.map +1 -0
  40. package/apps/storefront/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  41. package/apps/storefront/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  42. package/apps/storefront/node_modules/.vite/deps/astro_n_axobject-query.js +2705 -0
  43. package/apps/storefront/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  44. package/apps/storefront/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  45. package/apps/storefront/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  46. package/apps/storefront/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  47. package/apps/storefront/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  48. package/apps/storefront/node_modules/.vite/deps/audit-DKDpcdEI.js +1535 -0
  49. package/apps/storefront/node_modules/.vite/deps/audit-DKDpcdEI.js.map +1 -0
  50. package/apps/storefront/node_modules/.vite/deps/highlight-B9UoX1wa.js +48 -0
  51. package/apps/storefront/node_modules/.vite/deps/highlight-B9UoX1wa.js.map +1 -0
  52. package/apps/storefront/node_modules/.vite/deps/icons-BWi2wI9n.js +42 -0
  53. package/apps/storefront/node_modules/.vite/deps/icons-BWi2wI9n.js.map +1 -0
  54. package/apps/storefront/node_modules/.vite/deps/package.json +3 -0
  55. package/apps/storefront/node_modules/.vite/deps/preact.js +310 -0
  56. package/apps/storefront/node_modules/.vite/deps/preact.js.map +1 -0
  57. package/apps/storefront/node_modules/.vite/deps/preact_compat.js +420 -0
  58. package/apps/storefront/node_modules/.vite/deps/preact_compat.js.map +1 -0
  59. package/apps/storefront/node_modules/.vite/deps/preact_compat_jsx-runtime.js +4 -0
  60. package/apps/storefront/node_modules/.vite/deps/preact_debug.js +236 -0
  61. package/apps/storefront/node_modules/.vite/deps/preact_debug.js.map +1 -0
  62. package/apps/storefront/node_modules/.vite/deps/preact_devtools.js +12 -0
  63. package/apps/storefront/node_modules/.vite/deps/preact_devtools.js.map +1 -0
  64. package/apps/storefront/node_modules/.vite/deps/preact_hooks.js +192 -0
  65. package/apps/storefront/node_modules/.vite/deps/preact_hooks.js.map +1 -0
  66. package/apps/storefront/node_modules/.vite/deps/preact_jsx-dev-runtime.js +3 -0
  67. package/apps/storefront/node_modules/.vite/deps/preact_jsx-runtime.js +92 -0
  68. package/apps/storefront/node_modules/.vite/deps/preact_jsx-runtime.js.map +1 -0
  69. package/apps/storefront/node_modules/.vite/deps/preact_test-utils.js +59 -0
  70. package/apps/storefront/node_modules/.vite/deps/preact_test-utils.js.map +1 -0
  71. package/apps/storefront/node_modules/.vite/deps/rolldown-runtime-BPOCksWG.js +24 -0
  72. package/apps/storefront/node_modules/.vite/deps/settings-BVDV0Roj.js +189 -0
  73. package/apps/storefront/node_modules/.vite/deps/settings-BVDV0Roj.js.map +1 -0
  74. package/apps/storefront/node_modules/.vite/deps/static-html-DveYqwMo.js +13 -0
  75. package/apps/storefront/node_modules/.vite/deps/static-html-DveYqwMo.js.map +1 -0
  76. package/apps/storefront/node_modules/.vite/deps/toolbar-B3VIHCXg.js +458 -0
  77. package/apps/storefront/node_modules/.vite/deps/toolbar-B3VIHCXg.js.map +1 -0
  78. package/apps/storefront/node_modules/.vite/deps/ui-library-BK6ricUd.js +1016 -0
  79. package/apps/storefront/node_modules/.vite/deps/ui-library-BK6ricUd.js.map +1 -0
  80. package/apps/storefront/node_modules/.vite/deps/window-BvxDenKN.js +32 -0
  81. package/apps/storefront/node_modules/.vite/deps/window-BvxDenKN.js.map +1 -0
  82. package/apps/storefront/node_modules/.vite/deps/xray-YFiRUU0Z.js +113 -0
  83. package/apps/storefront/node_modules/.vite/deps/xray-YFiRUU0Z.js.map +1 -0
  84. package/apps/storefront/node_modules/.vite/deps_ssr/_metadata.json +8 -0
  85. package/apps/storefront/node_modules/.vite/deps_ssr/package.json +3 -0
  86. package/apps/storefront/src/layouts/Layout.astro +1 -1
  87. package/apps/storefront/src/lib/apps/registryAccess.ts +45 -0
  88. package/apps/storefront/src/lib/auth/adminEntry.ts +4 -4
  89. package/apps/storefront/src/lib/shared/capability.ts +68 -0
  90. package/apps/storefront/src/lib/shared/developerInvites.ts +1250 -0
  91. package/apps/storefront/src/lib/shared/invitePreflight.ts +123 -0
  92. package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +38 -4
  93. package/apps/storefront/src/lib/webhooks/emit.ts +1 -1
  94. package/apps/storefront/src/lib/webhooks/getDispatcher.ts +1 -1
  95. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +1 -1
  96. package/apps/storefront/src/pages/index.astro +1 -1
  97. package/apps/storefront/src/pages/products/[handle].astro +1 -1
  98. package/package.json +1 -1
  99. package/packages/public-runtime/node_modules/.bin/tsc +43 -0
  100. package/packages/public-runtime/node_modules/.bin/tsserver +43 -0
  101. package/packages/public-runtime/node_modules/.bin/vitest +43 -0
  102. package/pnpm-workspace.yaml +3 -0
  103. package/scripts/lib/tenant-dirs.mjs +80 -0
  104. package/scripts/tenant/lint-embeds.mjs +180 -0
  105. package/scripts/tenant/materialize-tenant-content.mjs +533 -0
  106. package/scripts/tenant/validate.mjs +837 -0
  107. package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +0 -68
  108. package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +0 -111
  109. package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +0 -2
  110. package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +0 -247
  111. package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +0 -73
  112. package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +0 -79
  113. package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +0 -10982
  114. package/apps/storefront/scripts/import-v84-through-ws2.ts +0 -38
  115. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +0 -2
  116. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +0 -4
  117. package/apps/storefront/scripts/install-v84-native-through-ws1.ts +0 -185
  118. package/apps/storefront/scripts/prove-clone-isolation.ts +0 -506
  119. package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +0 -148
  120. package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +0 -35
  121. package/apps/storefront/scripts/replay-v84-clean-clone.ts +0 -214
  122. package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +0 -18
  123. package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +0 -18
  124. package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +0 -18
  125. package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +0 -17
  126. package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +0 -15
  127. package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +0 -21
  128. package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +0 -42
  129. package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +0 -145
  130. package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +0 -18
  131. package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +0 -531
  132. package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +0 -878
  133. package/apps/storefront/src/components/admin/AdminBlogTab.astro +0 -1435
  134. package/apps/storefront/src/components/admin/AdminCheckoutSettingsPanel.astro +0 -226
  135. package/apps/storefront/src/components/admin/AdminCustomersTab.astro +0 -593
  136. package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +0 -465
  137. package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +0 -105
  138. package/apps/storefront/src/components/admin/AdminProductsTab.astro +0 -901
  139. package/apps/storefront/src/components/admin/AdminPublishTab.astro +0 -2983
  140. package/apps/storefront/src/components/admin/AdminReportingTab.astro +0 -435
  141. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +0 -782
  142. package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +0 -611
  143. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +0 -1223
  144. package/apps/storefront/src/lib/apps/adminService.ts +0 -116
  145. package/apps/storefront/src/lib/apps/adminSession.ts +0 -214
  146. package/apps/storefront/src/lib/apps/apiAuth.ts +0 -91
  147. package/apps/storefront/src/lib/apps/apiRoute.ts +0 -35
  148. package/apps/storefront/src/lib/apps/catalogMapper.ts +0 -39
  149. package/apps/storefront/src/lib/apps/consentConfigStore.ts +0 -228
  150. package/apps/storefront/src/lib/apps/gtmMigrationInstall.ts +0 -447
  151. package/apps/storefront/src/lib/apps/gtmMigrationReport.ts +0 -367
  152. package/apps/storefront/src/lib/apps/gtmMigrationTab.ts +0 -315
  153. package/apps/storefront/src/lib/apps/healthAggregate.ts +0 -66
  154. package/apps/storefront/src/lib/apps/integrationLifecycle.ts +0 -614
  155. package/apps/storefront/src/lib/apps/integrationLifecycleController.ts +0 -125
  156. package/apps/storefront/src/lib/apps/integrationReadModel.ts +0 -563
  157. package/apps/storefront/src/lib/apps/integrationsTab.ts +0 -547
  158. package/apps/storefront/src/lib/apps/integrationsView.ts +0 -106
  159. package/apps/storefront/src/lib/apps/managedAnalyticsService.ts +0 -599
  160. package/apps/storefront/src/lib/apps/tenantIntegrationMaterialization.ts +0 -170
  161. package/apps/storefront/src/lib/apps/tokenIssuer.ts +0 -121
  162. package/apps/storefront/src/lib/apps/tokenVerifier.ts +0 -148
  163. package/apps/storefront/src/lib/apps/v84CleanCloneReplayContext.ts +0 -16
  164. package/apps/storefront/src/lib/apps/v84CloneLifecycleCacheEvidence.ts +0 -105
  165. package/apps/storefront/src/lib/apps/v84CloneLifecycleEvidence.ts +0 -177
  166. package/apps/storefront/src/lib/apps/v84CloneLifecycleMaterialization.ts +0 -251
  167. package/apps/storefront/src/lib/apps/v84CloneLifecycleSemantics.ts +0 -241
  168. package/apps/storefront/src/lib/apps/v84CloneMigrationEvidence.ts +0 -211
  169. package/apps/storefront/src/lib/apps/v84CloneNativeInstallEvidence.ts +0 -409
  170. package/apps/storefront/src/lib/apps/v84NonportableCodeCoaching.ts +0 -368
  171. package/apps/storefront/src/lib/apps/v84NonportableRouteContract.ts +0 -188
  172. package/apps/storefront/src/lib/apps/v84NonportableRouteDogfood.ts +0 -291
  173. package/apps/storefront/src/lib/apps/ws1ManagedAnalyticsClient.ts +0 -95
  174. package/apps/storefront/src/lib/content-edit/applyDiff.ts +0 -91
  175. package/apps/storefront/src/lib/content-edit/client.ts +0 -286
  176. package/apps/storefront/src/lib/content-edit/index.ts +0 -14
  177. package/apps/storefront/src/lib/content-edit/route.ts +0 -25
  178. package/apps/storefront/src/lib/content-edit/types.ts +0 -19
  179. package/apps/storefront/src/lib/dev/cockpitStore.ts +0 -57
  180. package/apps/storefront/src/lib/publish/adminPublishTab.ts +0 -4625
  181. package/apps/storefront/src/lib/publish/apex-readiness.ts +0 -360
  182. package/apps/storefront/src/lib/publish/buildStream.ts +0 -269
  183. package/apps/storefront/src/lib/publish/collapsePersistence.ts +0 -72
  184. package/apps/storefront/src/lib/publish/dispatchHealth.ts +0 -267
  185. package/apps/storefront/src/lib/publish/domainState.ts +0 -360
  186. package/apps/storefront/src/lib/publish/lifecycleState.ts +0 -369
  187. package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +0 -29
  188. package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +0 -78
  189. package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +0 -62
  190. package/apps/storefront/src/lib/publish/mergeRowFindings.ts +0 -124
  191. package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +0 -75
  192. package/apps/storefront/src/lib/publish/panelPolling.ts +0 -78
  193. package/apps/storefront/src/lib/publish/publish-guard.ts +0 -88
  194. package/apps/storefront/src/lib/publish/publishFormat.ts +0 -165
  195. package/apps/storefront/src/lib/publish/publishHero.ts +0 -177
  196. package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +0 -66
  197. package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +0 -141
  198. package/apps/storefront/src/lib/publish/reviewRelease/index.ts +0 -41
  199. package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +0 -47
  200. package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +0 -42
  201. package/apps/storefront/src/lib/publish/reviewRelease/types.ts +0 -59
  202. package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +0 -440
  203. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +0 -161
  204. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +0 -325
  205. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +0 -241
  206. package/apps/storefront/src/lib/publish/shipWorkspace.ts +0 -439
  207. package/apps/storefront/src/lib/publish/sseRoute.ts +0 -119
  208. package/apps/storefront/src/lib/publish/stageBundleUpload.ts +0 -215
  209. package/apps/storefront/src/lib/publish/streamLogConsole.ts +0 -142
  210. package/apps/storefront/src/lib/publish/versionNumber.ts +0 -52
  211. package/apps/storefront/src/pages/admin/ops-timeline.astro +0 -420
  212. package/apps/storefront/src/pages/admin/review-release.astro +0 -265
  213. package/apps/storefront/src/pages/admin/select.astro +0 -96
  214. package/apps/storefront/src/pages/admin.astro +0 -570
  215. package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +0 -86
  216. package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +0 -174
  217. package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +0 -96
  218. package/apps/storefront/src/pages/api/apps/admin/health.ts +0 -44
  219. package/apps/storefront/src/pages/api/apps/admin/install.ts +0 -125
  220. package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +0 -118
  221. package/apps/storefront/src/pages/api/apps/admin/integrations.ts +0 -89
  222. package/apps/storefront/src/pages/api/apps/admin/list.ts +0 -26
  223. package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +0 -85
  224. package/apps/storefront/src/pages/api/apps/admin/resume.ts +0 -79
  225. package/apps/storefront/src/pages/api/apps/admin/suspend.ts +0 -80
  226. package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +0 -98
  227. package/apps/storefront/src/pages/api/apps/admin/update.ts +0 -126
  228. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +0 -70
  229. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +0 -51
  230. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +0 -255
  231. package/apps/storefront/src/pages/api/apps/oauth/token.ts +0 -87
  232. package/apps/storefront/src/pages/api/apps/v1/attribution.ts +0 -162
  233. package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +0 -39
  234. package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +0 -49
  235. package/apps/storefront/src/pages/api/apps/v1/health.ts +0 -32
  236. package/apps/storefront/src/pages/api/apps/v1/inventory.ts +0 -59
  237. package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +0 -48
  238. package/apps/storefront/src/pages/api/apps/v1/orders.ts +0 -73
  239. package/apps/storefront/src/pages/api/apps/v1/reports.ts +0 -21
  240. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +0 -74
  241. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +0 -47
  242. package/apps/storefront/src/pages/api/cache-purge.ts +0 -55
  243. package/apps/storefront/src/pages/api/compliance/tamper.ts +0 -88
  244. package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +0 -164
  245. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +0 -154
  246. package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +0 -120
  247. package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +0 -108
  248. package/apps/storefront/src/pages/api/publish/stage-bundle.ts +0 -70
  249. package/apps/storefront/src/pages/api/test/dev-session.ts +0 -133
  250. package/apps/storefront/src/pages/cockpit.astro +0 -1548
  251. package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +0 -119
  252. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +0 -105
  253. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +0 -177
  254. package/apps/storefront/src/pages/dashboard/index.astro +0 -160
@@ -1,878 +0,0 @@
1
- ---
2
- /**
3
- * Blog detail rail — B2.3 of docs/plans/2026-08-21-blog-platform-workstream.md.
4
- *
5
- * A row-select side-out for the admin Blog tab, mirroring the established
6
- * OrdersWorkspace pattern (components/admin/orders/OrdersWorkspace.astro): the
7
- * list panel and this sticky `aside` sit in a two-column workspace; one hidden
8
- * `[data-blog-detail-record]` is pre-rendered per post and the row-select script
9
- * toggles which one is visible + which row is `.selected`. Records are keyed by
10
- * `(collection, slug)` — the same `data-collection`/`data-slug` the B2.1 table
11
- * rows already carry — so this component adds NO markup to the table itself
12
- * (keeps merge friction with the concurrent B3.4 editor low).
13
- *
14
- * The rail shows: status, category/pillar, tags (claim tags), author, last-saved,
15
- * a preview link, a Draft→Preview→Publish→Live lifecycle timeline, the primary
16
- * Publish/Unpublish action, and Edit/Duplicate/Delete. Publish/Unpublish/Duplicate/
17
- * Delete ride the SAME content-only fast lane as the table (POST /api/admin/blog);
18
- * they open a content-only change to review & promote in the Publish tab. Edit
19
- * (in-browser authoring) is B3.4 — stubbed disabled here.
20
- *
21
- * B3.2 adds a Versions panel: per-post git-backed revision history (list +
22
- * diff) read through `/api/admin/blog-history` (MCP's session-authenticated
23
- * `repo_commits` / `repo_diff`, `@/lib/forge`). Restore re-submits a prior
24
- * revision's raw content through the SAME content-only fast lane (POST to
25
- * the same route, `action:"restore"`) — no new write path.
26
- */
27
- import { categorySlug } from "@/lib/blog/collection.js";
28
- import { REACTIONS, type Reaction } from "@/lib/blog/reactions.js";
29
- import type { BlogPostSummary, PostStatus } from "@/lib/blog/types.js";
30
-
31
- interface Props {
32
- rows: { collection: string; post: BlogPostSummary }[];
33
- /**
34
- * C1 editorial link-out: per-post open candidate PR, keyed `"<collection> <slug>"`.
35
- * Present only when the post has an OPEN candidate awaiting review; absent ⇒ the
36
- * post is idle/published and no Discussion link renders.
37
- */
38
- discussions?: Record<string, { prNumber: number; url: string }>;
39
- /**
40
- * E1.2 reader reaction tallies, keyed `"<collection> <slug>"` (same key as the
41
- * record). Absent ⇒ no counts fetched (all-zero line renders).
42
- */
43
- reactions?: Record<string, Record<Reaction, number>>;
44
- }
45
-
46
- const { rows, discussions = {}, reactions = {} } = Astro.props;
47
-
48
- const REACTION_META: Record<Reaction, { emoji: string; label: string }> = {
49
- like: { emoji: "👍", label: "Like" },
50
- insightful: { emoji: "💡", label: "Insightful" },
51
- celebrate: { emoji: "🎉", label: "Celebrate" },
52
- };
53
-
54
- const STATUS_LABEL: Record<PostStatus, string> = {
55
- published: "Published",
56
- draft: "Draft",
57
- scheduled: "Scheduled",
58
- unlisted: "Unlisted",
59
- };
60
- const STATUS_TONE: Record<PostStatus, string> = {
61
- published: "green",
62
- draft: "",
63
- scheduled: "blue",
64
- unlisted: "amber",
65
- };
66
-
67
- // The published lifecycle the timeline visualises. A post's status maps to the
68
- // stage it currently occupies; earlier stages render "done", the current stage
69
- // "current", later stages "pending". "Preview" is a transient stage (a candidate
70
- // change is open) we can't detect from the post's static status alone, so it is
71
- // shown as passed once a post is live and pending while it is still a draft.
72
- const STAGES = ["Draft", "Preview", "Publish", "Live"] as const;
73
- const STAGE_INDEX: Record<PostStatus, number> = {
74
- draft: 0,
75
- scheduled: 2,
76
- unlisted: 3,
77
- published: 3,
78
- };
79
-
80
- const primaryDate = (post: BlogPostSummary) => post.modifiedAt || post.publishedAt;
81
- const formatDate = (iso: string) => {
82
- const d = new Date(iso);
83
- return Number.isNaN(d.getTime())
84
- ? "—"
85
- : d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
86
- };
87
- // Reader URL for a post: /<collection>/<slug>/ (collection id === route base).
88
- const previewHref = (collection: string, slug: string) => `/${collection}/${slug}/`;
89
- ---
90
-
91
- <aside
92
- class="panel blog-detail-rail"
93
- id="blog-detail"
94
- aria-labelledby="blog-detail-title"
95
- data-blog-detail-root
96
- data-selected-collection={rows[0]?.collection ?? ""}
97
- data-selected-slug={rows[0]?.post.slug ?? ""}
98
- >
99
- {rows.map(({ collection, post }, index) => {
100
- const stage = STAGE_INDEX[post.status];
101
- const isPublished = post.status === "published";
102
- const discussion = discussions[`${collection} ${post.slug}`];
103
- const postReactions = reactions[`${collection} ${post.slug}`];
104
- return (
105
- <div
106
- class="blog-detail-record"
107
- data-blog-detail-record
108
- data-collection={collection}
109
- data-slug={post.slug}
110
- hidden={index !== 0}
111
- >
112
- <div class="blog-detail-head">
113
- <div class="blog-detail-head-main">
114
- <span class={`chip blog-status${STATUS_TONE[post.status] ? " " + STATUS_TONE[post.status] : ""}`}>
115
- {STATUS_LABEL[post.status]}
116
- </span>
117
- <h3 id={index === 0 ? "blog-detail-title" : undefined}>{post.title}</h3>
118
- <p class="blog-detail-slug">
119
- <code>{collection}/{post.slug}</code>
120
- </p>
121
- </div>
122
- </div>
123
-
124
- <ol class="blog-lifecycle" aria-label="Publishing lifecycle">
125
- {STAGES.map((label, i) => (
126
- <li
127
- class:list={[
128
- "blog-lifecycle-step",
129
- { done: i < stage, current: i === stage, pending: i > stage },
130
- ]}
131
- aria-current={i === stage ? "step" : undefined}
132
- >
133
- <span class="blog-lifecycle-dot" aria-hidden="true"></span>
134
- <span class="blog-lifecycle-label">{label}</span>
135
- </li>
136
- ))}
137
- </ol>
138
-
139
- <div class="blog-detail-grid">
140
- <div>
141
- <span>Category</span>
142
- <strong>{post.categories.length > 0 ? post.categories.join(", ") : "—"}</strong>
143
- </div>
144
- <div>
145
- <span>Author</span>
146
- <strong>{post.author || "—"}</strong>
147
- </div>
148
- <div>
149
- <span>Last saved</span>
150
- <strong>{formatDate(primaryDate(post))}</strong>
151
- </div>
152
- {post.status === "scheduled" && post.scheduledAt && (
153
- <div>
154
- <span>Scheduled</span>
155
- <strong>{formatDate(post.scheduledAt)}</strong>
156
- </div>
157
- )}
158
- </div>
159
-
160
- <div class="blog-detail-engagement">
161
- <span class="blog-detail-engagement-label">Reactions</span>
162
- <div class="blog-detail-reactions">
163
- {REACTIONS.map((reaction) => (
164
- <span class="blog-detail-reaction" title={REACTION_META[reaction].label}>
165
- <span aria-hidden="true">{REACTION_META[reaction].emoji}</span>
166
- <strong>{postReactions?.[reaction] ?? 0}</strong>
167
- </span>
168
- ))}
169
- </div>
170
- </div>
171
-
172
- {post.tags.length > 0 && (
173
- <div class="blog-detail-tags">
174
- <span class="blog-detail-tags-label">Tags</span>
175
- <div class="blog-detail-tags-list">
176
- {post.tags.map((tag) => (
177
- <span class="chip">{tag}</span>
178
- ))}
179
- </div>
180
- </div>
181
- )}
182
-
183
- <a
184
- class="blog-detail-preview"
185
- href={previewHref(collection, post.slug)}
186
- target="_blank"
187
- rel="noopener"
188
- >
189
- <svg><use href="#i-file"></use></svg>
190
- <span>{isPublished ? "View on site" : "Preview (visible once published)"}</span>
191
- </a>
192
-
193
- {discussion && (
194
- <a
195
- class="blog-detail-discussion"
196
- href={discussion.url}
197
- target="_blank"
198
- rel="noopener"
199
- >
200
- <svg><use href="#i-ask"></use></svg>
201
- <span>Discussion (#{discussion.prNumber})</span>
202
- <span class="blog-detail-discussion-out" aria-hidden="true">↗</span>
203
- </a>
204
- )}
205
-
206
- <div class="blog-detail-actions">
207
- {isPublished ? (
208
- <button
209
- class="btn"
210
- type="button"
211
- data-blog-rail-action="unpublish"
212
- data-blog-slug={post.slug}
213
- data-blog-collection={collection}
214
- >
215
- Unpublish
216
- </button>
217
- ) : (
218
- <button
219
- class="btn primary"
220
- type="button"
221
- data-blog-rail-action="publish"
222
- data-blog-slug={post.slug}
223
- data-blog-collection={collection}
224
- >
225
- Publish
226
- </button>
227
- )}
228
- {/* Edit (in-browser authoring) is B3.4 — inert placeholder here. */}
229
- <button
230
- class="btn"
231
- type="button"
232
- data-blog-rail-action="edit"
233
- data-blog-slug={post.slug}
234
- data-blog-collection={collection}
235
- title="In-browser editing arrives with the markdown editor (B3.4)"
236
- disabled
237
- >
238
- Edit
239
- </button>
240
- <button
241
- class="btn"
242
- type="button"
243
- data-blog-rail-action="duplicate"
244
- data-blog-slug={post.slug}
245
- data-blog-collection={collection}
246
- data-blog-title={post.title}
247
- >
248
- Duplicate
249
- </button>
250
- <button
251
- class="btn danger"
252
- type="button"
253
- data-blog-rail-action="delete"
254
- data-blog-slug={post.slug}
255
- data-blog-collection={collection}
256
- >
257
- Delete
258
- </button>
259
- <button
260
- class="btn"
261
- type="button"
262
- data-blog-rail-action="versions"
263
- data-blog-slug={post.slug}
264
- data-blog-collection={collection}
265
- aria-expanded="false"
266
- aria-controls={`blog-versions-${collection}-${post.slug}`}
267
- >
268
- Versions
269
- </button>
270
- </div>
271
-
272
- <div
273
- class="blog-versions"
274
- id={`blog-versions-${collection}-${post.slug}`}
275
- data-blog-versions
276
- hidden
277
- >
278
- <p class="blog-versions-status" data-blog-versions-status>Loading versions…</p>
279
- <ol class="blog-versions-list" data-blog-versions-list></ol>
280
- </div>
281
- </div>
282
- );
283
- })}
284
- </aside>
285
-
286
- <style>
287
- .blog-detail-rail {
288
- position: sticky;
289
- top: 86px;
290
- display: grid;
291
- gap: 12px;
292
- align-content: start;
293
- padding: 14px;
294
- box-shadow: var(--admin-shadow);
295
- min-width: 0;
296
- }
297
-
298
- .blog-detail-head {
299
- padding-bottom: 12px;
300
- border-bottom: 1px solid var(--admin-line);
301
- }
302
- .blog-detail-head h3 {
303
- margin: 8px 0 4px;
304
- font-size: 20px;
305
- overflow-wrap: anywhere;
306
- }
307
- .blog-detail-slug {
308
- margin: 0;
309
- font-size: 12px;
310
- }
311
- .blog-detail-slug code {
312
- color: var(--admin-muted);
313
- overflow-wrap: anywhere;
314
- }
315
-
316
- /* Draft → Preview → Publish → Live mini-timeline. */
317
- .blog-lifecycle {
318
- display: grid;
319
- grid-template-columns: repeat(4, 1fr);
320
- gap: 0;
321
- margin: 0;
322
- padding: 0;
323
- list-style: none;
324
- }
325
- .blog-lifecycle-step {
326
- display: grid;
327
- justify-items: center;
328
- gap: 6px;
329
- position: relative;
330
- padding-top: 4px;
331
- }
332
- /* Connector line between dots. */
333
- .blog-lifecycle-step::before {
334
- content: "";
335
- position: absolute;
336
- top: 10px;
337
- left: -50%;
338
- width: 100%;
339
- height: 2px;
340
- background: var(--admin-line-strong);
341
- }
342
- .blog-lifecycle-step:first-child::before {
343
- display: none;
344
- }
345
- .blog-lifecycle-step.done::before,
346
- .blog-lifecycle-step.current::before {
347
- background: var(--admin-teal);
348
- }
349
- .blog-lifecycle-dot {
350
- width: 13px;
351
- height: 13px;
352
- border-radius: 999px;
353
- border: 2px solid var(--admin-line-strong);
354
- background: var(--admin-surface);
355
- z-index: 1;
356
- }
357
- .blog-lifecycle-step.done .blog-lifecycle-dot {
358
- border-color: var(--admin-teal);
359
- background: var(--admin-teal);
360
- }
361
- .blog-lifecycle-step.current .blog-lifecycle-dot {
362
- border-color: var(--admin-teal);
363
- background: var(--admin-surface);
364
- box-shadow: 0 0 0 3px var(--admin-teal-soft);
365
- }
366
- .blog-lifecycle-label {
367
- font-size: 11px;
368
- font-weight: 760;
369
- color: var(--admin-muted);
370
- }
371
- .blog-lifecycle-step.current .blog-lifecycle-label,
372
- .blog-lifecycle-step.done .blog-lifecycle-label {
373
- color: var(--admin-ink);
374
- }
375
-
376
- .blog-detail-grid {
377
- display: grid;
378
- grid-template-columns: repeat(2, minmax(0, 1fr));
379
- gap: 8px;
380
- }
381
- .blog-detail-grid > div {
382
- min-width: 0;
383
- padding: 10px;
384
- border: 1px solid var(--admin-line);
385
- border-radius: var(--admin-radius-sm);
386
- background: var(--admin-surface-soft);
387
- }
388
- .blog-detail-grid span {
389
- color: var(--admin-muted);
390
- font-size: 11px;
391
- font-weight: 760;
392
- text-transform: uppercase;
393
- }
394
- .blog-detail-grid strong {
395
- display: block;
396
- margin-top: 4px;
397
- overflow-wrap: anywhere;
398
- font-size: 13px;
399
- }
400
-
401
- .blog-detail-engagement {
402
- display: grid;
403
- gap: 6px;
404
- }
405
- .blog-detail-engagement-label {
406
- color: var(--admin-muted);
407
- font-size: 11px;
408
- font-weight: 760;
409
- text-transform: uppercase;
410
- }
411
- .blog-detail-reactions {
412
- display: flex;
413
- flex-wrap: wrap;
414
- gap: 6px;
415
- }
416
- .blog-detail-reaction {
417
- display: inline-flex;
418
- align-items: center;
419
- gap: 5px;
420
- padding: 5px 9px;
421
- border: 1px solid var(--admin-line);
422
- border-radius: var(--admin-radius-sm);
423
- background: var(--admin-surface-soft);
424
- font-size: 13px;
425
- }
426
- .blog-detail-reaction strong {
427
- font-variant-numeric: tabular-nums;
428
- }
429
-
430
- .blog-detail-tags {
431
- display: grid;
432
- gap: 6px;
433
- }
434
- .blog-detail-tags-label {
435
- color: var(--admin-muted);
436
- font-size: 11px;
437
- font-weight: 760;
438
- text-transform: uppercase;
439
- }
440
- .blog-detail-tags-list {
441
- display: flex;
442
- flex-wrap: wrap;
443
- gap: 6px;
444
- }
445
-
446
- .blog-detail-preview {
447
- display: inline-flex;
448
- align-items: center;
449
- gap: 8px;
450
- padding: 9px 12px;
451
- border: 1px solid var(--admin-line-strong);
452
- border-radius: var(--admin-radius-sm);
453
- background: var(--admin-surface);
454
- color: var(--admin-ink);
455
- font-size: 13px;
456
- font-weight: 700;
457
- text-decoration: none;
458
- }
459
- .blog-detail-preview svg {
460
- width: 15px;
461
- height: 15px;
462
- }
463
- .blog-detail-preview:hover {
464
- border-color: var(--admin-teal);
465
- }
466
-
467
- .blog-detail-discussion {
468
- display: inline-flex;
469
- align-items: center;
470
- gap: 8px;
471
- padding: 9px 12px;
472
- border: 1px solid var(--admin-line-strong);
473
- border-radius: var(--admin-radius-sm);
474
- background: var(--admin-surface);
475
- color: var(--admin-ink);
476
- font-size: 13px;
477
- font-weight: 700;
478
- text-decoration: none;
479
- }
480
- .blog-detail-discussion svg {
481
- width: 15px;
482
- height: 15px;
483
- }
484
- .blog-detail-discussion:hover {
485
- border-color: var(--admin-teal);
486
- }
487
- .blog-detail-discussion-out {
488
- margin-left: auto;
489
- color: var(--admin-muted);
490
- }
491
-
492
- .blog-detail-actions {
493
- display: flex;
494
- flex-wrap: wrap;
495
- gap: 6px;
496
- padding-top: 12px;
497
- border-top: 1px solid var(--admin-line);
498
- }
499
- .blog-detail-actions .btn.danger {
500
- color: var(--admin-danger, #b42318);
501
- border-color: var(--admin-danger, #b42318);
502
- }
503
- .blog-detail-actions .btn.primary {
504
- flex: 1 1 auto;
505
- }
506
-
507
- .blog-status {
508
- text-transform: none;
509
- }
510
-
511
- .blog-versions {
512
- display: grid;
513
- gap: 8px;
514
- padding-top: 12px;
515
- border-top: 1px solid var(--admin-line);
516
- }
517
- .blog-versions-status {
518
- margin: 0;
519
- color: var(--admin-muted);
520
- font-size: 12px;
521
- }
522
- .blog-versions-list {
523
- display: grid;
524
- gap: 8px;
525
- margin: 0;
526
- padding: 0;
527
- list-style: none;
528
- }
529
- .blog-versions-item {
530
- display: grid;
531
- gap: 6px;
532
- padding: 10px;
533
- border: 1px solid var(--admin-line);
534
- border-radius: var(--admin-radius-sm);
535
- background: var(--admin-surface-soft);
536
- }
537
- .blog-versions-item-head {
538
- display: flex;
539
- align-items: baseline;
540
- justify-content: space-between;
541
- gap: 8px;
542
- flex-wrap: wrap;
543
- }
544
- .blog-versions-item-message {
545
- font-size: 13px;
546
- font-weight: 700;
547
- overflow-wrap: anywhere;
548
- }
549
- .blog-versions-item-meta {
550
- color: var(--admin-muted);
551
- font-size: 11px;
552
- }
553
- .blog-versions-item-actions {
554
- display: flex;
555
- gap: 6px;
556
- }
557
- .blog-versions-item-actions .btn {
558
- padding: 5px 10px;
559
- font-size: 12px;
560
- }
561
- .blog-versions-diff {
562
- margin: 0;
563
- padding: 8px;
564
- max-height: 260px;
565
- overflow: auto;
566
- border: 1px solid var(--admin-line);
567
- border-radius: var(--admin-radius-sm);
568
- background: var(--admin-surface);
569
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
570
- font-size: 11.5px;
571
- line-height: 1.5;
572
- white-space: pre-wrap;
573
- word-break: break-word;
574
- }
575
-
576
- @media (max-width: 1180px) {
577
- .blog-detail-rail {
578
- position: static;
579
- }
580
- }
581
- @media (max-width: 760px) {
582
- .blog-detail-grid {
583
- grid-template-columns: 1fr;
584
- }
585
- }
586
- </style>
587
-
588
- <script is:inline nonce={Astro.locals.cspNonce}>
589
- (() => {
590
- const root = document.getElementById("blog");
591
- const rail = root?.querySelector("[data-blog-detail-root]");
592
- if (!root || !rail) return;
593
-
594
- const rows = Array.from(root.querySelectorAll("[data-blog-row]"));
595
- const records = Array.from(rail.querySelectorAll("[data-blog-detail-record]"));
596
-
597
- const keyOf = (el) => (el.dataset.collection || "") + "" + (el.dataset.slug || "");
598
-
599
- const select = (collection, slug) => {
600
- const key = collection + "" + slug;
601
- for (const row of rows) {
602
- const on = keyOf(row) === key;
603
- row.classList.toggle("selected", on);
604
- row.setAttribute("aria-pressed", on ? "true" : "false");
605
- }
606
- for (const record of records) {
607
- record.hidden = keyOf(record) !== key;
608
- }
609
- rail.dataset.selectedCollection = collection;
610
- rail.dataset.selectedSlug = slug;
611
- };
612
-
613
- // Make table rows a keyboard-operable "open detail" affordance without editing
614
- // the B2.1 table markup: promote each row to a button-like control here.
615
- rows.forEach((row) => {
616
- row.setAttribute("role", "button");
617
- row.setAttribute("tabindex", "0");
618
- row.setAttribute("aria-controls", "blog-detail");
619
- const open = (event) => {
620
- // Don't hijack clicks/keys on the row's own action controls.
621
- if (event.target instanceof Element && event.target.closest("button, a, input, select")) {
622
- return;
623
- }
624
- const { collection, slug } = row.dataset;
625
- if (collection && slug) select(collection, slug);
626
- };
627
- row.addEventListener("click", open);
628
- row.addEventListener("keydown", (event) => {
629
- if (event.key === "Enter" || event.key === " ") {
630
- event.preventDefault();
631
- open(event);
632
- }
633
- });
634
- });
635
-
636
- // Rail action buttons ride the same content-only fast lane as the table.
637
- const msg = root.querySelector("[data-blog-msg]");
638
- const showMsg = (text, tone) => {
639
- if (!msg) return;
640
- msg.textContent = text;
641
- if (tone) msg.setAttribute("data-tone", tone);
642
- else msg.removeAttribute("data-tone");
643
- msg.hidden = !text;
644
- };
645
- const post = async (payload) => {
646
- const res = await fetch("/api/admin/blog", {
647
- method: "POST",
648
- headers: { "content-type": "application/json" },
649
- body: JSON.stringify(payload),
650
- });
651
- let data = {};
652
- try {
653
- data = await res.json();
654
- } catch {
655
- /* non-JSON error body */
656
- }
657
- return { ok: res.ok, status: res.status, data };
658
- };
659
- const submitted = (data) => {
660
- const pr = data && data.prNumber ? " (#" + data.prNumber + ")" : "";
661
- showMsg("Change opened" + pr + " — review & publish it in the Publish tab.", null);
662
- };
663
- const failed = (data, status) => {
664
- const why = (data && data.message) || "request failed (" + status + ")";
665
- showMsg("Could not complete: " + why, "error");
666
- };
667
-
668
- rail.querySelectorAll("[data-blog-rail-action]").forEach((button) => {
669
- const action = button.dataset.blogRailAction;
670
- // edit is B3.4 (inert); versions is a panel toggle, wired separately below.
671
- if (action === "edit" || action === "versions" || button.disabled) return;
672
- button.addEventListener("click", async () => {
673
- const collection = button.dataset.blogCollection;
674
- const slug = button.dataset.blogSlug;
675
- if (!collection || !slug) return;
676
-
677
- let payload;
678
- if (action === "duplicate") {
679
- // Duplicate = create a fresh draft copy via the existing `new` action.
680
- const title = ((button.dataset.blogTitle || slug) + " (copy)").trim();
681
- payload = { action: "new", collection, title };
682
- } else {
683
- if (action === "delete" && !confirm('Delete "' + slug + '"? This opens a content-only change.')) {
684
- return;
685
- }
686
- payload = { action, collection, slug };
687
- }
688
-
689
- button.disabled = true;
690
- showMsg("Working…", null);
691
- try {
692
- const { ok, status, data } = await post(payload);
693
- if (ok && data.ok) submitted(data);
694
- else failed(data, status);
695
- } catch (err) {
696
- failed({ message: String(err) }, 0);
697
- } finally {
698
- button.disabled = false;
699
- }
700
- });
701
- });
702
-
703
- // Versions panel (B3.2): lazy-loaded git-backed revision history per post,
704
- // read via /api/admin/blog-history (repo_commits / repo_diff). Restore rides
705
- // the SAME content-only fast lane (postHistory below → the route's own
706
- // getChangeClient().openCandidate, keyed by the same blog-<collection>-<slug>
707
- // changeId as every other blog action).
708
- const shortSha = (sha) => String(sha || "").slice(0, 7);
709
- const versionsLoaded = new Set(); // "collection/slug" already fetched once
710
-
711
- const fetchJson = async (url) => {
712
- const res = await fetch(url);
713
- let data = {};
714
- try {
715
- data = await res.json();
716
- } catch {
717
- /* non-JSON error body */
718
- }
719
- return { ok: res.ok, status: res.status, data };
720
- };
721
- const postHistory = async (payload) => {
722
- const res = await fetch("/api/admin/blog-history", {
723
- method: "POST",
724
- headers: { "content-type": "application/json" },
725
- body: JSON.stringify(payload),
726
- });
727
- let data = {};
728
- try {
729
- data = await res.json();
730
- } catch {
731
- /* non-JSON error body */
732
- }
733
- return { ok: res.ok, status: res.status, data };
734
- };
735
-
736
- function renderVersions(panel, collection, slug, commits) {
737
- const list = panel.querySelector("[data-blog-versions-list]");
738
- const status = panel.querySelector("[data-blog-versions-status]");
739
- list.innerHTML = "";
740
- if (!commits.length) {
741
- status.textContent = "No revision history yet.";
742
- status.hidden = false;
743
- return;
744
- }
745
- status.hidden = true;
746
-
747
- commits.forEach((commit, index) => {
748
- const li = document.createElement("li");
749
- li.className = "blog-versions-item";
750
-
751
- const head = document.createElement("div");
752
- head.className = "blog-versions-item-head";
753
- const message = document.createElement("span");
754
- message.className = "blog-versions-item-message";
755
- message.textContent = commit.message || "(no message)";
756
- const meta = document.createElement("span");
757
- meta.className = "blog-versions-item-meta";
758
- const when = commit.date ? new Date(commit.date).toLocaleString() : "";
759
- meta.textContent =
760
- shortSha(commit.sha) + " · " + (commit.author || "unknown") + (when ? " · " + when : "");
761
- head.append(message, meta);
762
-
763
- const actions = document.createElement("div");
764
- actions.className = "blog-versions-item-actions";
765
-
766
- const diffBtn = document.createElement("button");
767
- diffBtn.type = "button";
768
- diffBtn.className = "btn";
769
- diffBtn.textContent = "Diff";
770
- const parent = commits[index + 1];
771
- if (!parent) {
772
- diffBtn.disabled = true;
773
- diffBtn.title = "No earlier loaded revision to diff against";
774
- }
775
-
776
- const restoreBtn = document.createElement("button");
777
- restoreBtn.type = "button";
778
- restoreBtn.className = "btn";
779
- restoreBtn.textContent = "Restore";
780
-
781
- actions.append(diffBtn, restoreBtn);
782
- li.append(head, actions);
783
-
784
- const diffPre = document.createElement("pre");
785
- diffPre.className = "blog-versions-diff";
786
- diffPre.hidden = true;
787
- li.append(diffPre);
788
-
789
- diffBtn.addEventListener("click", async () => {
790
- if (!diffPre.hidden) {
791
- diffPre.hidden = true;
792
- return;
793
- }
794
- if (!parent) return;
795
- diffBtn.disabled = true;
796
- diffPre.hidden = false;
797
- diffPre.textContent = "Loading diff…";
798
- const url =
799
- "/api/admin/blog-history?collection=" + encodeURIComponent(collection) +
800
- "&slug=" + encodeURIComponent(slug) +
801
- "&base=" + encodeURIComponent(parent.sha) +
802
- "&head=" + encodeURIComponent(commit.sha);
803
- const { ok, data } = await fetchJson(url);
804
- diffBtn.disabled = false;
805
- if (!ok || !data.ok) {
806
- diffPre.textContent = "Could not load diff: " + ((data && data.message) || "request failed");
807
- return;
808
- }
809
- const files = data.files || [];
810
- diffPre.textContent = files.length
811
- ? files.map((f) => f.patch).join("\n\n")
812
- : "No changes between these revisions.";
813
- });
814
-
815
- restoreBtn.addEventListener("click", async () => {
816
- const ok0 = confirm(
817
- 'Restore "' + slug + '" to revision ' + shortSha(commit.sha) +
818
- '? This opens a content-only change.',
819
- );
820
- if (!ok0) return;
821
- restoreBtn.disabled = true;
822
- showMsg("Working…", null);
823
- try {
824
- const { ok, status: code, data } = await postHistory({
825
- action: "restore", collection, slug, sha: commit.sha,
826
- });
827
- if (ok && data.ok) submitted(data);
828
- else failed(data, code);
829
- } catch (err) {
830
- failed({ message: String(err) }, 0);
831
- } finally {
832
- restoreBtn.disabled = false;
833
- }
834
- });
835
-
836
- list.append(li);
837
- });
838
- }
839
-
840
- rail.querySelectorAll('[data-blog-rail-action="versions"]').forEach((button) => {
841
- button.addEventListener("click", async () => {
842
- const collection = button.dataset.blogCollection;
843
- const slug = button.dataset.blogSlug;
844
- const controls = button.getAttribute("aria-controls");
845
- if (!collection || !slug || !controls) return;
846
- const panel = document.getElementById(controls);
847
- if (!panel) return;
848
-
849
- const wasHidden = panel.hidden;
850
- panel.hidden = !wasHidden;
851
- button.setAttribute("aria-expanded", wasHidden ? "true" : "false");
852
- if (!wasHidden) return; // just closed — nothing to load
853
-
854
- const key = collection + "/" + slug;
855
- if (versionsLoaded.has(key)) return; // already fetched once this page load
856
-
857
- const status = panel.querySelector("[data-blog-versions-status]");
858
- const url =
859
- "/api/admin/blog-history?collection=" + encodeURIComponent(collection) +
860
- "&slug=" + encodeURIComponent(slug);
861
- const { ok, data } = await fetchJson(url);
862
- if (!ok || !data.ok) {
863
- status.textContent = "Could not load versions: " + ((data && data.message) || "request failed");
864
- status.hidden = false;
865
- return; // leave un-cached so re-opening retries
866
- }
867
- versionsLoaded.add(key);
868
- renderVersions(panel, collection, slug, data.commits || []);
869
- });
870
- });
871
-
872
- // Default selection: the first row (its record is the one rendered visible).
873
- if (rows[0]) {
874
- const { collection, slug } = rows[0].dataset;
875
- if (collection && slug) select(collection, slug);
876
- }
877
- })();
878
- </script>