@aurostack/stacks 0.1.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 (601) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +209 -0
  3. package/cli/src/extract.mjs +165 -0
  4. package/cli/src/fsx.mjs +197 -0
  5. package/cli/src/generate.mjs +235 -0
  6. package/cli/src/hooks.mjs +55 -0
  7. package/cli/src/manifest.mjs +163 -0
  8. package/cli/src/strip.mjs +208 -0
  9. package/cli/src/tokens.mjs +102 -0
  10. package/cli/stack.mjs +672 -0
  11. package/package.json +37 -0
  12. package/templates/nest-api/files/.bin/db.ts +123 -0
  13. package/templates/nest-api/files/.bin/dc.ts +74 -0
  14. package/templates/nest-api/files/.bin/helpers.ts +49 -0
  15. package/templates/nest-api/files/.bin/secrets.ts +68 -0
  16. package/templates/nest-api/files/.bin/start.ts +47 -0
  17. package/templates/nest-api/files/.dockerignore +7 -0
  18. package/templates/nest-api/files/.env.example +91 -0
  19. package/templates/nest-api/files/.env.test.example +75 -0
  20. package/templates/nest-api/files/.forgejo/workflows/build-and-push.yml +41 -0
  21. package/templates/nest-api/files/.forgejo/workflows/test-e2e.yml +83 -0
  22. package/templates/nest-api/files/.forgejo/workflows/test.yml +53 -0
  23. package/templates/nest-api/files/.npmrc.example +1 -0
  24. package/templates/nest-api/files/.nvmrc +1 -0
  25. package/templates/nest-api/files/.prettierrc +8 -0
  26. package/templates/nest-api/files/.swcrc +15 -0
  27. package/templates/nest-api/files/.watchmanconfig +3 -0
  28. package/templates/nest-api/files/.yarnrc.yml +1 -0
  29. package/templates/nest-api/files/Dockerfile +40 -0
  30. package/templates/nest-api/files/__mocks__/activation-twofa.js +7 -0
  31. package/templates/nest-api/files/__mocks__/activation.js +7 -0
  32. package/templates/nest-api/files/__mocks__/forgot-password.js +7 -0
  33. package/templates/nest-api/files/__mocks__/main-layout.js +7 -0
  34. package/templates/nest-api/files/__mocks__/password-changed.js +7 -0
  35. package/templates/nest-api/files/__mocks__/reset-password.js +7 -0
  36. package/templates/nest-api/files/__mocks__/set-password.js +7 -0
  37. package/templates/nest-api/files/__mocks__/welcome.js +7 -0
  38. package/templates/nest-api/files/_gitignore +62 -0
  39. package/templates/nest-api/files/compose.test.yml +33 -0
  40. package/templates/nest-api/files/compose.yml +39 -0
  41. package/templates/nest-api/files/emails/activation-twofa.tsx +58 -0
  42. package/templates/nest-api/files/emails/activation.tsx +36 -0
  43. package/templates/nest-api/files/emails/forgot-password.tsx +29 -0
  44. package/templates/nest-api/files/emails/main-layout.tsx +134 -0
  45. package/templates/nest-api/files/emails/password-changed.tsx +24 -0
  46. package/templates/nest-api/files/emails/reset-password.tsx +24 -0
  47. package/templates/nest-api/files/emails/set-password.tsx +25 -0
  48. package/templates/nest-api/files/emails/welcome.tsx +39 -0
  49. package/templates/nest-api/files/eslint.config.mjs +28 -0
  50. package/templates/nest-api/files/global.d.ts +5 -0
  51. package/templates/nest-api/files/nest-cli.json +8 -0
  52. package/templates/nest-api/files/package.json +150 -0
  53. package/templates/nest-api/files/prisma/schema/auth.prisma +75 -0
  54. package/templates/nest-api/files/prisma/schema/base.prisma +17 -0
  55. package/templates/nest-api/files/prisma/schema/enum.prisma +17 -0
  56. package/templates/nest-api/files/prisma/schema/notification.prisma +14 -0
  57. package/templates/nest-api/files/prisma.config.ts +13 -0
  58. package/templates/nest-api/files/public/.gitkeep +0 -0
  59. package/templates/nest-api/files/seeders/index.ts +27 -0
  60. package/templates/nest-api/files/seeders/util.ts +88 -0
  61. package/templates/nest-api/files/src/__tests__/app.controller.spec.ts +29 -0
  62. package/templates/nest-api/files/src/app.controller.ts +40 -0
  63. package/templates/nest-api/files/src/app.module.ts +268 -0
  64. package/templates/nest-api/files/src/app.resolver.ts +32 -0
  65. package/templates/nest-api/files/src/app.setup.ts +178 -0
  66. package/templates/nest-api/files/src/common/__tests__/auth.service.spec.ts +31 -0
  67. package/templates/nest-api/files/src/common/__tests__/cache.service.spec.ts +214 -0
  68. package/templates/nest-api/files/src/common/__tests__/config.service.spec.ts +56 -0
  69. package/templates/nest-api/files/src/common/__tests__/generator.service.spec.ts +114 -0
  70. package/templates/nest-api/files/src/common/__tests__/health.controller.spec.ts +72 -0
  71. package/templates/nest-api/files/src/common/__tests__/helpers.spec.ts +124 -0
  72. package/templates/nest-api/files/src/common/__tests__/mail.service.spec.ts +75 -0
  73. package/templates/nest-api/files/src/common/__tests__/prisma.indicator.spec.ts +39 -0
  74. package/templates/nest-api/files/src/common/__tests__/prisma.service.spec.ts +15 -0
  75. package/templates/nest-api/files/src/common/__tests__/records.spec.ts +87 -0
  76. package/templates/nest-api/files/src/common/__tests__/schemas.spec.ts +142 -0
  77. package/templates/nest-api/files/src/common/__tests__/throttler-storage.service.spec.ts +80 -0
  78. package/templates/nest-api/files/src/common/common.module.ts +72 -0
  79. package/templates/nest-api/files/src/common/constants/index.ts +2 -0
  80. package/templates/nest-api/files/src/common/constants/options.ts +52 -0
  81. package/templates/nest-api/files/src/common/constants/pino.ts +24 -0
  82. package/templates/nest-api/files/src/common/controllers/health.controller.ts +54 -0
  83. package/templates/nest-api/files/src/common/controllers/index.ts +2 -0
  84. package/templates/nest-api/files/src/common/controllers/metrics.controller.ts +21 -0
  85. package/templates/nest-api/files/src/common/decorators/index.ts +2 -0
  86. package/templates/nest-api/files/src/common/decorators/misc.decorator.ts +49 -0
  87. package/templates/nest-api/files/src/common/decorators/multi-part.decorator.ts +6 -0
  88. package/templates/nest-api/files/src/common/dto/filters.dto.ts +25 -0
  89. package/templates/nest-api/files/src/common/dto/index.ts +1 -0
  90. package/templates/nest-api/files/src/common/entity/index.ts +2 -0
  91. package/templates/nest-api/files/src/common/entity/ok.entity.ts +11 -0
  92. package/templates/nest-api/files/src/common/entity/pagination.entity.ts +24 -0
  93. package/templates/nest-api/files/src/common/exceptions/index.ts +1 -0
  94. package/templates/nest-api/files/src/common/exceptions/prisma.exception.ts +57 -0
  95. package/templates/nest-api/files/src/common/guards/feature-flag.guard.ts +51 -0
  96. package/templates/nest-api/files/src/common/guards/index.ts +1 -0
  97. package/templates/nest-api/files/src/common/interceptors/index.ts +1 -0
  98. package/templates/nest-api/files/src/common/interceptors/metrics.interceptor.ts +37 -0
  99. package/templates/nest-api/files/src/common/misc/helpers.ts +26 -0
  100. package/templates/nest-api/files/src/common/misc/index.ts +3 -0
  101. package/templates/nest-api/files/src/common/misc/origin-builder.ts +56 -0
  102. package/templates/nest-api/files/src/common/misc/prisma.indicator.ts +24 -0
  103. package/templates/nest-api/files/src/common/modules/index.ts +3 -0
  104. package/templates/nest-api/files/src/common/modules/logger.module.ts +104 -0
  105. package/templates/nest-api/files/src/common/modules/metrics.module.ts +28 -0
  106. package/templates/nest-api/files/src/common/modules/queue.module.ts +23 -0
  107. package/templates/nest-api/files/src/common/processors/index.ts +1 -0
  108. package/templates/nest-api/files/src/common/processors/mail/index.ts +47 -0
  109. package/templates/nest-api/files/src/common/processors/mail/process.ts +225 -0
  110. package/templates/nest-api/files/src/common/schemas/fields.ts +76 -0
  111. package/templates/nest-api/files/src/common/schemas/index.ts +1 -0
  112. package/templates/nest-api/files/src/common/services/auth-rate-limit.storage.ts +79 -0
  113. package/templates/nest-api/files/src/common/services/auth.service.ts +16 -0
  114. package/templates/nest-api/files/src/common/services/cache.service.ts +118 -0
  115. package/templates/nest-api/files/src/common/services/config.service.ts +388 -0
  116. package/templates/nest-api/files/src/common/services/feature-flag.service.ts +117 -0
  117. package/templates/nest-api/files/src/common/services/generator.service.ts +54 -0
  118. package/templates/nest-api/files/src/common/services/index.ts +10 -0
  119. package/templates/nest-api/files/src/common/services/logger.service.ts +14 -0
  120. package/templates/nest-api/files/src/common/services/mail.service.ts +44 -0
  121. package/templates/nest-api/files/src/common/services/prisma.service.ts +106 -0
  122. package/templates/nest-api/files/src/common/services/throttler-storage.service.ts +112 -0
  123. package/templates/nest-api/files/src/common/types/email.ts +55 -0
  124. package/templates/nest-api/files/src/common/types/index.ts +3 -0
  125. package/templates/nest-api/files/src/common/types/misc.ts +20 -0
  126. package/templates/nest-api/files/src/common/types/prisma.ts +18 -0
  127. package/templates/nest-api/files/src/common/utils/enum.ts +39 -0
  128. package/templates/nest-api/files/src/common/utils/helpers.ts +139 -0
  129. package/templates/nest-api/files/src/common/utils/index.ts +5 -0
  130. package/templates/nest-api/files/src/common/utils/ip.ts +22 -0
  131. package/templates/nest-api/files/src/common/utils/records.ts +66 -0
  132. package/templates/nest-api/files/src/common/utils/redis.keys.ts +31 -0
  133. package/templates/nest-api/files/src/lib/__tests__/access.spec.ts +33 -0
  134. package/templates/nest-api/files/src/lib/access.ts +114 -0
  135. package/templates/nest-api/files/src/lib/auth.ts +76 -0
  136. package/templates/nest-api/files/src/lib/rate-limit.ts +20 -0
  137. package/templates/nest-api/files/src/main.ts +28 -0
  138. package/templates/nest-api/files/src/media/__tests__/media.service.spec.ts +60 -0
  139. package/templates/nest-api/files/src/media/__tests__/s3.service.spec.ts +159 -0
  140. package/templates/nest-api/files/src/media/entity/file.entity.ts +7 -0
  141. package/templates/nest-api/files/src/media/entity/index.ts +1 -0
  142. package/templates/nest-api/files/src/media/media.module.ts +20 -0
  143. package/templates/nest-api/files/src/media/processors/index.ts +44 -0
  144. package/templates/nest-api/files/src/media/services/index.ts +2 -0
  145. package/templates/nest-api/files/src/media/services/media.service.ts +29 -0
  146. package/templates/nest-api/files/src/media/services/s3.service.ts +168 -0
  147. package/templates/nest-api/files/src/notifications/controllers/index.ts +1 -0
  148. package/templates/nest-api/files/src/notifications/controllers/notifications.controller.ts +72 -0
  149. package/templates/nest-api/files/src/notifications/dto/index.ts +1 -0
  150. package/templates/nest-api/files/src/notifications/dto/notification.dto.ts +11 -0
  151. package/templates/nest-api/files/src/notifications/entity/index.ts +1 -0
  152. package/templates/nest-api/files/src/notifications/entity/notification.entity.ts +37 -0
  153. package/templates/nest-api/files/src/notifications/notifications.module.ts +11 -0
  154. package/templates/nest-api/files/src/notifications/services/index.ts +1 -0
  155. package/templates/nest-api/files/src/notifications/services/notifications.service.ts +98 -0
  156. package/templates/nest-api/files/src/realtime/__tests__/events.gateway.spec.ts +88 -0
  157. package/templates/nest-api/files/src/realtime/__tests__/rooms.spec.ts +29 -0
  158. package/templates/nest-api/files/src/realtime/__tests__/ws-permissions.guard.spec.ts +54 -0
  159. package/templates/nest-api/files/src/realtime/adapters/redis-io.adapter.ts +57 -0
  160. package/templates/nest-api/files/src/realtime/constants/index.ts +68 -0
  161. package/templates/nest-api/files/src/realtime/decorators/index.ts +1 -0
  162. package/templates/nest-api/files/src/realtime/decorators/ws-permissions.decorator.ts +14 -0
  163. package/templates/nest-api/files/src/realtime/gateways/events.gateway.ts +187 -0
  164. package/templates/nest-api/files/src/realtime/guards/index.ts +2 -0
  165. package/templates/nest-api/files/src/realtime/guards/ws-auth.guard.ts +20 -0
  166. package/templates/nest-api/files/src/realtime/guards/ws-permissions.guard.ts +35 -0
  167. package/templates/nest-api/files/src/realtime/misc/session-from-socket.ts +54 -0
  168. package/templates/nest-api/files/src/realtime/realtime.module.ts +26 -0
  169. package/templates/nest-api/files/src/realtime/services/presence.service.ts +56 -0
  170. package/templates/nest-api/files/src/realtime/services/realtime.service.ts +72 -0
  171. package/templates/nest-api/files/src/users/__tests__/current-user.controller.spec.ts +77 -0
  172. package/templates/nest-api/files/src/users/__tests__/current-user.service.spec.ts +159 -0
  173. package/templates/nest-api/files/src/users/__tests__/user.entity.spec.ts +175 -0
  174. package/templates/nest-api/files/src/users/__tests__/users.controller.spec.ts +89 -0
  175. package/templates/nest-api/files/src/users/__tests__/users.service.spec.ts +398 -0
  176. package/templates/nest-api/files/src/users/controllers/current-user.controller.ts +82 -0
  177. package/templates/nest-api/files/src/users/controllers/index.ts +2 -0
  178. package/templates/nest-api/files/src/users/controllers/users.controller.ts +40 -0
  179. package/templates/nest-api/files/src/users/decorators/index.ts +1 -0
  180. package/templates/nest-api/files/src/users/decorators/permissions.decorator.ts +24 -0
  181. package/templates/nest-api/files/src/users/dto/avatar.dto.ts +13 -0
  182. package/templates/nest-api/files/src/users/dto/filters.dto.ts +8 -0
  183. package/templates/nest-api/files/src/users/dto/index.ts +2 -0
  184. package/templates/nest-api/files/src/users/entity/index.ts +1 -0
  185. package/templates/nest-api/files/src/users/entity/user.entity.ts +53 -0
  186. package/templates/nest-api/files/src/users/services/current-user.service.ts +46 -0
  187. package/templates/nest-api/files/src/users/services/index.ts +2 -0
  188. package/templates/nest-api/files/src/users/services/users.service.ts +88 -0
  189. package/templates/nest-api/files/src/users/users.module.ts +10 -0
  190. package/templates/nest-api/files/test/app.spec.ts +102 -0
  191. package/templates/nest-api/files/test/auth.spec.ts +108 -0
  192. package/templates/nest-api/files/test/current-user.spec.ts +154 -0
  193. package/templates/nest-api/files/test/factory/app.ts +172 -0
  194. package/templates/nest-api/files/test/factory/constants.ts +22 -0
  195. package/templates/nest-api/files/test/factory/dto.ts +27 -0
  196. package/templates/nest-api/files/test/factory/entity.ts +70 -0
  197. package/templates/nest-api/files/test/factory/gen.ts +75 -0
  198. package/templates/nest-api/files/test/factory/util.ts +21 -0
  199. package/templates/nest-api/files/test/notifications.spec.ts +176 -0
  200. package/templates/nest-api/files/test/rate-limit.spec.ts +180 -0
  201. package/templates/nest-api/files/test/setup.ts +1 -0
  202. package/templates/nest-api/files/test/users.spec.ts +437 -0
  203. package/templates/nest-api/files/tsconfig.build.json +13 -0
  204. package/templates/nest-api/files/tsconfig.json +35 -0
  205. package/templates/nest-api/files/vitest.config.ts +53 -0
  206. package/templates/nest-api/template.json +452 -0
  207. package/templates/node-worker/files/.bin/db.ts +123 -0
  208. package/templates/node-worker/files/.bin/helpers.ts +49 -0
  209. package/templates/node-worker/files/.bin/secrets.ts +68 -0
  210. package/templates/node-worker/files/.dockerignore +5 -0
  211. package/templates/node-worker/files/.env.example +21 -0
  212. package/templates/node-worker/files/.forgejo/workflows/build-and-push.yml +41 -0
  213. package/templates/node-worker/files/.nvmrc +1 -0
  214. package/templates/node-worker/files/.prettierignore +4 -0
  215. package/templates/node-worker/files/.prettierrc +8 -0
  216. package/templates/node-worker/files/.yarnrc.yml +5 -0
  217. package/templates/node-worker/files/Dockerfile +63 -0
  218. package/templates/node-worker/files/Dockerfile.dev +59 -0
  219. package/templates/node-worker/files/_gitignore +48 -0
  220. package/templates/node-worker/files/ecosystem.config.cjs +10 -0
  221. package/templates/node-worker/files/eslint.config.js +37 -0
  222. package/templates/node-worker/files/package.json +67 -0
  223. package/templates/node-worker/files/prisma/_gitignore +1 -0
  224. package/templates/node-worker/files/prisma/schema/base.prisma +36 -0
  225. package/templates/node-worker/files/prisma.config.ts +13 -0
  226. package/templates/node-worker/files/src/config.ts +55 -0
  227. package/templates/node-worker/files/src/db.ts +118 -0
  228. package/templates/node-worker/files/src/index.ts +7 -0
  229. package/templates/node-worker/files/src/logger.ts +45 -0
  230. package/templates/node-worker/files/src/types.ts +23 -0
  231. package/templates/node-worker/files/src/utils/browser.ts +16 -0
  232. package/templates/node-worker/files/src/utils/date.ts +68 -0
  233. package/templates/node-worker/files/src/utils/helpers.ts +17 -0
  234. package/templates/node-worker/files/src/utils/number.ts +47 -0
  235. package/templates/node-worker/files/src/utils/worker.ts +29 -0
  236. package/templates/node-worker/files/src/workers/constants.ts +6 -0
  237. package/templates/node-worker/files/src/workers/example/index.ts +27 -0
  238. package/templates/node-worker/files/src/workers/index.ts +1 -0
  239. package/templates/node-worker/files/tsconfig.json +30 -0
  240. package/templates/node-worker/template.json +135 -0
  241. package/templates/py-worker/files/.dockerignore +176 -0
  242. package/templates/py-worker/files/.env.example +20 -0
  243. package/templates/py-worker/files/.forgejo/workflows/build-amd-push.yml +39 -0
  244. package/templates/py-worker/files/Dockerfile +10 -0
  245. package/templates/py-worker/files/README.md +3 -0
  246. package/templates/py-worker/files/_gitignore +176 -0
  247. package/templates/py-worker/files/config.py +89 -0
  248. package/templates/py-worker/files/models/decorators.py +42 -0
  249. package/templates/py-worker/files/models/enums.py +32 -0
  250. package/templates/py-worker/files/models/mixins.py +61 -0
  251. package/templates/py-worker/files/requirements.txt +18 -0
  252. package/templates/py-worker/files/utils/db.py +67 -0
  253. package/templates/py-worker/files/utils/logger.py +97 -0
  254. package/templates/py-worker/files/utils/misc.py +45 -0
  255. package/templates/py-worker/files/utils/worker.py +80 -0
  256. package/templates/py-worker/files/worker.py +41 -0
  257. package/templates/py-worker/files/workers/hello/process.py +22 -0
  258. package/templates/py-worker/template.json +87 -0
  259. package/templates/react-app/README.md +78 -0
  260. package/templates/react-app/derive.sh +128 -0
  261. package/templates/react-app/files/.bin/openapi.js +32 -0
  262. package/templates/react-app/files/.bin/secrets.js +47 -0
  263. package/templates/react-app/files/.dockerignore +16 -0
  264. package/templates/react-app/files/.editorconfig +12 -0
  265. package/templates/react-app/files/.env.example +34 -0
  266. package/templates/react-app/files/.forgejo/workflows/build-and-push.yml +55 -0
  267. package/templates/react-app/files/.forgejo/workflows/ci.yml +39 -0
  268. package/templates/react-app/files/.husky/pre-commit +1 -0
  269. package/templates/react-app/files/.nvmrc +1 -0
  270. package/templates/react-app/files/.prettierignore +6 -0
  271. package/templates/react-app/files/.prettierrc +9 -0
  272. package/templates/react-app/files/.yarnrc.yml +2 -0
  273. package/templates/react-app/files/Dockerfile +52 -0
  274. package/templates/react-app/files/_gitignore +33 -0
  275. package/templates/react-app/files/build.sh +53 -0
  276. package/templates/react-app/files/docker/nginx.conf +134 -0
  277. package/templates/react-app/files/eslint.config.mjs +75 -0
  278. package/templates/react-app/files/index.html +62 -0
  279. package/templates/react-app/files/openapi-ts.config.ts +22 -0
  280. package/templates/react-app/files/package.json +80 -0
  281. package/templates/react-app/files/src/app/nav.tsx +32 -0
  282. package/templates/react-app/files/src/app/providers.tsx +39 -0
  283. package/templates/react-app/files/src/app/router.tsx +142 -0
  284. package/templates/react-app/files/src/app/store.ts +20 -0
  285. package/templates/react-app/files/src/features/auth/components/auth-card.tsx +23 -0
  286. package/templates/react-app/files/src/features/auth/components/checkbox-field.tsx +32 -0
  287. package/templates/react-app/files/src/features/auth/components/google-button.tsx +41 -0
  288. package/templates/react-app/files/src/features/auth/components/or-divider.tsx +9 -0
  289. package/templates/react-app/files/src/features/auth/components/password-field.tsx +70 -0
  290. package/templates/react-app/files/src/features/auth/components/submit-button.tsx +16 -0
  291. package/templates/react-app/files/src/features/auth/lib/auth-actions.ts +68 -0
  292. package/templates/react-app/files/src/features/auth/lib/password-strength.ts +20 -0
  293. package/templates/react-app/files/src/features/auth/lib/redirect.ts +6 -0
  294. package/templates/react-app/files/src/features/auth/lib/schemas.ts +37 -0
  295. package/templates/react-app/files/src/features/seo/routes.ts +65 -0
  296. package/templates/react-app/files/src/lib/env.ts +28 -0
  297. package/templates/react-app/files/src/lib/paths.ts +58 -0
  298. package/templates/react-app/files/src/main.tsx +19 -0
  299. package/templates/react-app/files/src/routes/admin/home.tsx +24 -0
  300. package/templates/react-app/files/src/routes/auth/forgot-password.tsx +93 -0
  301. package/templates/react-app/files/src/routes/auth/login.tsx +87 -0
  302. package/templates/react-app/files/src/routes/auth/reset-password.tsx +83 -0
  303. package/templates/react-app/files/src/routes/auth/signup.tsx +117 -0
  304. package/templates/react-app/files/src/routes/auth/verified.tsx +31 -0
  305. package/templates/react-app/files/src/routes/auth/verify-email.tsx +97 -0
  306. package/templates/react-app/files/src/routes/home.tsx +17 -0
  307. package/templates/react-app/files/src/routes/legal/privacy.tsx +26 -0
  308. package/templates/react-app/files/src/routes/legal/terms.tsx +28 -0
  309. package/templates/react-app/files/src/routes/marketing/home.tsx +18 -0
  310. package/templates/react-app/files/src/routes/settings.tsx +10 -0
  311. package/templates/react-app/files/src/shared/api/base-api.ts +24 -0
  312. package/templates/react-app/files/src/shared/api/base-query.ts +29 -0
  313. package/templates/react-app/files/src/shared/api/config.ts +21 -0
  314. package/templates/react-app/files/src/shared/api/index.ts +15 -0
  315. package/templates/react-app/files/src/shared/api/realtime-events.ts +34 -0
  316. package/templates/react-app/files/src/shared/api/socket.ts +41 -0
  317. package/templates/react-app/files/src/shared/api/store.ts +39 -0
  318. package/templates/react-app/files/src/shared/api/upload-query.ts +115 -0
  319. package/templates/react-app/files/src/shared/auth/client.ts +27 -0
  320. package/templates/react-app/files/src/shared/auth/config.ts +28 -0
  321. package/templates/react-app/files/src/shared/auth/guards.tsx +75 -0
  322. package/templates/react-app/files/src/shared/auth/hooks.ts +13 -0
  323. package/templates/react-app/files/src/shared/auth/index.ts +20 -0
  324. package/templates/react-app/files/src/shared/auth/logout.ts +23 -0
  325. package/templates/react-app/files/src/shared/auth/provider.tsx +52 -0
  326. package/templates/react-app/files/src/shared/auth/redirect.ts +65 -0
  327. package/templates/react-app/files/src/shared/auth/slice.ts +54 -0
  328. package/templates/react-app/files/src/shared/hooks/index.ts +6 -0
  329. package/templates/react-app/files/src/shared/layouts/app-shell/app-shell.tsx +225 -0
  330. package/templates/react-app/files/src/shared/layouts/app-shell/user-menu.tsx +74 -0
  331. package/templates/react-app/files/src/shared/layouts/auth-layout.tsx +137 -0
  332. package/templates/react-app/files/src/shared/layouts/error-boundary.tsx +38 -0
  333. package/templates/react-app/files/src/shared/layouts/index.ts +23 -0
  334. package/templates/react-app/files/src/shared/layouts/marketing/marketing-layout.tsx +29 -0
  335. package/templates/react-app/files/src/shared/layouts/marketing/section-heading.tsx +38 -0
  336. package/templates/react-app/files/src/shared/layouts/marketing/section.tsx +30 -0
  337. package/templates/react-app/files/src/shared/layouts/states/app-shell-skeleton.tsx +47 -0
  338. package/templates/react-app/files/src/shared/layouts/states/empty.tsx +22 -0
  339. package/templates/react-app/files/src/shared/layouts/states/forbidden.tsx +14 -0
  340. package/templates/react-app/files/src/shared/layouts/states/loading.tsx +11 -0
  341. package/templates/react-app/files/src/shared/layouts/states/not-found.tsx +21 -0
  342. package/templates/react-app/files/src/shared/layouts/states/page-skeleton.tsx +39 -0
  343. package/templates/react-app/files/src/shared/layouts/states/route-fallback.tsx +26 -0
  344. package/templates/react-app/files/src/shared/layouts/states/state-screen.tsx +22 -0
  345. package/templates/react-app/files/src/shared/types/README.md +28 -0
  346. package/templates/react-app/files/src/shared/types/api/index.ts +9 -0
  347. package/templates/react-app/files/src/shared/types/api/zod.gen.ts +5 -0
  348. package/templates/react-app/files/src/shared/types/index.ts +4 -0
  349. package/templates/react-app/files/src/shared/ui/components/brand/logo.tsx +51 -0
  350. package/templates/react-app/files/src/shared/ui/components/brand/social-icons.tsx +37 -0
  351. package/templates/react-app/files/src/shared/ui/components/form/form.tsx +120 -0
  352. package/templates/react-app/files/src/shared/ui/components/ui/alert.tsx +25 -0
  353. package/templates/react-app/files/src/shared/ui/components/ui/button.tsx +43 -0
  354. package/templates/react-app/files/src/shared/ui/components/ui/calendar.tsx +78 -0
  355. package/templates/react-app/files/src/shared/ui/components/ui/card.tsx +44 -0
  356. package/templates/react-app/files/src/shared/ui/components/ui/checkbox.tsx +16 -0
  357. package/templates/react-app/files/src/shared/ui/components/ui/date-picker.tsx +76 -0
  358. package/templates/react-app/files/src/shared/ui/components/ui/input.tsx +22 -0
  359. package/templates/react-app/files/src/shared/ui/components/ui/label.tsx +19 -0
  360. package/templates/react-app/files/src/shared/ui/components/ui/modal.tsx +94 -0
  361. package/templates/react-app/files/src/shared/ui/components/ui/popover.tsx +31 -0
  362. package/templates/react-app/files/src/shared/ui/components/ui/progress.tsx +33 -0
  363. package/templates/react-app/files/src/shared/ui/components/ui/search-input.tsx +69 -0
  364. package/templates/react-app/files/src/shared/ui/components/ui/select.tsx +80 -0
  365. package/templates/react-app/files/src/shared/ui/components/ui/skeleton.tsx +12 -0
  366. package/templates/react-app/files/src/shared/ui/components/ui/sonner.tsx +23 -0
  367. package/templates/react-app/files/src/shared/ui/components/ui/spinner.tsx +8 -0
  368. package/templates/react-app/files/src/shared/ui/components/ui/textarea.tsx +22 -0
  369. package/templates/react-app/files/src/shared/ui/index.ts +45 -0
  370. package/templates/react-app/files/src/shared/ui/lib/utils.ts +7 -0
  371. package/templates/react-app/files/src/shared/ui/lib/zod-setup.ts +28 -0
  372. package/templates/react-app/files/src/shared/ui/meta/brand.ts +18 -0
  373. package/templates/react-app/files/src/shared/ui/meta/meta-context.ts +30 -0
  374. package/templates/react-app/files/src/shared/ui/meta/meta-provider.tsx +24 -0
  375. package/templates/react-app/files/src/shared/ui/meta/page-meta.tsx +69 -0
  376. package/templates/react-app/files/src/shared/ui/meta/use-meta-config.ts +11 -0
  377. package/templates/react-app/files/src/shared/ui/styles/globals.css +285 -0
  378. package/templates/react-app/files/src/shared/ui/styles/theme.css +48 -0
  379. package/templates/react-app/files/src/shared/ui/theme/init-script.ts +23 -0
  380. package/templates/react-app/files/src/shared/ui/theme/theme-colors.ts +14 -0
  381. package/templates/react-app/files/src/shared/ui/theme/theme-context.ts +60 -0
  382. package/templates/react-app/files/src/shared/ui/theme/theme-provider.tsx +101 -0
  383. package/templates/react-app/files/src/shared/ui/theme/theme-toggle.tsx +40 -0
  384. package/templates/react-app/files/src/shared/ui/theme/use-theme.ts +11 -0
  385. package/templates/react-app/files/src/vite-env.d.ts +13 -0
  386. package/templates/react-app/files/tsconfig.json +26 -0
  387. package/templates/react-app/files/tsconfig.node.json +25 -0
  388. package/templates/react-app/files/vite.config.ts +344 -0
  389. package/templates/react-app/overrides/src/routes/auth/forgot-password.tsx +93 -0
  390. package/templates/react-app/overrides/src/routes/auth/login.tsx +87 -0
  391. package/templates/react-app/overrides/src/routes/auth/signup.tsx +117 -0
  392. package/templates/react-app/overrides/src/routes/auth/verified.tsx +31 -0
  393. package/templates/react-app/overrides/src/routes/auth/verify-email.tsx +97 -0
  394. package/templates/react-app/overrides/src/shared/auth/config.ts +28 -0
  395. package/templates/react-app/overrides/src/shared/auth/guards.tsx +75 -0
  396. package/templates/react-app/overrides/src/shared/auth/index.ts +20 -0
  397. package/templates/react-app/overrides/src/shared/auth/redirect.ts +65 -0
  398. package/templates/react-app/overrides/src/shared/hooks/index.ts +6 -0
  399. package/templates/react-app/overrides/src/shared/types/README.md +28 -0
  400. package/templates/react-app/template.json +222 -0
  401. package/templates/react-monorepo/files/.dockerignore +19 -0
  402. package/templates/react-monorepo/files/.editorconfig +12 -0
  403. package/templates/react-monorepo/files/.forgejo/workflows/build-and-push.yml +98 -0
  404. package/templates/react-monorepo/files/.forgejo/workflows/ci.yml +35 -0
  405. package/templates/react-monorepo/files/.husky/pre-commit +1 -0
  406. package/templates/react-monorepo/files/.nvmrc +1 -0
  407. package/templates/react-monorepo/files/.prettierignore +6 -0
  408. package/templates/react-monorepo/files/.prettierrc +9 -0
  409. package/templates/react-monorepo/files/.yarnrc.yml +2 -0
  410. package/templates/react-monorepo/files/Dockerfile +90 -0
  411. package/templates/react-monorepo/files/_gitignore +35 -0
  412. package/templates/react-monorepo/files/apps/admin/.bin/secrets.js +57 -0
  413. package/templates/react-monorepo/files/apps/admin/.env.example +16 -0
  414. package/templates/react-monorepo/files/apps/admin/eslint.config.mjs +3 -0
  415. package/templates/react-monorepo/files/apps/admin/index.html +57 -0
  416. package/templates/react-monorepo/files/apps/admin/package.json +33 -0
  417. package/templates/react-monorepo/files/apps/admin/src/App.tsx +3 -0
  418. package/templates/react-monorepo/files/apps/admin/src/app/nav.tsx +14 -0
  419. package/templates/react-monorepo/files/apps/admin/src/app/providers.tsx +42 -0
  420. package/templates/react-monorepo/files/apps/admin/src/app/router.tsx +50 -0
  421. package/templates/react-monorepo/files/apps/admin/src/app/store.ts +15 -0
  422. package/templates/react-monorepo/files/apps/admin/src/lib/env.ts +12 -0
  423. package/templates/react-monorepo/files/apps/admin/src/main.tsx +14 -0
  424. package/templates/react-monorepo/files/apps/admin/src/routes/home.tsx +17 -0
  425. package/templates/react-monorepo/files/apps/admin/src/routes/settings.tsx +10 -0
  426. package/templates/react-monorepo/files/apps/admin/src/vite-env.d.ts +14 -0
  427. package/templates/react-monorepo/files/apps/admin/tsconfig.json +10 -0
  428. package/templates/react-monorepo/files/apps/admin/vite.config.ts +13 -0
  429. package/templates/react-monorepo/files/apps/auth/.bin/secrets.js +57 -0
  430. package/templates/react-monorepo/files/apps/auth/.env.example +16 -0
  431. package/templates/react-monorepo/files/apps/auth/eslint.config.mjs +3 -0
  432. package/templates/react-monorepo/files/apps/auth/index.html +57 -0
  433. package/templates/react-monorepo/files/apps/auth/package.json +32 -0
  434. package/templates/react-monorepo/files/apps/auth/src/App.tsx +3 -0
  435. package/templates/react-monorepo/files/apps/auth/src/app/providers.tsx +42 -0
  436. package/templates/react-monorepo/files/apps/auth/src/app/router.tsx +19 -0
  437. package/templates/react-monorepo/files/apps/auth/src/app/store.ts +15 -0
  438. package/templates/react-monorepo/files/apps/auth/src/components/auth-card.tsx +23 -0
  439. package/templates/react-monorepo/files/apps/auth/src/components/checkbox-field.tsx +32 -0
  440. package/templates/react-monorepo/files/apps/auth/src/components/google-button.tsx +41 -0
  441. package/templates/react-monorepo/files/apps/auth/src/components/or-divider.tsx +9 -0
  442. package/templates/react-monorepo/files/apps/auth/src/components/password-field.tsx +70 -0
  443. package/templates/react-monorepo/files/apps/auth/src/components/submit-button.tsx +16 -0
  444. package/templates/react-monorepo/files/apps/auth/src/lib/auth-actions.ts +68 -0
  445. package/templates/react-monorepo/files/apps/auth/src/lib/env.ts +8 -0
  446. package/templates/react-monorepo/files/apps/auth/src/lib/password-strength.ts +20 -0
  447. package/templates/react-monorepo/files/apps/auth/src/lib/redirect.ts +6 -0
  448. package/templates/react-monorepo/files/apps/auth/src/lib/schemas.ts +37 -0
  449. package/templates/react-monorepo/files/apps/auth/src/main.tsx +14 -0
  450. package/templates/react-monorepo/files/apps/auth/src/routes/forgot-password.tsx +91 -0
  451. package/templates/react-monorepo/files/apps/auth/src/routes/login.tsx +82 -0
  452. package/templates/react-monorepo/files/apps/auth/src/routes/reset-password.tsx +83 -0
  453. package/templates/react-monorepo/files/apps/auth/src/routes/signup.tsx +117 -0
  454. package/templates/react-monorepo/files/apps/auth/src/routes/verified.tsx +30 -0
  455. package/templates/react-monorepo/files/apps/auth/src/routes/verify-email.tsx +96 -0
  456. package/templates/react-monorepo/files/apps/auth/src/vite-env.d.ts +14 -0
  457. package/templates/react-monorepo/files/apps/auth/tsconfig.json +10 -0
  458. package/templates/react-monorepo/files/apps/auth/vite.config.ts +13 -0
  459. package/templates/react-monorepo/files/apps/client/.bin/secrets.js +57 -0
  460. package/templates/react-monorepo/files/apps/client/.env.example +16 -0
  461. package/templates/react-monorepo/files/apps/client/eslint.config.mjs +3 -0
  462. package/templates/react-monorepo/files/apps/client/index.html +57 -0
  463. package/templates/react-monorepo/files/apps/client/package.json +35 -0
  464. package/templates/react-monorepo/files/apps/client/src/App.tsx +3 -0
  465. package/templates/react-monorepo/files/apps/client/src/app/nav.tsx +14 -0
  466. package/templates/react-monorepo/files/apps/client/src/app/providers.tsx +42 -0
  467. package/templates/react-monorepo/files/apps/client/src/app/router.tsx +50 -0
  468. package/templates/react-monorepo/files/apps/client/src/app/store.ts +15 -0
  469. package/templates/react-monorepo/files/apps/client/src/lib/env.ts +12 -0
  470. package/templates/react-monorepo/files/apps/client/src/main.tsx +19 -0
  471. package/templates/react-monorepo/files/apps/client/src/routes/home.tsx +17 -0
  472. package/templates/react-monorepo/files/apps/client/src/routes/settings.tsx +10 -0
  473. package/templates/react-monorepo/files/apps/client/src/vite-env.d.ts +15 -0
  474. package/templates/react-monorepo/files/apps/client/tsconfig.json +10 -0
  475. package/templates/react-monorepo/files/apps/client/vite.config.ts +120 -0
  476. package/templates/react-monorepo/files/apps/landing/.bin/secrets.js +57 -0
  477. package/templates/react-monorepo/files/apps/landing/.env.example +20 -0
  478. package/templates/react-monorepo/files/apps/landing/eslint.config.mjs +3 -0
  479. package/templates/react-monorepo/files/apps/landing/index.html +51 -0
  480. package/templates/react-monorepo/files/apps/landing/package.json +33 -0
  481. package/templates/react-monorepo/files/apps/landing/src/App.tsx +3 -0
  482. package/templates/react-monorepo/files/apps/landing/src/app/providers.tsx +42 -0
  483. package/templates/react-monorepo/files/apps/landing/src/app/router.tsx +21 -0
  484. package/templates/react-monorepo/files/apps/landing/src/app/store.ts +18 -0
  485. package/templates/react-monorepo/files/apps/landing/src/features/seo/routes.ts +65 -0
  486. package/templates/react-monorepo/files/apps/landing/src/lib/env.ts +10 -0
  487. package/templates/react-monorepo/files/apps/landing/src/main.tsx +14 -0
  488. package/templates/react-monorepo/files/apps/landing/src/routes/home.tsx +18 -0
  489. package/templates/react-monorepo/files/apps/landing/src/vite-env.d.ts +15 -0
  490. package/templates/react-monorepo/files/apps/landing/tsconfig.json +10 -0
  491. package/templates/react-monorepo/files/apps/landing/vite.config.ts +169 -0
  492. package/templates/react-monorepo/files/build.sh +61 -0
  493. package/templates/react-monorepo/files/docker/nginx.conf +136 -0
  494. package/templates/react-monorepo/files/package.json +37 -0
  495. package/templates/react-monorepo/files/packages/.gitkeep +0 -0
  496. package/templates/react-monorepo/files/packages/api/eslint.config.mjs +3 -0
  497. package/templates/react-monorepo/files/packages/api/package.json +30 -0
  498. package/templates/react-monorepo/files/packages/api/src/base-api.ts +24 -0
  499. package/templates/react-monorepo/files/packages/api/src/base-query.ts +29 -0
  500. package/templates/react-monorepo/files/packages/api/src/config.ts +21 -0
  501. package/templates/react-monorepo/files/packages/api/src/index.ts +15 -0
  502. package/templates/react-monorepo/files/packages/api/src/realtime-events.ts +34 -0
  503. package/templates/react-monorepo/files/packages/api/src/socket.ts +41 -0
  504. package/templates/react-monorepo/files/packages/api/src/store.ts +39 -0
  505. package/templates/react-monorepo/files/packages/api/src/upload-query.ts +115 -0
  506. package/templates/react-monorepo/files/packages/api/tsconfig.json +4 -0
  507. package/templates/react-monorepo/files/packages/auth/eslint.config.mjs +3 -0
  508. package/templates/react-monorepo/files/packages/auth/package.json +33 -0
  509. package/templates/react-monorepo/files/packages/auth/src/client.ts +27 -0
  510. package/templates/react-monorepo/files/packages/auth/src/config.ts +22 -0
  511. package/templates/react-monorepo/files/packages/auth/src/guards.tsx +61 -0
  512. package/templates/react-monorepo/files/packages/auth/src/hooks.ts +13 -0
  513. package/templates/react-monorepo/files/packages/auth/src/index.ts +20 -0
  514. package/templates/react-monorepo/files/packages/auth/src/logout.ts +23 -0
  515. package/templates/react-monorepo/files/packages/auth/src/provider.tsx +52 -0
  516. package/templates/react-monorepo/files/packages/auth/src/redirect.ts +37 -0
  517. package/templates/react-monorepo/files/packages/auth/src/slice.ts +54 -0
  518. package/templates/react-monorepo/files/packages/auth/tsconfig.json +4 -0
  519. package/templates/react-monorepo/files/packages/config/README.md +47 -0
  520. package/templates/react-monorepo/files/packages/config/eslint/base.mjs +83 -0
  521. package/templates/react-monorepo/files/packages/config/package.json +37 -0
  522. package/templates/react-monorepo/files/packages/config/tailwind/theme.css +48 -0
  523. package/templates/react-monorepo/files/packages/config/tsconfig/base.json +22 -0
  524. package/templates/react-monorepo/files/packages/config/tsconfig/node.json +9 -0
  525. package/templates/react-monorepo/files/packages/config/tsconfig/react.json +9 -0
  526. package/templates/react-monorepo/files/packages/config/vite/index.d.ts +18 -0
  527. package/templates/react-monorepo/files/packages/config/vite/index.mjs +129 -0
  528. package/templates/react-monorepo/files/packages/hooks/eslint.config.mjs +3 -0
  529. package/templates/react-monorepo/files/packages/hooks/package.json +16 -0
  530. package/templates/react-monorepo/files/packages/hooks/src/index.ts +3 -0
  531. package/templates/react-monorepo/files/packages/hooks/tsconfig.json +4 -0
  532. package/templates/react-monorepo/files/packages/layouts/eslint.config.mjs +3 -0
  533. package/templates/react-monorepo/files/packages/layouts/package.json +30 -0
  534. package/templates/react-monorepo/files/packages/layouts/src/app-shell/app-shell.tsx +225 -0
  535. package/templates/react-monorepo/files/packages/layouts/src/app-shell/user-menu.tsx +74 -0
  536. package/templates/react-monorepo/files/packages/layouts/src/auth-layout.tsx +137 -0
  537. package/templates/react-monorepo/files/packages/layouts/src/error-boundary.tsx +38 -0
  538. package/templates/react-monorepo/files/packages/layouts/src/index.ts +23 -0
  539. package/templates/react-monorepo/files/packages/layouts/src/marketing/marketing-layout.tsx +29 -0
  540. package/templates/react-monorepo/files/packages/layouts/src/marketing/section-heading.tsx +38 -0
  541. package/templates/react-monorepo/files/packages/layouts/src/marketing/section.tsx +30 -0
  542. package/templates/react-monorepo/files/packages/layouts/src/states/app-shell-skeleton.tsx +47 -0
  543. package/templates/react-monorepo/files/packages/layouts/src/states/empty.tsx +22 -0
  544. package/templates/react-monorepo/files/packages/layouts/src/states/forbidden.tsx +14 -0
  545. package/templates/react-monorepo/files/packages/layouts/src/states/loading.tsx +11 -0
  546. package/templates/react-monorepo/files/packages/layouts/src/states/not-found.tsx +21 -0
  547. package/templates/react-monorepo/files/packages/layouts/src/states/page-skeleton.tsx +39 -0
  548. package/templates/react-monorepo/files/packages/layouts/src/states/route-fallback.tsx +26 -0
  549. package/templates/react-monorepo/files/packages/layouts/src/states/state-screen.tsx +22 -0
  550. package/templates/react-monorepo/files/packages/layouts/tsconfig.json +4 -0
  551. package/templates/react-monorepo/files/packages/types/.bin/openapi.js +32 -0
  552. package/templates/react-monorepo/files/packages/types/.bin/secrets.js +57 -0
  553. package/templates/react-monorepo/files/packages/types/README.md +28 -0
  554. package/templates/react-monorepo/files/packages/types/eslint.config.mjs +3 -0
  555. package/templates/react-monorepo/files/packages/types/openapi-ts.config.ts +22 -0
  556. package/templates/react-monorepo/files/packages/types/package.json +29 -0
  557. package/templates/react-monorepo/files/packages/types/src/index.ts +4 -0
  558. package/templates/react-monorepo/files/packages/types/src/types/api/index.ts +9 -0
  559. package/templates/react-monorepo/files/packages/types/src/types/api/zod.gen.ts +5 -0
  560. package/templates/react-monorepo/files/packages/types/tsconfig.json +7 -0
  561. package/templates/react-monorepo/files/packages/ui/components.json +21 -0
  562. package/templates/react-monorepo/files/packages/ui/eslint.config.mjs +13 -0
  563. package/templates/react-monorepo/files/packages/ui/package.json +44 -0
  564. package/templates/react-monorepo/files/packages/ui/src/components/brand/logo.tsx +51 -0
  565. package/templates/react-monorepo/files/packages/ui/src/components/brand/social-icons.tsx +37 -0
  566. package/templates/react-monorepo/files/packages/ui/src/components/form/form.tsx +120 -0
  567. package/templates/react-monorepo/files/packages/ui/src/components/ui/alert.tsx +25 -0
  568. package/templates/react-monorepo/files/packages/ui/src/components/ui/button.tsx +43 -0
  569. package/templates/react-monorepo/files/packages/ui/src/components/ui/calendar.tsx +78 -0
  570. package/templates/react-monorepo/files/packages/ui/src/components/ui/card.tsx +44 -0
  571. package/templates/react-monorepo/files/packages/ui/src/components/ui/checkbox.tsx +16 -0
  572. package/templates/react-monorepo/files/packages/ui/src/components/ui/date-picker.tsx +76 -0
  573. package/templates/react-monorepo/files/packages/ui/src/components/ui/input.tsx +22 -0
  574. package/templates/react-monorepo/files/packages/ui/src/components/ui/label.tsx +19 -0
  575. package/templates/react-monorepo/files/packages/ui/src/components/ui/modal.tsx +94 -0
  576. package/templates/react-monorepo/files/packages/ui/src/components/ui/popover.tsx +31 -0
  577. package/templates/react-monorepo/files/packages/ui/src/components/ui/progress.tsx +33 -0
  578. package/templates/react-monorepo/files/packages/ui/src/components/ui/search-input.tsx +69 -0
  579. package/templates/react-monorepo/files/packages/ui/src/components/ui/select.tsx +80 -0
  580. package/templates/react-monorepo/files/packages/ui/src/components/ui/skeleton.tsx +12 -0
  581. package/templates/react-monorepo/files/packages/ui/src/components/ui/sonner.tsx +23 -0
  582. package/templates/react-monorepo/files/packages/ui/src/components/ui/spinner.tsx +8 -0
  583. package/templates/react-monorepo/files/packages/ui/src/components/ui/textarea.tsx +22 -0
  584. package/templates/react-monorepo/files/packages/ui/src/index.ts +45 -0
  585. package/templates/react-monorepo/files/packages/ui/src/lib/utils.ts +7 -0
  586. package/templates/react-monorepo/files/packages/ui/src/lib/zod-setup.ts +28 -0
  587. package/templates/react-monorepo/files/packages/ui/src/meta/brand.ts +18 -0
  588. package/templates/react-monorepo/files/packages/ui/src/meta/meta-context.ts +30 -0
  589. package/templates/react-monorepo/files/packages/ui/src/meta/meta-provider.tsx +24 -0
  590. package/templates/react-monorepo/files/packages/ui/src/meta/page-meta.tsx +69 -0
  591. package/templates/react-monorepo/files/packages/ui/src/meta/use-meta-config.ts +11 -0
  592. package/templates/react-monorepo/files/packages/ui/src/styles/globals.css +285 -0
  593. package/templates/react-monorepo/files/packages/ui/src/theme/init-script.ts +23 -0
  594. package/templates/react-monorepo/files/packages/ui/src/theme/theme-colors.ts +14 -0
  595. package/templates/react-monorepo/files/packages/ui/src/theme/theme-context.ts +60 -0
  596. package/templates/react-monorepo/files/packages/ui/src/theme/theme-provider.tsx +101 -0
  597. package/templates/react-monorepo/files/packages/ui/src/theme/theme-toggle.tsx +40 -0
  598. package/templates/react-monorepo/files/packages/ui/src/theme/use-theme.ts +11 -0
  599. package/templates/react-monorepo/files/packages/ui/tsconfig.json +9 -0
  600. package/templates/react-monorepo/files/turbo.json +24 -0
  601. package/templates/react-monorepo/template.json +230 -0
@@ -0,0 +1,69 @@
1
+ import { formatTitle } from './meta-context';
2
+ import { useMetaConfig } from './use-meta-config';
3
+
4
+ export type PageMetaProps = {
5
+ /**
6
+ * Page segment, not the whole title — the app's suffix is appended by
7
+ * `MetaProvider`. Pass `exact` to opt out.
8
+ */
9
+ title?: string;
10
+ /** Use `title` verbatim, without the app suffix. */
11
+ exact?: boolean;
12
+ /** Overrides the base description from `index.html` while mounted. */
13
+ description?: string;
14
+ /** e.g. `noindex, nofollow` for the private apps. */
15
+ robots?: string;
16
+ /** Absolute URL. */
17
+ canonical?: string;
18
+ };
19
+
20
+ /**
21
+ * Per-route document metadata. Render it anywhere inside a route — React 19
22
+ * hoists `<title>`, `<meta>` and `<link>` into `<head>` natively, so there is no
23
+ * head-management dependency and no effect involved.
24
+ *
25
+ * **Being effect-free is the point**: it is what makes the tags appear in
26
+ * server-rendered output, which an effect-based `document.title` hook can never
27
+ * do. Do not "optimise" this into a `useEffect`.
28
+ *
29
+ * Omit a field to inherit it: a route with no `description` keeps the base one
30
+ * from `index.html`, and React removes the tag again on unmount.
31
+ *
32
+ * ## Nesting
33
+ *
34
+ * Client-side, the innermost render wins for free — React claims the first
35
+ * existing `<title>` (so the static one from `index.html` is overwritten), and
36
+ * for any subsequent one it does `head.insertBefore(new, existing)`. Later
37
+ * renders therefore land *earlier* in `<head>`, and `document.title` is defined
38
+ * as the first `<title>` in tree order.
39
+ *
40
+ * ## Server-side rendering — read before wiring a prerender
41
+ *
42
+ * Order inverts on the server. `react-dom/server` emits tags in render order,
43
+ * so a document renders as:
44
+ *
45
+ * ```html
46
+ * <head><title>base from index.html</title><title>Route · Brand</title></head>
47
+ * ```
48
+ *
49
+ * and the **base** title wins, because it comes first. React does not
50
+ * deduplicate `<title>` or `<meta>` (only stylesheets get resource dedupe), so
51
+ * a prerender step MUST post-process the emitted HTML: keep the **last**
52
+ * `<title>` and the **last** `<meta name="description">`, dropping earlier
53
+ * duplicates. Without that, every prerendered page ships the base title and
54
+ * hydration silently corrects it — invisible in a browser, wrong for crawlers.
55
+ * Tracked on MAI-88.
56
+ */
57
+ export function PageMeta({ title, exact, description, robots, canonical }: PageMetaProps) {
58
+ const config = useMetaConfig();
59
+ const resolved = title === undefined ? undefined : exact ? title : formatTitle(title, config);
60
+
61
+ return (
62
+ <>
63
+ {resolved === undefined ? null : <title>{resolved}</title>}
64
+ {description === undefined ? null : <meta name="description" content={description} />}
65
+ {robots === undefined ? null : <meta name="robots" content={robots} />}
66
+ {canonical === undefined ? null : <link rel="canonical" href={canonical} />}
67
+ </>
68
+ );
69
+ }
@@ -0,0 +1,11 @@
1
+ import { useContext } from 'react';
2
+ import { MetaContext } from './meta-context';
3
+
4
+ /**
5
+ * The app's title template. Falls back to the un-suffixed default rather than
6
+ * throwing, so a component rendering `PageMeta` outside a `MetaProvider` still
7
+ * produces a usable title instead of blanking the tab.
8
+ */
9
+ export function useMetaConfig() {
10
+ return useContext(MetaContext);
11
+ }
@@ -0,0 +1,285 @@
1
+ /**
2
+ * @/shared/ui global stylesheet — the single CSS entry every app imports:
3
+ * import '@/shared/ui/styles/globals.css';
4
+ *
5
+ * Layers: self-hosted font -> Tailwind v4 -> brand tokens (theme.css) ->
6
+ * shadcn semantic tokens (light/dark) mapped for utilities. Design language =
7
+ * Wise: flat surfaces, no shadows, 1px ink hairlines, 24px radii, green CTA only.
8
+ */
9
+ @import 'tailwindcss';
10
+ @import './theme.css';
11
+
12
+ /**
13
+ * Inter, self-hosted. This was a `@import url(fonts.googleapis.com/...)` on the
14
+ * first line of this file, which is the slowest possible way to load a font: a
15
+ * CSS @import is only discovered after this stylesheet has been parsed, so it
16
+ * cost three SERIALIZED round trips across two extra origins (googleapis ->
17
+ * gstatic -> woff2) before text could paint. Worse for the installed PWA — the
18
+ * service worker cannot precache a cross-origin URL, so fonts hit the network on
19
+ * every cold start and failed outright offline, despite the shell being cached.
20
+ *
21
+ * One variable file replaces the six static weights that were being requested.
22
+ * 47 KB covers the whole 100-900 axis, and all six weights are in use
23
+ * (400/500/600/700/800/900). Latin only, and upright only — there is no italic
24
+ * anywhere in the codebase. Add `wght-italic` here if that ever changes.
25
+ *
26
+ * Declared by hand rather than `@import '@fontsource-variable/inter/wght.css'`
27
+ * because that pulls in all seven subsets (cyrillic, greek, vietnamese, ...).
28
+ * Vite would emit every one into dist, and `woff2` is in the PWA precache glob.
29
+ *
30
+ * Named `Inter` so the `--font-sans` / `--font-display` stacks need no change; an
31
+ * @font-face shadows a same-named system font, so this file always wins.
32
+ */
33
+ @font-face {
34
+ font-family: 'Inter';
35
+ font-style: normal;
36
+ font-weight: 100 900;
37
+ font-display: swap;
38
+ src: url('@fontsource-variable/inter/files/inter-latin-wght-normal.woff2') format('woff2');
39
+ unicode-range:
40
+ U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC,
41
+ U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
42
+ }
43
+
44
+ /* Scan workspace component packages for utility classes (they live under
45
+ node_modules in consumers, which Tailwind skips by default). The consuming
46
+ app's own src is auto-detected by Tailwind. Add a line here for any new
47
+ component package. */
48
+ @source '../**/*.{ts,tsx}';
49
+ @source '../../../layouts/src/**/*.{ts,tsx}';
50
+
51
+ @custom-variant dark (&:is(.dark *));
52
+
53
+ :root {
54
+ --radius: 0.75rem; /* 12px base (inputs); cards/buttons use explicit 24px */
55
+
56
+ --background: #eceee9;
57
+ --foreground: #0e0f0c;
58
+ --card: #ffffff;
59
+ --card-foreground: #0e0f0c;
60
+ --popover: #ffffff;
61
+ --popover-foreground: #0e0f0c;
62
+ --primary: #9fe870;
63
+ --primary-foreground: #0e0f0c;
64
+ --secondary: #f2f4ef;
65
+ --secondary-foreground: #0e0f0c;
66
+ --muted: #f2f4ef;
67
+ --muted-foreground: #7d7f7a;
68
+ --accent: #e2f6d5;
69
+ --accent-foreground: #163300;
70
+ --destructive: #c0242b;
71
+ --destructive-foreground: #ffffff;
72
+ --border: rgba(14, 15, 12, 0.09);
73
+ --input: rgba(14, 15, 12, 0.28);
74
+ --ring: rgba(159, 232, 112, 0.55);
75
+ --brand-panel: #e8ebe6;
76
+
77
+ /*
78
+ * Secondary text + link colours. These start life as fixed brand tokens in
79
+ * theme.css, which means they never changed with the theme — #454745
80
+ * body text sits at ~2:1 on a dark card, and the #163300 link is effectively
81
+ * invisible. Redeclared here (light values identical to the brand ones, so
82
+ * light mode is unchanged) and overridden in .dark below.
83
+ * Dark values are the design's own palette from `Client App.dc.html`.
84
+ */
85
+ --body: #454745;
86
+ --mute: #868685;
87
+ --link: #163300;
88
+
89
+ /*
90
+ * Status colours, same problem: the brand hexes are tuned for a white card and
91
+ * are muddy on a dark one. Light values match the brand tokens exactly; the
92
+ * dark ones are the design's own `--gain`/`--loss` (plus a lifted warning).
93
+ * These feed both the `text-*` and the translucent `bg-*` tint utilities.
94
+ */
95
+ --positive: #2ead4b;
96
+ --negative: #d03238;
97
+ --warning: #ffd11a;
98
+
99
+ /*
100
+ * Foreground for the `bg-warning/20` pill. The fixed brand `--color-warning-deep`
101
+ * (#b86700) was being used here, which is the classic pairing bug: the tint
102
+ * follows the theme, the text does not. On a dark card it measured 2.43:1 —
103
+ * unreadable. These are the design's own warning-pill foregrounds
104
+ * (`--pill-warn-fg`), and they measure 8.8–10:1 light / 6.5–8.6:1 dark.
105
+ */
106
+ --warning-content: #4a3b1c;
107
+
108
+ /*
109
+ * Active-tab underline (admin console tab bars). The design uses a literal
110
+ * #579b2e in both themes, which measures 2.93:1 on the light page background —
111
+ * under the 3:1 WCAG 1.4.11 needs for a non-text indicator. Darkened along the
112
+ * same hue (97.4°) to 3.55:1; the dark value is the same hue lifted for an ink
113
+ * canvas. Deliberately NOT `--primary`: Wise Green is the CTA accent, and an
114
+ * active tab is a state, not a call to action.
115
+ */
116
+ --tab-active: #4e8b29;
117
+
118
+ /*
119
+ * Categorical chart palette — for encoding *identity* (which holding is which),
120
+ * never magnitude. Six slots because a donut stops being readable past ~6
121
+ * slices, and because the hue budget is genuinely that small: excluding the
122
+ * gain (~144°) and loss (~30°) hue neighbourhoods removes ~100° of the wheel,
123
+ * and what's left can't hold 8 mutually distinguishable hues that also stay
124
+ * clear of those two colours in both themes. Verified min pairwise ΔE 40 (light)
125
+ * / 36 (dark) against Tableau10's 30 as the benchmark, every slot ≥32 ΔE from
126
+ * `--positive`/`--negative`/`--primary` so no slice reads as a gain, a loss or
127
+ * a CTA, and hue drift ≤3° between themes so a slice keeps its identity on
128
+ * toggle. Lightness is deliberately held in a narrow band per theme so no slice
129
+ * outweighs another.
130
+ *
131
+ * `--chart-other` is the "and the rest" bucket: neutral on purpose, so it
132
+ * recedes instead of reading as another holding. Adding a 7th hue here will
133
+ * break the guarantees above — collapse into Other instead.
134
+ */
135
+ /*
136
+ * Single magnitude hue for bar charts. Bar rows are ONE series — identity comes
137
+ * from the row label, so every bar is the same colour; colouring them by value
138
+ * would double-encode the length. Separate from `--chart-1` despite sharing the
139
+ * light value: that slot is tuned for small donut slices, and its dark value
140
+ * (#55bca4, OKLCH L 0.727) sits outside the 0.48–0.67 lightness band a dark-mode
141
+ * bar fill needs. Both values pass the dataviz validator on all five checks.
142
+ */
143
+ --chart-bar: #009e84;
144
+
145
+ --chart-1: #009e84;
146
+ --chart-2: #0096c7;
147
+ --chart-3: #6767c7;
148
+ --chart-4: #ac7e9a;
149
+ --chart-5: #c27c2d;
150
+ --chart-6: #737548;
151
+ --chart-other: #8f958e;
152
+
153
+ /*
154
+ * Modal / drawer backdrop. Needs to be darker on a dark page to still read as
155
+ * a separation layer — the design specifies exactly this pair.
156
+ */
157
+ --scrim: rgba(14, 15, 12, 0.45);
158
+ }
159
+
160
+ .dark {
161
+ --background: #0e0f0c;
162
+ --foreground: #f4f6f1;
163
+ --card: #191b16;
164
+ --card-foreground: #f4f6f1;
165
+ --popover: #191b16;
166
+ --popover-foreground: #f4f6f1;
167
+ --primary: #9fe870;
168
+ --primary-foreground: #0e0f0c;
169
+ --secondary: #23261f;
170
+ --secondary-foreground: #f4f6f1;
171
+ --muted: #23261f;
172
+ --muted-foreground: #8b8d88;
173
+ --accent: rgba(159, 232, 112, 0.15);
174
+ --accent-foreground: #9fe870;
175
+ --destructive: #ff8085;
176
+ --destructive-foreground: #0e0f0c;
177
+ --border: rgba(232, 235, 230, 0.11);
178
+ --input: rgba(232, 235, 230, 0.3);
179
+ --ring: rgba(159, 232, 112, 0.4);
180
+ --brand-panel: #14160f;
181
+
182
+ --body: #c4c7c0;
183
+ --mute: #8b8d88;
184
+ /* Links become the brand green on dark — ink-deep is unreadable there. */
185
+ --link: #9fe870;
186
+
187
+ --positive: #6fdd8b;
188
+ --negative: #ff8085;
189
+ --warning: #ffd95e;
190
+ /* On a dark tint the deep olive inverts to the warning colour itself. */
191
+ --warning-content: #ffd95e;
192
+
193
+ /* Same 97.4° hue as light, lifted for the ink canvas (9.1:1 on --background). */
194
+ --tab-active: #74c742;
195
+
196
+ /* Same hue as light, stepped into the dark band (see the light block for why). */
197
+ --chart-bar: #35a68d;
198
+
199
+ /* Same hues as light, lifted for the dark card (see the light block for why). */
200
+ --chart-1: #55bca4;
201
+ --chart-2: #43c8ff;
202
+ --chart-3: #a7a1fc;
203
+ --chart-4: #eab8d6;
204
+ --chart-5: #e29d56;
205
+ --chart-6: #c9c998;
206
+ --chart-other: #7f857e;
207
+
208
+ --scrim: rgba(0, 0, 0, 0.6);
209
+ }
210
+
211
+ @theme inline {
212
+ --color-background: var(--background);
213
+ --color-foreground: var(--foreground);
214
+ --color-card: var(--card);
215
+ --color-card-foreground: var(--card-foreground);
216
+ --color-popover: var(--popover);
217
+ --color-popover-foreground: var(--popover-foreground);
218
+ --color-primary: var(--primary);
219
+ --color-primary-foreground: var(--primary-foreground);
220
+ --color-secondary: var(--secondary);
221
+ --color-secondary-foreground: var(--secondary-foreground);
222
+ --color-muted: var(--muted);
223
+ --color-muted-foreground: var(--muted-foreground);
224
+ --color-accent: var(--accent);
225
+ --color-accent-foreground: var(--accent-foreground);
226
+ --color-destructive: var(--destructive);
227
+ --color-destructive-foreground: var(--destructive-foreground);
228
+ --color-border: var(--border);
229
+ --color-input: var(--input);
230
+ --color-ring: var(--ring);
231
+ --color-brand-panel: var(--brand-panel);
232
+
233
+ /*
234
+ * Re-point the secondary-text utilities (`text-body`, `text-mute`,
235
+ * `text-ink-deep`) at the theme-aware vars above. theme.css defines these
236
+ * as fixed brand hexes, and this layer wins — so `text-body` finally responds
237
+ * to the theme instead of staying #454745 on a dark card.
238
+ */
239
+ --color-body: var(--body);
240
+ --color-mute: var(--mute);
241
+ --color-ink-deep: var(--link);
242
+ --color-positive: var(--positive);
243
+ --color-negative: var(--negative);
244
+ --color-warning: var(--warning);
245
+ /* Pair `bg-warning/20` with `text-warning-content`, never `text-warning-deep`. */
246
+ --color-warning-content: var(--warning-content);
247
+ /*
248
+ * `bg-chart-bar` for CSS bar fills. The `--chart-1..6` donut slots are
249
+ * deliberately NOT mapped to utilities — the donut reads them through
250
+ * `readVar()` in JS — so this is the only chart colour with a class.
251
+ */
252
+ --color-chart-bar: var(--chart-bar);
253
+ /* Enables `border-tab-active` on the selected tab's 2px underline. */
254
+ --color-tab-active: var(--tab-active);
255
+ /* Enables `bg-scrim` — already carries its own alpha, so no /45 modifier. */
256
+ --color-scrim: var(--scrim);
257
+ }
258
+
259
+ @layer base {
260
+ * {
261
+ border-color: var(--color-border);
262
+ }
263
+
264
+ /*
265
+ * Tells the browser which palette its own UI should use — native scrollbars,
266
+ * form controls, autofill backgrounds and the text caret. Without it these
267
+ * stay light-styled over a dark page. The theme provider also sets this
268
+ * inline on <html> so it flips instantly with the class.
269
+ */
270
+ :root {
271
+ color-scheme: light;
272
+ }
273
+
274
+ .dark {
275
+ color-scheme: dark;
276
+ }
277
+
278
+ body {
279
+ background-color: var(--color-background);
280
+ color: var(--color-foreground);
281
+ font-family: var(--font-sans);
282
+ -webkit-font-smoothing: antialiased;
283
+ text-rendering: optimizeLegibility;
284
+ }
285
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Acme Corp — shared Tailwind v4 brand tokens (Wise-derived design system).
3
+ * Source of truth: Claude Design "Acme Corp Design System" (colors_and_type.css).
4
+ *
5
+ * Consume in an app's main CSS entry (via @/shared/ui/styles/globals.css):
6
+ * @import 'tailwindcss';
7
+ * @import './theme.css';
8
+ *
9
+ * These are the BRAND tokens (palette shades, ink, semantic, fonts). The shadcn
10
+ * semantic layer (--background/--foreground/--primary/...) is layered on top by
11
+ * @/shared/ui/styles/globals.css, which owns light/dark theming.
12
+ *
13
+ * Brand rule: Wise Green (#9fe870) is the CTA accent ONLY — never a success color.
14
+ */
15
+ @theme {
16
+ /* Brand green shades (primary itself is owned by the shadcn layer) */
17
+ --color-primary-active: #cdffad;
18
+ --color-primary-neutral: #c5edab;
19
+ --color-primary-pale: #e2f6d5;
20
+ --color-on-primary: #0e0f0c;
21
+
22
+ /* Surfaces & ink */
23
+ --color-sage: #e8ebe6;
24
+ --color-ink: #0e0f0c;
25
+ --color-ink-deep: #163300;
26
+ --color-body: #454745;
27
+ --color-mute: #868685;
28
+
29
+ /* Semantic — status only, never the brand green */
30
+ --color-positive: #2ead4b;
31
+ --color-positive-deep: #054d28;
32
+ --color-warning: #ffd11a;
33
+ --color-warning-deep: #b86700;
34
+ --color-negative: #d03238;
35
+ --color-negative-deep: #a72027;
36
+
37
+ /* Illustrative accents (inside illustrations only, never UI fills) */
38
+ --color-accent-orange: #ffc091;
39
+ --color-accent-cyan: #38c8ff;
40
+
41
+ /* Typography — Inter stands in for proprietary Wise Sans */
42
+ /* 'Manrope' was listed here but never loaded — and sat *after* Inter, so it
43
+ could never have been used even if it were. Removed as dead. If the design
44
+ really wants Manrope for display type it needs its own @font-face AND to be
45
+ ordered ahead of Inter — a deliberate visual change, not a cleanup. */
46
+ --font-display: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
47
+ --font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
48
+ }
@@ -0,0 +1,23 @@
1
+ import { THEME_COLORS } from './theme-colors';
2
+
3
+ /**
4
+ * Blocking snippet for each app's `index.html`, placed in `<head>` **before**
5
+ * the stylesheet link.
6
+ *
7
+ * Without it the page paints light, then React mounts and flips it to dark —
8
+ * a visible flash on every load, worst on a slow mobile connection where the
9
+ * bundle takes seconds to arrive. It duplicates a few lines of `theme-context`
10
+ * on purpose: it must run before any module loads, so it cannot import them.
11
+ *
12
+ * The colours are interpolated from `THEME_COLORS` so this isn't another copy to
13
+ * keep in sync. `THEME_STORAGE_KEY` still is one — it has to be a literal here.
14
+ */
15
+ export const themeInitScript = `(function(){try{
16
+ var m=localStorage.getItem('acme-theme');
17
+ var d=m==='dark'||((m===null||m==='system')&&matchMedia('(prefers-color-scheme: dark)').matches);
18
+ var r=document.documentElement;
19
+ r.classList.toggle('dark',d);
20
+ r.style.colorScheme=d?'dark':'light';
21
+ var t=document.querySelector('meta[name="theme-color"]');
22
+ if(t)t.setAttribute('content',d?'${THEME_COLORS.dark}':'${THEME_COLORS.light}');
23
+ }catch(e){}})();`;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The page background per theme, as a raw value.
3
+ *
4
+ * This module deliberately has **no imports**. Build-time config needs these
5
+ * values too — `apps/client/vite.config.ts` bakes them into the PWA manifest —
6
+ * and importing `theme-context` there would pull React into the Vite config.
7
+ * Keep it dependency-free so both the runtime and the build can read it.
8
+ *
9
+ * Must match `--background` in `globals.css` for each theme.
10
+ */
11
+ export const THEME_COLORS = {
12
+ light: '#eceee9',
13
+ dark: '#0e0f0c'
14
+ } as const;
@@ -0,0 +1,60 @@
1
+ import { createContext } from 'react';
2
+ import { THEME_COLORS } from './theme-colors';
3
+
4
+ /** What the user chose. `system` follows the OS and keeps following it. */
5
+ export type ThemeMode = 'system' | 'light' | 'dark';
6
+ /** What is actually on screen once `system` is resolved. */
7
+ export type ResolvedTheme = 'light' | 'dark';
8
+
9
+ export type ThemeContextValue = {
10
+ mode: ThemeMode;
11
+ resolved: ResolvedTheme;
12
+ setMode: (mode: ThemeMode) => void;
13
+ /** Cycles light → dark → system. */
14
+ toggle: () => void;
15
+ };
16
+
17
+ /**
18
+ * Shared across every app. Kept in its own module (no components) so the
19
+ * provider and the hook can live in separate files without tripping
20
+ * `react-refresh/only-export-components`.
21
+ */
22
+ export const ThemeContext = createContext<ThemeContextValue | null>(null);
23
+
24
+ /** localStorage key. Per-origin, so each app remembers its own choice. */
25
+ export const THEME_STORAGE_KEY = 'acme-theme';
26
+
27
+ /**
28
+ * Re-exported so consumers keep importing theme colours from one place. The
29
+ * values live in `theme-colors.ts`, which has no imports so the Vite config can
30
+ * read them at build time (see the PWA manifest in `apps/client/vite.config.ts`).
31
+ */
32
+ export { THEME_COLORS };
33
+
34
+ export function isThemeMode(value: unknown): value is ThemeMode {
35
+ return value === 'system' || value === 'light' || value === 'dark';
36
+ }
37
+
38
+ /** Read the stored preference. Safe on the server and in private-mode browsers. */
39
+ export function readStoredMode(): ThemeMode {
40
+ if (typeof window === 'undefined') return 'system';
41
+ try {
42
+ const stored = window.localStorage.getItem(THEME_STORAGE_KEY);
43
+ return isThemeMode(stored) ? stored : 'system';
44
+ } catch {
45
+ return 'system';
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Apply a resolved theme to the document: the `dark` class that Tailwind's
51
+ * `@custom-variant dark` keys off, `color-scheme` so native scrollbars, form
52
+ * controls and autofill follow, and the PWA status-bar colour.
53
+ */
54
+ export function applyTheme(resolved: ResolvedTheme): void {
55
+ if (typeof document === 'undefined') return;
56
+ const root = document.documentElement;
57
+ root.classList.toggle('dark', resolved === 'dark');
58
+ root.style.colorScheme = resolved;
59
+ document.querySelector('meta[name="theme-color"]')?.setAttribute('content', THEME_COLORS[resolved]);
60
+ }
@@ -0,0 +1,101 @@
1
+ import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react';
2
+ import {
3
+ ThemeContext,
4
+ THEME_STORAGE_KEY,
5
+ applyTheme,
6
+ isThemeMode,
7
+ readStoredMode,
8
+ type ResolvedTheme,
9
+ type ThemeMode
10
+ } from './theme-context';
11
+
12
+ const QUERY = '(prefers-color-scheme: dark)';
13
+
14
+ function systemTheme(): ResolvedTheme {
15
+ if (typeof window === 'undefined') return 'light';
16
+ return window.matchMedia(QUERY).matches ? 'dark' : 'light';
17
+ }
18
+
19
+ function resolve(mode: ThemeMode): ResolvedTheme {
20
+ return mode === 'system' ? systemTheme() : mode;
21
+ }
22
+
23
+ /**
24
+ * Theme provider for every app. Wrap the app root once, above the router.
25
+ *
26
+ * The initial class is set by the inline snippet in `index.html` (see
27
+ * `themeInitScript`) so there is no flash of the wrong theme before React
28
+ * mounts; this provider then takes over and keeps the DOM in sync.
29
+ */
30
+ export function ThemeProvider({ children }: { children: ReactNode }) {
31
+ const [mode, setModeState] = useState<ThemeMode>(readStoredMode);
32
+ const [resolved, setResolved] = useState<ResolvedTheme>(() => resolve(readStoredMode()));
33
+
34
+ const setMode = useCallback((next: ThemeMode) => {
35
+ setModeState(next);
36
+ const nextResolved = resolve(next);
37
+ setResolved(nextResolved);
38
+ applyTheme(nextResolved);
39
+ try {
40
+ window.localStorage.setItem(THEME_STORAGE_KEY, next);
41
+ } catch {
42
+ // Private mode / storage disabled — the theme still applies for this session.
43
+ }
44
+ }, []);
45
+
46
+ // Follow the OS while in `system` mode.
47
+ useEffect(() => {
48
+ if (mode !== 'system') return;
49
+ const media = window.matchMedia(QUERY);
50
+ const sync = () => {
51
+ const next: ResolvedTheme = media.matches ? 'dark' : 'light';
52
+ setResolved(next);
53
+ applyTheme(next);
54
+ };
55
+
56
+ media.addEventListener('change', sync);
57
+
58
+ // The `change` event alone is not enough for an installed PWA. It can sit
59
+ // frozen for days, and the OS flipping theme while it is backgrounded — an
60
+ // automatic sunset switch, typically — delivers no event, so the app resumes
61
+ // on the stale theme until a full reload. A browser tab hides this because it
62
+ // gets navigated and discarded far more often. Re-read on resume as well;
63
+ // `pageshow` additionally covers a back/forward-cache restore.
64
+ const onVisible = () => {
65
+ if (document.visibilityState === 'visible') sync();
66
+ };
67
+ document.addEventListener('visibilitychange', onVisible);
68
+ window.addEventListener('pageshow', sync);
69
+
70
+ return () => {
71
+ media.removeEventListener('change', sync);
72
+ document.removeEventListener('visibilitychange', onVisible);
73
+ window.removeEventListener('pageshow', sync);
74
+ };
75
+ }, [mode]);
76
+
77
+ // Keep other tabs of the same app in step.
78
+ useEffect(() => {
79
+ const onStorage = (event: StorageEvent) => {
80
+ if (event.key !== THEME_STORAGE_KEY || !isThemeMode(event.newValue)) return;
81
+ setModeState(event.newValue);
82
+ const next = resolve(event.newValue);
83
+ setResolved(next);
84
+ applyTheme(next);
85
+ };
86
+ window.addEventListener('storage', onStorage);
87
+ return () => window.removeEventListener('storage', onStorage);
88
+ }, []);
89
+
90
+ const value = useMemo(
91
+ () => ({
92
+ mode,
93
+ resolved,
94
+ setMode,
95
+ toggle: () => setMode(mode === 'light' ? 'dark' : mode === 'dark' ? 'system' : 'light')
96
+ }),
97
+ [mode, resolved, setMode]
98
+ );
99
+
100
+ return <ThemeContext value={value}>{children}</ThemeContext>;
101
+ }
@@ -0,0 +1,40 @@
1
+ import { Monitor, Moon, Sun } from 'lucide-react';
2
+ import { cn } from '../lib/utils';
3
+ import { useTheme } from './use-theme';
4
+ import type { ThemeMode } from './theme-context';
5
+
6
+ const NEXT_LABEL: Record<ThemeMode, string> = {
7
+ light: 'Switch to dark theme',
8
+ dark: 'Switch to system theme',
9
+ system: 'Switch to light theme'
10
+ };
11
+
12
+ const ICONS: Record<ThemeMode, React.ReactNode> = {
13
+ light: <Sun className="size-4" aria-hidden="true" />,
14
+ dark: <Moon className="size-4" aria-hidden="true" />,
15
+ system: <Monitor className="size-4" aria-hidden="true" />
16
+ };
17
+
18
+ /**
19
+ * Single-button theme control that cycles light → dark → system. The icon shows
20
+ * the *current* mode, while the accessible name says what pressing it will do —
21
+ * an icon-only control has to announce its action, not just its state.
22
+ */
23
+ export function ThemeToggle({ className }: { className?: string }) {
24
+ const { mode, toggle } = useTheme();
25
+
26
+ return (
27
+ <button
28
+ type="button"
29
+ onClick={toggle}
30
+ title={NEXT_LABEL[mode]}
31
+ aria-label={NEXT_LABEL[mode]}
32
+ className={cn(
33
+ 'flex size-9 items-center justify-center rounded-full bg-secondary text-foreground transition-colors hover:bg-accent',
34
+ className
35
+ )}
36
+ >
37
+ {ICONS[mode]}
38
+ </button>
39
+ );
40
+ }
@@ -0,0 +1,11 @@
1
+ import { useContext } from 'react';
2
+ import { ThemeContext, type ThemeContextValue } from './theme-context';
3
+
4
+ /** Current theme + setters. Throws if used outside <ThemeProvider>. */
5
+ export function useTheme(): ThemeContextValue {
6
+ const value = useContext(ThemeContext);
7
+ if (!value) {
8
+ throw new Error('useTheme must be used inside a <ThemeProvider>.');
9
+ }
10
+ return value;
11
+ }
@@ -0,0 +1,13 @@
1
+ /// <reference types="vite/client" />
2
+ /// <reference types="vite-plugin-pwa/react" /> // @feature pwa
3
+
4
+ interface ImportMetaEnv {
5
+ VITE_APP_API_URL: string;
6
+ VITE_APP_ALLOWED_HOSTS: string;
7
+ VITE_APP_SITE_URL: string; // @feature marketing
8
+ VITE_APP_SUPPORT_EMAIL: string; // @feature marketing
9
+ }
10
+
11
+ interface ImportMeta {
12
+ readonly env: ImportMetaEnv;
13
+ }