@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,164 @@
1
+ import type { Actor } from '@meith/authorization'
2
+ import type { Translator } from '@meith/i18n'
3
+ import type {
4
+ FooterModel,
5
+ HeaderModel,
6
+ LinkModel,
7
+ LogoModel,
8
+ UserPanelModel,
9
+ ViewerModel,
10
+ } from '@meith/theme-kit'
11
+
12
+ import { count } from './count'
13
+ import { memberHref } from './member-profile'
14
+ import { buildNavigation, defaultNavigationItems } from './navigation'
15
+ import { timezoneLabel, untranslated } from './time'
16
+
17
+ export const BOARD_TITLE = 'Meith'
18
+
19
+ export const TIMEZONE_LABEL = 'UTC'
20
+
21
+ export function buildViewerModel(
22
+ actor: Actor,
23
+ options: {
24
+ displayName?: string | null
25
+ canAccessAdminCp?: boolean
26
+ canAccessModCp?: boolean
27
+ avatarUrl?: string | null
28
+ } = {},
29
+ ): ViewerModel {
30
+ const isGuest = actor.userId === null
31
+
32
+ return {
33
+ isGuest,
34
+ userId: actor.userId,
35
+ username: options.displayName ?? null,
36
+ profileHref: actor.userId === null ? null : memberHref(actor.userId),
37
+ avatarUrl: options.avatarUrl ?? null,
38
+ canAccessAdminCp: options.canAccessAdminCp ?? false,
39
+ canAccessModCp: options.canAccessModCp ?? false,
40
+ }
41
+ }
42
+
43
+ export function buildHeaderModel(
44
+ viewer: ViewerModel,
45
+ navigation: readonly LinkModel[] = [],
46
+ boardTitle: string = BOARD_TITLE,
47
+ logo: LogoModel | null = null,
48
+ ): HeaderModel {
49
+ return {
50
+ boardTitle,
51
+ homeHref: '/',
52
+ viewer,
53
+ navigation,
54
+ ...(logo === null ? {} : { logo }),
55
+ }
56
+ }
57
+
58
+ export function buildBoardNavigation(
59
+ viewer: ViewerModel,
60
+ options: { searchEnabled?: boolean; t?: Translator } = {},
61
+ ): readonly LinkModel[] {
62
+ return buildNavigation(defaultNavigationItems(), viewer, options)
63
+ }
64
+
65
+ const ACCOUNT_GROUP = 'account'
66
+
67
+ const STAFF_GROUP = 'staff'
68
+
69
+ const NOTIFICATIONS_HREF = '/notifications'
70
+
71
+ const MESSAGES_HREF = '/messages'
72
+
73
+ export function buildUserPanelModel(
74
+ viewer: ViewerModel,
75
+ options: {
76
+ unreadNotifications?: number
77
+ unreadMessages?: number
78
+ registrationOpen?: boolean
79
+ t?: Translator
80
+ } = {},
81
+ ): UserPanelModel {
82
+ const t = options.t ?? untranslated()
83
+
84
+ const links: readonly LinkModel[] = viewer.isGuest
85
+ ? [
86
+ { label: t.t('nav.signIn'), href: '/login' },
87
+ ...(options.registrationOpen === false
88
+ ? []
89
+ : [{ label: t.t('nav.register'), href: '/register' }]),
90
+ ]
91
+ : viewer.profileHref === null
92
+ ? []
93
+ : [
94
+ { label: t.t('nav.profile'), href: viewer.profileHref, group: ACCOUNT_GROUP },
95
+ { label: t.t('nav.userCp'), href: '/usercp', group: ACCOUNT_GROUP },
96
+ { label: t.t('nav.notifications'), href: NOTIFICATIONS_HREF, group: ACCOUNT_GROUP },
97
+ { label: t.t('nav.messages'), href: MESSAGES_HREF, group: ACCOUNT_GROUP },
98
+ { label: t.t('nav.subscriptions'), href: '/subscriptions', group: ACCOUNT_GROUP },
99
+ ...(viewer.canAccessAdminCp
100
+ ? [{ label: t.t('nav.adminCp'), href: '/admin', group: STAFF_GROUP }]
101
+ : []),
102
+ ...(viewer.canAccessModCp
103
+ ? [
104
+ { label: t.t('nav.modCp'), href: '/modcp', group: STAFF_GROUP },
105
+ { label: t.t('nav.moderationQueue'), href: '/moderation', group: STAFF_GROUP },
106
+ { label: t.t('nav.reports'), href: '/moderation/reports', group: STAFF_GROUP },
107
+ ]
108
+ : []),
109
+ ]
110
+
111
+ return {
112
+ viewer,
113
+ links,
114
+ unreadNotifications: count(options.unreadNotifications ?? 0, t),
115
+ unreadMessages: count(options.unreadMessages ?? 0, t),
116
+ ...(viewer.isGuest
117
+ ? {}
118
+ : { notificationsHref: NOTIFICATIONS_HREF, messagesHref: MESSAGES_HREF }),
119
+ }
120
+ }
121
+
122
+ export type PanelKey = 'usercp' | 'modcp' | 'admincp'
123
+
124
+ const PANELS: readonly {
125
+ readonly key: PanelKey
126
+ readonly messageKey: string
127
+ readonly href: string
128
+ }[] = [
129
+ { key: 'usercp', messageKey: 'nav.userCp', href: '/usercp' },
130
+ { key: 'modcp', messageKey: 'nav.modCp', href: '/modcp' },
131
+ { key: 'admincp', messageKey: 'nav.adminCp', href: '/admin' },
132
+ ]
133
+
134
+ export function buildPanelLinks(input: {
135
+ readonly current: PanelKey
136
+ readonly canAccessModCp?: boolean
137
+ readonly canAccessAdminCp?: boolean
138
+ readonly t?: Translator
139
+ }): readonly LinkModel[] {
140
+ const t = input.t ?? untranslated()
141
+ const reachable: Record<PanelKey, boolean> = {
142
+ usercp: true,
143
+ modcp: input.canAccessModCp === true,
144
+ admincp: input.canAccessAdminCp === true,
145
+ }
146
+
147
+ return PANELS.filter((panel) => panel.key !== input.current && reachable[panel.key]).map(
148
+ (panel) => ({ label: t.t(panel.messageKey), href: panel.href }),
149
+ )
150
+ }
151
+
152
+ export function buildFooterModel(
153
+ links: readonly LinkModel[] = [],
154
+ boardTitle: string = BOARD_TITLE,
155
+ zone: string = TIMEZONE_LABEL,
156
+ translator: Translator = untranslated(),
157
+ ): FooterModel {
158
+ return {
159
+ boardTitle,
160
+ links,
161
+ timezoneLabel: timezoneLabel(zone),
162
+ poweredBy: { label: translator.t('nav.poweredBy'), href: 'https://meith.dev' },
163
+ }
164
+ }
@@ -0,0 +1,12 @@
1
+ export function leadingId(value: string): number | null {
2
+ const match = /^(\d+)(?:-|$)/.exec(value)
3
+ if (!match) return null
4
+ const id = Number(match[1])
5
+ return Number.isSafeInteger(id) && id > 0 ? id : null
6
+ }
7
+
8
+ export function numericId(value: string): number | null {
9
+ if (!/^[1-9]\d*$/.test(value)) return null
10
+ const id = Number(value)
11
+ return Number.isSafeInteger(id) ? id : null
12
+ }
@@ -0,0 +1,44 @@
1
+ import { localeDirection, type Translator } from '@meith/i18n'
2
+
3
+ export function escapeHtml(value: string): string {
4
+ return value
5
+ .replace(/&/g, '&amp;')
6
+ .replace(/</g, '&lt;')
7
+ .replace(/>/g, '&gt;')
8
+ .replace(/"/g, '&quot;')
9
+ .replace(/'/g, '&#39;')
10
+ }
11
+
12
+ export function handOffPage(input: {
13
+ readonly label: string
14
+ readonly authorizationUrl: string
15
+ readonly t: Translator
16
+ }): string {
17
+ const label = input.label
18
+ const target = escapeHtml(input.authorizationUrl)
19
+
20
+ return [
21
+ '<!doctype html>',
22
+ `<html lang="${escapeHtml(input.t.locale)}" dir="${localeDirection(input.t.locale)}">`,
23
+ '<head>',
24
+ '<meta charset="utf-8">',
25
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
26
+ '<meta name="robots" content="noindex, nofollow">',
27
+ `<meta http-equiv="refresh" content="0; url=${target}">`,
28
+ `<title>${escapeHtml(input.t.t('authSsoHandOff.title', { provider: label }))}</title>`,
29
+ '<style>',
30
+ ':root{color-scheme:light dark}',
31
+ 'body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;',
32
+ 'font-family:system-ui,sans-serif;padding:2rem;text-align:center}',
33
+ '</style>',
34
+ '</head>',
35
+ '<body>',
36
+ '<main>',
37
+ `<p>${escapeHtml(input.t.t('authSsoHandOff.message', { provider: label }))}</p>`,
38
+ `<p><a href="${target}">${escapeHtml(input.t.t('authSsoHandOff.continue', { provider: label }))}</a></p>`,
39
+ '</main>',
40
+ '</body>',
41
+ '</html>',
42
+ '',
43
+ ].join('\n')
44
+ }
@@ -0,0 +1,48 @@
1
+ import type { ErrorCode } from '@meith/core'
2
+ import type { Translator } from '@meith/i18n'
3
+
4
+ import { untranslated } from './time'
5
+
6
+ export type SsoNoticeKind = 'info' | 'warning'
7
+
8
+ export interface SsoNotice {
9
+ readonly kind: SsoNoticeKind
10
+ readonly messageKey: string
11
+ }
12
+
13
+ export interface SsoNoticeCopy {
14
+ readonly kind: SsoNoticeKind
15
+ readonly message: string
16
+ }
17
+
18
+ const NOTICES: Readonly<Record<string, SsoNotice>> = {
19
+ linked: { kind: 'info', messageKey: 'sso.linked' },
20
+ unlinked: { kind: 'info', messageKey: 'sso.unlinked' },
21
+ approval: { kind: 'info', messageKey: 'sso.approval' },
22
+ expired: { kind: 'warning', messageKey: 'sso.expired' },
23
+ refused: { kind: 'warning', messageKey: 'sso.refused' },
24
+ off: { kind: 'warning', messageKey: 'sso.off' },
25
+ conflict: { kind: 'warning', messageKey: 'sso.conflict' },
26
+ forbidden: { kind: 'warning', messageKey: 'sso.forbidden' },
27
+ invalid: { kind: 'warning', messageKey: 'sso.invalid' },
28
+ slowdown: { kind: 'warning', messageKey: 'sso.slowdown' },
29
+ failed: { kind: 'warning', messageKey: 'sso.failed' },
30
+ }
31
+
32
+ export function ssoNotice(
33
+ code: string | undefined,
34
+ t: Translator = untranslated(),
35
+ ): SsoNoticeCopy | null {
36
+ if (code === undefined) return null
37
+
38
+ const notice = NOTICES[code] ?? NOTICES.failed
39
+ return notice === undefined ? null : { kind: notice.kind, message: t.t(notice.messageKey) }
40
+ }
41
+
42
+ export function ssoOutcomeCode(error: ErrorCode | null): string {
43
+ if (error === 'CONFLICT') return 'conflict'
44
+ if (error === 'FORBIDDEN') return 'forbidden'
45
+ if (error === 'VALIDATION') return 'invalid'
46
+ if (error === 'RATE_LIMITED') return 'slowdown'
47
+ return 'failed'
48
+ }
@@ -0,0 +1,76 @@
1
+ import type { Translator } from '@meith/i18n'
2
+ import type { SubscriptionMode, SubscriptionRow } from '@meith/subscriptions'
3
+ import { MODE_LABELS, SUBSCRIPTION_MODES } from '@meith/subscriptions'
4
+ import type { TimeModel } from '@meith/theme-kit'
5
+
6
+ import { formatTime, untranslated } from './time'
7
+
8
+ export interface SubscriptionRowView {
9
+ readonly key: string
10
+ readonly target: 'thread' | 'forum'
11
+ readonly targetId: number
12
+ readonly title: string
13
+ readonly href: string
14
+ readonly mode: SubscriptionMode
15
+ readonly modeLabel: string
16
+ readonly since: TimeModel
17
+ readonly pending: string | null
18
+ }
19
+
20
+ export interface SubscriptionsView {
21
+ readonly threads: readonly SubscriptionRowView[]
22
+ readonly forums: readonly SubscriptionRowView[]
23
+ readonly modes: readonly { readonly value: SubscriptionMode; readonly label: string }[]
24
+ readonly total: number
25
+ }
26
+
27
+ export function buildSubscriptionsView(input: {
28
+ readonly rows: readonly SubscriptionRow[]
29
+ readonly now: Date
30
+ readonly t?: Translator
31
+ }): SubscriptionsView {
32
+ const rows = input.rows.map((row) => toRow(row, input.now, input.t))
33
+
34
+ return {
35
+ threads: rows.filter((row) => row.target === 'thread'),
36
+ forums: rows.filter((row) => row.target === 'forum'),
37
+ modes: SUBSCRIPTION_MODES.map((value) => ({ value, label: MODE_LABELS[value] })),
38
+ total: rows.length,
39
+ }
40
+ }
41
+
42
+ function toRow(row: SubscriptionRow, now: Date, t: Translator | undefined): SubscriptionRowView {
43
+ return {
44
+ key: `${row.target}:${row.targetId}`,
45
+ target: row.target,
46
+ targetId: row.targetId,
47
+ title: row.title,
48
+ href: row.href,
49
+ mode: row.mode,
50
+ modeLabel: MODE_LABELS[row.mode],
51
+ since: formatTime(row.createdAt, now, t),
52
+ pending:
53
+ row.pending === 0 ? null : `${row.pending} new ${row.pending === 1 ? 'post' : 'posts'}`,
54
+ }
55
+ }
56
+
57
+ export function subscriptionNotice(
58
+ query: {
59
+ readonly followed?: string | undefined
60
+ readonly stopped?: string | undefined
61
+ },
62
+ t: Translator = untranslated(),
63
+ ): string | null {
64
+ if (query.followed !== undefined) return t.t('subscription.followed')
65
+ if (query.stopped !== undefined) return t.t('subscription.stopped')
66
+ return null
67
+ }
68
+
69
+ export function unsubscribeNotice(
70
+ done: string | undefined,
71
+ t: Translator = untranslated(),
72
+ ): string | null {
73
+ if (done === 'email') return t.t('subscription.emailsOff')
74
+ if (done === 'one') return t.t('subscription.doneOne')
75
+ return null
76
+ }
@@ -0,0 +1,167 @@
1
+ import type { TokenKind } from './theme-tokens'
2
+
3
+ export type Scheme = 'light' | 'dark'
4
+
5
+ export const SCHEMES: readonly Scheme[] = ['light', 'dark']
6
+
7
+ export type FieldScheme = Scheme | 'both'
8
+
9
+ export interface EditableToken {
10
+ readonly name: string
11
+ readonly label: string
12
+ readonly hint: string
13
+ readonly kind: TokenKind
14
+ readonly light: string
15
+ readonly dark: string
16
+ readonly overrideLight: string
17
+ readonly overrideDark: string
18
+ }
19
+
20
+ export type Draft = Record<string, string>
21
+
22
+ export function fieldName(name: string, scheme: FieldScheme): string {
23
+ return `token.${scheme}.${name}`
24
+ }
25
+
26
+ export function schemesFor(token: EditableToken): readonly FieldScheme[] {
27
+ return token.kind === 'colour' ? ['light', 'dark'] : ['both']
28
+ }
29
+
30
+ export function shippedValue(token: EditableToken, scheme: FieldScheme): string {
31
+ return scheme === 'dark' ? token.dark : token.light
32
+ }
33
+
34
+ export function savedValue(token: EditableToken, scheme: FieldScheme): string {
35
+ return scheme === 'dark' ? token.overrideDark : token.overrideLight
36
+ }
37
+
38
+ export function draftValue(draft: Draft, token: EditableToken, scheme: FieldScheme): string {
39
+ return (draft[fieldName(token.name, scheme)] ?? '').trim()
40
+ }
41
+
42
+ export function initialDraft(
43
+ tokens: readonly EditableToken[],
44
+ restored?: Draft | undefined,
45
+ ): Draft {
46
+ const draft: Draft = {}
47
+ for (const token of tokens) {
48
+ for (const scheme of schemesFor(token)) {
49
+ const field = fieldName(token.name, scheme)
50
+ draft[field] = restored?.[field] ?? savedValue(token, scheme)
51
+ }
52
+ }
53
+ return draft
54
+ }
55
+
56
+ export function shippedValues(
57
+ tokens: readonly EditableToken[],
58
+ scheme: Scheme,
59
+ ): Record<string, string> {
60
+ const values: Record<string, string> = {}
61
+ for (const token of tokens) values[token.name] = shippedValue(token, scheme)
62
+ return values
63
+ }
64
+
65
+ export function savedValues(
66
+ tokens: readonly EditableToken[],
67
+ scheme: Scheme,
68
+ ): Record<string, string> {
69
+ const values: Record<string, string> = {}
70
+ for (const token of tokens) {
71
+ const field: FieldScheme = token.kind === 'colour' ? scheme : 'both'
72
+ const saved = savedValue(token, field)
73
+ values[token.name] = saved === '' ? shippedValue(token, scheme) : saved
74
+ }
75
+ return values
76
+ }
77
+
78
+ export function effectiveValues(
79
+ tokens: readonly EditableToken[],
80
+ draft: Draft,
81
+ scheme: Scheme,
82
+ ): Record<string, string> {
83
+ const values: Record<string, string> = {}
84
+ for (const token of tokens) {
85
+ const field: FieldScheme = token.kind === 'colour' ? scheme : 'both'
86
+ const override = draftValue(draft, token, field)
87
+ values[token.name] = override === '' ? shippedValue(token, scheme) : override
88
+ }
89
+ return values
90
+ }
91
+
92
+ export function cssVariables(values: Readonly<Record<string, string>>): Record<string, string> {
93
+ const style: Record<string, string> = {}
94
+ for (const [name, value] of Object.entries(values)) style[`--${name}`] = value
95
+ return style
96
+ }
97
+
98
+ export type ChangeState = 'saved' | 'added' | 'edited' | 'cleared'
99
+
100
+ export interface TokenChange {
101
+ readonly token: EditableToken
102
+ readonly scheme: FieldScheme
103
+ readonly shipped: string
104
+ readonly saved: string
105
+ readonly draft: string
106
+ readonly state: ChangeState
107
+ readonly next: string
108
+ readonly current: string
109
+ }
110
+
111
+ function stateOf(saved: string, draft: string): ChangeState | null {
112
+ if (draft === '' && saved === '') return null
113
+ if (draft === saved) return 'saved'
114
+ if (draft === '') return 'cleared'
115
+ return saved === '' ? 'added' : 'edited'
116
+ }
117
+
118
+ export function tokenChanges(
119
+ tokens: readonly EditableToken[],
120
+ draft: Draft,
121
+ ): readonly TokenChange[] {
122
+ const changes: TokenChange[] = []
123
+
124
+ for (const token of tokens) {
125
+ for (const scheme of schemesFor(token)) {
126
+ const saved = savedValue(token, scheme)
127
+ const value = draftValue(draft, token, scheme)
128
+ const state = stateOf(saved, value)
129
+ if (state === null) continue
130
+
131
+ const shipped = shippedValue(token, scheme)
132
+ changes.push({
133
+ token,
134
+ scheme,
135
+ shipped,
136
+ saved,
137
+ draft: value,
138
+ state,
139
+ next: value === '' ? shipped : value,
140
+ current: saved === '' ? shipped : saved,
141
+ })
142
+ }
143
+ }
144
+
145
+ return changes
146
+ }
147
+
148
+ export interface ChangeCounts {
149
+ readonly overridden: number
150
+ readonly unsaved: number
151
+ readonly tokens: number
152
+ }
153
+
154
+ export function changeCounts(changes: readonly TokenChange[]): ChangeCounts {
155
+ return {
156
+ overridden: changes.filter((change) => change.draft !== '').length,
157
+ unsaved: changes.filter((change) => change.state !== 'saved').length,
158
+ tokens: new Set(changes.filter((change) => change.draft !== '').map((c) => c.token.name)).size,
159
+ }
160
+ }
161
+
162
+ export function matchesQuery(token: EditableToken, query: string): boolean {
163
+ const needle = query.trim().toLowerCase()
164
+ if (needle === '') return true
165
+
166
+ return [token.name, token.label, token.hint].some((field) => field.toLowerCase().includes(needle))
167
+ }
@@ -0,0 +1,30 @@
1
+ export const THEME_COOKIE = 'meith_theme'
2
+ export const SCHEME_COOKIE = 'meith_scheme'
3
+
4
+ export const PREFERENCE_COOKIE_MAX_AGE = 60 * 60 * 24 * 365
5
+
6
+ export const THEME_QUERY = 'theme'
7
+
8
+ const THEME_KEY = /^[a-z0-9][a-z0-9-]{0,31}$/
9
+
10
+ export function themeFromQuery(value: string | null | undefined): string | null {
11
+ if (typeof value !== 'string') return null
12
+
13
+ const key = value.trim().toLowerCase()
14
+ return THEME_KEY.test(key) ? key : null
15
+ }
16
+
17
+ export const COLOUR_SCHEMES = ['system', 'light', 'dark'] as const
18
+ export type ColourSchemePreference = (typeof COLOUR_SCHEMES)[number]
19
+
20
+ export function isColourScheme(value: unknown): value is ColourSchemePreference {
21
+ return COLOUR_SCHEMES.includes(value as ColourSchemePreference)
22
+ }
23
+
24
+ export function schemeClass(scheme: ColourSchemePreference): string {
25
+ return scheme === 'system' ? '' : scheme
26
+ }
27
+
28
+ export function colorSchemeProperty(scheme: ColourSchemePreference): string {
29
+ return scheme === 'system' ? 'light dark' : scheme
30
+ }