@meith/web 0.16.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 (511) hide show
  1. package/LICENSE.md +165 -0
  2. package/app/(auth)/layout.tsx +6 -0
  3. package/app/(auth)/login/page.tsx +106 -0
  4. package/app/(auth)/login/verify/page.tsx +55 -0
  5. package/app/(auth)/register/page.tsx +83 -0
  6. package/app/(auth)/reset/confirm/page.tsx +45 -0
  7. package/app/(auth)/reset/page.tsx +22 -0
  8. package/app/(auth)/verify/page.tsx +44 -0
  9. package/app/(auth)/verify/resend/page.tsx +51 -0
  10. package/app/(board)/[slug]/feed.xml/route.ts +48 -0
  11. package/app/(board)/[slug]/new/page.tsx +112 -0
  12. package/app/(board)/[slug]/page.tsx +403 -0
  13. package/app/(board)/discover/[view]/page.tsx +115 -0
  14. package/app/(board)/discover/page.tsx +5 -0
  15. package/app/(board)/layout.tsx +22 -0
  16. package/app/(board)/member/[id]/page.tsx +187 -0
  17. package/app/(board)/member/[id]/reputation/page.tsx +195 -0
  18. package/app/(board)/member/by-name/[name]/page.tsx +32 -0
  19. package/app/(board)/messages/[id]/page.tsx +103 -0
  20. package/app/(board)/messages/compose/page.tsx +60 -0
  21. package/app/(board)/messages/layout.tsx +5 -0
  22. package/app/(board)/messages/page.tsx +179 -0
  23. package/app/(board)/modcp/forums/page.tsx +59 -0
  24. package/app/(board)/modcp/ip/page.tsx +158 -0
  25. package/app/(board)/modcp/layout.tsx +5 -0
  26. package/app/(board)/modcp/log/page.tsx +98 -0
  27. package/app/(board)/modcp/page.tsx +112 -0
  28. package/app/(board)/moderation/layout.tsx +5 -0
  29. package/app/(board)/moderation/page.tsx +111 -0
  30. package/app/(board)/moderation/reports/page.tsx +193 -0
  31. package/app/(board)/moderation/warn/page.tsx +158 -0
  32. package/app/(board)/notifications/layout.tsx +5 -0
  33. package/app/(board)/notifications/page.tsx +144 -0
  34. package/app/(board)/notifications/preferences/page.tsx +65 -0
  35. package/app/(board)/online/page.tsx +119 -0
  36. package/app/(board)/page.tsx +195 -0
  37. package/app/(board)/plugins/[key]/[[...path]]/page.tsx +72 -0
  38. package/app/(board)/privacy/page.tsx +12 -0
  39. package/app/(board)/report/page.tsx +71 -0
  40. package/app/(board)/rules/page.tsx +12 -0
  41. package/app/(board)/search/[token]/page.tsx +130 -0
  42. package/app/(board)/search/page.tsx +289 -0
  43. package/app/(board)/stats/page.tsx +202 -0
  44. package/app/(board)/subscriptions/layout.tsx +5 -0
  45. package/app/(board)/subscriptions/page.tsx +142 -0
  46. package/app/(board)/terms/page.tsx +12 -0
  47. package/app/(board)/thread/[slug]/edit/page.tsx +107 -0
  48. package/app/(board)/thread/[slug]/feed.xml/route.ts +42 -0
  49. package/app/(board)/thread/[slug]/page.tsx +707 -0
  50. package/app/(board)/thread/[slug]/poll/page.tsx +41 -0
  51. package/app/(board)/thread/[slug]/post/[postId]/history/page.tsx +160 -0
  52. package/app/(board)/thread/[slug]/reply/page.tsx +136 -0
  53. package/app/(board)/unsubscribe/page.tsx +73 -0
  54. package/app/(board)/usercp/avatar/page.tsx +56 -0
  55. package/app/(board)/usercp/contacts/page.tsx +141 -0
  56. package/app/(board)/usercp/email/confirm/page.tsx +52 -0
  57. package/app/(board)/usercp/layout.tsx +23 -0
  58. package/app/(board)/usercp/options/page.tsx +56 -0
  59. package/app/(board)/usercp/page.tsx +71 -0
  60. package/app/(board)/usercp/profile/page.tsx +81 -0
  61. package/app/(board)/usercp/security/page.tsx +238 -0
  62. package/app/(board)/usercp/security/verify/page.tsx +92 -0
  63. package/app/(board)/usercp/signature/page.tsx +67 -0
  64. package/app/admin/antispam/page.tsx +155 -0
  65. package/app/admin/api/marketplace/screenshot/route.ts +68 -0
  66. package/app/admin/api/plugins/[key]/[...path]/route.ts +27 -0
  67. package/app/admin/api-tokens/page.tsx +129 -0
  68. package/app/admin/content/announcements/page.tsx +110 -0
  69. package/app/admin/content/attachments/page.tsx +199 -0
  70. package/app/admin/content/navigation/page.tsx +123 -0
  71. package/app/admin/content/page.tsx +170 -0
  72. package/app/admin/forums/[id]/page.tsx +96 -0
  73. package/app/admin/forums/[id]/permissions/page.tsx +108 -0
  74. package/app/admin/forums/page.tsx +45 -0
  75. package/app/admin/groups/[id]/page.tsx +133 -0
  76. package/app/admin/groups/memberships/page.tsx +56 -0
  77. package/app/admin/groups/page.tsx +104 -0
  78. package/app/admin/groups/promotions/page.tsx +143 -0
  79. package/app/admin/layout.tsx +86 -0
  80. package/app/admin/log/page.tsx +116 -0
  81. package/app/admin/page.tsx +206 -0
  82. package/app/admin/plugins/[key]/[[...path]]/page.tsx +276 -0
  83. package/app/admin/plugins/browse/page.tsx +70 -0
  84. package/app/admin/plugins/page.tsx +165 -0
  85. package/app/admin/security/page.tsx +124 -0
  86. package/app/admin/settings/page.tsx +190 -0
  87. package/app/admin/system/page.tsx +303 -0
  88. package/app/admin/themes/[key]/page.tsx +110 -0
  89. package/app/admin/themes/browse/page.tsx +70 -0
  90. package/app/admin/themes/page.tsx +163 -0
  91. package/app/admin/users/[id]/merge/page.tsx +136 -0
  92. package/app/admin/users/[id]/page.tsx +208 -0
  93. package/app/admin/users/mail/page.tsx +68 -0
  94. package/app/admin/users/page.tsx +260 -0
  95. package/app/admin/users/prune/page.tsx +186 -0
  96. package/app/api/health/route.ts +11 -0
  97. package/app/api/metrics/route.ts +66 -0
  98. package/app/api/plugins/[key]/[...path]/route.ts +27 -0
  99. package/app/api/read/all/route.ts +15 -0
  100. package/app/api/read/forum/[id]/route.ts +29 -0
  101. package/app/api/read/thread/[id]/route.ts +40 -0
  102. package/app/api/ready/route.ts +63 -0
  103. package/app/api/system/tick/route.ts +77 -0
  104. package/app/api/v1/[...path]/route.ts +208 -0
  105. package/app/api/v1/openapi.json/route.ts +12 -0
  106. package/app/apple-icon.ts +11 -0
  107. package/app/atom.xml/route.ts +7 -0
  108. package/app/attachment/[id]/route.ts +10 -0
  109. package/app/attachment/[id]/thumb/route.ts +10 -0
  110. package/app/auth/passkey/options/route.ts +145 -0
  111. package/app/auth/passkey/verify/route.ts +241 -0
  112. package/app/auth/resume/route.ts +55 -0
  113. package/app/auth/sso/[provider]/callback/route.ts +173 -0
  114. package/app/auth/sso/[provider]/go/route.ts +57 -0
  115. package/app/auth/sso/[provider]/route.ts +105 -0
  116. package/app/avatar/[id]/route.ts +31 -0
  117. package/app/brand/icon-192.png/route.ts +7 -0
  118. package/app/brand/icon-512.png/route.ts +7 -0
  119. package/app/brand/icon-maskable.png/route.ts +7 -0
  120. package/app/demo/stripe/[[...path]]/route.ts +27 -0
  121. package/app/error.tsx +15 -0
  122. package/app/feed.xml/route.ts +7 -0
  123. package/app/forumdisplay.php/page.tsx +11 -0
  124. package/app/group/[id]/badge/[scheme]/route.ts +26 -0
  125. package/app/icon.ts +13 -0
  126. package/app/install/page.tsx +221 -0
  127. package/app/jump/page.tsx +28 -0
  128. package/app/layout.tsx +124 -0
  129. package/app/logo/[scheme]/route.ts +24 -0
  130. package/app/manifest.ts +43 -0
  131. package/app/member.php/page.tsx +11 -0
  132. package/app/memberlist.php/page.tsx +11 -0
  133. package/app/not-found.tsx +14 -0
  134. package/app/opengraph-image.ts +11 -0
  135. package/app/redirect/page.tsx +44 -0
  136. package/app/robots.txt/route.ts +35 -0
  137. package/app/showthread.php/page.tsx +11 -0
  138. package/app/sitemap/[page]/route.ts +64 -0
  139. package/app/sitemap.xml/route.ts +31 -0
  140. package/app/twitter-image.ts +11 -0
  141. package/app/viewforum.php/page.tsx +11 -0
  142. package/app/viewtopic.php/page.tsx +11 -0
  143. package/bin/forum-web.mjs +241 -0
  144. package/components.json +21 -0
  145. package/instrumentation.ts +18 -0
  146. package/next.config.mjs +182 -0
  147. package/package.json +99 -0
  148. package/postcss.config.mjs +7 -0
  149. package/proxy.ts +160 -0
  150. package/src/components/account/active-sessions.tsx +87 -0
  151. package/src/components/account/avatar-form.tsx +119 -0
  152. package/src/components/account/credential-proof-form.tsx +76 -0
  153. package/src/components/account/logout-form.tsx +14 -0
  154. package/src/components/account/notification-forms.tsx +157 -0
  155. package/src/components/account/passkey-enrol.tsx +81 -0
  156. package/src/components/account/passkey-proof-button.tsx +47 -0
  157. package/src/components/account/push-device-form.tsx +193 -0
  158. package/src/components/account/relation-forms.tsx +69 -0
  159. package/src/components/account/reputation-forms.tsx +124 -0
  160. package/src/components/account/security-activity.tsx +46 -0
  161. package/src/components/account/sign-in-methods.tsx +166 -0
  162. package/src/components/account/subscription-forms.tsx +151 -0
  163. package/src/components/account/two-factor-forms.tsx +226 -0
  164. package/src/components/account/usercp-forms.tsx +387 -0
  165. package/src/components/account/usercp-nav.tsx +14 -0
  166. package/src/components/account/usercp-shell.tsx +34 -0
  167. package/src/components/admin/admin-forms.tsx +92 -0
  168. package/src/components/admin/admin-nav.tsx +52 -0
  169. package/src/components/admin/admin-undo.tsx +32 -0
  170. package/src/components/admin/api-token-forms.tsx +89 -0
  171. package/src/components/admin/badge-forms.tsx +97 -0
  172. package/src/components/admin/branding-forms.tsx +102 -0
  173. package/src/components/admin/content-forms.tsx +712 -0
  174. package/src/components/admin/form-bits.tsx +11 -0
  175. package/src/components/admin/forum-forms.tsx +524 -0
  176. package/src/components/admin/forum-tree.tsx +454 -0
  177. package/src/components/admin/group-forms.tsx +646 -0
  178. package/src/components/admin/mail-test-card.tsx +90 -0
  179. package/src/components/admin/marketplace-forms.tsx +30 -0
  180. package/src/components/admin/marketplace-listing.tsx +123 -0
  181. package/src/components/admin/navigation-forms.tsx +229 -0
  182. package/src/components/admin/navigation-tree.tsx +442 -0
  183. package/src/components/admin/oklch-picker.tsx +180 -0
  184. package/src/components/admin/plugin-forms.tsx +225 -0
  185. package/src/components/admin/settings-form.tsx +148 -0
  186. package/src/components/admin/system-forms.tsx +169 -0
  187. package/src/components/admin/theme-changes.tsx +354 -0
  188. package/src/components/admin/theme-forms.tsx +636 -0
  189. package/src/components/admin/theme-palette.tsx +119 -0
  190. package/src/components/admin/theme-preview.tsx +461 -0
  191. package/src/components/admin/user-bulk-toolbar.tsx +99 -0
  192. package/src/components/admin/user-forms.tsx +488 -0
  193. package/src/components/auth/auth-page.tsx +45 -0
  194. package/src/components/auth/form-controls.tsx +117 -0
  195. package/src/components/auth/login-form.tsx +47 -0
  196. package/src/components/auth/passkey-client.ts +171 -0
  197. package/src/components/auth/passkey-second-factor.tsx +69 -0
  198. package/src/components/auth/passkey-sign-in.tsx +61 -0
  199. package/src/components/auth/register-form.tsx +136 -0
  200. package/src/components/auth/resend-verification-form.tsx +34 -0
  201. package/src/components/auth/reset-confirm-form.tsx +43 -0
  202. package/src/components/auth/reset-request-form.tsx +35 -0
  203. package/src/components/auth/second-factor-form.tsx +51 -0
  204. package/src/components/auth/sso-buttons.tsx +47 -0
  205. package/src/components/board/live-region.tsx +74 -0
  206. package/src/components/board/search-off-notice.tsx +18 -0
  207. package/src/components/board/section-page.tsx +114 -0
  208. package/src/components/content/attachment-field.tsx +51 -0
  209. package/src/components/content/composer-field.ts +31 -0
  210. package/src/components/content/composer-ids.ts +6 -0
  211. package/src/components/content/composer-intents.tsx +27 -0
  212. package/src/components/content/composer-recovery.tsx +215 -0
  213. package/src/components/content/edit-post-form.tsx +121 -0
  214. package/src/components/content/markdown-editor.tsx +590 -0
  215. package/src/components/content/markdown-syntax.ts +53 -0
  216. package/src/components/content/mention-token.ts +23 -0
  217. package/src/components/content/multiquote-button.tsx +32 -0
  218. package/src/components/content/multiquote-selection.tsx +105 -0
  219. package/src/components/content/multiquote.ts +125 -0
  220. package/src/components/content/new-thread-form.tsx +169 -0
  221. package/src/components/content/poll-edit-form.tsx +96 -0
  222. package/src/components/content/poll.tsx +89 -0
  223. package/src/components/content/quote-in-place.tsx +51 -0
  224. package/src/components/content/reply-form.tsx +113 -0
  225. package/src/components/content/thanks-button.tsx +65 -0
  226. package/src/components/content/thread-rating.tsx +143 -0
  227. package/src/components/install/install-form.tsx +497 -0
  228. package/src/components/legal/legal-document.tsx +22 -0
  229. package/src/components/messages/message-forms.tsx +152 -0
  230. package/src/components/moderation/inline-moderation-form.tsx +158 -0
  231. package/src/components/moderation/modcp-nav.tsx +15 -0
  232. package/src/components/moderation/modcp-shell.tsx +41 -0
  233. package/src/components/moderation/queue-form.tsx +96 -0
  234. package/src/components/moderation/report-forms.tsx +120 -0
  235. package/src/components/moderation/signature-lock-form.tsx +83 -0
  236. package/src/components/moderation/thread-surgery-form.tsx +96 -0
  237. package/src/components/moderation/thread-tools-form.tsx +137 -0
  238. package/src/components/moderation/warning-forms.tsx +125 -0
  239. package/src/components/profile/custom-field.tsx +79 -0
  240. package/src/components/shell/appearance-icons.tsx +35 -0
  241. package/src/components/shell/board-notice.tsx +21 -0
  242. package/src/components/shell/copy-record.ts +20 -0
  243. package/src/components/shell/copy.tsx +17 -0
  244. package/src/components/shell/crash-notice.tsx +86 -0
  245. package/src/components/shell/demo-banner.tsx +49 -0
  246. package/src/components/shell/group-name-style.tsx +6 -0
  247. package/src/components/shell/measure.ts +1 -0
  248. package/src/components/shell/notification-menu.tsx +361 -0
  249. package/src/components/shell/offline-notice.tsx +29 -0
  250. package/src/components/shell/onboarding-banner.tsx +39 -0
  251. package/src/components/shell/page-shell.tsx +167 -0
  252. package/src/components/shell/panel-list.tsx +95 -0
  253. package/src/components/shell/panel-nav.tsx +40 -0
  254. package/src/components/shell/panel-overview.tsx +81 -0
  255. package/src/components/shell/panel-page.tsx +96 -0
  256. package/src/components/shell/panel-pagination.tsx +28 -0
  257. package/src/components/shell/panel-shell.tsx +31 -0
  258. package/src/components/shell/theme-runtime-style.tsx +6 -0
  259. package/src/components/shell/theme-switcher.tsx +97 -0
  260. package/src/components/shell/timezone-probe.tsx +45 -0
  261. package/src/components/shell/view-tabs.tsx +60 -0
  262. package/src/config.ts +13 -0
  263. package/src/server/absent-services.ts +29 -0
  264. package/src/server/admin-actions.ts +171 -0
  265. package/src/server/admin-settings-actions.ts +83 -0
  266. package/src/server/admin-undo-actions.ts +70 -0
  267. package/src/server/admin-undo.ts +75 -0
  268. package/src/server/admin.ts +112 -0
  269. package/src/server/announcements.ts +82 -0
  270. package/src/server/antispam.ts +310 -0
  271. package/src/server/api/content.ts +486 -0
  272. package/src/server/api/http.ts +119 -0
  273. package/src/server/api/members.ts +109 -0
  274. package/src/server/api/messages.ts +113 -0
  275. package/src/server/api/registry.ts +30 -0
  276. package/src/server/api/search.ts +98 -0
  277. package/src/server/api/subscriptions.ts +106 -0
  278. package/src/server/api-auth.ts +66 -0
  279. package/src/server/api-token-actions.ts +85 -0
  280. package/src/server/api-tokens-admin.ts +95 -0
  281. package/src/server/appearance-actions.ts +39 -0
  282. package/src/server/attachment-download.ts +43 -0
  283. package/src/server/attachment-embed.ts +63 -0
  284. package/src/server/attachment-upload-actions.ts +92 -0
  285. package/src/server/attachments.ts +257 -0
  286. package/src/server/auth-actions.ts +479 -0
  287. package/src/server/auth-config.ts +61 -0
  288. package/src/server/auth-events.ts +92 -0
  289. package/src/server/auth-form-state.ts +14 -0
  290. package/src/server/auth-mail.ts +104 -0
  291. package/src/server/avatars.ts +83 -0
  292. package/src/server/board-latest-actions.ts +7 -0
  293. package/src/server/board-latest.tsx +103 -0
  294. package/src/server/board-offline.ts +23 -0
  295. package/src/server/board-url.ts +14 -0
  296. package/src/server/brand-assets.ts +151 -0
  297. package/src/server/brand-mark.tsx +110 -0
  298. package/src/server/branding-actions.ts +53 -0
  299. package/src/server/branding.ts +101 -0
  300. package/src/server/cache-targets.ts +13 -0
  301. package/src/server/code-highlighting.ts +33 -0
  302. package/src/server/container.ts +402 -0
  303. package/src/server/content-actions.ts +459 -0
  304. package/src/server/content-admin-actions.ts +492 -0
  305. package/src/server/content-admin.ts +95 -0
  306. package/src/server/content-security-policy.ts +35 -0
  307. package/src/server/context.ts +32 -0
  308. package/src/server/cookies.ts +139 -0
  309. package/src/server/credential-proof-actions.ts +42 -0
  310. package/src/server/credential-proof.ts +35 -0
  311. package/src/server/current-location.ts +19 -0
  312. package/src/server/demo-stripe.ts +104 -0
  313. package/src/server/demo-uploads.ts +28 -0
  314. package/src/server/demo.ts +80 -0
  315. package/src/server/discovery.ts +104 -0
  316. package/src/server/embeds.ts +113 -0
  317. package/src/server/error-notice.tsx +41 -0
  318. package/src/server/federation-actions.ts +113 -0
  319. package/src/server/federation.ts +138 -0
  320. package/src/server/feed-builder.ts +72 -0
  321. package/src/server/feed-routes.ts +122 -0
  322. package/src/server/fixture-actor-source.ts +62 -0
  323. package/src/server/fixture-forum-repo.ts +59 -0
  324. package/src/server/fixture-member-profile-repo.ts +30 -0
  325. package/src/server/fixture-post-repo.ts +108 -0
  326. package/src/server/fixture-thread-repo.ts +98 -0
  327. package/src/server/form-state-reporter.ts +34 -0
  328. package/src/server/form-values.ts +23 -0
  329. package/src/server/forum-admin-actions.ts +353 -0
  330. package/src/server/forum-admin.ts +98 -0
  331. package/src/server/group-admin-actions.ts +341 -0
  332. package/src/server/group-admin.ts +98 -0
  333. package/src/server/group-badge-actions.ts +61 -0
  334. package/src/server/group-badge.ts +62 -0
  335. package/src/server/group-identity.ts +104 -0
  336. package/src/server/i18n-catalogs.ts +42 -0
  337. package/src/server/i18n.ts +69 -0
  338. package/src/server/image-upload.ts +109 -0
  339. package/src/server/inline-moderation-actions.ts +129 -0
  340. package/src/server/install-actions.ts +124 -0
  341. package/src/server/install.ts +281 -0
  342. package/src/server/legacy-redirect.ts +83 -0
  343. package/src/server/legal.ts +43 -0
  344. package/src/server/location-header.ts +37 -0
  345. package/src/server/mail-brand.ts +43 -0
  346. package/src/server/mail-health.ts +41 -0
  347. package/src/server/mail-send.ts +44 -0
  348. package/src/server/mail-test-actions.ts +67 -0
  349. package/src/server/mail-test.ts +56 -0
  350. package/src/server/markdown-pipeline.ts +31 -0
  351. package/src/server/marketplace-actions.ts +47 -0
  352. package/src/server/marketplace-admin.ts +260 -0
  353. package/src/server/mention-actions.ts +21 -0
  354. package/src/server/mention-search.ts +35 -0
  355. package/src/server/message-actions.ts +110 -0
  356. package/src/server/messages.ts +141 -0
  357. package/src/server/modcp.ts +125 -0
  358. package/src/server/moderation-actions.ts +165 -0
  359. package/src/server/navigation-admin-actions.ts +217 -0
  360. package/src/server/navigation.ts +112 -0
  361. package/src/server/nonce.ts +13 -0
  362. package/src/server/notification-actions.ts +84 -0
  363. package/src/server/notification-audience.ts +10 -0
  364. package/src/server/notification-menu-actions.ts +82 -0
  365. package/src/server/notification-menu.ts +96 -0
  366. package/src/server/notifications.ts +93 -0
  367. package/src/server/onboarding-actions.ts +27 -0
  368. package/src/server/onboarding.ts +89 -0
  369. package/src/server/page-metadata.ts +14 -0
  370. package/src/server/passkey-challenge.ts +42 -0
  371. package/src/server/plugin-admin-actions.ts +232 -0
  372. package/src/server/plugin-admin.ts +276 -0
  373. package/src/server/plugin-host.ts +143 -0
  374. package/src/server/plugin-pages.ts +192 -0
  375. package/src/server/plugin-panel.ts +49 -0
  376. package/src/server/plugin-routes.ts +298 -0
  377. package/src/server/plugin-view.tsx +58 -0
  378. package/src/server/poll-actions.ts +104 -0
  379. package/src/server/poll-scope.ts +52 -0
  380. package/src/server/post-notifications.ts +71 -0
  381. package/src/server/post-scope.ts +86 -0
  382. package/src/server/presence.ts +177 -0
  383. package/src/server/profile-fields.ts +112 -0
  384. package/src/server/push-actions.ts +113 -0
  385. package/src/server/push.ts +40 -0
  386. package/src/server/redirect-back.ts +15 -0
  387. package/src/server/registration.ts +7 -0
  388. package/src/server/relation-actions.ts +89 -0
  389. package/src/server/relations.ts +45 -0
  390. package/src/server/reply-core.ts +173 -0
  391. package/src/server/report-actions.ts +165 -0
  392. package/src/server/report-scope.ts +21 -0
  393. package/src/server/reputation-actions.ts +146 -0
  394. package/src/server/reputation-target.ts +49 -0
  395. package/src/server/reputation.ts +70 -0
  396. package/src/server/request-fingerprint.ts +65 -0
  397. package/src/server/safe-path.ts +16 -0
  398. package/src/server/same-origin.ts +45 -0
  399. package/src/server/search-page.ts +313 -0
  400. package/src/server/search.ts +45 -0
  401. package/src/server/secret-auth.ts +20 -0
  402. package/src/server/see-other.ts +11 -0
  403. package/src/server/seed-board.ts +417 -0
  404. package/src/server/session-actions.ts +78 -0
  405. package/src/server/session-cookies.ts +124 -0
  406. package/src/server/session-key.ts +17 -0
  407. package/src/server/settings.ts +47 -0
  408. package/src/server/signatures.ts +62 -0
  409. package/src/server/sso-handshake.ts +79 -0
  410. package/src/server/stats.ts +63 -0
  411. package/src/server/subscription-actions.ts +161 -0
  412. package/src/server/surgery-actions.ts +180 -0
  413. package/src/server/syndication.ts +77 -0
  414. package/src/server/system-admin-actions.ts +134 -0
  415. package/src/server/system-admin.ts +90 -0
  416. package/src/server/test-container.ts +114 -0
  417. package/src/server/theme-admin-actions.ts +242 -0
  418. package/src/server/theme-admin.ts +203 -0
  419. package/src/server/theme-runtime.ts +89 -0
  420. package/src/server/theme-style.ts +279 -0
  421. package/src/server/theme.ts +52 -0
  422. package/src/server/thread-core.ts +207 -0
  423. package/src/server/thread-rating-actions.ts +55 -0
  424. package/src/server/thread-tool-actions.ts +154 -0
  425. package/src/server/two-factor-actions.ts +178 -0
  426. package/src/server/two-factor.ts +86 -0
  427. package/src/server/upgrade-notice.ts +41 -0
  428. package/src/server/user-admin-actions.ts +572 -0
  429. package/src/server/user-admin.ts +165 -0
  430. package/src/server/usercp-actions.ts +263 -0
  431. package/src/server/usercp-mail.ts +120 -0
  432. package/src/server/viewer-preferences.ts +64 -0
  433. package/src/server/warning-actions.ts +122 -0
  434. package/src/styles/globals.css +969 -0
  435. package/src/theme/contract.fixture.ts +793 -0
  436. package/src/view/account-copy.ts +351 -0
  437. package/src/view/admin-content-copy.ts +66 -0
  438. package/src/view/admin-copy.ts +106 -0
  439. package/src/view/admin-forum-tree-copy.ts +40 -0
  440. package/src/view/admin-group-copy.ts +80 -0
  441. package/src/view/admin-log.ts +58 -0
  442. package/src/view/admin-nav.ts +164 -0
  443. package/src/view/admin-navigation-copy.ts +58 -0
  444. package/src/view/admin-panel-copy.ts +194 -0
  445. package/src/view/admin-settings.ts +173 -0
  446. package/src/view/admin-theme-copy.ts +229 -0
  447. package/src/view/admin-user-copy.ts +96 -0
  448. package/src/view/attachments.ts +51 -0
  449. package/src/view/auth-copy.ts +147 -0
  450. package/src/view/board-index.ts +167 -0
  451. package/src/view/board-latest.ts +101 -0
  452. package/src/view/breadcrumb.ts +50 -0
  453. package/src/view/contacts.ts +85 -0
  454. package/src/view/content-copy.ts +118 -0
  455. package/src/view/contrast.ts +286 -0
  456. package/src/view/copy.ts +101 -0
  457. package/src/view/count.ts +8 -0
  458. package/src/view/discovery-view.ts +74 -0
  459. package/src/view/editor-toolbar.ts +84 -0
  460. package/src/view/error-notice.ts +52 -0
  461. package/src/view/feed.ts +130 -0
  462. package/src/view/forum-cursor.ts +54 -0
  463. package/src/view/forum-display.ts +152 -0
  464. package/src/view/forum-jump.ts +60 -0
  465. package/src/view/forum-notice.ts +24 -0
  466. package/src/view/inline-moderation.ts +87 -0
  467. package/src/view/install-copy.ts +95 -0
  468. package/src/view/legal.ts +58 -0
  469. package/src/view/locale.ts +40 -0
  470. package/src/view/marketplace-panel.ts +51 -0
  471. package/src/view/member-identity.ts +23 -0
  472. package/src/view/member-profile.ts +65 -0
  473. package/src/view/messages.ts +222 -0
  474. package/src/view/metadata.ts +75 -0
  475. package/src/view/modcp-nav.ts +76 -0
  476. package/src/view/moderation-copy.ts +84 -0
  477. package/src/view/moderation-queue.ts +67 -0
  478. package/src/view/navigation-arrange.ts +188 -0
  479. package/src/view/navigation.ts +173 -0
  480. package/src/view/notification-menu.ts +202 -0
  481. package/src/view/notifications.ts +84 -0
  482. package/src/view/onboarding.ts +26 -0
  483. package/src/view/pager.ts +191 -0
  484. package/src/view/panel-nav.ts +203 -0
  485. package/src/view/plugin-panel.ts +42 -0
  486. package/src/view/post-form.ts +61 -0
  487. package/src/view/post-link.ts +29 -0
  488. package/src/view/presence.ts +99 -0
  489. package/src/view/promotion-rules.ts +52 -0
  490. package/src/view/redirect-notice.ts +21 -0
  491. package/src/view/reputation.ts +107 -0
  492. package/src/view/search-controls.ts +103 -0
  493. package/src/view/search-results.ts +291 -0
  494. package/src/view/security-activity.ts +73 -0
  495. package/src/view/setting-groups.ts +43 -0
  496. package/src/view/shell.ts +164 -0
  497. package/src/view/slug-id.ts +12 -0
  498. package/src/view/sso-hand-off.ts +44 -0
  499. package/src/view/sso-notices.ts +48 -0
  500. package/src/view/subscriptions.ts +76 -0
  501. package/src/view/theme-draft.ts +167 -0
  502. package/src/view/theme-preference.ts +30 -0
  503. package/src/view/theme-tokens.ts +333 -0
  504. package/src/view/thread-view.ts +280 -0
  505. package/src/view/time.ts +31 -0
  506. package/src/view/timezone.ts +35 -0
  507. package/src/view/two-factor.ts +6 -0
  508. package/src/view/usercp-nav.ts +103 -0
  509. package/src/view/usercp.ts +196 -0
  510. package/src/view/warnings.ts +128 -0
  511. package/src/view/word-filter.ts +5 -0
@@ -0,0 +1,137 @@
1
+ 'use client'
2
+
3
+ import { useActionState } from 'react'
4
+
5
+ import { EMPTY_STATE } from '@/server/auth-form-state'
6
+ import { threadToolAction } from '@/server/thread-tool-actions'
7
+
8
+ import { FormError } from '../auth/form-controls'
9
+ import { type Copy, fromCopy } from '../shell/copy'
10
+
11
+ const BUTTON =
12
+ 'inline-flex h-8 items-center rounded-md border border-border px-3 text-xs font-medium hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
13
+
14
+ function ToolButton({
15
+ threadId,
16
+ tool,
17
+ label,
18
+ destructive,
19
+ }: {
20
+ threadId: number
21
+ tool: string
22
+ label: string
23
+ destructive?: boolean
24
+ }) {
25
+ const [state, action] = useActionState(threadToolAction, EMPTY_STATE)
26
+
27
+ return (
28
+ <form action={action} className="inline">
29
+ <FormError message={state.error} />
30
+ <input type="hidden" name="threadId" value={threadId} />
31
+ <input type="hidden" name="tool" value={tool} />
32
+ <button
33
+ type="submit"
34
+ className={
35
+ destructive === true ? `${BUTTON} border-destructive/40 text-destructive` : BUTTON
36
+ }
37
+ >
38
+ {label}
39
+ </button>
40
+ </form>
41
+ )
42
+ }
43
+
44
+ export interface MoveOption {
45
+ readonly id: number
46
+ readonly title: string
47
+ }
48
+
49
+ export function ThreadToolsForm({
50
+ threadId,
51
+ isLocked,
52
+ isSticky,
53
+ rights,
54
+ moveTargets,
55
+ heading,
56
+ copy,
57
+ children,
58
+ }: {
59
+ threadId: number
60
+ isLocked: boolean
61
+ isSticky: boolean
62
+ rights: { lock: boolean; stick: boolean; move: boolean; delete: boolean }
63
+ moveTargets: readonly MoveOption[]
64
+ heading: string
65
+ copy: Copy
66
+ children?: React.ReactNode
67
+ }) {
68
+ const [moveState, moveAction] = useActionState(threadToolAction, EMPTY_STATE)
69
+
70
+ return (
71
+ <section
72
+ aria-label={heading}
73
+ className="flex flex-wrap items-center gap-3 rounded-lg border border-border bg-secondary px-4 py-3"
74
+ >
75
+ <span className="text-xs font-medium text-muted-foreground">{heading}</span>
76
+
77
+ {rights.lock && (
78
+ <ToolButton
79
+ threadId={threadId}
80
+ tool={isLocked ? 'unlock' : 'lock'}
81
+ label={
82
+ isLocked
83
+ ? fromCopy(copy, 'moderationForm.tool.unlock')
84
+ : fromCopy(copy, 'moderationForm.tool.lock')
85
+ }
86
+ />
87
+ )}
88
+ {rights.stick && (
89
+ <ToolButton
90
+ threadId={threadId}
91
+ tool={isSticky ? 'unstick' : 'stick'}
92
+ label={
93
+ isSticky
94
+ ? fromCopy(copy, 'moderationForm.tool.unpin')
95
+ : fromCopy(copy, 'moderationForm.tool.pin')
96
+ }
97
+ />
98
+ )}
99
+ {rights.delete && (
100
+ <ToolButton
101
+ threadId={threadId}
102
+ tool="delete"
103
+ label={fromCopy(copy, 'moderationForm.tool.deleteThread')}
104
+ destructive
105
+ />
106
+ )}
107
+
108
+ {rights.move && moveTargets.length > 0 && (
109
+ <form action={moveAction} className="flex items-center gap-2">
110
+ <FormError message={moveState.error} />
111
+ <input type="hidden" name="threadId" value={threadId} />
112
+ <label className="flex items-center gap-2 text-xs">
113
+ <span className="sr-only">{fromCopy(copy, 'moderationForm.moveTo')}</span>
114
+ <select
115
+ name="toForumId"
116
+ className="h-8 rounded-md border border-border bg-background px-2 text-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
117
+ >
118
+ {moveTargets.map((forum) => (
119
+ <option key={forum.id} value={forum.id}>
120
+ {forum.title}
121
+ </option>
122
+ ))}
123
+ </select>
124
+ </label>
125
+ <button type="submit" name="tool" value="move" className={BUTTON}>
126
+ {fromCopy(copy, 'moderationForm.tool.move')}
127
+ </button>
128
+ <button type="submit" name="tool" value="copy" className={BUTTON}>
129
+ {fromCopy(copy, 'moderationForm.tool.copy')}
130
+ </button>
131
+ </form>
132
+ )}
133
+
134
+ {children}
135
+ </section>
136
+ )
137
+ }
@@ -0,0 +1,125 @@
1
+ 'use client'
2
+
3
+ import { useActionState } from 'react'
4
+
5
+ import { EMPTY_STATE } from '@/server/auth-form-state'
6
+ import { issueWarningAction, revokeWarningAction } from '@/server/warning-actions'
7
+
8
+ import { FormError } from '../auth/form-controls'
9
+ import { type Copy, fromCopy } from '../shell/copy'
10
+
11
+ const FIELD =
12
+ 'w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
13
+
14
+ const BUTTON =
15
+ 'inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
16
+
17
+ export interface WarningTypeOption {
18
+ readonly id: number
19
+ readonly label: string
20
+ }
21
+
22
+ export function IssueWarningForm({
23
+ userId,
24
+ postId,
25
+ types,
26
+ copy,
27
+ }: {
28
+ userId: number
29
+ postId: number | null
30
+ types: readonly WarningTypeOption[]
31
+ copy: Copy
32
+ }) {
33
+ const [state, action] = useActionState(issueWarningAction, EMPTY_STATE)
34
+
35
+ return (
36
+ <form
37
+ action={action}
38
+ className="flex flex-col gap-4 rounded-lg border border-border bg-card p-5"
39
+ >
40
+ <h2 className="text-lg font-semibold tracking-tight">
41
+ {fromCopy(copy, 'moderationForm.warn.title')}
42
+ </h2>
43
+ <FormError message={state.error} />
44
+
45
+ <input type="hidden" name="userId" value={userId} />
46
+ {postId !== null && <input type="hidden" name="postId" value={postId} />}
47
+
48
+ <label className="flex flex-col gap-1 text-sm">
49
+ <span className="font-medium">{fromCopy(copy, 'moderationForm.warn.reason')}</span>
50
+ <select name="typeId" className={FIELD} defaultValue="">
51
+ <option value="">{fromCopy(copy, 'moderationForm.warn.somethingElse')}</option>
52
+ {types.map((type) => (
53
+ <option key={type.id} value={type.id}>
54
+ {type.label}
55
+ </option>
56
+ ))}
57
+ </select>
58
+ </label>
59
+
60
+ <div className="grid gap-4 sm:grid-cols-[1fr_8rem]">
61
+ <label className="flex flex-col gap-1 text-sm">
62
+ <span className="font-medium">{fromCopy(copy, 'moderationForm.warn.titleLabel')}</span>
63
+ <input type="text" name="title" maxLength={150} className={FIELD} />
64
+ <span className="text-xs text-muted-foreground">
65
+ {fromCopy(copy, 'moderationForm.warn.titleHint')}
66
+ </span>
67
+ </label>
68
+ <label className="flex flex-col gap-1 text-sm">
69
+ <span className="font-medium">{fromCopy(copy, 'moderationForm.warn.points')}</span>
70
+ <input type="number" name="points" min={1} max={100} className={FIELD} />
71
+ </label>
72
+ </div>
73
+
74
+ <label className="flex flex-col gap-1 text-sm">
75
+ <span className="font-medium">{fromCopy(copy, 'moderationForm.warn.what')}</span>
76
+ <textarea name="reason" rows={4} maxLength={2000} className={FIELD} required />
77
+ <span className="text-xs text-muted-foreground">
78
+ {fromCopy(copy, 'moderationForm.warn.whatHint')}
79
+ </span>
80
+ </label>
81
+
82
+ <div>
83
+ <button type="submit" className={BUTTON}>
84
+ {fromCopy(copy, 'moderationForm.warn.submit')}
85
+ </button>
86
+ </div>
87
+ </form>
88
+ )
89
+ }
90
+
91
+ export function RevokeWarningForm({
92
+ warningId,
93
+ userId,
94
+ copy,
95
+ }: {
96
+ warningId: number
97
+ userId: number
98
+ copy: Copy
99
+ }) {
100
+ const [state, action] = useActionState(revokeWarningAction, EMPTY_STATE)
101
+
102
+ return (
103
+ <form action={action} className="flex flex-wrap items-center gap-2">
104
+ <FormError message={state.error} />
105
+ <input type="hidden" name="warningId" value={warningId} />
106
+ <input type="hidden" name="userId" value={userId} />
107
+ <label className="flex-1">
108
+ <span className="sr-only">{fromCopy(copy, 'moderationForm.warn.revokeSr')}</span>
109
+ <input
110
+ type="text"
111
+ name="reason"
112
+ maxLength={2000}
113
+ placeholder={fromCopy(copy, 'moderationForm.warn.revokePlaceholder')}
114
+ className="h-8 w-full rounded-md border border-border bg-background px-2 text-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
115
+ />
116
+ </label>
117
+ <button
118
+ type="submit"
119
+ className="inline-flex h-8 shrink-0 items-center rounded-md border border-border px-3 text-xs font-medium hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
120
+ >
121
+ {fromCopy(copy, 'moderationForm.warn.revoke')}
122
+ </button>
123
+ </form>
124
+ )
125
+ }
@@ -0,0 +1,79 @@
1
+ 'use client'
2
+
3
+ export interface CustomFieldInput {
4
+ readonly key: string
5
+ readonly label: string
6
+ readonly description: string | null
7
+ readonly type: string | null
8
+ readonly options: readonly string[]
9
+ readonly value: string
10
+ readonly maxLength: number
11
+ readonly required: boolean
12
+ }
13
+
14
+ export function fieldInputName(key: string): string {
15
+ return `field:${key}`
16
+ }
17
+
18
+ export function CustomField({ field, className }: { field: CustomFieldInput; className: string }) {
19
+ const name = fieldInputName(field.key)
20
+
21
+ const control =
22
+ field.type === 'textarea' ? (
23
+ <textarea
24
+ name={name}
25
+ defaultValue={field.value}
26
+ className={className}
27
+ rows={4}
28
+ maxLength={field.maxLength}
29
+ required={field.required}
30
+ />
31
+ ) : field.type === 'select' ? (
32
+ <select
33
+ name={name}
34
+ defaultValue={field.value}
35
+ className={className}
36
+ required={field.required}
37
+ >
38
+ {field.required ? null : <option value="">—</option>}
39
+ {field.options.map((option) => (
40
+ <option key={option} value={option}>
41
+ {option}
42
+ </option>
43
+ ))}
44
+ </select>
45
+ ) : field.type === 'checkbox' ? (
46
+ <input
47
+ type="checkbox"
48
+ name={name}
49
+ value="yes"
50
+ defaultChecked={field.value !== ''}
51
+ className="size-4 rounded border-border"
52
+ required={field.required}
53
+ />
54
+ ) : (
55
+ <input
56
+ type="text"
57
+ name={name}
58
+ defaultValue={field.value}
59
+ className={className}
60
+ maxLength={field.maxLength}
61
+ required={field.required}
62
+ inputMode={field.type === 'number' ? 'numeric' : field.type === 'url' ? 'url' : undefined}
63
+ />
64
+ )
65
+
66
+ return (
67
+ // biome-ignore lint/a11y/noLabelWithoutControl: the control is built above and inserted as {control}
68
+ <label className="flex flex-col gap-1 text-sm">
69
+ <span className="font-medium">
70
+ {field.label}
71
+ {field.required ? <span aria-hidden> *</span> : null}
72
+ </span>
73
+ {control}
74
+ {field.description === null ? null : (
75
+ <span className="text-xs text-muted-foreground">{field.description}</span>
76
+ )}
77
+ </label>
78
+ )
79
+ }
@@ -0,0 +1,35 @@
1
+ const SHARED = {
2
+ viewBox: '0 0 16 16',
3
+ fill: 'none',
4
+ stroke: 'currentColor',
5
+ strokeWidth: 1.5,
6
+ strokeLinecap: 'round',
7
+ strokeLinejoin: 'round',
8
+ className: 'size-3.5 shrink-0',
9
+ } as const
10
+
11
+ export function SunIcon() {
12
+ return (
13
+ <svg {...SHARED} aria-hidden="true">
14
+ <circle cx="8" cy="8" r="3" />
15
+ <path d="M8 1v1.5M8 13.5V15M15 8h-1.5M2.5 8H1M12.95 3.05l-1.06 1.06M4.11 11.89l-1.06 1.06M12.95 12.95l-1.06-1.06M4.11 4.11L3.05 3.05" />
16
+ </svg>
17
+ )
18
+ }
19
+
20
+ export function MonitorIcon() {
21
+ return (
22
+ <svg {...SHARED} aria-hidden="true">
23
+ <rect x="1.5" y="2.5" width="13" height="9" rx="1.5" />
24
+ <path d="M5.5 14.5h5M8 11.5v3" />
25
+ </svg>
26
+ )
27
+ }
28
+
29
+ export function MoonIcon() {
30
+ return (
31
+ <svg {...SHARED} aria-hidden="true">
32
+ <path d="M13.5 9.5A6 6 0 0 1 6.5 2.5a6 6 0 1 0 7 7Z" />
33
+ </svg>
34
+ )
35
+ }
@@ -0,0 +1,21 @@
1
+ import 'server-only'
2
+
3
+ import { type NoticeModel, requireSlot, slotCopy } from '@meith/theme-kit'
4
+
5
+ import { getActor } from '@/server/context'
6
+ import { getTranslator } from '@/server/i18n'
7
+ import { filterView, viewerRef } from '@/server/plugin-view'
8
+ import { currentTheme } from '@/server/theme'
9
+
10
+ export async function BoardNotice({ kind, message, dismissHref }: NoticeModel) {
11
+ const theme = await currentTheme()
12
+ const Notice = requireSlot(theme, 'Notice')
13
+
14
+ const model = await filterView(
15
+ 'view.notice',
16
+ { kind, message, dismissHref },
17
+ viewerRef(await getActor()),
18
+ )
19
+
20
+ return <Notice {...model} copy={slotCopy(theme, 'Notice', await getTranslator())} />
21
+ }
@@ -0,0 +1,20 @@
1
+ import { formatMessage, type MessageArgs } from '@meith/i18n'
2
+
3
+ export type Copy = Readonly<Record<string, string>>
4
+
5
+ export function fromCopy(copy: Copy, key: string): string {
6
+ return copy[key] ?? key
7
+ }
8
+
9
+ export function formatFromCopy(copy: Copy, key: string, args: MessageArgs): string {
10
+ const pattern = copy[key]
11
+ if (pattern === undefined) return key
12
+
13
+ const locale = typeof document === 'undefined' ? 'en' : document.documentElement.lang || 'en'
14
+
15
+ try {
16
+ return formatMessage(pattern, args, { locale, timeZone: 'UTC' })
17
+ } catch {
18
+ return pattern
19
+ }
20
+ }
@@ -0,0 +1,17 @@
1
+ 'use client'
2
+
3
+ import { createContext, useContext } from 'react'
4
+
5
+ import type { Copy } from './copy-record'
6
+
7
+ export { type Copy, formatFromCopy, fromCopy } from './copy-record'
8
+
9
+ const CopyContext = createContext<Copy>({})
10
+
11
+ export function CopyProvider({ copy, children }: { copy: Copy; children: React.ReactNode }) {
12
+ return <CopyContext.Provider value={copy}>{children}</CopyContext.Provider>
13
+ }
14
+
15
+ export function useCopy(): Copy {
16
+ return useContext(CopyContext)
17
+ }
@@ -0,0 +1,86 @@
1
+ 'use client'
2
+
3
+ import { Component, createContext, type ReactNode, useContext } from 'react'
4
+
5
+ import { CRASH_NOTICE_STATUS, type CrashNoticeCopy } from '@/view/error-notice'
6
+
7
+ interface CrashNoticeValue {
8
+ readonly notice: ReactNode
9
+ readonly requestId: string | null
10
+ readonly copy: CrashNoticeCopy
11
+ }
12
+
13
+ const CrashNoticeContext = createContext<CrashNoticeValue>({
14
+ notice: null,
15
+ requestId: null,
16
+ copy: { title: '', message: '', homeLabel: '' },
17
+ })
18
+
19
+ export function CrashNoticeProvider({
20
+ notice,
21
+ requestId,
22
+ copy,
23
+ children,
24
+ }: CrashNoticeValue & { readonly children: ReactNode }) {
25
+ return (
26
+ <CrashNoticeContext.Provider value={{ notice, requestId, copy }}>
27
+ {children}
28
+ </CrashNoticeContext.Provider>
29
+ )
30
+ }
31
+
32
+ export function CrashNotice() {
33
+ const { notice, requestId, copy } = useContext(CrashNoticeContext)
34
+ const plain = <PlainNotice requestId={requestId} copy={copy} />
35
+
36
+ if (notice === null || notice === undefined) return plain
37
+
38
+ return <ThemeGuard fallback={plain}>{notice}</ThemeGuard>
39
+ }
40
+
41
+ class ThemeGuard extends Component<
42
+ { readonly fallback: ReactNode; readonly children: ReactNode },
43
+ { readonly failed: boolean }
44
+ > {
45
+ override state = { failed: false }
46
+
47
+ static getDerivedStateFromError(): { failed: boolean } {
48
+ return { failed: true }
49
+ }
50
+
51
+ override render(): ReactNode {
52
+ return this.state.failed ? this.props.fallback : this.props.children
53
+ }
54
+ }
55
+
56
+ function PlainNotice({
57
+ requestId,
58
+ copy,
59
+ }: {
60
+ readonly requestId: string | null
61
+ readonly copy: CrashNoticeCopy
62
+ }) {
63
+ return (
64
+ <section className="w-full max-w-lg rounded-lg border border-border bg-card p-6 text-card-foreground">
65
+ <p className="text-xs font-medium tracking-wide text-destructive uppercase">
66
+ Error {CRASH_NOTICE_STATUS}
67
+ </p>
68
+ <h1 className="mt-1 text-2xl font-semibold tracking-tight">{copy.title}</h1>
69
+ <p className="mt-2 text-sm text-muted-foreground">{copy.message}</p>
70
+
71
+ <a
72
+ href="/"
73
+ className="mt-5 inline-block rounded-md border border-border px-3 py-2 text-sm hover:bg-secondary"
74
+ >
75
+ {copy.homeLabel}
76
+ </a>
77
+
78
+ {requestId !== null && (
79
+ <p className="mt-4 text-xs text-muted-foreground">
80
+ Quote this if you report it:{' '}
81
+ <code className="font-mono text-foreground select-all">{requestId}</code>
82
+ </p>
83
+ )}
84
+ </section>
85
+ )
86
+ }
@@ -0,0 +1,49 @@
1
+ import { demoBannerModel } from '@/server/demo'
2
+ import { getTranslator } from '@/server/i18n'
3
+ import { splitAround } from '@/view/copy'
4
+
5
+ /**
6
+ * The strip at the top of a demo board.
7
+ *
8
+ * Rendered from the root layout rather than through a theme slot, on purpose:
9
+ * the slot contract is frozen, and a visiting administrator can switch themes.
10
+ * A banner a theme could decline to render is a banner that stops telling the
11
+ * next visitor how to log in, on the first theme that has not heard of it.
12
+ */
13
+ export async function DemoBanner() {
14
+ const banner = await demoBannerModel()
15
+ if (banner === null) return null
16
+
17
+ const t = await getTranslator()
18
+ const [loginLead, loginTail] = splitAround(t, 'demo.loginAs', 'logins')
19
+
20
+ return (
21
+ <aside
22
+ className="border-b border-border bg-muted px-4 py-2 text-sm text-muted-foreground"
23
+ aria-label={t.t('demo.aria')}
24
+ >
25
+ <div className="mx-auto flex max-w-6xl flex-wrap items-baseline gap-x-4 gap-y-1">
26
+ <strong className="font-semibold text-foreground">{t.t('demo.title')}</strong>
27
+
28
+ <span>
29
+ {loginLead}
30
+ {banner.logins.map((login, index) => (
31
+ <span key={login.username}>
32
+ {index > 0 && ', '}
33
+ <code className="rounded bg-background px-1 py-0.5 font-mono text-foreground">
34
+ {login.username} / {login.password}
35
+ </code>
36
+ </span>
37
+ ))}
38
+ {loginTail}
39
+ </span>
40
+
41
+ <span>
42
+ {banner.resetsIn === null
43
+ ? t.t('demo.wiped')
44
+ : t.t('demo.wipedIn', { when: banner.resetsIn })}
45
+ </span>
46
+ </div>
47
+ </aside>
48
+ )
49
+ }
@@ -0,0 +1,6 @@
1
+ import { getGroupStyle } from '@/server/group-identity'
2
+
3
+ export async function GroupNameStyle() {
4
+ const css = await getGroupStyle()
5
+ return css === '' ? null : <style id="forum-group-names">{css}</style>
6
+ }
@@ -0,0 +1 @@
1
+ export const BOARD_MEASURE = 'mx-auto w-full max-w-6xl px-4 sm:px-6'