@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,119 @@
1
+ 'use client'
2
+
3
+ import type { EditableToken } from '@/view/theme-draft'
4
+
5
+ import { type Copy, fromCopy } from '../shell/copy'
6
+
7
+ export type CellState = 'clean' | 'saved' | 'unsaved'
8
+
9
+ export interface PaletteCell {
10
+ readonly token: EditableToken
11
+ readonly light: string
12
+ readonly dark: string
13
+ readonly state: CellState
14
+ readonly visible: boolean
15
+ }
16
+
17
+ const STATE_NOTE_KEY: Readonly<Record<CellState, string>> = {
18
+ clean: '',
19
+ saved: 'adminTheme.palette.changed',
20
+ unsaved: 'adminTheme.palette.unsaved',
21
+ }
22
+
23
+ function Swatch({ cell }: { cell: PaletteCell }) {
24
+ if (cell.token.kind !== 'colour') {
25
+ return (
26
+ <span className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-md border border-border bg-muted px-1">
27
+ <span className="truncate font-mono text-[0.5625rem] text-muted-foreground">
28
+ {cell.light}
29
+ </span>
30
+ </span>
31
+ )
32
+ }
33
+
34
+ return (
35
+ <span
36
+ aria-hidden
37
+ className="flex size-8 shrink-0 overflow-hidden rounded-md border border-border"
38
+ >
39
+ <span className="flex-1" style={{ background: cell.light }} />
40
+ <span className="flex-1" style={{ background: cell.dark }} />
41
+ </span>
42
+ )
43
+ }
44
+
45
+ function Cell({
46
+ cell,
47
+ expanded,
48
+ copy,
49
+ onSelect,
50
+ }: {
51
+ cell: PaletteCell
52
+ expanded: boolean
53
+ copy: Copy
54
+ onSelect: () => void
55
+ }) {
56
+ const note = cell.state === 'clean' ? '' : fromCopy(copy, STATE_NOTE_KEY[cell.state])
57
+
58
+ return (
59
+ <button
60
+ type="button"
61
+ hidden={!cell.visible}
62
+ aria-expanded={expanded}
63
+ aria-controls={`token-${cell.token.name}`}
64
+ onClick={onSelect}
65
+ className={`flex items-center gap-2 rounded-md border p-2 text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring ${
66
+ expanded
67
+ ? 'border-primary bg-accent'
68
+ : cell.state === 'unsaved'
69
+ ? 'border-primary'
70
+ : cell.state === 'saved'
71
+ ? 'border-foreground/40'
72
+ : 'border-border hover:bg-accent hover:text-accent-foreground'
73
+ }`}
74
+ >
75
+ <Swatch cell={cell} />
76
+ <span className="flex min-w-0 flex-col">
77
+ <span className="truncate text-xs font-medium">{cell.token.label}</span>
78
+ <span className="truncate font-mono text-[0.6875rem] text-muted-foreground">
79
+ {cell.token.name}
80
+ </span>
81
+ {note !== '' && (
82
+ <span
83
+ className={`truncate text-[0.6875rem] ${
84
+ cell.state === 'unsaved' ? 'font-medium text-primary' : 'text-muted-foreground'
85
+ }`}
86
+ >
87
+ {note}
88
+ </span>
89
+ )}
90
+ </span>
91
+ </button>
92
+ )
93
+ }
94
+
95
+ export function PaletteGrid({
96
+ cells,
97
+ selected,
98
+ copy,
99
+ onSelect,
100
+ }: {
101
+ cells: readonly PaletteCell[]
102
+ selected: string | null
103
+ copy: Copy
104
+ onSelect: (name: string) => void
105
+ }) {
106
+ return (
107
+ <div className="grid grid-cols-[repeat(auto-fill,minmax(9.5rem,1fr))] gap-2">
108
+ {cells.map((cell) => (
109
+ <Cell
110
+ key={cell.token.name}
111
+ cell={cell}
112
+ expanded={selected === cell.token.name}
113
+ copy={copy}
114
+ onSelect={() => onSelect(cell.token.name)}
115
+ />
116
+ ))}
117
+ </div>
118
+ )
119
+ }
@@ -0,0 +1,461 @@
1
+ 'use client'
2
+
3
+ import { useId, useState } from 'react'
4
+
5
+ import { type Copy, formatFromCopy, fromCopy } from '../shell/copy'
6
+
7
+ export const PREVIEW_SCENES = [
8
+ {
9
+ key: 'forums',
10
+ titleKey: 'adminTheme.preview.scene.forums.title',
11
+ blurbKey: 'adminTheme.preview.scene.forums.blurb',
12
+ },
13
+ {
14
+ key: 'thread',
15
+ titleKey: 'adminTheme.preview.scene.thread.title',
16
+ blurbKey: 'adminTheme.preview.scene.thread.blurb',
17
+ },
18
+ {
19
+ key: 'controls',
20
+ titleKey: 'adminTheme.preview.scene.controls.title',
21
+ blurbKey: 'adminTheme.preview.scene.controls.blurb',
22
+ },
23
+ ] as const
24
+
25
+ export type PreviewScene = (typeof PREVIEW_SCENES)[number]['key']
26
+
27
+ const CHIP =
28
+ 'inline-flex h-8 items-center justify-center rounded-md border px-3 text-xs font-medium focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
29
+
30
+ function ForumsScene({ copy }: { copy: Copy }) {
31
+ return (
32
+ <div className="flex flex-col gap-3">
33
+ <div
34
+ data-token="surface"
35
+ className="flex items-center justify-between gap-2 rounded-md bg-surface px-3 py-2"
36
+ >
37
+ <span className="font-heading text-sm font-semibold">
38
+ {fromCopy(copy, 'adminTheme.sample.boardName')}
39
+ </span>
40
+ <span data-token="muted-foreground" className="text-xs text-muted-foreground">
41
+ {fromCopy(copy, 'adminTheme.sample.signedInAs')}
42
+ </span>
43
+ </div>
44
+
45
+ <div className="overflow-hidden rounded-md border border-border shadow-elevation">
46
+ <p data-token="surface" className="bg-surface px-3 py-2 text-xs font-medium">
47
+ {fromCopy(copy, 'adminTheme.sample.category')}
48
+ </p>
49
+
50
+ <div
51
+ data-token="card"
52
+ className="flex flex-col divide-y divide-border bg-card text-card-foreground"
53
+ >
54
+ <div className="flex items-baseline justify-between gap-3 p-3">
55
+ <span className="flex min-w-0 flex-col">
56
+ <span data-token="forum-unread" className="text-sm font-semibold text-forum-unread">
57
+ {fromCopy(copy, 'adminTheme.sample.announcements')}
58
+ </span>
59
+ <span className="text-xs text-muted-foreground">
60
+ {copy['adminTheme.sample.lastPostAdminLead']}
61
+ <span data-token="group-admin" className="font-medium text-group-admin">
62
+ ada
63
+ </span>
64
+ {copy['adminTheme.sample.lastPostAdminTail']}
65
+ </span>
66
+ </span>
67
+ <span data-token="muted-foreground" className="shrink-0 text-xs text-muted-foreground">
68
+ {formatFromCopy(copy, 'adminTheme.sample.threadCount', { count: 128 })}
69
+ </span>
70
+ </div>
71
+
72
+ <div className="flex items-baseline justify-between gap-3 p-3">
73
+ <span className="flex min-w-0 flex-col">
74
+ <span data-token="forum-read" className="text-sm font-semibold text-forum-read">
75
+ {fromCopy(copy, 'adminTheme.sample.introductions')}
76
+ </span>
77
+ <span className="text-xs text-muted-foreground">
78
+ {copy['adminTheme.sample.lastPostModLead']}
79
+ <span data-token="group-mod" className="font-medium text-group-mod">
80
+ grace
81
+ </span>
82
+ {copy['adminTheme.sample.lastPostModTail']}
83
+ </span>
84
+ </span>
85
+ <span className="shrink-0 text-xs text-muted-foreground">
86
+ {formatFromCopy(copy, 'adminTheme.sample.threadCount', { count: 1204 })}
87
+ </span>
88
+ </div>
89
+
90
+ <div className="flex items-baseline justify-between gap-3 p-3">
91
+ <span className="flex min-w-0 flex-col">
92
+ <span data-token="forum-locked" className="text-sm font-semibold text-forum-locked">
93
+ {fromCopy(copy, 'adminTheme.sample.archive')}
94
+ </span>
95
+ <span className="text-xs text-muted-foreground">
96
+ {fromCopy(copy, 'adminTheme.sample.closedToNewPosts')}
97
+ </span>
98
+ </span>
99
+ <span className="shrink-0 text-xs text-muted-foreground">
100
+ {formatFromCopy(copy, 'adminTheme.sample.threadCount', { count: 96 })}
101
+ </span>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <div
107
+ data-token="card"
108
+ className="flex flex-wrap items-center gap-2 rounded-md bg-card p-3 text-xs shadow-elevation"
109
+ >
110
+ <span data-token="thread-pinned" className="font-medium text-thread-pinned">
111
+ {fromCopy(copy, 'adminTheme.sample.pinnedThread')}
112
+ </span>
113
+ <span className="text-muted-foreground">·</span>
114
+ <span data-token="thread-locked" className="font-medium text-thread-locked">
115
+ {fromCopy(copy, 'adminTheme.sample.lockedThread')}
116
+ </span>
117
+ <span className="text-muted-foreground">·</span>
118
+ <span data-token="thread-moved" className="font-medium text-thread-moved">
119
+ {fromCopy(copy, 'adminTheme.sample.movedThread')}
120
+ </span>
121
+ </div>
122
+ </div>
123
+ )
124
+ }
125
+
126
+ function ThreadScene({ copy }: { copy: Copy }) {
127
+ return (
128
+ <div className="flex flex-col gap-3">
129
+ <p data-token="muted-foreground" className="text-xs text-muted-foreground">
130
+ {copy['adminTheme.sample.breadcrumbLead']}
131
+ <span data-token="foreground" className="text-foreground">
132
+ {fromCopy(copy, 'adminTheme.sample.threadTitle')}
133
+ </span>
134
+ {copy['adminTheme.sample.breadcrumbTail']}
135
+ </p>
136
+
137
+ <div className="flex flex-wrap items-center gap-2">
138
+ <h4 className="font-heading text-base font-semibold">
139
+ {fromCopy(copy, 'adminTheme.sample.threadTitle')}
140
+ </h4>
141
+ <span
142
+ data-token="thread-pinned"
143
+ className="rounded-full border border-border px-2 py-0.5 text-xs text-thread-pinned"
144
+ >
145
+ {fromCopy(copy, 'adminTheme.sample.pinned')}
146
+ </span>
147
+ <span
148
+ data-token="thread-unapproved"
149
+ className="rounded-full border border-border px-2 py-0.5 text-xs text-thread-unapproved"
150
+ >
151
+ {fromCopy(copy, 'adminTheme.sample.awaitingApproval')}
152
+ </span>
153
+ </div>
154
+
155
+ <article
156
+ data-token="post-highlight"
157
+ className="rounded-md bg-post-highlight p-3 shadow-elevation"
158
+ >
159
+ <p className="flex flex-wrap items-baseline gap-2 text-xs">
160
+ <span data-token="group-supermod" className="font-semibold text-group-supermod">
161
+ grace
162
+ </span>
163
+ <span data-token="muted-foreground" className="text-muted-foreground">
164
+ {fromCopy(copy, 'adminTheme.sample.supermodMeta')}
165
+ </span>
166
+ </p>
167
+ <p data-token="card-foreground" className="mt-2 text-sm">
168
+ {fromCopy(copy, 'adminTheme.sample.permalinkNote')}
169
+ </p>
170
+ <p data-token="muted" className="mt-2 rounded-md bg-muted px-2 py-1 font-mono text-xs">
171
+ pnpm meith upgrade
172
+ </p>
173
+ </article>
174
+
175
+ <article data-token="post-own" className="rounded-md bg-post-own p-3 shadow-elevation">
176
+ <p className="flex flex-wrap items-baseline gap-2 text-xs">
177
+ <span data-token="group-admin" className="font-semibold text-group-admin">
178
+ ada
179
+ </span>
180
+ <span className="text-muted-foreground">
181
+ {fromCopy(copy, 'adminTheme.sample.adminMeta')}
182
+ </span>
183
+ </p>
184
+ <p className="mt-2 text-sm">
185
+ {copy['adminTheme.sample.ownPostLead']}
186
+ <a
187
+ data-token="primary"
188
+ href="#preview"
189
+ className="font-medium underline underline-offset-2"
190
+ >
191
+ {fromCopy(copy, 'adminTheme.sample.ordinaryLink')}
192
+ </a>
193
+ {copy['adminTheme.sample.ownPostTail']}
194
+ </p>
195
+ </article>
196
+
197
+ <article
198
+ data-token="post-unapproved"
199
+ className="rounded-md bg-post-unapproved p-3 text-sm shadow-elevation"
200
+ >
201
+ <p className="flex flex-wrap items-baseline gap-2 text-xs">
202
+ <span data-token="group-banned" className="font-semibold text-group-banned">
203
+ wren
204
+ </span>
205
+ <span className="text-muted-foreground">
206
+ {fromCopy(copy, 'adminTheme.sample.bannedMeta')}
207
+ </span>
208
+ </p>
209
+ <p className="mt-2">{fromCopy(copy, 'adminTheme.sample.heldPost')}</p>
210
+ </article>
211
+ </div>
212
+ )
213
+ }
214
+
215
+ function ControlsScene({ copy }: { copy: Copy }) {
216
+ return (
217
+ <div className="flex flex-col gap-3">
218
+ <div className="flex flex-wrap items-center gap-2">
219
+ <span
220
+ data-token="primary"
221
+ className="inline-flex h-9 items-center rounded-md bg-primary px-3 text-sm font-medium text-primary-foreground"
222
+ >
223
+ <span data-token="primary-foreground">
224
+ {fromCopy(copy, 'adminTheme.sample.postReply')}
225
+ </span>
226
+ </span>
227
+ <span
228
+ data-token="primary-hover"
229
+ className="inline-flex h-9 items-center rounded-md bg-primary-hover px-3 text-sm font-medium text-primary-foreground"
230
+ >
231
+ {fromCopy(copy, 'adminTheme.sample.hovered')}
232
+ </span>
233
+ <span
234
+ data-token="secondary"
235
+ className="inline-flex h-9 items-center rounded-md bg-secondary px-3 text-sm font-medium text-secondary-foreground"
236
+ >
237
+ <span data-token="secondary-foreground">
238
+ {fromCopy(copy, 'adminTheme.sample.previewButton')}
239
+ </span>
240
+ </span>
241
+ <span
242
+ data-token="destructive"
243
+ className="inline-flex h-9 items-center rounded-md bg-destructive px-3 text-sm font-medium text-destructive-foreground"
244
+ >
245
+ <span data-token="destructive-foreground">
246
+ {fromCopy(copy, 'adminTheme.sample.delete')}
247
+ </span>
248
+ </span>
249
+ </div>
250
+
251
+ <div
252
+ data-token="card"
253
+ className="flex flex-col gap-1 rounded-md bg-card p-3 shadow-elevation"
254
+ >
255
+ <span className="text-xs font-medium">
256
+ {fromCopy(copy, 'adminTheme.sample.threadTitleLabel')}
257
+ </span>
258
+ <span data-token="ring" className="rounded-md outline-2 outline-offset-2 outline-ring">
259
+ <span
260
+ data-token="input"
261
+ className="flex h-9 items-center rounded-md border border-input bg-background px-3 text-xs"
262
+ >
263
+ {fromCopy(copy, 'adminTheme.sample.threadTitle')}
264
+ </span>
265
+ </span>
266
+ <span className="text-xs text-muted-foreground">
267
+ {fromCopy(copy, 'adminTheme.sample.fieldNote')}
268
+ </span>
269
+ </div>
270
+
271
+ <div
272
+ data-token="accent"
273
+ className="flex flex-wrap items-center gap-2 rounded-md bg-accent p-3 text-xs text-accent-foreground"
274
+ >
275
+ <span data-token="accent-foreground">{fromCopy(copy, 'adminTheme.sample.hoveredRow')}</span>
276
+ </div>
277
+
278
+ <div
279
+ data-token="card"
280
+ className="flex flex-col gap-2 rounded-md bg-card p-3 text-xs shadow-elevation"
281
+ >
282
+ <span className="font-medium">{fromCopy(copy, 'adminTheme.sample.moderationQueue')}</span>
283
+ <span className="flex flex-wrap gap-3">
284
+ <span data-token="moderation-pending" className="font-medium text-moderation-pending">
285
+ {fromCopy(copy, 'adminTheme.sample.queueWaiting')}
286
+ </span>
287
+ <span data-token="moderation-approved" className="font-medium text-moderation-approved">
288
+ {fromCopy(copy, 'adminTheme.sample.queueApproved')}
289
+ </span>
290
+ <span data-token="moderation-rejected" className="font-medium text-moderation-rejected">
291
+ {fromCopy(copy, 'adminTheme.sample.queueRejected')}
292
+ </span>
293
+ <span data-token="thread-deleted" className="font-medium text-thread-deleted">
294
+ {fromCopy(copy, 'adminTheme.sample.queueDeleted')}
295
+ </span>
296
+ </span>
297
+ </div>
298
+ </div>
299
+ )
300
+ }
301
+
302
+ function Scene({ scene, copy }: { scene: PreviewScene; copy: Copy }) {
303
+ if (scene === 'forums') return <ForumsScene copy={copy} />
304
+ if (scene === 'thread') return <ThreadScene copy={copy} />
305
+ return <ControlsScene copy={copy} />
306
+ }
307
+
308
+ function SampleFrame({
309
+ palette,
310
+ dark,
311
+ label,
312
+ scene,
313
+ copy,
314
+ onPick,
315
+ }: {
316
+ palette: React.CSSProperties
317
+ dark?: boolean
318
+ label: string
319
+ scene: PreviewScene
320
+ copy: Copy
321
+ onPick?: ((token: string) => void) | undefined
322
+ }) {
323
+ const pick = (event: React.MouseEvent<HTMLElement>): void => {
324
+ if (onPick === undefined) return
325
+ const tagged = (event.target as HTMLElement).closest<HTMLElement>('[data-token]')
326
+ const token = tagged?.dataset.token
327
+ if (token === undefined || token === '') return
328
+ event.preventDefault()
329
+ onPick(token)
330
+ }
331
+
332
+ return (
333
+ <div className="flex min-w-0 flex-1 flex-col gap-2">
334
+ <p className="text-xs font-medium text-muted-foreground">{label}</p>
335
+ <div
336
+ data-theme-preview
337
+ style={palette}
338
+ onClick={onPick === undefined ? undefined : pick}
339
+ className={`flex flex-col gap-3 rounded-lg border border-border bg-background p-3 text-foreground ${
340
+ dark === true ? 'dark' : ''
341
+ } ${
342
+ onPick === undefined
343
+ ? ''
344
+ : '[&_[data-token]]:cursor-pointer [&_[data-token]:hover]:outline-2 [&_[data-token]:hover]:outline-offset-2 [&_[data-token]:hover]:outline-ring'
345
+ }`}
346
+ >
347
+ <Scene scene={scene} copy={copy} />
348
+ </div>
349
+ </div>
350
+ )
351
+ }
352
+
353
+ export function ThemePreview({
354
+ copy,
355
+ light,
356
+ dark,
357
+ hydrated,
358
+ onPick,
359
+ }: {
360
+ copy: Copy
361
+ light: React.CSSProperties
362
+ dark: React.CSSProperties
363
+ hydrated: boolean
364
+ onPick?: ((token: string) => void) | undefined
365
+ }) {
366
+ const [scene, setScene] = useState<PreviewScene>('forums')
367
+ const groupId = useId()
368
+
369
+ const shown = hydrated ? PREVIEW_SCENES.filter((entry) => entry.key === scene) : PREVIEW_SCENES
370
+
371
+ return (
372
+ <section className="flex flex-col gap-3" id="preview">
373
+ <div className="flex flex-col gap-1">
374
+ <h3 className="text-base font-semibold tracking-tight">
375
+ {fromCopy(copy, 'adminTheme.preview.title')}
376
+ </h3>
377
+ <p className="text-xs text-muted-foreground">
378
+ {fromCopy(copy, 'adminTheme.preview.blurb')}
379
+ </p>
380
+ {hydrated && onPick !== undefined && (
381
+ <p className="text-xs text-muted-foreground">
382
+ {copy['adminTheme.preview.clickNoteLead']}
383
+ <strong className="font-medium text-foreground">
384
+ {fromCopy(copy, 'adminTheme.preview.clickNoteStrong')}
385
+ </strong>
386
+ {copy['adminTheme.preview.clickNoteTail']}
387
+ </p>
388
+ )}
389
+ </div>
390
+
391
+ {hydrated && (
392
+ <div
393
+ role="group"
394
+ aria-label={fromCopy(copy, 'adminTheme.preview.sceneGroup')}
395
+ className="flex flex-wrap gap-2"
396
+ >
397
+ {PREVIEW_SCENES.map((entry) => (
398
+ <button
399
+ key={entry.key}
400
+ type="button"
401
+ aria-pressed={scene === entry.key}
402
+ onClick={() => setScene(entry.key)}
403
+ className={`${CHIP} ${
404
+ scene === entry.key
405
+ ? 'border-primary bg-primary text-primary-foreground'
406
+ : 'border-border hover:bg-accent hover:text-accent-foreground'
407
+ }`}
408
+ >
409
+ {fromCopy(copy, entry.titleKey)}
410
+ </button>
411
+ ))}
412
+ </div>
413
+ )}
414
+
415
+ {shown.map((entry) => (
416
+ <div key={entry.key} className="flex flex-col gap-2">
417
+ <p className="text-xs text-muted-foreground" id={`${groupId}-${entry.key}`}>
418
+ {fromCopy(copy, entry.blurbKey)}
419
+ </p>
420
+ <div className="flex flex-col gap-3">
421
+ <SampleFrame
422
+ label={fromCopy(copy, 'adminTheme.scheme.light')}
423
+ palette={light}
424
+ scene={entry.key}
425
+ copy={copy}
426
+ onPick={onPick}
427
+ />
428
+ <SampleFrame
429
+ label={fromCopy(copy, 'adminTheme.scheme.dark')}
430
+ palette={dark}
431
+ scene={entry.key}
432
+ copy={copy}
433
+ dark
434
+ onPick={onPick}
435
+ />
436
+ </div>
437
+ </div>
438
+ ))}
439
+ </section>
440
+ )
441
+ }
442
+
443
+ export function ValidatedSample({ scene = 'forums', copy }: { scene?: PreviewScene; copy: Copy }) {
444
+ return (
445
+ <div className="flex flex-col gap-3 sm:flex-row">
446
+ <SampleFrame
447
+ label={fromCopy(copy, 'adminTheme.scheme.light')}
448
+ palette={{}}
449
+ scene={scene}
450
+ copy={copy}
451
+ />
452
+ <SampleFrame
453
+ label={fromCopy(copy, 'adminTheme.scheme.dark')}
454
+ palette={{}}
455
+ scene={scene}
456
+ copy={copy}
457
+ dark
458
+ />
459
+ </div>
460
+ )
461
+ }
@@ -0,0 +1,99 @@
1
+ 'use client'
2
+
3
+ import { useActionState } from 'react'
4
+
5
+ import { EMPTY_STATE } from '@/server/auth-form-state'
6
+ import { bulkUserAction } from '@/server/user-admin-actions'
7
+
8
+ import { FormError, SubmitButton } from '../auth/form-controls'
9
+ import type { Copy } from '../shell/copy'
10
+ import { AdminUndo } from './admin-undo'
11
+ import { INPUT, Saved } from './form-bits'
12
+
13
+ interface Labels {
14
+ readonly action: string
15
+ readonly apply: string
16
+ readonly ban: string
17
+ readonly banned: string
18
+ readonly choose: string
19
+ readonly group: string
20
+ readonly grouped: string
21
+ readonly prune: string
22
+ readonly pruneReview: string
23
+ readonly reason: string
24
+ readonly selectPage: string
25
+ }
26
+
27
+ export function UserBulkToolbar({
28
+ groups,
29
+ labels,
30
+ copy,
31
+ }: {
32
+ groups: readonly { readonly id: number; readonly title: string }[]
33
+ labels: Labels
34
+ copy: Copy
35
+ }) {
36
+ const [state, action] = useActionState(bulkUserAction, EMPTY_STATE)
37
+ const selection = state.values?.selection
38
+
39
+ return (
40
+ <div className="flex flex-col gap-3 rounded-md border border-border bg-card p-4 text-card-foreground">
41
+ <FormError message={state.error} />
42
+ <Saved when={state.notice === 'bulk-banned'}>{labels.banned}</Saved>
43
+ <Saved when={state.notice === 'bulk-grouped'}>{labels.grouped}</Saved>
44
+ <AdminUndo undo={state.undo} copy={copy} />
45
+ {selection !== undefined && (
46
+ <a
47
+ href={`/admin/users/prune?selection=${encodeURIComponent(selection)}`}
48
+ className="text-sm font-medium text-primary underline-offset-4 hover:underline"
49
+ >
50
+ {labels.pruneReview}
51
+ </a>
52
+ )}
53
+ <form
54
+ id="admin-user-bulk"
55
+ action={action}
56
+ className="flex flex-col gap-3 sm:flex-row sm:items-end"
57
+ >
58
+ <label className="flex min-w-48 flex-col gap-1 text-sm">
59
+ <span className="font-medium">{labels.action}</span>
60
+ <select name="bulkAction" defaultValue="" className={INPUT} required>
61
+ <option value="">{labels.choose}</option>
62
+ <option value="ban">{labels.ban}</option>
63
+ <option value="group">{labels.group}</option>
64
+ <option value="prune">{labels.prune}</option>
65
+ </select>
66
+ </label>
67
+ <label className="flex min-w-48 flex-col gap-1 text-sm">
68
+ <span className="font-medium">{labels.group}</span>
69
+ <select name="groupId" defaultValue="" className={INPUT}>
70
+ <option value="">{labels.choose}</option>
71
+ {groups.map((group) => (
72
+ <option key={group.id} value={group.id}>
73
+ {group.title}
74
+ </option>
75
+ ))}
76
+ </select>
77
+ </label>
78
+ <label className="flex min-w-48 flex-1 flex-col gap-1 text-sm">
79
+ <span className="font-medium">{labels.reason}</span>
80
+ <input name="reason" className={INPUT} />
81
+ </label>
82
+ <button
83
+ type="button"
84
+ className="inline-flex h-10 items-center justify-center rounded-md border border-border px-4 text-sm font-medium"
85
+ onClick={() => {
86
+ for (const checkbox of document.querySelectorAll<HTMLInputElement>(
87
+ '[data-admin-user-select]',
88
+ )) {
89
+ checkbox.checked = true
90
+ }
91
+ }}
92
+ >
93
+ {labels.selectPage}
94
+ </button>
95
+ <SubmitButton>{labels.apply}</SubmitButton>
96
+ </form>
97
+ </div>
98
+ )
99
+ }