@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,361 @@
1
+ 'use client'
2
+
3
+ import { useActionState } from 'react'
4
+
5
+ import { Popover } from '@meith/ui/popover'
6
+ import { Tabs } from '@meith/ui/tabs'
7
+
8
+ import type { FormState } from '@/server/auth-form-state'
9
+ import { EMPTY_STATE } from '@/server/auth-form-state'
10
+ import {
11
+ markAllNotificationsSeenAction,
12
+ markMessagesSeenAction,
13
+ markNotificationSeenAction,
14
+ } from '@/server/notification-menu-actions'
15
+ import type {
16
+ NotificationMenuModel,
17
+ NotificationMenuRow,
18
+ NotificationMenuTab,
19
+ NotificationMenuTabKind,
20
+ } from '@/view/notification-menu'
21
+
22
+ import { type Copy, formatFromCopy, fromCopy } from './copy'
23
+
24
+ const UNREAD_KEY: Readonly<Record<NotificationMenuTabKind, string>> = {
25
+ notifications: 'board.notificationMenu.unreadNotifications',
26
+ messages: 'board.notificationMenu.unreadMessages',
27
+ mod: 'board.notificationMenu.unreadMod',
28
+ }
29
+
30
+ function BellIcon() {
31
+ return (
32
+ <svg
33
+ aria-hidden="true"
34
+ viewBox="0 0 20 20"
35
+ className="size-5"
36
+ fill="none"
37
+ stroke="currentColor"
38
+ strokeWidth="1.6"
39
+ strokeLinecap="round"
40
+ strokeLinejoin="round"
41
+ >
42
+ <path d="M10 2.5a4.5 4.5 0 0 0-4.5 4.5c0 3.5-1.5 4.5-1.5 4.5h12s-1.5-1-1.5-4.5A4.5 4.5 0 0 0 10 2.5Z" />
43
+ <path d="M8.5 14.5a1.75 1.75 0 0 0 3 0" />
44
+ </svg>
45
+ )
46
+ }
47
+
48
+ function CountPill({ value }: { value: number }) {
49
+ return (
50
+ <span
51
+ aria-hidden="true"
52
+ className="inline-flex min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[0.625rem] font-semibold leading-4 text-primary-foreground tabular-nums"
53
+ >
54
+ {value > 99 ? '99+' : value}
55
+ </span>
56
+ )
57
+ }
58
+
59
+ function CheckIcon() {
60
+ return (
61
+ <svg
62
+ aria-hidden="true"
63
+ viewBox="0 0 16 16"
64
+ className="size-4"
65
+ fill="none"
66
+ stroke="currentColor"
67
+ strokeWidth="1.8"
68
+ strokeLinecap="round"
69
+ strokeLinejoin="round"
70
+ >
71
+ <path d="m3.5 8.5 3 3 6-7" />
72
+ </svg>
73
+ )
74
+ }
75
+
76
+ function HiddenFields({
77
+ fields,
78
+ }: {
79
+ fields: readonly { readonly name: string; readonly value: number }[]
80
+ }) {
81
+ return (
82
+ <>
83
+ {fields.map((field) => (
84
+ <input
85
+ key={`${field.name}-${field.value}`}
86
+ type="hidden"
87
+ name={field.name}
88
+ value={field.value}
89
+ />
90
+ ))}
91
+ </>
92
+ )
93
+ }
94
+
95
+ function SeenIconForm({
96
+ action,
97
+ hidden,
98
+ label,
99
+ }: {
100
+ action: (prev: FormState, form: FormData) => Promise<FormState>
101
+ hidden: readonly { readonly name: string; readonly value: number }[]
102
+ label: string
103
+ }) {
104
+ const [, formAction, isPending] = useActionState(action, EMPTY_STATE)
105
+
106
+ return (
107
+ <form action={formAction} className="shrink-0">
108
+ <HiddenFields fields={hidden} />
109
+ <button
110
+ type="submit"
111
+ disabled={isPending}
112
+ title={label}
113
+ className="inline-flex size-7 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:opacity-60"
114
+ >
115
+ <span className="sr-only">{label}</span>
116
+ <CheckIcon />
117
+ </button>
118
+ </form>
119
+ )
120
+ }
121
+
122
+ function SeenForm({
123
+ action,
124
+ hidden,
125
+ label,
126
+ working,
127
+ }: {
128
+ action: (prev: FormState, form: FormData) => Promise<FormState>
129
+ hidden: readonly { readonly name: string; readonly value: number }[]
130
+ label: string
131
+ working: string
132
+ }) {
133
+ const [, formAction, isPending] = useActionState(action, EMPTY_STATE)
134
+
135
+ return (
136
+ <form action={formAction}>
137
+ <HiddenFields fields={hidden} />
138
+ <button
139
+ type="submit"
140
+ disabled={isPending}
141
+ className="shrink-0 rounded-md px-2 py-1 text-xs font-medium text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:opacity-60"
142
+ >
143
+ {isPending ? working : label}
144
+ </button>
145
+ </form>
146
+ )
147
+ }
148
+
149
+ function Row({
150
+ row,
151
+ kind,
152
+ copy,
153
+ }: {
154
+ row: NotificationMenuRow
155
+ kind: NotificationMenuTabKind
156
+ copy: Copy
157
+ }) {
158
+ const body = (
159
+ <>
160
+ <span className="flex items-baseline justify-between gap-2">
161
+ <span className="truncate text-sm font-medium text-foreground">
162
+ {row.tag !== null && (
163
+ <span className="mr-1.5 text-[0.625rem] font-semibold uppercase tracking-wide text-muted-foreground">
164
+ {row.tag}
165
+ </span>
166
+ )}
167
+ {row.subject}
168
+ </span>
169
+ <time dateTime={row.at.iso} className="shrink-0 text-[0.6875rem] text-muted-foreground">
170
+ {row.at.label}
171
+ </time>
172
+ </span>
173
+ {row.meta !== null && (
174
+ <span className="mt-0.5 block truncate text-xs text-muted-foreground">{row.meta}</span>
175
+ )}
176
+ </>
177
+ )
178
+
179
+ return (
180
+ <li className="flex items-center gap-1 rounded-md pr-1">
181
+ {row.href === null ? (
182
+ <span className="min-w-0 flex-1 px-2 py-2">{body}</span>
183
+ ) : (
184
+ <a
185
+ href={row.href}
186
+ className="min-w-0 flex-1 rounded-md px-2 py-2 hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
187
+ >
188
+ {body}
189
+ </a>
190
+ )}
191
+
192
+ {row.isUnread && row.seenId !== null && (
193
+ <SeenIconForm
194
+ action={kind === 'messages' ? markMessagesSeenAction : markNotificationSeenAction}
195
+ hidden={[
196
+ kind === 'messages'
197
+ ? { name: 'copyId', value: row.seenId }
198
+ : { name: 'notificationId', value: row.seenId },
199
+ ]}
200
+ label={fromCopy(copy, 'board.notificationMenu.markSeen')}
201
+ />
202
+ )}
203
+ </li>
204
+ )
205
+ }
206
+
207
+ function Panel({ tab, copy }: { tab: NotificationMenuTab; copy: Copy }) {
208
+ const unreadRows = tab.rows.filter((row) => row.isUnread && row.seenId !== null)
209
+
210
+ return (
211
+ <Tabs.Panel
212
+ value={tab.kind}
213
+ className="flex flex-col gap-1 px-1 pb-1 focus-visible:outline-none"
214
+ >
215
+ {tab.rows.length === 0 ? (
216
+ <p className="px-3 py-6 text-center text-sm text-muted-foreground">{tab.emptyLabel}</p>
217
+ ) : (
218
+ <ul className="flex max-h-80 flex-col gap-0.5 overflow-y-auto">
219
+ {tab.rows.map((row) => (
220
+ <Row key={row.key} row={row} kind={tab.kind} copy={copy} />
221
+ ))}
222
+ </ul>
223
+ )}
224
+
225
+ <div className="flex items-center justify-between gap-2 border-t border-border px-2 pt-2">
226
+ <a
227
+ href={tab.allHref}
228
+ className="rounded-md px-2 py-1 text-xs font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
229
+ >
230
+ {fromCopy(copy, 'board.notificationMenu.viewAll')}
231
+ </a>
232
+
233
+ {unreadRows.length > 0 && (
234
+ <SeenForm
235
+ action={
236
+ tab.kind === 'messages' ? markMessagesSeenAction : markAllNotificationsSeenAction
237
+ }
238
+ hidden={
239
+ tab.kind === 'messages'
240
+ ? unreadRows.flatMap((row) =>
241
+ row.seenId === null ? [] : [{ name: 'copyId', value: row.seenId }],
242
+ )
243
+ : []
244
+ }
245
+ label={fromCopy(copy, 'board.notificationMenu.markAllSeen')}
246
+ working={fromCopy(copy, 'form.working')}
247
+ />
248
+ )}
249
+ </div>
250
+ </Tabs.Panel>
251
+ )
252
+ }
253
+
254
+ export function NotificationMenu({ model, copy }: { model: NotificationMenuModel; copy: Copy }) {
255
+ const first = model.tabs[0]?.kind ?? 'notifications'
256
+
257
+ return (
258
+ <div className="flex items-center">
259
+ <noscript
260
+ dangerouslySetInnerHTML={{
261
+ __html:
262
+ '<style>[data-notif="menu"]{display:none}[data-notif="plain"]{display:flex!important}</style>',
263
+ }}
264
+ />
265
+
266
+ <span data-notif="menu" className="flex items-center">
267
+ <Popover.Root>
268
+ <Popover.Trigger className="relative inline-flex size-9 items-center justify-center rounded-md border border-transparent text-muted-foreground transition-colors hover:bg-muted hover:text-foreground data-[popup-open]:bg-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring">
269
+ <BellIcon />
270
+ {model.total > 0 && (
271
+ <span className="absolute -right-0.5 -top-0.5">
272
+ <CountPill value={model.total} />
273
+ </span>
274
+ )}
275
+ <span className="sr-only">
276
+ {model.total > 0
277
+ ? formatFromCopy(copy, 'board.notificationMenu.totalUnread', { count: model.total })
278
+ : fromCopy(copy, 'board.notificationMenu.trigger')}
279
+ </span>
280
+ </Popover.Trigger>
281
+
282
+ <Popover.Portal>
283
+ <Popover.Positioner
284
+ align="end"
285
+ sideOffset={8}
286
+ collisionPadding={8}
287
+ className="z-50 outline-none"
288
+ >
289
+ <Popover.Popup className="w-[min(22rem,calc(100vw-1rem))] origin-[var(--transform-origin)] overflow-hidden rounded-lg border border-border bg-card text-card-foreground shadow-lg transition-[opacity,transform] duration-100 ease-out data-[starting-style]:scale-95 data-[starting-style]:opacity-0 data-[ending-style]:scale-95 data-[ending-style]:opacity-0">
290
+ <div className="flex items-center justify-between border-b border-border px-3 py-2">
291
+ <Popover.Title className="text-sm font-semibold">
292
+ {fromCopy(copy, 'board.notificationMenu.heading')}
293
+ </Popover.Title>
294
+ <Popover.Close className="rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring">
295
+ <span className="sr-only">
296
+ {fromCopy(copy, 'board.notificationMenu.close')}
297
+ </span>
298
+ <svg
299
+ aria-hidden="true"
300
+ viewBox="0 0 14 14"
301
+ className="size-3.5"
302
+ fill="none"
303
+ stroke="currentColor"
304
+ strokeWidth="1.6"
305
+ strokeLinecap="round"
306
+ >
307
+ <path d="m3.5 3.5 7 7m0-7-7 7" />
308
+ </svg>
309
+ </Popover.Close>
310
+ </div>
311
+
312
+ <Tabs.Root defaultValue={first}>
313
+ <Tabs.List className="flex gap-1 border-b border-border p-2">
314
+ {model.tabs.map((tab) => (
315
+ <Tabs.Tab
316
+ key={tab.kind}
317
+ value={tab.kind}
318
+ className="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:hover:bg-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
319
+ >
320
+ {tab.label}
321
+ {tab.unread > 0 && (
322
+ <span className="inline-flex min-w-4 items-center justify-center rounded-full bg-current/20 px-1 text-[0.625rem] font-semibold leading-4 tabular-nums">
323
+ {tab.unread > 99 ? '99+' : tab.unread}
324
+ </span>
325
+ )}
326
+ </Tabs.Tab>
327
+ ))}
328
+ </Tabs.List>
329
+
330
+ {model.tabs.map((tab) => (
331
+ <Panel key={tab.kind} tab={tab} copy={copy} />
332
+ ))}
333
+ </Tabs.Root>
334
+ </Popover.Popup>
335
+ </Popover.Positioner>
336
+ </Popover.Portal>
337
+ </Popover.Root>
338
+ </span>
339
+
340
+ <nav
341
+ data-notif="plain"
342
+ style={{ display: 'none' }}
343
+ aria-label={fromCopy(copy, 'board.notificationMenu.trigger')}
344
+ className="flex-wrap items-center gap-x-2 gap-y-1"
345
+ >
346
+ {model.tabs
347
+ .filter((tab) => tab.unread > 0)
348
+ .map((tab) => (
349
+ <a
350
+ key={tab.kind}
351
+ href={tab.allHref}
352
+ className="inline-flex items-center gap-1 rounded-full border border-border px-2 py-0.5 text-xs font-medium text-foreground hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
353
+ >
354
+ <span className="tabular-nums">{tab.unread}</span>
355
+ <span className="sr-only"> {fromCopy(copy, UNREAD_KEY[tab.kind])}</span>
356
+ </a>
357
+ ))}
358
+ </nav>
359
+ </div>
360
+ )
361
+ }
@@ -0,0 +1,29 @@
1
+ import { buttonVariants, Card, CardContent } from '@meith/ui'
2
+
3
+ import { type Copy, fromCopy } from './copy-record'
4
+
5
+ export function OfflineNotice({ message, copy }: { message: string; copy: Copy }) {
6
+ return (
7
+ <main
8
+ id="board-content"
9
+ tabIndex={-1}
10
+ className="flex min-h-screen flex-1 items-center justify-center px-6 py-12"
11
+ >
12
+ <Card className="w-full max-w-lg">
13
+ <CardContent className="p-6">
14
+ <p className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
15
+ {fromCopy(copy, 'offline.kicker')}
16
+ </p>
17
+ <h1 className="mt-1 text-2xl font-semibold tracking-tight text-balance">
18
+ {fromCopy(copy, 'offline.title')}
19
+ </h1>
20
+ <p className="mt-2 text-sm whitespace-pre-line text-muted-foreground">{message}</p>
21
+
22
+ <a href="/login" className={`mt-5 ${buttonVariants({ variant: 'primary' })}`}>
23
+ {fromCopy(copy, 'offline.logIn')}
24
+ </a>
25
+ </CardContent>
26
+ </Card>
27
+ </main>
28
+ )
29
+ }
@@ -0,0 +1,39 @@
1
+ import { BOARD_MEASURE } from '@/components/shell/measure'
2
+ import type { OnboardingNoticeModel } from '@/server/onboarding'
3
+ import { dismissOnboardingAction } from '@/server/onboarding-actions'
4
+
5
+ export function OnboardingBanner({
6
+ panel,
7
+ message,
8
+ ctaHref,
9
+ ctaLabel,
10
+ dismissLabel,
11
+ }: OnboardingNoticeModel) {
12
+ return (
13
+ <div className="border-b border-border bg-muted text-sm text-foreground">
14
+ <div className={`${BOARD_MEASURE} flex flex-wrap items-center justify-between gap-3 py-2`}>
15
+ <p>
16
+ {message}
17
+ {ctaHref !== null && ctaLabel !== null && (
18
+ <>
19
+ {' '}
20
+ <a
21
+ href={ctaHref}
22
+ className="font-medium underline underline-offset-2 hover:no-underline"
23
+ >
24
+ {ctaLabel}
25
+ </a>
26
+ </>
27
+ )}
28
+ </p>
29
+
30
+ <form action={dismissOnboardingAction}>
31
+ <input type="hidden" name="panel" value={panel} />
32
+ <button type="submit" className="text-xs text-muted-foreground hover:text-foreground">
33
+ {dismissLabel}
34
+ </button>
35
+ </form>
36
+ </div>
37
+ </div>
38
+ )
39
+ }
@@ -0,0 +1,167 @@
1
+ import type { Actor } from '@meith/authorization'
2
+ import { currentRequestId } from '@meith/core/logger'
3
+ import { requireSlot, slotCopy } from '@meith/theme-kit'
4
+
5
+ import { LogoutForm } from '@/components/account/logout-form'
6
+ import { NotificationMenu } from '@/components/shell/notification-menu'
7
+ import { OnboardingBanner } from '@/components/shell/onboarding-banner'
8
+ import { ThemeSwitcher } from '@/components/shell/theme-switcher'
9
+ import { avatarsFor } from '@/server/avatars'
10
+ import { currentLogo } from '@/server/branding'
11
+ import { getContainer } from '@/server/container'
12
+ import { getTranslator } from '@/server/i18n'
13
+ import { legalFooterLinks } from '@/server/legal'
14
+ import { unreadMessageCount } from '@/server/messages'
15
+ import { boardNavigation } from '@/server/navigation'
16
+ import { buildNotificationMenuModel } from '@/server/notification-menu'
17
+ import { unreadNotificationCount } from '@/server/notifications'
18
+ import { onboardingBanner } from '@/server/onboarding'
19
+ import { boardRegion, filterView, viewerRef } from '@/server/plugin-view'
20
+ import { touchCurrentLocation } from '@/server/presence'
21
+ import { registrationOpen } from '@/server/registration'
22
+ import { touchActivity } from '@/server/relations'
23
+ import { getSettings } from '@/server/settings'
24
+ import { currentTheme } from '@/server/theme'
25
+ import { getViewerPreferences } from '@/server/viewer-preferences'
26
+ import { buildForumJumpModel } from '@/view/forum-jump'
27
+ import { notificationMenuCopy } from '@/view/notification-menu'
28
+ import {
29
+ buildFooterModel,
30
+ buildHeaderModel,
31
+ buildUserPanelModel,
32
+ buildViewerModel,
33
+ } from '@/view/shell'
34
+
35
+ async function buildJumpModel(actor: Actor) {
36
+ const { authorizer, forums } = getContainer()
37
+ const [rows, visible] = await Promise.all([
38
+ forums.listAll(),
39
+ authorizer.forumIdsWhere(actor, 'forum.view'),
40
+ ])
41
+
42
+ return buildForumJumpModel({ rows, visibleForumIds: new Set(visible), t: await getTranslator() })
43
+ }
44
+
45
+ export async function PageShell({ actor, children }: { actor: Actor; children: React.ReactNode }) {
46
+ const theme = await currentTheme()
47
+ const Shell = requireSlot(theme, 'Shell')
48
+ const Header = requireSlot(theme, 'Header')
49
+ const UserPanel = requireSlot(theme, 'UserPanel')
50
+ const Footer = requireSlot(theme, 'Footer')
51
+ const ForumJump = requireSlot(theme, 'ForumJump')
52
+
53
+ const settings = await getSettings()
54
+ const boardTitle = settings.get('board.name')
55
+
56
+ const displayName =
57
+ actor.userId === null
58
+ ? null
59
+ : ((
60
+ await getContainer()
61
+ .memberProfiles.findPublicById(actor.userId)
62
+ .catch(() => null)
63
+ )?.username ?? null)
64
+
65
+ const ownAvatar =
66
+ actor.userId === null ? null : ((await avatarsFor([actor.userId])).get(actor.userId) ?? null)
67
+
68
+ const viewer = buildViewerModel(actor, {
69
+ displayName,
70
+ canAccessAdminCp: actor.global.canAccessAdminCp === true,
71
+ canAccessModCp: actor.global.canAccessModCp === true,
72
+ avatarUrl: ownAvatar,
73
+ })
74
+ const header = buildHeaderModel(
75
+ viewer,
76
+ await boardNavigation(actor, viewer),
77
+ boardTitle,
78
+ await currentLogo(boardTitle),
79
+ )
80
+
81
+ const unreadNotifications = await unreadNotificationCount(actor.userId)
82
+
83
+ const unreadMessages = await unreadMessageCount(actor.userId)
84
+
85
+ const notificationMenu = viewer.isGuest ? null : await buildNotificationMenuModel(actor)
86
+
87
+ await touchActivity(actor.userId)
88
+
89
+ await touchCurrentLocation()
90
+
91
+ const preferences = await getViewerPreferences()
92
+
93
+ const pluginContext = { ...viewerRef(actor), requestId: currentRequestId() ?? null }
94
+
95
+ const shellModel = await filterView('view.shell', { boardTitle, viewer }, pluginContext)
96
+ const headerModel = await filterView('view.header', header, pluginContext)
97
+ const userPanelModel = await filterView(
98
+ 'view.user-panel',
99
+ buildUserPanelModel(viewer, {
100
+ unreadNotifications,
101
+ unreadMessages,
102
+ registrationOpen: await registrationOpen(),
103
+ t: await getTranslator(),
104
+ }),
105
+ pluginContext,
106
+ )
107
+ const footerModel = await filterView(
108
+ 'view.footer',
109
+ buildFooterModel(
110
+ await legalFooterLinks(),
111
+ boardTitle,
112
+ preferences.timezone,
113
+ await getTranslator(),
114
+ ),
115
+ pluginContext,
116
+ )
117
+
118
+ const built = await buildJumpModel(actor).catch(() => null)
119
+ const jump = built === null ? null : await filterView('view.forum-jump', built, pluginContext)
120
+
121
+ const onboarding = await onboardingBanner(actor).catch(() => null)
122
+
123
+ const translator = await getTranslator()
124
+
125
+ return (
126
+ <Shell
127
+ boardTitle={shellModel.boardTitle}
128
+ viewer={shellModel.viewer}
129
+ copy={slotCopy(theme, 'Shell', translator)}
130
+ >
131
+ <Header {...headerModel} copy={slotCopy(theme, 'Header', translator)}>
132
+ <UserPanel
133
+ {...userPanelModel}
134
+ {...(notificationMenu === null
135
+ ? {}
136
+ : {
137
+ regions: {
138
+ notifications: (
139
+ <NotificationMenu
140
+ model={notificationMenu}
141
+ copy={notificationMenuCopy(translator)}
142
+ />
143
+ ),
144
+ },
145
+ })}
146
+ copy={slotCopy(theme, 'UserPanel', translator)}
147
+ >
148
+ {viewer.isGuest ? null : <LogoutForm label={translator.t('nav.logOut')} />}
149
+ </UserPanel>
150
+ </Header>
151
+
152
+ {await boardRegion('header.notice', actor)}
153
+
154
+ {onboarding !== null && <OnboardingBanner {...onboarding} />}
155
+
156
+ {children}
157
+
158
+ {jump !== null && jump.forums.length > 0 && (
159
+ <ForumJump {...jump} copy={slotCopy(theme, 'ForumJump', translator)} />
160
+ )}
161
+
162
+ <ThemeSwitcher />
163
+
164
+ <Footer {...footerModel} copy={slotCopy(theme, 'Footer', translator)} />
165
+ </Shell>
166
+ )
167
+ }
@@ -0,0 +1,95 @@
1
+ import { buttonVariants, cn, Empty, EmptyDescription, EmptyTitle } from '@meith/ui'
2
+
3
+ export const PANEL_LIST =
4
+ 'flex flex-col divide-y divide-border overflow-hidden rounded-xl border border-border bg-card shadow-elevation'
5
+
6
+ export const PANEL_ROW =
7
+ 'flex flex-wrap items-center justify-between gap-x-3 gap-y-2 px-4 py-3 transition-colors hover:bg-muted/40'
8
+
9
+ export const PANEL_CARD =
10
+ 'flex flex-col gap-3 rounded-xl border border-border bg-card p-4 shadow-elevation'
11
+
12
+ export const PANEL_NOTE =
13
+ 'rounded-xl border border-border bg-card p-4 text-sm text-muted-foreground shadow-elevation'
14
+
15
+ export function PanelList({
16
+ children,
17
+ className,
18
+ label,
19
+ }: {
20
+ readonly children: React.ReactNode
21
+ readonly className?: string
22
+ readonly label?: string
23
+ }) {
24
+ return (
25
+ <ul
26
+ className={cn(PANEL_LIST, className)}
27
+ {...(label === undefined ? {} : { 'aria-label': label })}
28
+ >
29
+ {children}
30
+ </ul>
31
+ )
32
+ }
33
+
34
+ export function PanelRow({
35
+ title,
36
+ meta,
37
+ badges,
38
+ actions,
39
+ className,
40
+ }: {
41
+ readonly title: React.ReactNode
42
+ readonly meta?: React.ReactNode
43
+ readonly badges?: React.ReactNode
44
+ readonly actions?: React.ReactNode
45
+ readonly className?: string
46
+ }) {
47
+ return (
48
+ <li className={cn(PANEL_ROW, className)}>
49
+ <span className="flex min-w-0 flex-1 flex-col gap-0.5">
50
+ <span className="flex min-w-0 flex-wrap items-center gap-2">
51
+ <span className="truncate text-sm font-medium text-foreground">{title}</span>
52
+ {badges}
53
+ </span>
54
+ {meta !== undefined && <span className="text-xs text-muted-foreground">{meta}</span>}
55
+ </span>
56
+
57
+ {actions !== undefined && (
58
+ <span className="flex shrink-0 flex-wrap items-center gap-2 text-sm">{actions}</span>
59
+ )}
60
+ </li>
61
+ )
62
+ }
63
+
64
+ export function PanelActionLink({
65
+ href,
66
+ children,
67
+ variant = 'outline',
68
+ }: {
69
+ readonly href: string
70
+ readonly children: React.ReactNode
71
+ readonly variant?: 'outline' | 'primary' | 'ghost'
72
+ }) {
73
+ return (
74
+ <a href={href} className={buttonVariants({ variant, size: 'sm' })}>
75
+ {children}
76
+ </a>
77
+ )
78
+ }
79
+
80
+ export function PanelEmpty({
81
+ title,
82
+ description,
83
+ }: {
84
+ readonly title: string
85
+ readonly description?: React.ReactNode
86
+ }) {
87
+ return (
88
+ <div className="rounded-xl border border-border bg-card shadow-elevation">
89
+ <Empty className="py-10">
90
+ <EmptyTitle>{title}</EmptyTitle>
91
+ {description !== undefined && <EmptyDescription>{description}</EmptyDescription>}
92
+ </Empty>
93
+ </div>
94
+ )
95
+ }