create-fate 0.0.0 → 1.0.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 (266) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +24 -0
  3. package/bin/create-fate.mjs +412 -0
  4. package/package.json +26 -3
  5. package/templates/fate/README.md +8 -0
  6. package/templates/fate/drizzle/.github/workflows/push.yml +27 -0
  7. package/templates/fate/drizzle/.oxfmtrc.jsonc +23 -0
  8. package/templates/fate/drizzle/.vite-hooks/pre-commit +1 -0
  9. package/templates/fate/drizzle/.vscode/extensions.json +12 -0
  10. package/templates/fate/drizzle/.vscode/settings.json +25 -0
  11. package/templates/fate/drizzle/AGENTS.md +55 -0
  12. package/templates/fate/drizzle/README.md +112 -0
  13. package/templates/fate/drizzle/_gitignore +20 -0
  14. package/templates/fate/drizzle/client/package.json +58 -0
  15. package/templates/fate/drizzle/client/pages/index.tsx +96 -0
  16. package/templates/fate/drizzle/client/pages/layout.tsx +104 -0
  17. package/templates/fate/drizzle/client/pages/login.tsx +107 -0
  18. package/templates/fate/drizzle/client/pages/post/[id].tsx +22 -0
  19. package/templates/fate/drizzle/client/pages/search.tsx +95 -0
  20. package/templates/fate/drizzle/client/public/icon.svg +1 -0
  21. package/templates/fate/drizzle/client/src/App.css +153 -0
  22. package/templates/fate/drizzle/client/src/lib/AvailableLanguages.tsx +5 -0
  23. package/templates/fate/drizzle/client/src/lib/cx.tsx +6 -0
  24. package/templates/fate/drizzle/client/src/lib/env.tsx +5 -0
  25. package/templates/fate/drizzle/client/src/ui/Badge.tsx +31 -0
  26. package/templates/fate/drizzle/client/src/ui/Button.tsx +108 -0
  27. package/templates/fate/drizzle/client/src/ui/Card.tsx +32 -0
  28. package/templates/fate/drizzle/client/src/ui/CommentCard.tsx +75 -0
  29. package/templates/fate/drizzle/client/src/ui/Error.tsx +19 -0
  30. package/templates/fate/drizzle/client/src/ui/H2.tsx +15 -0
  31. package/templates/fate/drizzle/client/src/ui/H3.tsx +20 -0
  32. package/templates/fate/drizzle/client/src/ui/Header.tsx +96 -0
  33. package/templates/fate/drizzle/client/src/ui/Input.tsx +17 -0
  34. package/templates/fate/drizzle/client/src/ui/Link.tsx +11 -0
  35. package/templates/fate/drizzle/client/src/ui/LocaleSwitcher.tsx +52 -0
  36. package/templates/fate/drizzle/client/src/ui/PostCard.tsx +284 -0
  37. package/templates/fate/drizzle/client/src/ui/Section.tsx +23 -0
  38. package/templates/fate/drizzle/client/src/ui/UserCard.tsx +131 -0
  39. package/templates/fate/drizzle/client/src/user/AuthClient.tsx +6 -0
  40. package/templates/fate/drizzle/client/translations/de_DE.json +689 -0
  41. package/templates/fate/drizzle/client/translations/ja_JP.json +689 -0
  42. package/templates/fate/drizzle/client/vite.config.ts +39 -0
  43. package/templates/fate/drizzle/client/void.json +29 -0
  44. package/templates/fate/drizzle/docker-compose.yml +11 -0
  45. package/templates/fate/drizzle/package.json +46 -0
  46. package/templates/fate/drizzle/pnpm-workspace.yaml +36 -0
  47. package/templates/fate/drizzle/public/fate-logo-dark.svg +4 -0
  48. package/templates/fate/drizzle/public/fate-logo.svg +4 -0
  49. package/templates/fate/drizzle/server/.env.example +5 -0
  50. package/templates/fate/drizzle/server/drizzle.config.ts +11 -0
  51. package/templates/fate/drizzle/server/package.json +48 -0
  52. package/templates/fate/drizzle/server/src/__tests__/index.test.tsx +5 -0
  53. package/templates/fate/drizzle/server/src/app.tsx +44 -0
  54. package/templates/fate/drizzle/server/src/drizzle/db.ts +25 -0
  55. package/templates/fate/drizzle/server/src/drizzle/queries.ts +89 -0
  56. package/templates/fate/drizzle/server/src/drizzle/schema.ts +174 -0
  57. package/templates/fate/drizzle/server/src/drizzle/seed.tsx +77 -0
  58. package/templates/fate/drizzle/server/src/drizzle/seedData.ts +205 -0
  59. package/templates/fate/drizzle/server/src/index.tsx +24 -0
  60. package/templates/fate/drizzle/server/src/lib/auth.ts +33 -0
  61. package/templates/fate/drizzle/server/src/lib/env.ts +3 -0
  62. package/templates/fate/drizzle/server/src/lib/origins.ts +18 -0
  63. package/templates/fate/drizzle/server/src/router.ts +15 -0
  64. package/templates/fate/drizzle/server/src/trpc/connection.ts +4 -0
  65. package/templates/fate/drizzle/server/src/trpc/context.ts +22 -0
  66. package/templates/fate/drizzle/server/src/trpc/init.ts +51 -0
  67. package/templates/fate/drizzle/server/src/trpc/routers/comment.ts +146 -0
  68. package/templates/fate/drizzle/server/src/trpc/routers/post.ts +99 -0
  69. package/templates/fate/drizzle/server/src/trpc/routers/user.ts +68 -0
  70. package/templates/fate/drizzle/server/src/trpc/views.ts +75 -0
  71. package/templates/fate/drizzle/server/src/user/SessionUser.tsx +17 -0
  72. package/templates/fate/drizzle/server/vite.config.ts +8 -0
  73. package/templates/fate/drizzle/tsconfig.json +33 -0
  74. package/templates/fate/drizzle/vite.config.ts +71 -0
  75. package/templates/fate/http/.github/workflows/push.yml +27 -0
  76. package/templates/fate/http/.oxfmtrc.jsonc +23 -0
  77. package/templates/fate/http/.vite-hooks/pre-commit +1 -0
  78. package/templates/fate/http/.vscode/extensions.json +12 -0
  79. package/templates/fate/http/.vscode/settings.json +25 -0
  80. package/templates/fate/http/AGENTS.md +55 -0
  81. package/templates/fate/http/README.md +112 -0
  82. package/templates/fate/http/_gitignore +20 -0
  83. package/templates/fate/http/client/package.json +56 -0
  84. package/templates/fate/http/client/pages/index.tsx +96 -0
  85. package/templates/fate/http/client/pages/layout.tsx +89 -0
  86. package/templates/fate/http/client/pages/login.tsx +107 -0
  87. package/templates/fate/http/client/pages/post/[id].tsx +22 -0
  88. package/templates/fate/http/client/pages/search.tsx +95 -0
  89. package/templates/fate/http/client/public/icon.svg +1 -0
  90. package/templates/fate/http/client/src/App.css +153 -0
  91. package/templates/fate/http/client/src/lib/AvailableLanguages.tsx +5 -0
  92. package/templates/fate/http/client/src/lib/cx.tsx +6 -0
  93. package/templates/fate/http/client/src/lib/env.tsx +5 -0
  94. package/templates/fate/http/client/src/ui/Badge.tsx +31 -0
  95. package/templates/fate/http/client/src/ui/Button.tsx +108 -0
  96. package/templates/fate/http/client/src/ui/Card.tsx +32 -0
  97. package/templates/fate/http/client/src/ui/CommentCard.tsx +75 -0
  98. package/templates/fate/http/client/src/ui/Error.tsx +19 -0
  99. package/templates/fate/http/client/src/ui/H2.tsx +15 -0
  100. package/templates/fate/http/client/src/ui/H3.tsx +20 -0
  101. package/templates/fate/http/client/src/ui/Header.tsx +96 -0
  102. package/templates/fate/http/client/src/ui/Input.tsx +17 -0
  103. package/templates/fate/http/client/src/ui/Link.tsx +11 -0
  104. package/templates/fate/http/client/src/ui/LocaleSwitcher.tsx +52 -0
  105. package/templates/fate/http/client/src/ui/PostCard.tsx +281 -0
  106. package/templates/fate/http/client/src/ui/Section.tsx +23 -0
  107. package/templates/fate/http/client/src/ui/UserCard.tsx +131 -0
  108. package/templates/fate/http/client/src/user/AuthClient.tsx +6 -0
  109. package/templates/fate/http/client/translations/de_DE.json +689 -0
  110. package/templates/fate/http/client/translations/ja_JP.json +689 -0
  111. package/templates/fate/http/client/vite.config.ts +40 -0
  112. package/templates/fate/http/client/void.json +29 -0
  113. package/templates/fate/http/docker-compose.yml +11 -0
  114. package/templates/fate/http/package.json +46 -0
  115. package/templates/fate/http/pnpm-workspace.yaml +36 -0
  116. package/templates/fate/http/public/fate-logo-dark.svg +4 -0
  117. package/templates/fate/http/public/fate-logo.svg +4 -0
  118. package/templates/fate/http/server/.env.example +5 -0
  119. package/templates/fate/http/server/drizzle.config.ts +11 -0
  120. package/templates/fate/http/server/package.json +46 -0
  121. package/templates/fate/http/server/src/__tests__/index.test.tsx +5 -0
  122. package/templates/fate/http/server/src/app.tsx +36 -0
  123. package/templates/fate/http/server/src/drizzle/db.ts +25 -0
  124. package/templates/fate/http/server/src/drizzle/queries.ts +89 -0
  125. package/templates/fate/http/server/src/drizzle/schema.ts +174 -0
  126. package/templates/fate/http/server/src/drizzle/seed.tsx +77 -0
  127. package/templates/fate/http/server/src/drizzle/seedData.ts +205 -0
  128. package/templates/fate/http/server/src/fate/context.ts +20 -0
  129. package/templates/fate/http/server/src/fate/server.ts +285 -0
  130. package/templates/fate/http/server/src/fate/views.ts +75 -0
  131. package/templates/fate/http/server/src/index.tsx +24 -0
  132. package/templates/fate/http/server/src/lib/auth.ts +33 -0
  133. package/templates/fate/http/server/src/lib/env.ts +3 -0
  134. package/templates/fate/http/server/src/lib/origins.ts +18 -0
  135. package/templates/fate/http/server/src/router.ts +2 -0
  136. package/templates/fate/http/server/src/user/SessionUser.tsx +17 -0
  137. package/templates/fate/http/server/vite.config.ts +8 -0
  138. package/templates/fate/http/tsconfig.json +33 -0
  139. package/templates/fate/http/vite.config.ts +71 -0
  140. package/templates/fate/prisma/.github/workflows/push.yml +27 -0
  141. package/templates/fate/prisma/.oxfmtrc.jsonc +23 -0
  142. package/templates/fate/prisma/.vite-hooks/pre-commit +1 -0
  143. package/templates/fate/prisma/.vscode/extensions.json +12 -0
  144. package/templates/fate/prisma/.vscode/settings.json +25 -0
  145. package/templates/fate/prisma/AGENTS.md +55 -0
  146. package/templates/fate/prisma/README.md +126 -0
  147. package/templates/fate/prisma/_gitignore +20 -0
  148. package/templates/fate/prisma/client/package.json +58 -0
  149. package/templates/fate/prisma/client/pages/index.tsx +96 -0
  150. package/templates/fate/prisma/client/pages/layout.tsx +94 -0
  151. package/templates/fate/prisma/client/pages/login.tsx +107 -0
  152. package/templates/fate/prisma/client/pages/post/[id].tsx +22 -0
  153. package/templates/fate/prisma/client/pages/search.tsx +95 -0
  154. package/templates/fate/prisma/client/public/icon.svg +1 -0
  155. package/templates/fate/prisma/client/src/App.css +153 -0
  156. package/templates/fate/prisma/client/src/lib/AvailableLanguages.tsx +5 -0
  157. package/templates/fate/prisma/client/src/lib/cx.tsx +6 -0
  158. package/templates/fate/prisma/client/src/lib/env.tsx +5 -0
  159. package/templates/fate/prisma/client/src/ui/Badge.tsx +31 -0
  160. package/templates/fate/prisma/client/src/ui/Button.tsx +108 -0
  161. package/templates/fate/prisma/client/src/ui/Card.tsx +32 -0
  162. package/templates/fate/prisma/client/src/ui/CommentCard.tsx +75 -0
  163. package/templates/fate/prisma/client/src/ui/Error.tsx +19 -0
  164. package/templates/fate/prisma/client/src/ui/H2.tsx +15 -0
  165. package/templates/fate/prisma/client/src/ui/H3.tsx +20 -0
  166. package/templates/fate/prisma/client/src/ui/Header.tsx +96 -0
  167. package/templates/fate/prisma/client/src/ui/Input.tsx +17 -0
  168. package/templates/fate/prisma/client/src/ui/Link.tsx +11 -0
  169. package/templates/fate/prisma/client/src/ui/LocaleSwitcher.tsx +52 -0
  170. package/templates/fate/prisma/client/src/ui/PostCard.tsx +281 -0
  171. package/templates/fate/prisma/client/src/ui/Section.tsx +23 -0
  172. package/templates/fate/prisma/client/src/ui/UserCard.tsx +131 -0
  173. package/templates/fate/prisma/client/src/user/AuthClient.tsx +6 -0
  174. package/templates/fate/prisma/client/translations/de_DE.json +689 -0
  175. package/templates/fate/prisma/client/translations/ja_JP.json +689 -0
  176. package/templates/fate/prisma/client/vite.config.ts +39 -0
  177. package/templates/fate/prisma/client/void.json +29 -0
  178. package/templates/fate/prisma/docker-compose.yml +11 -0
  179. package/templates/fate/prisma/package.json +47 -0
  180. package/templates/fate/prisma/pnpm-workspace.yaml +39 -0
  181. package/templates/fate/prisma/public/fate-logo-dark.svg +4 -0
  182. package/templates/fate/prisma/public/fate-logo.svg +4 -0
  183. package/templates/fate/prisma/server/.env.example +5 -0
  184. package/templates/fate/prisma/server/package.json +42 -0
  185. package/templates/fate/prisma/server/prisma.config.ts +21 -0
  186. package/templates/fate/prisma/server/src/__tests__/index.test.tsx +5 -0
  187. package/templates/fate/prisma/server/src/app.tsx +40 -0
  188. package/templates/fate/prisma/server/src/index.tsx +24 -0
  189. package/templates/fate/prisma/server/src/lib/auth.ts +31 -0
  190. package/templates/fate/prisma/server/src/lib/env.ts +3 -0
  191. package/templates/fate/prisma/server/src/lib/origins.ts +18 -0
  192. package/templates/fate/prisma/server/src/prisma/prisma.ts +9 -0
  193. package/templates/fate/prisma/server/src/prisma/schema.prisma +98 -0
  194. package/templates/fate/prisma/server/src/prisma/seed.tsx +71 -0
  195. package/templates/fate/prisma/server/src/prisma/seedData.ts +205 -0
  196. package/templates/fate/prisma/server/src/router.ts +14 -0
  197. package/templates/fate/prisma/server/src/trpc/connection.ts +4 -0
  198. package/templates/fate/prisma/server/src/trpc/context.ts +22 -0
  199. package/templates/fate/prisma/server/src/trpc/init.ts +8 -0
  200. package/templates/fate/prisma/server/src/trpc/routers/comment.ts +176 -0
  201. package/templates/fate/prisma/server/src/trpc/routers/post.ts +154 -0
  202. package/templates/fate/prisma/server/src/trpc/routers/user.ts +71 -0
  203. package/templates/fate/prisma/server/src/trpc/views.ts +77 -0
  204. package/templates/fate/prisma/server/src/user/SessionUser.tsx +17 -0
  205. package/templates/fate/prisma/server/vite.config.ts +8 -0
  206. package/templates/fate/prisma/tsconfig.json +33 -0
  207. package/templates/fate/prisma/vite.config.ts +72 -0
  208. package/templates/fate/void/.oxfmtrc.jsonc +14 -0
  209. package/templates/fate/void/README.md +55 -0
  210. package/templates/fate/void/auth.ts +22 -0
  211. package/templates/fate/void/db/migrations/20260508073357_initial_schema.sql +133 -0
  212. package/templates/fate/void/db/migrations/20260508120500_seed_void_demo.sql +170 -0
  213. package/templates/fate/void/db/migrations/meta/20260508073357_snapshot.json +868 -0
  214. package/templates/fate/void/db/migrations/meta/_journal.json +20 -0
  215. package/templates/fate/void/db/queries.ts +211 -0
  216. package/templates/fate/void/db/schema.ts +325 -0
  217. package/templates/fate/void/db/seed-auth.ts +31 -0
  218. package/templates/fate/void/db/seed.ts +163 -0
  219. package/templates/fate/void/middleware/01.shared.ts +29 -0
  220. package/templates/fate/void/package.json +65 -0
  221. package/templates/fate/void/pages/category/[id].tsx +23 -0
  222. package/templates/fate/void/pages/index.tsx +153 -0
  223. package/templates/fate/void/pages/layout.tsx +46 -0
  224. package/templates/fate/void/pages/login.tsx +103 -0
  225. package/templates/fate/void/pages/post/[id].tsx +20 -0
  226. package/templates/fate/void/pages/search.tsx +81 -0
  227. package/templates/fate/void/pnpm-workspace.yaml +31 -0
  228. package/templates/fate/void/public/.assetsignore +1 -0
  229. package/templates/fate/void/public/icon.svg +1 -0
  230. package/templates/fate/void/routes/fate-live.ts +8 -0
  231. package/templates/fate/void/routes/fate.ts +8 -0
  232. package/templates/fate/void/seedData.ts +535 -0
  233. package/templates/fate/void/src/App.css +130 -0
  234. package/templates/fate/void/src/fate/__tests__/commentPermissions.test.ts +31 -0
  235. package/templates/fate/void/src/fate/commentPermissions.ts +17 -0
  236. package/templates/fate/void/src/fate/context.ts +15 -0
  237. package/templates/fate/void/src/fate/live.ts +6 -0
  238. package/templates/fate/void/src/fate/server.ts +401 -0
  239. package/templates/fate/void/src/fate/views.ts +164 -0
  240. package/templates/fate/void/src/lib/cx.tsx +6 -0
  241. package/templates/fate/void/src/lib/formatLabel.tsx +6 -0
  242. package/templates/fate/void/src/lib/shared.ts +10 -0
  243. package/templates/fate/void/src/ui/Badge.tsx +31 -0
  244. package/templates/fate/void/src/ui/Button.tsx +92 -0
  245. package/templates/fate/void/src/ui/Card.tsx +29 -0
  246. package/templates/fate/void/src/ui/CategoryCard.tsx +105 -0
  247. package/templates/fate/void/src/ui/CommentCard.tsx +80 -0
  248. package/templates/fate/void/src/ui/CreatePost.tsx +111 -0
  249. package/templates/fate/void/src/ui/Error.tsx +19 -0
  250. package/templates/fate/void/src/ui/EventCard.tsx +152 -0
  251. package/templates/fate/void/src/ui/H2.tsx +15 -0
  252. package/templates/fate/void/src/ui/H3.tsx +20 -0
  253. package/templates/fate/void/src/ui/Header.tsx +96 -0
  254. package/templates/fate/void/src/ui/Input.tsx +33 -0
  255. package/templates/fate/void/src/ui/Link.tsx +11 -0
  256. package/templates/fate/void/src/ui/Number.tsx +5 -0
  257. package/templates/fate/void/src/ui/PostCard.tsx +294 -0
  258. package/templates/fate/void/src/ui/Section.tsx +23 -0
  259. package/templates/fate/void/src/ui/TagBadge.tsx +23 -0
  260. package/templates/fate/void/src/ui/UserCard.tsx +129 -0
  261. package/templates/fate/void/src/user/AuthClient.tsx +1 -0
  262. package/templates/fate/void/src/user/SessionUser.tsx +17 -0
  263. package/templates/fate/void/tsconfig.json +34 -0
  264. package/templates/fate/void/vite.config.ts +83 -0
  265. package/templates/fate/void/void.json +32 -0
  266. package/templates/fate/void/wrangler.jsonc +16 -0
@@ -0,0 +1,153 @@
1
+ @import 'tailwindcss';
2
+
3
+ body {
4
+ font-family:
5
+ system-ui,
6
+ -apple-system,
7
+ BlinkMacSystemFont,
8
+ 'Segoe UI',
9
+ 'Open Sans',
10
+ 'Helvetica Neue',
11
+ sans-serif;
12
+
13
+ background-color: hsl(var(--background));
14
+ color: hsl(var(--foreground));
15
+ }
16
+
17
+ @theme {
18
+ --color-accent: hsl(var(--accent));
19
+ --color-accent-foreground: hsl(var(--accent-foreground));
20
+
21
+ --color-background: hsl(var(--background));
22
+ --color-foreground: hsl(var(--foreground));
23
+ --color-border: hsl(var(--border));
24
+ --color-ring: hsl(var(--ring));
25
+
26
+ --color-card: hsl(var(--card));
27
+ --color-card-foreground: hsl(var(--card-foreground));
28
+
29
+ --color-muted: hsl(var(--muted));
30
+ --color-muted-foreground: hsl(var(--muted-foreground));
31
+
32
+ --color-secondary: hsl(var(--secondary));
33
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
34
+
35
+ --color-destructive: hsl(var(--destructive));
36
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
37
+ }
38
+
39
+ @layer utilities {
40
+ .text-balance {
41
+ text-wrap: balance;
42
+ }
43
+ }
44
+
45
+ @layer base {
46
+ :root {
47
+ --background: 0 0% 100%;
48
+ --foreground: 0 0% 3.9%;
49
+ --card: 0 0% 100%;
50
+ --card-foreground: 0 0% 3.9%;
51
+ --popover: 0 0% 100%;
52
+ --popover-foreground: 0 0% 3.9%;
53
+ --primary: 0 0% 9%;
54
+ --primary-foreground: 0 0% 98%;
55
+ --secondary: 0 0% 96.1%;
56
+ --secondary-foreground: 0 0% 9%;
57
+ --muted: 0 0% 96.1%;
58
+ --muted-foreground: 0 0 40%;
59
+ --accent: 0 0% 96.1%;
60
+ --accent-foreground: 0 0% 9%;
61
+ --destructive: 0 84.2% 60.2%;
62
+ --destructive-foreground: 0 0% 98%;
63
+ --border: 0 0% 89.8%;
64
+ --input: 0 0% 89.8%;
65
+ --ring: 0 0% 3.9%;
66
+ --chart-1: 12 76% 61%;
67
+ --chart-2: 173 58% 39%;
68
+ --chart-3: 197 37% 24%;
69
+ --chart-4: 43 74% 66%;
70
+ --chart-5: 27 87% 67%;
71
+ --radius: 0.5rem;
72
+ --sidebar-background: 0 0% 98%;
73
+ --sidebar-foreground: 240 5.3% 26.1%;
74
+ --sidebar-primary: 240 5.9% 10%;
75
+ --sidebar-primary-foreground: 0 0% 98%;
76
+ --sidebar-accent: 240 4.8% 95.9%;
77
+ --sidebar-accent-foreground: 240 5.9% 10%;
78
+ --sidebar-border: 220 13% 91%;
79
+ --sidebar-ring: 217.2 91.2% 59.8%;
80
+ }
81
+ @media (prefers-color-scheme: dark) {
82
+ :root {
83
+ --background: 0 0% 3.9%;
84
+ --foreground: 0 0% 98%;
85
+ --card: 0 0% 3.9%;
86
+ --card-foreground: 0 0% 98%;
87
+ --popover: 0 0% 3.9%;
88
+ --popover-foreground: 0 0% 98%;
89
+ --primary: 0 0% 98%;
90
+ --primary-foreground: 0 0% 9%;
91
+ --secondary: 0 0% 14.9%;
92
+ --secondary-foreground: 0 0% 98%;
93
+ --muted: 0 0% 14.9%;
94
+ --muted-foreground: 0 0% 63.9%;
95
+ --accent: 0 0% 14.9%;
96
+ --accent-foreground: 0 0% 98%;
97
+ --destructive: 0 62.8% 30.6%;
98
+ --destructive-foreground: 0 0% 98%;
99
+ --border: 0 0% 14.9%;
100
+ --input: 0 0% 14.9%;
101
+ --ring: 0 0% 83.1%;
102
+ --chart-1: 220 70% 50%;
103
+ --chart-2: 160 60% 45%;
104
+ --chart-3: 30 80% 55%;
105
+ --chart-4: 280 65% 60%;
106
+ --chart-5: 340 75% 55%;
107
+ --sidebar-background: 240 5.9% 10%;
108
+ --sidebar-foreground: 240 4.8% 95.9%;
109
+ --sidebar-primary: 224.3 76.3% 48%;
110
+ --sidebar-primary-foreground: 0 0% 100%;
111
+ --sidebar-accent: 240 3.7% 15.9%;
112
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
113
+ --sidebar-border: 240 3.7% 15.9%;
114
+ --sidebar-ring: 217.2 91.2% 59.8%;
115
+ }
116
+ }
117
+
118
+ * {
119
+ @apply border-border;
120
+ }
121
+ }
122
+
123
+ textarea {
124
+ resize: none;
125
+ }
126
+
127
+ .squircle {
128
+ border-radius: 32px;
129
+ corner-shape: squircle;
130
+ }
131
+
132
+ ::backdrop {
133
+ backdrop-filter: blur(4px);
134
+ }
135
+
136
+ [popover] {
137
+ transition:
138
+ opacity 150ms ease-in-out,
139
+ display 150ms ease-out allow-discrete,
140
+ overlay 150ms ease-out allow-discrete;
141
+ opacity: 0;
142
+ }
143
+
144
+ [popover]:popover-open {
145
+ opacity: 1;
146
+ display: block;
147
+ }
148
+
149
+ @starting-style {
150
+ [popover]:popover-open {
151
+ opacity: 0;
152
+ }
153
+ }
@@ -0,0 +1,5 @@
1
+ export default new Map([
2
+ ['en_US', 'English'],
3
+ ['de_DE', 'Deutsch (German)'],
4
+ ['ja_JP', '日本語 (Japanese)'],
5
+ ]);
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export default function cx(...inputs: Array<ClassValue>) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,5 @@
1
+ import defineEnv from '@nkzw/define-env';
2
+
3
+ export default defineEnv(['SERVER_URL'], {
4
+ SERVER_URL: import.meta.env.VITE_SERVER_URL,
5
+ });
@@ -0,0 +1,31 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority';
2
+ import { HTMLAttributes } from 'react';
3
+ import cx from '../lib/cx.tsx';
4
+
5
+ const badgeVariants = cva(
6
+ 'inline-flex items-center squircle border px-1.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
7
+ {
8
+ defaultVariants: {
9
+ variant: 'default',
10
+ },
11
+ variants: {
12
+ variant: {
13
+ default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
14
+ destructive:
15
+ 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
16
+ outline: 'text-foreground',
17
+ secondary:
18
+ 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
19
+ },
20
+ },
21
+ },
22
+ );
23
+
24
+ export interface BadgeProps
25
+ extends HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
26
+
27
+ function Badge({ className, variant, ...props }: BadgeProps) {
28
+ return <div className={cx(badgeVariants({ variant }), className)} {...props} />;
29
+ }
30
+
31
+ export { Badge, badgeVariants };
@@ -0,0 +1,108 @@
1
+ import { Slot } from '@radix-ui/react-slot';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { type ButtonHTMLAttributes, type ReactNode, useOptimistic, useTransition } from 'react';
4
+ import { useFormStatus } from 'react-dom';
5
+ import cx from '../lib/cx.tsx';
6
+
7
+ const buttonVariants = cva(
8
+ 'inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap squircle text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
9
+ {
10
+ defaultVariants: {
11
+ size: 'default',
12
+ variant: 'default',
13
+ },
14
+ variants: {
15
+ size: {
16
+ default: 'h-10 px-3 py-2 active:pt-[11px] active:pb-[9px]',
17
+ icon: 'h-10 w-10',
18
+ lg: 'h-11 squircle px-6 active:pt-[11px] active:pb-[9px]',
19
+ sm: 'h-9 squircle px-2 active:pt-[11px] active:pb-[9px]',
20
+ },
21
+ variant: {
22
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
23
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
24
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
25
+ link: 'text-primary underline-offset-4 hover:underline',
26
+ outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
27
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
28
+ },
29
+ },
30
+ },
31
+ );
32
+
33
+ const Button = ({
34
+ action,
35
+ asChild = false,
36
+ className,
37
+ disabled,
38
+ onClick: initialOnClick,
39
+ size,
40
+ variant,
41
+ ...props
42
+ }: ButtonHTMLAttributes<HTMLButtonElement> &
43
+ VariantProps<typeof buttonVariants> & {
44
+ action?: () => void;
45
+ asChild?: boolean;
46
+ }) => {
47
+ const Component = asChild ? Slot : 'button';
48
+
49
+ const [isPending, startTransition] = useTransition();
50
+
51
+ const onClick = initialOnClick || (action ? () => startTransition(action) : undefined);
52
+
53
+ return (
54
+ <Component
55
+ className={cx(buttonVariants({ className, size, variant }))}
56
+ disabled={disabled !== undefined ? disabled : isPending}
57
+ onClick={onClick}
58
+ {...props}
59
+ />
60
+ );
61
+ };
62
+
63
+ const AsyncButton = ({
64
+ action,
65
+ asChild = false,
66
+ children,
67
+ className,
68
+ disabled,
69
+ pendingPlaceholder = '...',
70
+ size,
71
+ variant,
72
+ ...props
73
+ }: ButtonHTMLAttributes<HTMLButtonElement> &
74
+ VariantProps<typeof buttonVariants> & {
75
+ action?: () => Promise<unknown> | void;
76
+ asChild?: boolean;
77
+ pendingPlaceholder?: ReactNode;
78
+ }) => {
79
+ const Component = asChild ? Slot : 'button';
80
+
81
+ const [optimisticIsPending, setOptimisticIsPending] = useOptimistic(false);
82
+ const [transitionIsPending, startTransition] = useTransition();
83
+ const { pending: formIsPending } = useFormStatus();
84
+
85
+ const isPending = transitionIsPending || optimisticIsPending || formIsPending;
86
+
87
+ const onClick = action
88
+ ? () => {
89
+ startTransition(async () => {
90
+ setOptimisticIsPending(true);
91
+ await action();
92
+ });
93
+ }
94
+ : undefined;
95
+
96
+ return (
97
+ <Component
98
+ className={cx(buttonVariants({ className, size, variant }))}
99
+ disabled={disabled || isPending || undefined}
100
+ onClick={onClick}
101
+ {...props}
102
+ >
103
+ {isPending ? pendingPlaceholder : children}
104
+ </Component>
105
+ );
106
+ };
107
+
108
+ export { AsyncButton, Button, buttonVariants };
@@ -0,0 +1,32 @@
1
+ import { VStack } from '@nkzw/stack';
2
+ import { HTMLAttributes } from 'react';
3
+ import cx from '../lib/cx.tsx';
4
+
5
+ export default function Card({
6
+ children,
7
+ className,
8
+ ...props
9
+ }: {
10
+ children: React.ReactNode;
11
+ className?: string;
12
+ } & HTMLAttributes<HTMLDivElement>) {
13
+ return (
14
+ <div
15
+ className={cx(
16
+ 'relative overflow-hidden squircle border border-gray-200/70 bg-white/85 shadow-[0_16px_70px_-32px_rgba(15,23,42,0.45)] dark:border-neutral-800 dark:bg-neutral-900/80 transition duration-200 ease-out hover:shadow-[0_24px_80px_-38px_rgba(15,23,42,0.55)]',
17
+ className,
18
+ )}
19
+ {...props}
20
+ >
21
+ <div className="pointer-events-none absolute inset-0 bg-linear-to-br from-blue-500/10 via-transparent to-indigo-500/15 opacity-30 transition duration-200" />
22
+ <VStack
23
+ className="relative z-10 flex h-full w-full"
24
+ gap={16}
25
+ horizontalPadding={20}
26
+ verticalPadding={16}
27
+ >
28
+ {children}
29
+ </VStack>
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,75 @@
1
+ import type { Comment } from '@app/server/src/trpc/views.ts';
2
+ import Stack from '@nkzw/stack';
3
+ import { X } from 'lucide-react';
4
+ import { useView, view, ViewRef } from 'react-fate';
5
+ import { useFateClient } from 'react-fate';
6
+ import { Button } from './Button.tsx';
7
+ import Link from './Link.tsx';
8
+
9
+ export const CommentView = view<Comment>()({
10
+ author: {
11
+ id: true,
12
+ name: true,
13
+ username: true,
14
+ },
15
+ content: true,
16
+ id: true,
17
+ });
18
+
19
+ export const CommentViewWithPostCount = view<Comment>()({
20
+ ...CommentView,
21
+ post: { commentCount: true },
22
+ });
23
+
24
+ export default function CommentCard({
25
+ comment: commentRef,
26
+ link,
27
+ post,
28
+ }: {
29
+ comment: ViewRef<'Comment'>;
30
+ link?: boolean;
31
+ post: { commentCount: number; id: string; title: string };
32
+ }) {
33
+ const fate = useFateClient();
34
+ const comment = useView(CommentView, commentRef);
35
+ const { author } = comment;
36
+
37
+ return (
38
+ <div
39
+ className="group squircle border border-gray-200/80 bg-gray-100/50 p-3 text-sm dark:border-neutral-800 dark:bg-neutral-900/40"
40
+ key={comment.id}
41
+ >
42
+ <Stack between gap={16}>
43
+ <p className="font-medium text-gray-900 dark:text-gray-200">
44
+ {author?.name ?? 'Anonymous'}
45
+ </p>
46
+ <Button
47
+ className="opacity-0 transition-opacity group-hover:opacity-100 focus:opacity-100"
48
+ onClick={async () => {
49
+ await fate.mutations.comment.delete({
50
+ delete: true,
51
+ input: { id: comment.id },
52
+ optimistic: {
53
+ post: { commentCount: post.commentCount - 1, id: post.id },
54
+ },
55
+ view: view<Comment>()({
56
+ id: true,
57
+ post: { commentCount: true },
58
+ }),
59
+ });
60
+ }}
61
+ size="sm"
62
+ variant="ghost"
63
+ >
64
+ <X size={14} />
65
+ </Button>
66
+ </Stack>
67
+ <p className="text-foreground/80">{comment.content}</p>
68
+ {link && (
69
+ <Link className="text-blue-500 underline hover:no-underline" to={`/post/${post.id}`}>
70
+ {post.title}
71
+ </Link>
72
+ )}
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,19 @@
1
+ import { VStack } from '@nkzw/stack';
2
+
3
+ export default function Error({ error: initialError }: { error: unknown }) {
4
+ const error = initialError instanceof Error ? (initialError as Error) : null;
5
+ return (
6
+ <VStack gap={12}>
7
+ <h3 className="text-xl font-semibold text-red-700">Error</h3>
8
+ <code>{error ? error.stack || `fate Error: ${error.message}` : String(initialError)}</code>
9
+ <div>
10
+ <a
11
+ className="cursor-pointer underline decoration-gray-300 hover:no-underline"
12
+ onClick={() => window.location.reload()}
13
+ >
14
+ <fbt desc="Try again button">Try again</fbt>
15
+ </a>
16
+ </div>
17
+ </VStack>
18
+ );
19
+ }
@@ -0,0 +1,15 @@
1
+ import cx from '../lib/cx.tsx';
2
+
3
+ export default function H2({
4
+ children,
5
+ className,
6
+ }: {
7
+ children: React.ReactNode;
8
+ className?: string;
9
+ }) {
10
+ return (
11
+ <h2 className={cx('text-2xl font-semibold text-gray-900 dark:text-gray-50', className)}>
12
+ {children}
13
+ </h2>
14
+ );
15
+ }
@@ -0,0 +1,20 @@
1
+ import cx from '../lib/cx.tsx';
2
+
3
+ export default function H2({
4
+ children,
5
+ className,
6
+ }: {
7
+ children: React.ReactNode;
8
+ className?: string;
9
+ }) {
10
+ return (
11
+ <h3
12
+ className={cx(
13
+ 'bg-linear-to-r from-slate-900 to-slate-600 bg-clip-text text-lg font-semibold text-transparent transition dark:from-white dark:to-slate-200 uppercase tracking-widest duration-150 opacity-100 hover:opacity-70 active:translate-y-[1.5px]',
14
+ className,
15
+ )}
16
+ >
17
+ {children}
18
+ </h3>
19
+ );
20
+ }
@@ -0,0 +1,96 @@
1
+ import Stack from '@nkzw/stack';
2
+ import { useRouter } from '@void/react';
3
+ import { ChevronLeft, LogIn, LogOut, Search } from 'lucide-react';
4
+ import AuthClient from '../user/AuthClient.tsx';
5
+ import { Button } from './Button.tsx';
6
+ import Link from './Link.tsx';
7
+ import LocaleSwitcher from './LocaleSwitcher.tsx';
8
+
9
+ export default function Header() {
10
+ const router = useRouter();
11
+ const pathname = router.path;
12
+ const { data: session, isPending } = AuthClient.useSession();
13
+
14
+ return (
15
+ <header className="sticky top-0 z-50 border-b border-white/60 bg-white/60 backdrop-blur dark:border-neutral-800 dark:bg-neutral-950/70">
16
+ <Stack
17
+ alignCenter
18
+ between
19
+ className="max-w-8xl relative container mx-auto h-16 px-6 lg:px-8"
20
+ gap={16}
21
+ >
22
+ <Link
23
+ href="/"
24
+ onClick={() =>
25
+ pathname !== '/' && history.state?.idx > 0 ? history.back() : router.visit('/')
26
+ }
27
+ >
28
+ <Stack alignCenter gap={12}>
29
+ <div className="squircle relative flex h-10 w-10 items-center justify-center bg-linear-to-br from-gray-100 to-gray-200 text-white shadow-lg shadow-gray-500/20 transition duration-250 hover:scale-110 active:scale-90">
30
+ {pathname === '/' ? (
31
+ <svg
32
+ className="h-6 w-6 text-gray-500"
33
+ preserveAspectRatio="xMidYMax"
34
+ viewBox="0 0 160 160"
35
+ xmlns="http://www.w3.org/2000/svg"
36
+ >
37
+ <path
38
+ d="M108.301 17.465h.272c1.797-.005 3.563.177 5.331.496l.349.063c5.588 1.02 10.731 3.447 15.201 6.933l.347.267c1.579 1.26 3.083 2.684 4.386 4.227l.161.186c4.018 4.719 6.924 10.686 7.767 16.852l.033.239c.262 1.848.351 3.688.352 5.551l.001.307c-.001 1.623-.092 3.188-.386 4.788l-.05.292c-1.003 5.874-3.476 11.447-7.286 16.034l-.29.358c-1.422 1.721-3.011 3.382-4.797 4.729l-.444.354c-3.046 2.405-6.436 4.147-10.084 5.442l-.497.186c-1.766.627-3.615.974-5.465 1.266l-.364.058c-1.591.238-3.185.286-4.791.281h-.499l-1.339-.002h-1.408c-.886-.001-1.772-.001-2.656-.003-3.085-.001-6.163-.005-9.247-.008v-12.78l3.606-.006 2.282-.007c1.205-.006 2.407-.01 3.611-.011.878 0 1.754-.003 2.63-.007.462-.002.928-.005 1.389-.002 6.302.01 12.003-1.72 16.65-6.117l.291-.272c3.951-3.807 6.097-9.302 6.241-14.736.04-3.078-.476-6.048-1.68-8.892l-.104-.244c-2.382-5.559-6.98-9.439-12.504-11.675-2.204-.816-4.522-1.16-6.863-1.134-.301.004-.606.004-.909.001-1.677.003-3.304.127-4.924.591l-.331.098c-2.625.758-5.093 1.928-7.254 3.607l-.323.249c-1.046.817-2.031 1.682-2.873 2.71l-.154.179c-2.354 2.782-3.9 5.988-4.727 9.529l-.054.229c-.391 1.8-.423 3.633-.42 5.471l-.001.74c.002 1.4.001 2.801-.004 4.201l-.004 3.779c0 1.824-.001 3.649-.003 5.473l-.006 2.813-.005 6.076-.001.267-.008 8.614c-.001 2.023-.004 4.045-.006 6.066v.259l-.004 2.55-.006 5.46c0 1.331-.001 2.658-.003 3.985-.002.72-.002 1.438-.002 2.156 0 .658 0 1.317-.002 1.978v.708c.004 2.098-.07 4.214-.545 6.263l-.077.359-.161.716c-.071.302-.136.605-.199.909-.55 2.538-1.6 5.067-2.832 7.348l-.214.42c-.965 1.921-2.22 3.714-3.574 5.38l-.152.183c-.78.973-1.639 1.866-2.519 2.751l-.308.313a25.81 25.81 0 0 1-2.226 1.955l-.22.179c-3.698 2.953-7.975 5.044-12.538 6.262-.212.056-.422.115-.631.176-8.366 2.287-17.8.738-25.278-3.512-3.639-2.122-7.171-4.945-9.721-8.307-.121-.15-.241-.298-.356-.448a36.247 36.247 0 0 1-2.605-3.814l-.144-.234c-.889-1.466-1.562-3.034-2.222-4.617l-.11-.265c-3.253-7.791-2.911-17.367.184-25.133.865-2.069 1.869-4.089 3.123-5.951l.172-.261a31.91 31.91 0 0 1 2.547-3.29l.229-.263c.13-.152.266-.304.396-.452l.425-.493c1.216-1.389 2.593-2.615 4.068-3.723.208-.157.414-.315.62-.476 1.184-.901 2.437-1.665 3.729-2.4l.272-.159c1.586-.893 3.235-1.573 4.934-2.21l.253-.097c4.232-1.567 8.615-1.836 13.079-1.82 1.359 0 2.732.002 4.093.007 1.209.005 2.417.006 3.621.006 1.899.002 3.794.007 5.688.012v12.78l-3.612.013-2.282.011c-1.201.005-2.401.012-3.61.016-.876.003-1.751.005-2.629.013-.463.002-.924.005-1.387.006-4.322.005-8.193.592-12.033 2.662l-.217.114c-1.458.773-2.697 1.731-3.908 2.849-.206.185-.413.362-.63.535-.435.376-.794.788-1.16 1.232l-.153.18a21.18 21.18 0 0 0-4.474 8.651l-.094.342c-.42 1.682-.54 3.395-.536 5.127v.272c.019 5.672 2.392 10.671 6.206 14.769l.215.237c3.296 3.453 8.483 5.917 13.247 6.046.448.008.896.01 1.345.01.276 0 .553.003.835.005 4.917.019 9.864-1.73 13.577-4.995l.303-.267c.299-.263.592-.527.881-.799l.262-.247c.787-.756 1.451-1.599 2.104-2.474l.151-.201c1.929-2.606 3.117-5.723 3.635-8.909l.059-.362c.194-1.296.21-2.573.207-3.883l.003-.74v-2.02c0-.73 0-1.459.005-2.188v-3.782c0-1.827.004-3.654.007-5.481 0-.939 0-1.877.004-2.816l.003-6.082v-.268c.005-2.876.008-5.75.008-8.627l.013-8.883c.004-1.823.004-3.646.004-5.469 0-1.33.004-2.659.004-3.989.003-.721.003-1.442.003-2.159v-1.98c.005-.237.005-.473 0-.711-.011-8.523 3.072-17.023 8.941-23.268.244-.263.483-.531.717-.805.416-.48.861-.918 1.324-1.352l.252-.232a26.259 26.259 0 0 1 2.184-1.812l.46-.349c1.39-1.058 2.837-1.951 4.375-2.775l.318-.172c5.005-2.666 10.5-3.877 16.15-3.889Z"
39
+ fill="currentColor"
40
+ style={{ transformBox: 'fill-box', transformOrigin: '50% 50%' }}
41
+ transform="rotate(-31 0 0)"
42
+ />
43
+ </svg>
44
+ ) : (
45
+ <ChevronLeft className="h-6 w-6 text-gray-500" />
46
+ )}
47
+ </div>
48
+ <div className="opacity-100 transition-opacity duration-250 hover:opacity-70">
49
+ <span className="bg-linear-to-r from-gray-500 to-gray-900 bg-clip-text text-xl font-semibold text-transparent dark:from-gray-200 dark:to-white">
50
+ <span className="italic">fate</span>
51
+ </span>
52
+ <p className="hidden text-xs text-muted-foreground sm:block">
53
+ <fbt desc="fate library tagline">A modern data client for React.</fbt>
54
+ </p>
55
+ </div>
56
+ </Stack>
57
+ </Link>
58
+ <Stack alignCenter>
59
+ <LocaleSwitcher />
60
+ <Button asChild size="sm" variant="ghost">
61
+ <Link to="/search">
62
+ <Search className="h-4 w-4" />
63
+ <span className="hidden sm:inline">
64
+ <fbt desc="Search button">Search</fbt>
65
+ </span>
66
+ </Link>
67
+ </Button>
68
+ {session ? (
69
+ <>
70
+ <Button asChild size="sm" variant="ghost">
71
+ <Stack
72
+ alignCenter
73
+ as="a"
74
+ className="text-sm font-medium text-gray-700 transition hover:text-gray-900 dark:text-gray-200 dark:hover:text-white"
75
+ gap={4}
76
+ onClick={() => AuthClient.signOut()}
77
+ >
78
+ <LogOut className="h-4 w-4" />
79
+ <span className="hidden sm:inline">
80
+ <fbt desc="Logout button">Logout</fbt>
81
+ </span>
82
+ </Stack>
83
+ </Button>
84
+ </>
85
+ ) : !isPending ? (
86
+ <Button asChild size="sm" variant="ghost">
87
+ <Link to="/login">
88
+ <LogIn className="h-4 w-4" /> <fbt desc="Login button">Login</fbt>
89
+ </Link>
90
+ </Button>
91
+ ) : null}
92
+ </Stack>
93
+ </Stack>
94
+ </header>
95
+ );
96
+ }
@@ -0,0 +1,17 @@
1
+ import { InputHTMLAttributes, Ref } from 'react';
2
+ import cx from '../lib/cx.tsx';
3
+
4
+ export default function Input({
5
+ className,
6
+ ...props
7
+ }: InputHTMLAttributes<HTMLInputElement> & { ref?: Ref<HTMLInputElement> }) {
8
+ return (
9
+ <input
10
+ className={cx(
11
+ 'border-input bg-background text-foreground focus-visible:ring-ring focus-visible:ring-offset-background flex w-32 squircle border px-3 py-2 text-sm shadow-sm transition-colors focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:bg-neutral-900/40',
12
+ className,
13
+ )}
14
+ {...props}
15
+ />
16
+ );
17
+ }
@@ -0,0 +1,11 @@
1
+ import { Link as VoidLink } from '@void/react';
2
+ import type { ComponentProps } from 'react';
3
+
4
+ export type LinkProps = Omit<ComponentProps<typeof VoidLink>, 'href'> & {
5
+ href?: string;
6
+ to?: string;
7
+ };
8
+
9
+ export default function Link({ href, to, ...props }: LinkProps) {
10
+ return <VoidLink href={href ?? to ?? '/'} {...props} />;
11
+ }
@@ -0,0 +1,52 @@
1
+ import { VStack } from '@nkzw/stack';
2
+ import { cx } from 'class-variance-authority';
3
+ import { useLocaleContext } from 'fbtee';
4
+ import { Languages } from 'lucide-react';
5
+ import AvailableLanguages from '../lib/AvailableLanguages.tsx';
6
+ import { Button } from './Button.tsx';
7
+ import Card from './Card.tsx';
8
+
9
+ export default function LocaleSwitcher() {
10
+ const { locale: currentLocale, setLocale } = useLocaleContext();
11
+
12
+ return (
13
+ <>
14
+ <Button asChild size="sm" variant="ghost">
15
+ <button popoverTarget="locale-switcher" popoverTargetAction="show">
16
+ <Languages className="h-4 w-4" />
17
+ <span className="hidden sm:inline">
18
+ <fbt desc="Locale switcher button">Change Language</fbt>
19
+ </span>
20
+ </button>
21
+ </Button>
22
+ <Card
23
+ className="translate-y-50 justify-self-center border-slate-100 bg-white shadow transition-all dark:bg-black"
24
+ id="locale-switcher"
25
+ popover="auto"
26
+ >
27
+ <VStack alignCenter center className="w-64" gap={12} padding={16}>
28
+ {[...AvailableLanguages.entries()].map(([locale, name]) => (
29
+ <Button
30
+ action={() => {
31
+ setLocale(locale);
32
+ localStorage.setItem('fbtee:locale', locale);
33
+ }}
34
+ asChild
35
+ className={cx(
36
+ 'transition-background border-slate-200 bg-transparent duration-150 hover:border-slate-300 hover:bg-slate-200 dark:hover:bg-slate-900 dark:text-white active:py-2',
37
+ {
38
+ 'bg-slate-100 dark:bg-slate-900': currentLocale === locale,
39
+ },
40
+ )}
41
+ key={locale}
42
+ size="sm"
43
+ variant="outline"
44
+ >
45
+ <a className="w-full flex-1 cursor-pointer p-2">{name}</a>
46
+ </Button>
47
+ ))}
48
+ </VStack>
49
+ </Card>
50
+ </>
51
+ );
52
+ }