@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,47 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+
5
+ import { Button } from '@meith/ui/button'
6
+
7
+ import { passkeyMessage, proveWithPasskey } from '@/components/auth/passkey-client'
8
+
9
+ import type { Copy } from '../shell/copy-record'
10
+
11
+ export function PasskeyProofButton({
12
+ next,
13
+ copy,
14
+ waitingLabel,
15
+ verifyLabel,
16
+ }: {
17
+ readonly next: string
18
+ readonly copy: Copy
19
+ readonly waitingLabel: string
20
+ readonly verifyLabel: string
21
+ }) {
22
+ const [busy, setBusy] = useState(false)
23
+ const [error, setError] = useState<string | null>(null)
24
+
25
+ return (
26
+ <div className="flex flex-col items-start gap-2 rounded-lg border border-border bg-card p-5">
27
+ <Button
28
+ type="button"
29
+ variant="primary"
30
+ disabled={busy}
31
+ onClick={() => {
32
+ setBusy(true)
33
+ setError(null)
34
+ proveWithPasskey(next, copy)
35
+ .then((target) => window.location.assign(target))
36
+ .catch((problem: unknown) => {
37
+ setError(passkeyMessage(problem, copy))
38
+ setBusy(false)
39
+ })
40
+ }}
41
+ >
42
+ {busy ? waitingLabel : verifyLabel}
43
+ </Button>
44
+ {error === null ? null : <p className="text-sm text-destructive">{error}</p>}
45
+ </div>
46
+ )
47
+ }
@@ -0,0 +1,193 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useState } from 'react'
4
+
5
+ import { subscribeToPushAction, unsubscribeFromPushAction } from '@/server/push-actions'
6
+
7
+ import { FormError, FormNotice } from '../auth/form-controls'
8
+ import { type Copy, fromCopy } from '../shell/copy'
9
+
10
+ const BUTTON =
11
+ 'inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:opacity-60'
12
+
13
+ type Status = 'checking' | 'unsupported' | 'blocked' | 'off' | 'on' | 'working'
14
+
15
+ function applicationServerKey(publicKey: string): Uint8Array<ArrayBuffer> {
16
+ const padded = publicKey.replace(/-/g, '+').replace(/_/g, '/')
17
+ const raw = atob(padded + '='.repeat((4 - (padded.length % 4)) % 4))
18
+
19
+ const bytes = new Uint8Array(raw.length)
20
+ for (let index = 0; index < raw.length; index += 1) bytes[index] = raw.charCodeAt(index)
21
+ return bytes
22
+ }
23
+
24
+ function keyOf(subscription: PushSubscription, name: 'p256dh' | 'auth'): string {
25
+ const key = subscription.getKey(name)
26
+ if (key === null) return ''
27
+
28
+ let binary = ''
29
+ for (const byte of new Uint8Array(key)) binary += String.fromCharCode(byte)
30
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
31
+ }
32
+
33
+ export function PushDeviceForm({ publicKey, copy }: { publicKey: string; copy: Copy }) {
34
+ const [status, setStatus] = useState<Status>('checking')
35
+ const [error, setError] = useState<string | undefined>(undefined)
36
+ const [notice, setNotice] = useState<string | undefined>(undefined)
37
+
38
+ const supported =
39
+ typeof navigator !== 'undefined' &&
40
+ 'serviceWorker' in navigator &&
41
+ 'PushManager' in window &&
42
+ 'Notification' in window
43
+
44
+ useEffect(() => {
45
+ if (!supported) {
46
+ setStatus('unsupported')
47
+ return
48
+ }
49
+
50
+ let cancelled = false
51
+
52
+ void (async () => {
53
+ try {
54
+ const registration = await navigator.serviceWorker.register('/sw.js', {
55
+ scope: '/',
56
+ updateViaCache: 'none',
57
+ })
58
+ const subscription = await registration.pushManager.getSubscription()
59
+ if (cancelled) return
60
+
61
+ if (subscription !== null) {
62
+ await subscribeToPushAction({
63
+ endpoint: subscription.endpoint,
64
+ p256dh: keyOf(subscription, 'p256dh'),
65
+ auth: keyOf(subscription, 'auth'),
66
+ })
67
+ if (!cancelled) setStatus('on')
68
+ return
69
+ }
70
+
71
+ setStatus(Notification.permission === 'denied' ? 'blocked' : 'off')
72
+ } catch {
73
+ if (!cancelled) setStatus('unsupported')
74
+ }
75
+ })()
76
+
77
+ return () => {
78
+ cancelled = true
79
+ }
80
+ }, [supported])
81
+
82
+ const subscribe = useCallback(async () => {
83
+ setError(undefined)
84
+ setNotice(undefined)
85
+ setStatus('working')
86
+
87
+ try {
88
+ const permission = await Notification.requestPermission()
89
+ if (permission !== 'granted') {
90
+ setStatus(permission === 'denied' ? 'blocked' : 'off')
91
+ return
92
+ }
93
+
94
+ const registration = await navigator.serviceWorker.ready
95
+ const subscription = await registration.pushManager.subscribe({
96
+ userVisibleOnly: true,
97
+ applicationServerKey: applicationServerKey(publicKey),
98
+ })
99
+
100
+ const result = await subscribeToPushAction({
101
+ endpoint: subscription.endpoint,
102
+ p256dh: keyOf(subscription, 'p256dh'),
103
+ auth: keyOf(subscription, 'auth'),
104
+ })
105
+
106
+ if (result.error !== undefined) {
107
+ await subscription.unsubscribe()
108
+ setError(result.error)
109
+ setStatus('off')
110
+ return
111
+ }
112
+
113
+ setNotice(result.notice)
114
+ setStatus('on')
115
+ } catch {
116
+ setError(fromCopy(copy, 'accountForm.push.failed'))
117
+ setStatus('off')
118
+ }
119
+ }, [copy, publicKey])
120
+
121
+ const unsubscribe = useCallback(async () => {
122
+ setError(undefined)
123
+ setNotice(undefined)
124
+ setStatus('working')
125
+
126
+ try {
127
+ const registration = await navigator.serviceWorker.ready
128
+ const subscription = await registration.pushManager.getSubscription()
129
+
130
+ if (subscription !== null) {
131
+ const result = await unsubscribeFromPushAction(subscription.endpoint)
132
+ await subscription.unsubscribe()
133
+ if (result.error !== undefined) {
134
+ setError(result.error)
135
+ setStatus('on')
136
+ return
137
+ }
138
+ setNotice(result.notice)
139
+ }
140
+
141
+ setStatus('off')
142
+ } catch {
143
+ setError(fromCopy(copy, 'accountForm.push.failed'))
144
+ setStatus('on')
145
+ }
146
+ }, [copy])
147
+
148
+ return (
149
+ <section className="flex flex-col gap-3 rounded-lg border border-border bg-card p-5">
150
+ <h2 className="text-sm font-medium">{fromCopy(copy, 'accountForm.push.heading')}</h2>
151
+
152
+ <FormError message={error} />
153
+ <FormNotice message={notice} />
154
+
155
+ <p className="text-sm text-muted-foreground">
156
+ {fromCopy(
157
+ copy,
158
+ status === 'checking'
159
+ ? 'form.working'
160
+ : status === 'unsupported'
161
+ ? 'accountForm.push.unsupported'
162
+ : status === 'blocked'
163
+ ? 'accountForm.push.blocked'
164
+ : status === 'on'
165
+ ? 'accountForm.push.subscribed'
166
+ : 'accountForm.push.unsubscribed',
167
+ )}
168
+ </p>
169
+
170
+ {(status === 'off' || status === 'on' || status === 'working') && (
171
+ <div>
172
+ <button
173
+ type="button"
174
+ className={BUTTON}
175
+ disabled={status === 'working'}
176
+ onClick={status === 'on' ? unsubscribe : subscribe}
177
+ >
178
+ {fromCopy(
179
+ copy,
180
+ status === 'working'
181
+ ? 'form.working'
182
+ : status === 'on'
183
+ ? 'accountForm.push.unsubscribe'
184
+ : 'accountForm.push.subscribe',
185
+ )}
186
+ </button>
187
+ </div>
188
+ )}
189
+
190
+ <p className="text-xs text-muted-foreground">{fromCopy(copy, 'accountForm.push.blurb')}</p>
191
+ </section>
192
+ )
193
+ }
@@ -0,0 +1,69 @@
1
+ 'use client'
2
+
3
+ import { useActionState } from 'react'
4
+
5
+ import { EMPTY_STATE } from '@/server/auth-form-state'
6
+ import { removeRelationAction, setRelationAction } from '@/server/relation-actions'
7
+
8
+ import { FormError } from '../auth/form-controls'
9
+
10
+ const LINK_BUTTON =
11
+ 'text-sm 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'
12
+
13
+ const QUIET_BUTTON =
14
+ 'text-sm text-muted-foreground hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
15
+
16
+ export function SetRelationForm({
17
+ userId,
18
+ username,
19
+ kind,
20
+ returnTo,
21
+ label,
22
+ }: {
23
+ userId: number
24
+ username: string
25
+ kind: 'buddy' | 'ignore'
26
+ returnTo: string
27
+ label: string
28
+ }) {
29
+ const [state, action] = useActionState(setRelationAction, EMPTY_STATE)
30
+
31
+ return (
32
+ <form action={action} className="inline-flex flex-col gap-1">
33
+ <FormError message={state.error} />
34
+ <input type="hidden" name="userId" value={userId} />
35
+ <input type="hidden" name="username" value={username} />
36
+ <input type="hidden" name="kind" value={kind} />
37
+ <input type="hidden" name="returnTo" value={returnTo} />
38
+ <button type="submit" className={kind === 'buddy' ? LINK_BUTTON : QUIET_BUTTON}>
39
+ {label}
40
+ </button>
41
+ </form>
42
+ )
43
+ }
44
+
45
+ export function RemoveRelationForm({
46
+ userId,
47
+ username,
48
+ returnTo,
49
+ label = 'Remove',
50
+ }: {
51
+ userId: number
52
+ username: string
53
+ returnTo: string
54
+ label?: string
55
+ }) {
56
+ const [state, action] = useActionState(removeRelationAction, EMPTY_STATE)
57
+
58
+ return (
59
+ <form action={action} className="inline-flex flex-col gap-1">
60
+ <FormError message={state.error} />
61
+ <input type="hidden" name="userId" value={userId} />
62
+ <input type="hidden" name="username" value={username} />
63
+ <input type="hidden" name="returnTo" value={returnTo} />
64
+ <button type="submit" className={QUIET_BUTTON}>
65
+ {label}
66
+ </button>
67
+ </form>
68
+ )
69
+ }
@@ -0,0 +1,124 @@
1
+ 'use client'
2
+
3
+ import { useActionState } from 'react'
4
+
5
+ import { EMPTY_STATE } from '@/server/auth-form-state'
6
+ import { rateMemberAction, withdrawRatingAction } from '@/server/reputation-actions'
7
+
8
+ import { FormError } from '../auth/form-controls'
9
+ import { type Copy, fromCopy } from '../shell/copy'
10
+
11
+ const FIELD =
12
+ 'w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
13
+
14
+ const CARD = 'flex flex-col gap-3 rounded-lg border border-border bg-card p-5'
15
+
16
+ const CHOICE =
17
+ 'inline-flex h-9 items-center justify-center rounded-md border border-border px-4 text-sm font-medium hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
18
+
19
+ export function RateMemberForm({
20
+ userId,
21
+ postId = null,
22
+ returnTo,
23
+ allowNegative,
24
+ commentRequired,
25
+ existingComment = null,
26
+ existingPoints = null,
27
+ remainingLabel = null,
28
+ copy,
29
+ }: {
30
+ userId: number
31
+ postId?: number | null
32
+ returnTo: string
33
+ allowNegative: boolean
34
+ commentRequired: boolean
35
+ existingComment?: string | null
36
+ existingPoints?: number | null
37
+ remainingLabel?: string | null
38
+ copy: Copy
39
+ }) {
40
+ const [state, action] = useActionState(rateMemberAction, EMPTY_STATE)
41
+
42
+ return (
43
+ <form action={action} className={CARD}>
44
+ <FormError message={state.error} />
45
+ <input type="hidden" name="userId" value={userId} />
46
+ {postId === null ? null : <input type="hidden" name="postId" value={postId} />}
47
+ <input type="hidden" name="returnTo" value={returnTo} />
48
+
49
+ <h2 className="text-lg font-semibold tracking-tight">
50
+ {existingPoints === null
51
+ ? fromCopy(copy, 'accountForm.rate.title')
52
+ : fromCopy(copy, 'accountForm.rate.changeTitle')}
53
+ </h2>
54
+
55
+ <label className="flex flex-col gap-1 text-sm">
56
+ <span className="font-medium">
57
+ {commentRequired
58
+ ? fromCopy(copy, 'accountForm.rate.why')
59
+ : fromCopy(copy, 'accountForm.rate.whyOptional')}
60
+ </span>
61
+ <textarea
62
+ name="comment"
63
+ defaultValue={state.values?.comment ?? existingComment ?? ''}
64
+ className={FIELD}
65
+ rows={3}
66
+ maxLength={500}
67
+ required={commentRequired}
68
+ />
69
+ <span className="text-xs text-muted-foreground">
70
+ {fromCopy(copy, 'accountForm.rate.whyHint')}
71
+ </span>
72
+ </label>
73
+
74
+ <div className="flex flex-wrap items-center gap-2">
75
+ <button type="submit" name="points" value="1" className={CHOICE}>
76
+ {allowNegative
77
+ ? fromCopy(copy, 'accountForm.rate.positive')
78
+ : fromCopy(copy, 'accountForm.rate.thanks')}
79
+ </button>
80
+ <button type="submit" name="points" value="0" className={CHOICE}>
81
+ {allowNegative
82
+ ? fromCopy(copy, 'accountForm.rate.neutral')
83
+ : fromCopy(copy, 'accountForm.rate.justComment')}
84
+ </button>
85
+ {allowNegative && (
86
+ <button type="submit" name="points" value="-1" className={CHOICE}>
87
+ {fromCopy(copy, 'accountForm.rate.negative')}
88
+ </button>
89
+ )}
90
+ </div>
91
+
92
+ {remainingLabel !== null && <p className="text-xs text-muted-foreground">{remainingLabel}</p>}
93
+ </form>
94
+ )
95
+ }
96
+
97
+ export function WithdrawRatingForm({
98
+ ratingId,
99
+ userId,
100
+ returnTo,
101
+ copy,
102
+ }: {
103
+ ratingId: number
104
+ userId: number
105
+ returnTo: string
106
+ copy: Copy
107
+ }) {
108
+ const [state, action] = useActionState(withdrawRatingAction, EMPTY_STATE)
109
+
110
+ return (
111
+ <form action={action} className="inline-flex flex-col gap-1">
112
+ <FormError message={state.error} />
113
+ <input type="hidden" name="ratingId" value={ratingId} />
114
+ <input type="hidden" name="userId" value={userId} />
115
+ <input type="hidden" name="returnTo" value={returnTo} />
116
+ <button
117
+ type="submit"
118
+ className="text-xs text-muted-foreground hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
119
+ >
120
+ {fromCopy(copy, 'accountForm.rate.withdraw')}
121
+ </button>
122
+ </form>
123
+ )
124
+ }
@@ -0,0 +1,46 @@
1
+ import { type Copy, fromCopy } from '../shell/copy-record'
2
+
3
+ export interface ActivityView {
4
+ readonly id: number
5
+ readonly label: string
6
+ readonly detail: string
7
+ }
8
+
9
+ export function SecurityActivity({
10
+ events,
11
+ copy,
12
+ }: {
13
+ readonly events: readonly ActivityView[]
14
+ readonly copy: Copy
15
+ }) {
16
+ return (
17
+ <section className="flex flex-col gap-4 rounded-lg border border-border bg-card p-5">
18
+ <div className="flex flex-col gap-1">
19
+ <h2 className="text-lg font-semibold tracking-tight">
20
+ {fromCopy(copy, 'accountForm.activity.title')}
21
+ </h2>
22
+ <p className="text-xs text-muted-foreground">
23
+ {fromCopy(copy, 'accountForm.activity.blurb')}
24
+ </p>
25
+ </div>
26
+
27
+ {events.length === 0 ? (
28
+ <p className="text-sm text-muted-foreground">
29
+ {fromCopy(copy, 'accountForm.activity.none')}
30
+ </p>
31
+ ) : (
32
+ <ul className="flex flex-col gap-2">
33
+ {events.map((event) => (
34
+ <li
35
+ key={event.id}
36
+ className="flex flex-wrap items-baseline justify-between gap-2 border-b border-border pb-2 text-sm last:border-0 last:pb-0"
37
+ >
38
+ <span className="font-medium">{event.label}</span>
39
+ <span className="text-xs text-muted-foreground">{event.detail}</span>
40
+ </li>
41
+ ))}
42
+ </ul>
43
+ )}
44
+ </section>
45
+ )
46
+ }
@@ -0,0 +1,166 @@
1
+ 'use client'
2
+
3
+ import { useActionState } from 'react'
4
+
5
+ import { Button } from '@meith/ui/button'
6
+
7
+ import { EMPTY_STATE } from '@/server/auth-form-state'
8
+ import { removePasskeyAction, unlinkIdentityAction } from '@/server/federation-actions'
9
+
10
+ import { FormError } from '../auth/form-controls'
11
+ import { type Copy, fromCopy } from '../shell/copy'
12
+
13
+ const CARD = 'flex flex-col gap-4 rounded-lg border border-border bg-card p-5'
14
+
15
+ const ROW =
16
+ 'flex flex-wrap items-center justify-between gap-3 rounded-md border border-border px-3 py-2'
17
+
18
+ export interface LinkedIdentityView {
19
+ readonly id: number
20
+ readonly provider: string
21
+ readonly label: string
22
+ readonly detail: string | null
23
+ readonly usage: string
24
+ }
25
+
26
+ export interface PasskeyView {
27
+ readonly id: number
28
+ readonly label: string
29
+ readonly usage: string
30
+ }
31
+
32
+ export interface OfferedProvider {
33
+ readonly id: string
34
+ readonly cta: string
35
+ }
36
+
37
+ export function LinkedSignIns({
38
+ linked,
39
+ offered,
40
+ manageable,
41
+ copy,
42
+ }: {
43
+ readonly linked: readonly LinkedIdentityView[]
44
+ readonly offered: readonly OfferedProvider[]
45
+ readonly manageable: boolean
46
+ readonly copy: Copy
47
+ }) {
48
+ const [state, action] = useActionState(unlinkIdentityAction, EMPTY_STATE)
49
+
50
+ return (
51
+ <section className={CARD}>
52
+ <div className="flex flex-col gap-1">
53
+ <h2 className="text-lg font-semibold tracking-tight">
54
+ {fromCopy(copy, 'accountForm.linked.title')}
55
+ </h2>
56
+ <p className="text-xs text-muted-foreground">
57
+ {fromCopy(copy, 'accountForm.linked.blurb')}
58
+ </p>
59
+ </div>
60
+
61
+ <FormError message={state.error} />
62
+
63
+ {linked.length === 0 ? (
64
+ <p className="text-sm text-muted-foreground">{fromCopy(copy, 'accountForm.linked.none')}</p>
65
+ ) : (
66
+ <ul className="flex flex-col gap-2">
67
+ {linked.map((identity) => (
68
+ <li key={identity.id} className={ROW}>
69
+ <span className="flex flex-col text-sm">
70
+ <span className="font-medium">
71
+ {identity.label}
72
+ {identity.detail === null ? null : (
73
+ <span className="font-normal text-muted-foreground"> — {identity.detail}</span>
74
+ )}
75
+ </span>
76
+ <span className="text-xs text-muted-foreground">{identity.usage}</span>
77
+ </span>
78
+
79
+ {manageable ? (
80
+ <form action={action}>
81
+ <input type="hidden" name="identityId" value={identity.id} />
82
+ <Button type="submit" variant="destructive" size="sm">
83
+ {fromCopy(copy, 'accountForm.linked.unlink')}
84
+ </Button>
85
+ </form>
86
+ ) : null}
87
+ </li>
88
+ ))}
89
+ </ul>
90
+ )}
91
+
92
+ {manageable && offered.length > 0 ? (
93
+ <div className="flex flex-wrap gap-2">
94
+ {offered.map((provider) => (
95
+ <form key={provider.id} method="post" action={`/auth/sso/${provider.id}`}>
96
+ <input type="hidden" name="mode" value="link" />
97
+ <button
98
+ type="submit"
99
+ className="inline-flex h-9 items-center rounded-md border border-border bg-card px-3 text-sm font-medium hover:bg-muted"
100
+ >
101
+ {provider.cta}
102
+ </button>
103
+ </form>
104
+ ))}
105
+ </div>
106
+ ) : null}
107
+ </section>
108
+ )
109
+ }
110
+
111
+ export function PasskeyList({
112
+ passkeys,
113
+ manageable,
114
+ copy,
115
+ children,
116
+ }: {
117
+ readonly passkeys: readonly PasskeyView[]
118
+ readonly manageable: boolean
119
+ readonly copy: Copy
120
+ readonly children?: React.ReactNode
121
+ }) {
122
+ const [state, action] = useActionState(removePasskeyAction, EMPTY_STATE)
123
+
124
+ return (
125
+ <section className={CARD}>
126
+ <div className="flex flex-col gap-1">
127
+ <h2 className="text-lg font-semibold tracking-tight">
128
+ {fromCopy(copy, 'accountForm.passkeys.title')}
129
+ </h2>
130
+ <p className="text-xs text-muted-foreground">
131
+ {fromCopy(copy, 'accountForm.passkeys.blurb')}
132
+ </p>
133
+ </div>
134
+
135
+ <FormError message={state.error} />
136
+
137
+ {passkeys.length === 0 ? (
138
+ <p className="text-sm text-muted-foreground">
139
+ {fromCopy(copy, 'accountForm.passkeys.none')}
140
+ </p>
141
+ ) : (
142
+ <ul className="flex flex-col gap-2">
143
+ {passkeys.map((passkey) => (
144
+ <li key={passkey.id} className={ROW}>
145
+ <span className="flex flex-col text-sm">
146
+ <span className="font-medium">{passkey.label}</span>
147
+ <span className="text-xs text-muted-foreground">{passkey.usage}</span>
148
+ </span>
149
+
150
+ {manageable ? (
151
+ <form action={action}>
152
+ <input type="hidden" name="passkeyId" value={passkey.id} />
153
+ <Button type="submit" variant="destructive" size="sm">
154
+ {fromCopy(copy, 'accountForm.passkeys.remove')}
155
+ </Button>
156
+ </form>
157
+ ) : null}
158
+ </li>
159
+ ))}
160
+ </ul>
161
+ )}
162
+
163
+ {manageable ? children : null}
164
+ </section>
165
+ )
166
+ }