kaze 1.3.1 → 2.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 (263) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -1
  3. data/README.md +1 -1
  4. data/lib/kaze/commands/app/hotwire_stack.rb +2 -0
  5. data/lib/kaze/commands/app/inertia_react_stack.rb +5 -1
  6. data/lib/kaze/commands/app/inertia_vue_stack.rb +5 -1
  7. data/lib/kaze/commands/app.rb +9 -2
  8. data/lib/kaze/version.rb +1 -1
  9. data/stubs/default/config/routes.rb +11 -4
  10. data/stubs/default/test/integration/appearance_test.rb +11 -0
  11. data/stubs/default/test/integration/password_update_test.rb +9 -1
  12. data/stubs/hotwire/app/assets/tailwind/application.css +123 -0
  13. data/stubs/hotwire/app/components/breadcrumbs_component.rb +32 -0
  14. data/stubs/hotwire/app/components/danger_button_component.rb +1 -1
  15. data/stubs/hotwire/app/components/dropdown_component.html.erb +2 -2
  16. data/stubs/hotwire/app/components/dropdown_component.rb +5 -1
  17. data/stubs/hotwire/app/components/dropdown_link_component.rb +1 -1
  18. data/stubs/hotwire/app/components/heading_component.rb +19 -0
  19. data/stubs/hotwire/app/components/icon_component.rb +30 -0
  20. data/stubs/hotwire/app/components/input_error_component.rb +1 -1
  21. data/stubs/hotwire/app/components/input_label_component.rb +3 -3
  22. data/stubs/hotwire/app/components/modal_component.html.erb +2 -2
  23. data/stubs/hotwire/app/components/primary_button_component.rb +1 -1
  24. data/stubs/hotwire/app/components/secondary_button_component.rb +1 -1
  25. data/stubs/hotwire/app/components/sidebar_menu_button_component.rb +25 -0
  26. data/stubs/hotwire/app/components/text_input_component.rb +1 -1
  27. data/stubs/hotwire/app/components/user_avatar_component.rb +27 -0
  28. data/stubs/hotwire/app/controllers/application_controller.rb +1 -0
  29. data/stubs/hotwire/app/controllers/concerns/sidebar_state.rb +14 -0
  30. data/stubs/hotwire/app/controllers/settings/appearance_controller.rb +7 -0
  31. data/stubs/hotwire/app/controllers/settings/password_controller.rb +19 -0
  32. data/stubs/hotwire/app/controllers/{profile_controller.rb → settings/profile_controller.rb} +3 -4
  33. data/stubs/hotwire/app/views/auth/login.html.erb +1 -1
  34. data/stubs/hotwire/app/views/dashboard/index.html.erb +6 -11
  35. data/stubs/hotwire/app/views/layouts/_appearance.html.erb +18 -0
  36. data/stubs/hotwire/app/views/layouts/_settings.html.erb +26 -0
  37. data/stubs/hotwire/app/views/layouts/_sidebar.html.erb +56 -0
  38. data/stubs/hotwire/app/views/layouts/_sidebar_header.html.erb +11 -0
  39. data/stubs/hotwire/app/views/layouts/_sidebar_user.html.erb +34 -0
  40. data/stubs/hotwire/app/views/layouts/application.html.erb +25 -15
  41. data/stubs/hotwire/app/views/layouts/guest.html.erb +1 -0
  42. data/stubs/hotwire/app/views/settings/appearance/_update_appearance_form.html.erb +28 -0
  43. data/stubs/hotwire/app/views/settings/appearance/edit.html.erb +10 -0
  44. data/stubs/hotwire/app/views/{profile/partials → settings/password}/_update_password_form.html.erb +2 -10
  45. data/stubs/hotwire/app/views/settings/password/edit.html.erb +10 -0
  46. data/stubs/hotwire/app/views/{profile/partials → settings/profile}/_confirm_user_deletion_modal.html.erb +2 -2
  47. data/stubs/hotwire/app/views/settings/profile/_delete_user_form.html.erb +6 -0
  48. data/stubs/hotwire/app/views/{profile/partials → settings/profile}/_update_profile_information_form.html.erb +4 -12
  49. data/stubs/hotwire/app/views/{profile → settings/profile}/destroy.turbo_stream.erb +1 -1
  50. data/stubs/hotwire/app/views/settings/profile/edit.html.erb +14 -0
  51. data/stubs/hotwire/app/views/welcome/index.html.erb +1 -0
  52. data/stubs/inertia-common/app/controllers/concerns/handle_inertia_requests.rb +4 -1
  53. data/stubs/inertia-common/app/controllers/settings/appearance_controller.rb +7 -0
  54. data/stubs/inertia-common/app/controllers/settings/password_controller.rb +17 -0
  55. data/stubs/inertia-common/app/controllers/{profile_controller.rb → settings/profile_controller.rb} +2 -2
  56. data/stubs/inertia-common/test/integration/appearance_test.rb +11 -0
  57. data/stubs/inertia-common/test/integration/password_update_test.rb +10 -2
  58. data/stubs/inertia-react-ts/app/assets/tailwind/application.css +124 -0
  59. data/stubs/inertia-react-ts/app/javascript/Components/AppLogo.tsx +17 -0
  60. data/stubs/inertia-react-ts/app/javascript/Components/AppShell.tsx +9 -0
  61. data/stubs/inertia-react-ts/app/javascript/Components/AppSidebar.tsx +65 -0
  62. data/stubs/inertia-react-ts/app/javascript/Components/AppSidebarHeader.tsx +14 -0
  63. data/stubs/inertia-react-ts/app/javascript/Components/Breadcrumbs.tsx +43 -0
  64. data/stubs/inertia-react-ts/app/javascript/Components/Heading.tsx +19 -0
  65. data/stubs/inertia-react-ts/app/javascript/Components/InputError.tsx +1 -1
  66. data/stubs/inertia-react-ts/app/javascript/Components/NavFooter.tsx +32 -0
  67. data/stubs/inertia-react-ts/app/javascript/Components/NavMain.tsx +32 -0
  68. data/stubs/inertia-react-ts/app/javascript/Components/NavUser.tsx +39 -0
  69. data/stubs/inertia-react-ts/app/javascript/Components/UserInfo.tsx +23 -0
  70. data/stubs/inertia-react-ts/app/javascript/Components/UserMenuContent.tsx +57 -0
  71. data/stubs/inertia-react-ts/app/javascript/Components/ui/avatar.tsx +51 -0
  72. data/stubs/inertia-react-ts/app/javascript/Components/ui/breadcrumb.tsx +109 -0
  73. data/stubs/inertia-react-ts/app/javascript/Components/ui/button.tsx +49 -0
  74. data/stubs/inertia-react-ts/app/javascript/Components/ui/checkbox.tsx +27 -0
  75. data/stubs/inertia-react-ts/app/javascript/Components/ui/dialog.tsx +109 -0
  76. data/stubs/inertia-react-ts/app/javascript/Components/ui/dropdown-menu.tsx +217 -0
  77. data/stubs/inertia-react-ts/app/javascript/Components/ui/input.tsx +21 -0
  78. data/stubs/inertia-react-ts/app/javascript/Components/ui/label.tsx +19 -0
  79. data/stubs/inertia-react-ts/app/javascript/Components/ui/separator.tsx +26 -0
  80. data/stubs/inertia-react-ts/app/javascript/Components/ui/sheet.tsx +137 -0
  81. data/stubs/inertia-react-ts/app/javascript/Components/ui/sidebar.tsx +719 -0
  82. data/stubs/inertia-react-ts/app/javascript/Components/ui/skeleton.tsx +13 -0
  83. data/stubs/inertia-react-ts/app/javascript/Components/ui/tooltip.tsx +55 -0
  84. data/stubs/inertia-react-ts/app/javascript/Layouts/AuthenticatedLayout.tsx +19 -127
  85. data/stubs/inertia-react-ts/app/javascript/Layouts/GuestLayout.tsx +2 -2
  86. data/stubs/inertia-react-ts/app/javascript/Layouts/SettingsLayout.tsx +57 -0
  87. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ForgotPassword.tsx +6 -6
  88. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Login.tsx +17 -12
  89. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Register.tsx +14 -14
  90. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ResetPassword.tsx +10 -10
  91. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/VerifyEmail.tsx +2 -2
  92. data/stubs/inertia-react-ts/app/javascript/Pages/Dashboard.tsx +13 -8
  93. data/stubs/inertia-react-ts/app/javascript/Pages/Settings/Appearance.tsx +33 -0
  94. data/stubs/inertia-react-ts/app/javascript/Pages/Settings/Partials/DeleteUserForm.tsx +107 -0
  95. data/stubs/inertia-react-ts/app/javascript/Pages/Settings/Partials/UpdateAppearanceForm.tsx +41 -0
  96. data/stubs/inertia-react-ts/app/javascript/Pages/{Profile → Settings}/Partials/UpdatePasswordForm.tsx +12 -20
  97. data/stubs/inertia-react-ts/app/javascript/Pages/{Profile → Settings}/Partials/UpdateProfileInformationForm.tsx +13 -21
  98. data/stubs/inertia-react-ts/app/javascript/Pages/Settings/Password.tsx +33 -0
  99. data/stubs/inertia-react-ts/app/javascript/Pages/Settings/Profile.tsx +35 -0
  100. data/stubs/inertia-react-ts/app/javascript/entrypoints/application.tsx +6 -1
  101. data/stubs/inertia-react-ts/app/javascript/hooks/use-initials.tsx +23 -0
  102. data/stubs/inertia-react-ts/app/javascript/hooks/use-mobile-navigation.tsx +11 -0
  103. data/stubs/inertia-react-ts/app/javascript/hooks/use-mobile.tsx +36 -0
  104. data/stubs/inertia-react-ts/app/javascript/lib/appearance.ts +22 -0
  105. data/stubs/inertia-react-ts/app/javascript/lib/utils.ts +6 -0
  106. data/stubs/inertia-react-ts/app/javascript/types/global.d.ts +2 -2
  107. data/stubs/inertia-react-ts/app/javascript/types/index.d.ts +14 -0
  108. data/stubs/inertia-react-ts/app/views/layouts/application.html.erb +19 -1
  109. data/stubs/inertia-react-ts/components.json +21 -0
  110. data/stubs/inertia-react-ts/package.json +13 -0
  111. data/stubs/inertia-vue-ts/app/assets/tailwind/application.css +124 -0
  112. data/stubs/inertia-vue-ts/app/javascript/Components/AppLogo.vue +17 -0
  113. data/stubs/inertia-vue-ts/app/javascript/Components/AppShell.vue +12 -0
  114. data/stubs/inertia-vue-ts/app/javascript/Components/AppSidebar.vue +65 -0
  115. data/stubs/inertia-vue-ts/app/javascript/Components/AppSidebarHeader.vue +20 -0
  116. data/stubs/inertia-vue-ts/app/javascript/Components/Breadcrumbs.vue +32 -0
  117. data/stubs/inertia-vue-ts/app/javascript/Components/Heading.vue +15 -0
  118. data/stubs/inertia-vue-ts/app/javascript/Components/InputError.vue +1 -1
  119. data/stubs/inertia-vue-ts/app/javascript/Components/NavFooter.vue +33 -0
  120. data/stubs/inertia-vue-ts/app/javascript/Components/NavMain.vue +32 -0
  121. data/stubs/inertia-vue-ts/app/javascript/Components/NavUser.vue +40 -0
  122. data/stubs/inertia-vue-ts/app/javascript/Components/UserInfo.vue +25 -0
  123. data/stubs/inertia-vue-ts/app/javascript/Components/UserMenuContent.vue +47 -0
  124. data/stubs/inertia-vue-ts/app/javascript/Components/ui/avatar/Avatar.vue +18 -0
  125. data/stubs/inertia-vue-ts/app/javascript/Components/ui/avatar/AvatarFallback.vue +21 -0
  126. data/stubs/inertia-vue-ts/app/javascript/Components/ui/avatar/AvatarImage.vue +16 -0
  127. data/stubs/inertia-vue-ts/app/javascript/Components/ui/avatar/index.ts +3 -0
  128. data/stubs/inertia-vue-ts/app/javascript/Components/ui/breadcrumb/Breadcrumb.vue +17 -0
  129. data/stubs/inertia-vue-ts/app/javascript/Components/ui/breadcrumb/BreadcrumbEllipsis.vue +23 -0
  130. data/stubs/inertia-vue-ts/app/javascript/Components/ui/breadcrumb/BreadcrumbItem.vue +17 -0
  131. data/stubs/inertia-vue-ts/app/javascript/Components/ui/breadcrumb/BreadcrumbLink.vue +21 -0
  132. data/stubs/inertia-vue-ts/app/javascript/Components/ui/breadcrumb/BreadcrumbList.vue +17 -0
  133. data/stubs/inertia-vue-ts/app/javascript/Components/ui/breadcrumb/BreadcrumbPage.vue +20 -0
  134. data/stubs/inertia-vue-ts/app/javascript/Components/ui/breadcrumb/BreadcrumbSeparator.vue +22 -0
  135. data/stubs/inertia-vue-ts/app/javascript/Components/ui/breadcrumb/index.ts +7 -0
  136. data/stubs/inertia-vue-ts/app/javascript/Components/ui/button/Button.vue +31 -0
  137. data/stubs/inertia-vue-ts/app/javascript/Components/ui/button/index.ts +34 -0
  138. data/stubs/inertia-vue-ts/app/javascript/Components/ui/checkbox/Checkbox.vue +35 -0
  139. data/stubs/inertia-vue-ts/app/javascript/Components/ui/checkbox/index.ts +1 -0
  140. data/stubs/inertia-vue-ts/app/javascript/Components/ui/dialog/Dialog.vue +15 -0
  141. data/stubs/inertia-vue-ts/app/javascript/Components/ui/dialog/DialogClose.vue +12 -0
  142. data/stubs/inertia-vue-ts/app/javascript/Components/ui/dialog/DialogContent.vue +52 -0
  143. data/stubs/inertia-vue-ts/app/javascript/Components/ui/dialog/DialogDescription.vue +23 -0
  144. data/stubs/inertia-vue-ts/app/javascript/Components/ui/dialog/DialogFooter.vue +25 -0
  145. data/stubs/inertia-vue-ts/app/javascript/Components/ui/dialog/DialogHeader.vue +14 -0
  146. data/stubs/inertia-vue-ts/app/javascript/Components/ui/dialog/DialogOverlay.vue +26 -0
  147. data/stubs/inertia-vue-ts/app/javascript/Components/ui/dialog/DialogTitle.vue +23 -0
  148. data/stubs/inertia-vue-ts/app/javascript/Components/ui/dialog/DialogTrigger.vue +12 -0
  149. data/stubs/inertia-vue-ts/app/javascript/Components/ui/dialog/index.ts +9 -0
  150. data/stubs/inertia-vue-ts/app/javascript/Components/ui/dropdown-menu/DropdownMenu.vue +15 -0
  151. data/stubs/inertia-vue-ts/app/javascript/Components/ui/dropdown-menu/DropdownMenuContent.vue +37 -0
  152. data/stubs/inertia-vue-ts/app/javascript/Components/ui/dropdown-menu/DropdownMenuGroup.vue +12 -0
  153. data/stubs/inertia-vue-ts/app/javascript/Components/ui/dropdown-menu/DropdownMenuItem.vue +41 -0
  154. data/stubs/inertia-vue-ts/app/javascript/Components/ui/dropdown-menu/DropdownMenuLabel.vue +23 -0
  155. data/stubs/inertia-vue-ts/app/javascript/Components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -0
  156. data/stubs/inertia-vue-ts/app/javascript/Components/ui/dropdown-menu/DropdownMenuTrigger.vue +14 -0
  157. data/stubs/inertia-vue-ts/app/javascript/Components/ui/dropdown-menu/index.ts +7 -0
  158. data/stubs/inertia-vue-ts/app/javascript/Components/ui/input/Input.vue +50 -0
  159. data/stubs/inertia-vue-ts/app/javascript/Components/ui/input/index.ts +1 -0
  160. data/stubs/inertia-vue-ts/app/javascript/Components/ui/label/Label.vue +26 -0
  161. data/stubs/inertia-vue-ts/app/javascript/Components/ui/label/index.ts +1 -0
  162. data/stubs/inertia-vue-ts/app/javascript/Components/ui/separator/Separator.vue +29 -0
  163. data/stubs/inertia-vue-ts/app/javascript/Components/ui/separator/index.ts +1 -0
  164. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sheet/Sheet.vue +19 -0
  165. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sheet/SheetClose.vue +15 -0
  166. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sheet/SheetContent.vue +62 -0
  167. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sheet/SheetDescription.vue +21 -0
  168. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sheet/SheetFooter.vue +16 -0
  169. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sheet/SheetHeader.vue +15 -0
  170. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sheet/SheetOverlay.vue +21 -0
  171. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sheet/SheetTitle.vue +21 -0
  172. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sheet/SheetTrigger.vue +15 -0
  173. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sheet/index.ts +8 -0
  174. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/Sidebar.vue +96 -0
  175. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarContent.vue +18 -0
  176. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarFooter.vue +18 -0
  177. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarGroup.vue +18 -0
  178. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarGroupAction.vue +27 -0
  179. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarGroupContent.vue +18 -0
  180. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarGroupLabel.vue +25 -0
  181. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarHeader.vue +18 -0
  182. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarInput.vue +22 -0
  183. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarInset.vue +21 -0
  184. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarMenu.vue +18 -0
  185. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarMenuAction.vue +35 -0
  186. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarMenuBadge.vue +26 -0
  187. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarMenuButton.vue +48 -0
  188. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarMenuButtonChild.vue +36 -0
  189. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarMenuItem.vue +18 -0
  190. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarMenuSkeleton.vue +35 -0
  191. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarMenuSub.vue +22 -0
  192. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarMenuSubButton.vue +36 -0
  193. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarMenuSubItem.vue +18 -0
  194. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarProvider.vue +82 -0
  195. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarRail.vue +33 -0
  196. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarSeparator.vue +19 -0
  197. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/SidebarTrigger.vue +28 -0
  198. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/index.ts +60 -0
  199. data/stubs/inertia-vue-ts/app/javascript/Components/ui/sidebar/utils.ts +19 -0
  200. data/stubs/inertia-vue-ts/app/javascript/Components/ui/skeleton/Skeleton.vue +17 -0
  201. data/stubs/inertia-vue-ts/app/javascript/Components/ui/skeleton/index.ts +1 -0
  202. data/stubs/inertia-vue-ts/app/javascript/Components/ui/tooltip/Tooltip.vue +19 -0
  203. data/stubs/inertia-vue-ts/app/javascript/Components/ui/tooltip/TooltipContent.vue +34 -0
  204. data/stubs/inertia-vue-ts/app/javascript/Components/ui/tooltip/TooltipProvider.vue +14 -0
  205. data/stubs/inertia-vue-ts/app/javascript/Components/ui/tooltip/TooltipTrigger.vue +15 -0
  206. data/stubs/inertia-vue-ts/app/javascript/Components/ui/tooltip/index.ts +4 -0
  207. data/stubs/inertia-vue-ts/app/javascript/Layouts/AuthenticatedLayout.vue +17 -147
  208. data/stubs/inertia-vue-ts/app/javascript/Layouts/SettingsLayout.vue +57 -0
  209. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ForgotPassword.vue +7 -7
  210. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Login.vue +10 -12
  211. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Register.vue +12 -14
  212. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ResetPassword.vue +8 -10
  213. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/VerifyEmail.vue +3 -3
  214. data/stubs/inertia-vue-ts/app/javascript/Pages/Dashboard.vue +15 -10
  215. data/stubs/inertia-vue-ts/app/javascript/Pages/Settings/Appearance.vue +33 -0
  216. data/stubs/inertia-vue-ts/app/javascript/Pages/Settings/Partials/DeleteUserForm.vue +96 -0
  217. data/stubs/inertia-vue-ts/app/javascript/Pages/Settings/Partials/UpdateAppearanceForm.vue +42 -0
  218. data/stubs/inertia-vue-ts/app/javascript/Pages/{Profile → Settings}/Partials/UpdatePasswordForm.vue +12 -20
  219. data/stubs/inertia-vue-ts/app/javascript/Pages/{Profile → Settings}/Partials/UpdateProfileInformationForm.vue +12 -20
  220. data/stubs/inertia-vue-ts/app/javascript/Pages/Settings/Password.vue +33 -0
  221. data/stubs/inertia-vue-ts/app/javascript/Pages/Settings/Profile.vue +40 -0
  222. data/stubs/inertia-vue-ts/app/javascript/composables/useInitials.ts +29 -0
  223. data/stubs/inertia-vue-ts/app/javascript/lib/appearance.ts +22 -0
  224. data/stubs/inertia-vue-ts/app/javascript/lib/utils.ts +6 -0
  225. data/stubs/inertia-vue-ts/app/javascript/types/index.d.ts +14 -0
  226. data/stubs/inertia-vue-ts/app/javascript/types/vue-shims.d.ts +10 -0
  227. data/stubs/inertia-vue-ts/app/views/layouts/application.html.erb +19 -1
  228. data/stubs/inertia-vue-ts/components.json +20 -0
  229. data/stubs/inertia-vue-ts/package.json +8 -1
  230. data/stubs/inertia-vue-ts/vite.config.ts +25 -1
  231. metadata +174 -43
  232. data/stubs/hotwire/app/components/nav_link_component.rb +0 -15
  233. data/stubs/hotwire/app/components/responsive_nav_link_component.rb +0 -15
  234. data/stubs/hotwire/app/controllers/password_controller.rb +0 -13
  235. data/stubs/hotwire/app/views/layouts/_navigation.html.erb +0 -82
  236. data/stubs/hotwire/app/views/profile/edit.html.erb +0 -24
  237. data/stubs/hotwire/app/views/profile/partials/_delete_user_form.html.erb +0 -14
  238. data/stubs/inertia-common/app/controllers/password_controller.rb +0 -13
  239. data/stubs/inertia-react-ts/app/javascript/Components/Checkbox.tsx +0 -11
  240. data/stubs/inertia-react-ts/app/javascript/Components/DangerButton.tsx +0 -20
  241. data/stubs/inertia-react-ts/app/javascript/Components/Dropdown.tsx +0 -100
  242. data/stubs/inertia-react-ts/app/javascript/Components/InputLabel.tsx +0 -14
  243. data/stubs/inertia-react-ts/app/javascript/Components/Modal.tsx +0 -66
  244. data/stubs/inertia-react-ts/app/javascript/Components/NavLink.tsx +0 -21
  245. data/stubs/inertia-react-ts/app/javascript/Components/PrimaryButton.tsx +0 -20
  246. data/stubs/inertia-react-ts/app/javascript/Components/ResponsiveNavLink.tsx +0 -21
  247. data/stubs/inertia-react-ts/app/javascript/Components/SecondaryButton.tsx +0 -22
  248. data/stubs/inertia-react-ts/app/javascript/Components/TextInput.tsx +0 -32
  249. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Edit.tsx +0 -32
  250. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.tsx +0 -102
  251. data/stubs/inertia-vue-ts/app/javascript/Components/Checkbox.vue +0 -29
  252. data/stubs/inertia-vue-ts/app/javascript/Components/DangerButton.vue +0 -7
  253. data/stubs/inertia-vue-ts/app/javascript/Components/Dropdown.vue +0 -75
  254. data/stubs/inertia-vue-ts/app/javascript/Components/DropdownLink.vue +0 -16
  255. data/stubs/inertia-vue-ts/app/javascript/Components/InputLabel.vue +0 -12
  256. data/stubs/inertia-vue-ts/app/javascript/Components/Modal.vue +0 -109
  257. data/stubs/inertia-vue-ts/app/javascript/Components/NavLink.vue +0 -21
  258. data/stubs/inertia-vue-ts/app/javascript/Components/PrimaryButton.vue +0 -7
  259. data/stubs/inertia-vue-ts/app/javascript/Components/ResponsiveNavLink.vue +0 -21
  260. data/stubs/inertia-vue-ts/app/javascript/Components/SecondaryButton.vue +0 -19
  261. data/stubs/inertia-vue-ts/app/javascript/Components/TextInput.vue +0 -23
  262. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Edit.vue +0 -38
  263. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.vue +0 -99
@@ -0,0 +1,107 @@
1
+ import { useRef, useState, FormEventHandler } from 'react'
2
+ import { Button } from '@/Components/ui/button'
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogDescription,
7
+ DialogFooter,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ } from '@/Components/ui/dialog'
11
+ import { Input } from '@/Components/ui/input'
12
+ import { Label } from '@/Components/ui/label'
13
+ import InputError from '@/Components/InputError'
14
+ import { useForm } from '@inertiajs/react'
15
+ import { profile_destroy_path } from '@/routes'
16
+
17
+ export default function DeleteUserForm({ className = '' }: { className?: string }) {
18
+ const [confirmingUserDeletion, setConfirmingUserDeletion] = useState(false)
19
+ const passwordInput = useRef<HTMLInputElement>(null)
20
+
21
+ const {
22
+ data,
23
+ setData,
24
+ delete: destroy,
25
+ processing,
26
+ reset,
27
+ errors,
28
+ clearErrors,
29
+ } = useForm({
30
+ password: '',
31
+ })
32
+
33
+ const confirmUserDeletion = () => {
34
+ setConfirmingUserDeletion(true)
35
+ }
36
+
37
+ const deleteUser: FormEventHandler = (e) => {
38
+ e.preventDefault()
39
+
40
+ destroy(profile_destroy_path(), {
41
+ preserveScroll: true,
42
+ onSuccess: () => closeModal(),
43
+ onError: () => passwordInput.current?.focus(),
44
+ onFinish: () => reset(),
45
+ })
46
+ }
47
+
48
+ const closeModal = () => {
49
+ setConfirmingUserDeletion(false)
50
+
51
+ clearErrors()
52
+ reset()
53
+ }
54
+
55
+ return (
56
+ <section className={`space-y-6 ${className}`}>
57
+ <Button variant="destructive" onClick={confirmUserDeletion}>
58
+ Delete Account
59
+ </Button>
60
+
61
+ <Dialog open={confirmingUserDeletion} onOpenChange={(open) => !open && closeModal()}>
62
+ <DialogContent>
63
+ <form onSubmit={deleteUser}>
64
+ <DialogHeader>
65
+ <DialogTitle>Are you sure you want to delete your account?</DialogTitle>
66
+
67
+ <DialogDescription>
68
+ Once your account is deleted, all of its resources and data will be permanently deleted. Please enter
69
+ your password to confirm you would like to permanently delete your account.
70
+ </DialogDescription>
71
+ </DialogHeader>
72
+
73
+ <div className="mt-6">
74
+ <Label htmlFor="password" className="sr-only">
75
+ Password
76
+ </Label>
77
+
78
+ <Input
79
+ id="password"
80
+ type="password"
81
+ name="password"
82
+ ref={passwordInput}
83
+ value={data.password}
84
+ onChange={(e) => setData('password', e.target.value)}
85
+ className="mt-1 block w-3/4"
86
+ autoFocus
87
+ placeholder="Password"
88
+ />
89
+
90
+ <InputError message={errors.password} className="mt-2" />
91
+ </div>
92
+
93
+ <DialogFooter>
94
+ <Button type="button" variant="outline" onClick={closeModal}>
95
+ Cancel
96
+ </Button>
97
+
98
+ <Button variant="destructive" disabled={processing}>
99
+ Delete Account
100
+ </Button>
101
+ </DialogFooter>
102
+ </form>
103
+ </DialogContent>
104
+ </Dialog>
105
+ </section>
106
+ )
107
+ }
@@ -0,0 +1,41 @@
1
+ import { useState } from 'react'
2
+ import { LucideIcon, Monitor, Moon, Sun } from 'lucide-react'
3
+ import { Appearance, getAppearance, setAppearance } from '@/lib/appearance'
4
+ import { cn } from '@/lib/utils'
5
+
6
+ const options: { value: Appearance; icon: LucideIcon; label: string }[] = [
7
+ { value: 'light', icon: Sun, label: 'Light' },
8
+ { value: 'dark', icon: Moon, label: 'Dark' },
9
+ { value: 'system', icon: Monitor, label: 'System' },
10
+ ]
11
+
12
+ export default function UpdateAppearanceForm({ className = '' }: { className?: string }) {
13
+ const [appearance, setCurrentAppearance] = useState<Appearance>(getAppearance)
14
+
15
+ const select = (value: Appearance) => {
16
+ setCurrentAppearance(value)
17
+ setAppearance(value)
18
+ }
19
+
20
+ return (
21
+ <div className={cn('inline-flex gap-1 rounded-lg bg-neutral-100 p-1 dark:bg-neutral-800', className)}>
22
+ {options.map(({ value, icon: Icon, label }) => (
23
+ <button
24
+ key={value}
25
+ type="button"
26
+ onClick={() => select(value)}
27
+ aria-pressed={appearance === value}
28
+ className={cn(
29
+ 'flex items-center rounded-md px-3.5 py-1.5 transition-colors outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50',
30
+ appearance === value
31
+ ? 'bg-white shadow-xs dark:bg-neutral-700 dark:text-neutral-100'
32
+ : 'text-neutral-500 hover:bg-neutral-200/60 hover:text-black dark:text-neutral-400 dark:hover:bg-neutral-700/60',
33
+ )}
34
+ >
35
+ <Icon className="-ml-1 h-4 w-4" />
36
+ <span className="ml-1.5 text-sm">{label}</span>
37
+ </button>
38
+ ))}
39
+ </div>
40
+ )
41
+ }
@@ -1,8 +1,8 @@
1
1
  import { useRef, FormEventHandler } from 'react'
2
+ import { Button } from '@/Components/ui/button'
3
+ import { Input } from '@/Components/ui/input'
4
+ import { Label } from '@/Components/ui/label'
2
5
  import InputError from '@/Components/InputError'
3
- import InputLabel from '@/Components/InputLabel'
4
- import PrimaryButton from '@/Components/PrimaryButton'
5
- import TextInput from '@/Components/TextInput'
6
6
  import { useForm } from '@inertiajs/react'
7
7
  import { Transition } from '@headlessui/react'
8
8
  import { password_update_path } from '@/routes'
@@ -39,19 +39,11 @@ export default function UpdatePasswordForm({ className = '' }: { className?: str
39
39
 
40
40
  return (
41
41
  <section className={className}>
42
- <header>
43
- <h2 className="text-lg font-medium text-gray-900 dark:text-gray-100">Update Password</h2>
44
-
45
- <p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
46
- Ensure your account is using a long, random password to stay secure.
47
- </p>
48
- </header>
49
-
50
- <form onSubmit={updatePassword} className="mt-6 space-y-6">
42
+ <form onSubmit={updatePassword} className="space-y-6">
51
43
  <div>
52
- <InputLabel htmlFor="current_password" value="Current Password" />
44
+ <Label htmlFor="current_password">Current Password</Label>
53
45
 
54
- <TextInput
46
+ <Input
55
47
  id="current_password"
56
48
  ref={currentPasswordInput}
57
49
  value={data.current_password}
@@ -65,9 +57,9 @@ export default function UpdatePasswordForm({ className = '' }: { className?: str
65
57
  </div>
66
58
 
67
59
  <div>
68
- <InputLabel htmlFor="password" value="New Password" />
60
+ <Label htmlFor="password">New Password</Label>
69
61
 
70
- <TextInput
62
+ <Input
71
63
  id="password"
72
64
  ref={passwordInput}
73
65
  value={data.password}
@@ -81,9 +73,9 @@ export default function UpdatePasswordForm({ className = '' }: { className?: str
81
73
  </div>
82
74
 
83
75
  <div>
84
- <InputLabel htmlFor="password_confirmation" value="Confirm Password" />
76
+ <Label htmlFor="password_confirmation">Confirm Password</Label>
85
77
 
86
- <TextInput
78
+ <Input
87
79
  id="password_confirmation"
88
80
  value={data.password_confirmation}
89
81
  onChange={(e) => setData('password_confirmation', e.target.value)}
@@ -96,7 +88,7 @@ export default function UpdatePasswordForm({ className = '' }: { className?: str
96
88
  </div>
97
89
 
98
90
  <div className="flex items-center gap-4">
99
- <PrimaryButton disabled={processing}>Save</PrimaryButton>
91
+ <Button disabled={processing}>Save</Button>
100
92
 
101
93
  <Transition
102
94
  show={recentlySuccessful}
@@ -105,7 +97,7 @@ export default function UpdatePasswordForm({ className = '' }: { className?: str
105
97
  leave="transition ease-in-out"
106
98
  leaveTo="opacity-0"
107
99
  >
108
- <p className="text-sm text-gray-600 dark:text-gray-400">Saved.</p>
100
+ <p className="text-sm text-muted-foreground">Saved.</p>
109
101
  </Transition>
110
102
  </div>
111
103
  </form>
@@ -1,7 +1,7 @@
1
1
  import InputError from '@/Components/InputError'
2
- import InputLabel from '@/Components/InputLabel'
3
- import PrimaryButton from '@/Components/PrimaryButton'
4
- import TextInput from '@/Components/TextInput'
2
+ import { Button } from '@/Components/ui/button'
3
+ import { Input } from '@/Components/ui/input'
4
+ import { Label } from '@/Components/ui/label'
5
5
  import { Link, useForm, usePage } from '@inertiajs/react'
6
6
  import { Transition } from '@headlessui/react'
7
7
  import { FormEventHandler } from 'react'
@@ -31,25 +31,17 @@ export default function UpdateProfileInformation({
31
31
 
32
32
  return (
33
33
  <section className={className}>
34
- <header>
35
- <h2 className="text-lg font-medium text-gray-900 dark:text-gray-100">Profile Information</h2>
36
-
37
- <p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
38
- Update your account's profile information and email address.
39
- </p>
40
- </header>
41
-
42
- <form onSubmit={submit} className="mt-6 space-y-6">
34
+ <form onSubmit={submit} className="space-y-6">
43
35
  <div>
44
- <InputLabel htmlFor="name" value="Name" />
36
+ <Label htmlFor="name">Name</Label>
45
37
 
46
- <TextInput
38
+ <Input
47
39
  id="name"
48
40
  className="mt-1 block w-full"
49
41
  value={data.name}
50
42
  onChange={(e) => setData('name', e.target.value)}
51
43
  required
52
- isFocused
44
+ autoFocus
53
45
  autoComplete="name"
54
46
  />
55
47
 
@@ -57,9 +49,9 @@ export default function UpdateProfileInformation({
57
49
  </div>
58
50
 
59
51
  <div>
60
- <InputLabel htmlFor="email" value="Email" />
52
+ <Label htmlFor="email">Email</Label>
61
53
 
62
- <TextInput
54
+ <Input
63
55
  id="email"
64
56
  type="email"
65
57
  className="mt-1 block w-full"
@@ -74,13 +66,13 @@ export default function UpdateProfileInformation({
74
66
 
75
67
  {mustVerifyEmail && user.email_verified_at === null && (
76
68
  <div>
77
- <p className="text-sm mt-2 text-gray-800 dark:text-gray-200">
69
+ <p className="mt-2 text-sm text-foreground">
78
70
  Your email address is unverified.
79
71
  <Link
80
72
  href={verification_send_path()}
81
73
  method="post"
82
74
  as="button"
83
- className="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800"
75
+ className="rounded-md text-sm text-muted-foreground underline outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
84
76
  >
85
77
  Click here to re-send the verification email.
86
78
  </Link>
@@ -95,7 +87,7 @@ export default function UpdateProfileInformation({
95
87
  )}
96
88
 
97
89
  <div className="flex items-center gap-4">
98
- <PrimaryButton disabled={processing}>Save</PrimaryButton>
90
+ <Button disabled={processing}>Save</Button>
99
91
 
100
92
  <Transition
101
93
  show={recentlySuccessful}
@@ -104,7 +96,7 @@ export default function UpdateProfileInformation({
104
96
  leave="transition ease-in-out"
105
97
  leaveTo="opacity-0"
106
98
  >
107
- <p className="text-sm text-gray-600 dark:text-gray-400">Saved.</p>
99
+ <p className="text-sm text-muted-foreground">Saved.</p>
108
100
  </Transition>
109
101
  </div>
110
102
  </form>
@@ -0,0 +1,33 @@
1
+ import { Head } from '@inertiajs/react'
2
+ import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
3
+ import SettingsLayout from '@/Layouts/SettingsLayout'
4
+ import Heading from '@/Components/Heading'
5
+ import UpdatePasswordForm from './Partials/UpdatePasswordForm'
6
+ import { password_edit_path } from '@/routes'
7
+ import { BreadcrumbItem } from '@/types'
8
+
9
+ const breadcrumbs: BreadcrumbItem[] = [
10
+ {
11
+ title: 'Password settings',
12
+ href: password_edit_path(),
13
+ },
14
+ ]
15
+
16
+ export default function Password() {
17
+ return (
18
+ <AuthenticatedLayout breadcrumbs={breadcrumbs}>
19
+ <Head title="Password settings" />
20
+
21
+ <SettingsLayout>
22
+ <div className="space-y-6">
23
+ <Heading
24
+ variant="small"
25
+ title="Update password"
26
+ description="Ensure your account is using a long, random password to stay secure"
27
+ />
28
+ <UpdatePasswordForm />
29
+ </div>
30
+ </SettingsLayout>
31
+ </AuthenticatedLayout>
32
+ )
33
+ }
@@ -0,0 +1,35 @@
1
+ import { Head } from '@inertiajs/react'
2
+ import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
3
+ import SettingsLayout from '@/Layouts/SettingsLayout'
4
+ import Heading from '@/Components/Heading'
5
+ import DeleteUserForm from './Partials/DeleteUserForm'
6
+ import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm'
7
+ import { profile_edit_path } from '@/routes'
8
+ import { BreadcrumbItem, PageProps } from '@/types'
9
+
10
+ const breadcrumbs: BreadcrumbItem[] = [
11
+ {
12
+ title: 'Profile settings',
13
+ href: profile_edit_path(),
14
+ },
15
+ ]
16
+
17
+ export default function Profile({ mustVerifyEmail, status }: PageProps<{ mustVerifyEmail: boolean; status?: string }>) {
18
+ return (
19
+ <AuthenticatedLayout breadcrumbs={breadcrumbs}>
20
+ <Head title="Profile settings" />
21
+
22
+ <SettingsLayout>
23
+ <div className="space-y-6">
24
+ <Heading variant="small" title="Profile information" description="Update your name and email address" />
25
+ <UpdateProfileInformationForm mustVerifyEmail={mustVerifyEmail} status={status} />
26
+ </div>
27
+
28
+ <div className="space-y-6">
29
+ <Heading variant="small" title="Delete account" description="Delete your account and all of its resources" />
30
+ <DeleteUserForm />
31
+ </div>
32
+ </SettingsLayout>
33
+ </AuthenticatedLayout>
34
+ )
35
+ }
@@ -3,6 +3,7 @@ import '../../assets/builds/tailwind.css'
3
3
 
4
4
  import { createRoot } from 'react-dom/client'
5
5
  import { createInertiaApp, type ResolvedComponent } from '@inertiajs/react'
6
+ import { TooltipProvider } from '@/Components/ui/tooltip'
6
7
 
7
8
  const pages = import.meta.glob<{ default: ResolvedComponent }>('../Pages/**/*.tsx')
8
9
 
@@ -24,7 +25,11 @@ createInertiaApp({
24
25
  setup({ el, App, props }) {
25
26
  const root = createRoot(el)
26
27
 
27
- root.render(<App {...props} />)
28
+ root.render(
29
+ <TooltipProvider delayDuration={0}>
30
+ <App {...props} />
31
+ </TooltipProvider>,
32
+ )
28
33
  },
29
34
  progress: {
30
35
  color: '#4B5563',
@@ -0,0 +1,23 @@
1
+ import { useCallback } from 'react'
2
+
3
+ export type GetInitialsFn = (fullName: string) => string
4
+
5
+ function getInitial(name: string): string {
6
+ return Array.from(name)[0] ?? ''
7
+ }
8
+
9
+ export function useInitials(): GetInitialsFn {
10
+ return useCallback((fullName: string): string => {
11
+ const names = fullName.trim().split(/\s+/u).filter(Boolean)
12
+
13
+ if (names.length === 0) {
14
+ return ''
15
+ }
16
+
17
+ if (names.length === 1) {
18
+ return getInitial(names[0]).toUpperCase()
19
+ }
20
+
21
+ return `${getInitial(names[0])}${getInitial(names[names.length - 1])}`.toUpperCase()
22
+ }, [])
23
+ }
@@ -0,0 +1,11 @@
1
+ import { useCallback } from 'react'
2
+
3
+ export type CleanupFn = () => void
4
+
5
+ export function useMobileNavigation(): CleanupFn {
6
+ return useCallback(() => {
7
+ // The dropdown leaves pointer-events on the body when it closes during a
8
+ // page change, which would make the new page unclickable.
9
+ document.body.style.removeProperty('pointer-events')
10
+ }, [])
11
+ }
@@ -0,0 +1,36 @@
1
+ import { useSyncExternalStore } from 'react'
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ const mql =
6
+ typeof window === 'undefined'
7
+ ? undefined
8
+ : window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
9
+
10
+ function mediaQueryListener(callback: (event: MediaQueryListEvent) => void) {
11
+ if (!mql) {
12
+ return () => {}
13
+ }
14
+
15
+ mql.addEventListener('change', callback)
16
+
17
+ return () => {
18
+ mql.removeEventListener('change', callback)
19
+ }
20
+ }
21
+
22
+ function isSmallerThanBreakpoint(): boolean {
23
+ return mql?.matches ?? false
24
+ }
25
+
26
+ function getServerSnapshot(): boolean {
27
+ return false
28
+ }
29
+
30
+ export function useIsMobile(): boolean {
31
+ return useSyncExternalStore(
32
+ mediaQueryListener,
33
+ isSmallerThanBreakpoint,
34
+ getServerSnapshot,
35
+ )
36
+ }
@@ -0,0 +1,22 @@
1
+ export type Appearance = 'light' | 'dark' | 'system'
2
+
3
+ const STORAGE_KEY = 'appearance'
4
+
5
+ export function getAppearance(): Appearance {
6
+ const stored = localStorage.getItem(STORAGE_KEY)
7
+
8
+ return stored === 'light' || stored === 'dark' ? stored : 'system'
9
+ }
10
+
11
+ export function applyAppearance(appearance: Appearance = getAppearance()): void {
12
+ const dark =
13
+ appearance === 'dark' || (appearance === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
14
+
15
+ document.documentElement.classList.toggle('dark', dark)
16
+ }
17
+
18
+ export function setAppearance(appearance: Appearance): void {
19
+ localStorage.setItem(STORAGE_KEY, appearance)
20
+
21
+ applyAppearance(appearance)
22
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -1,6 +1,6 @@
1
- import { PageProps as InertiaPageProps } from '@inertiajs/core';
1
+ import { PageProps as InertiaPageProps } from '@inertiajs/core'
2
2
  import { AxiosInstance } from 'axios'
3
- import { PageProps as AppPageProps } from './';
3
+ import { PageProps as AppPageProps } from './'
4
4
 
5
5
  declare global {
6
6
  interface Window {
@@ -1,3 +1,5 @@
1
+ import { LucideIcon } from 'lucide-react'
2
+
1
3
  export interface User {
2
4
  id: number
3
5
  name: string
@@ -5,8 +7,20 @@ export interface User {
5
7
  email_verified_at?: string
6
8
  }
7
9
 
10
+ export interface NavItem {
11
+ title: string
12
+ href: string
13
+ icon?: LucideIcon
14
+ }
15
+
16
+ export interface BreadcrumbItem {
17
+ title: string
18
+ href: string
19
+ }
20
+
8
21
  export type PageProps<T extends Record<string, unknown> = Record<string, unknown>> = T & {
9
22
  auth: {
10
23
  user: User
11
24
  }
25
+ sidebar_open: boolean
12
26
  }
@@ -6,12 +6,30 @@
6
6
  <title><%= content_for(:title) || "Rails" %></title>
7
7
  <%= csrf_meta_tags %>
8
8
  <%= csp_meta_tag %>
9
+ <%# Apply the stored appearance before first paint, so the page never flashes the wrong theme. %>
10
+ <%= javascript_tag nonce: true do %>
11
+ (function () {
12
+ var preference = localStorage.getItem('appearance') || 'system'
13
+ var query = window.matchMedia('(prefers-color-scheme: dark)')
14
+
15
+ document.documentElement.classList.toggle(
16
+ 'dark',
17
+ preference === 'dark' || (preference === 'system' && query.matches)
18
+ )
19
+
20
+ query.addEventListener('change', function () {
21
+ if ((localStorage.getItem('appearance') || 'system') === 'system') {
22
+ document.documentElement.classList.toggle('dark', query.matches)
23
+ }
24
+ })
25
+ })()
26
+ <% end %>
9
27
  <%= vite_client_tag %>
10
28
  <%= vite_react_refresh_tag %>
11
29
  <%= vite_typescript_tag 'application.tsx', defer: true %>
12
30
  </head>
13
31
  <body class="font-sans antialiased">
14
- <div class="min-h-screen bg-gray-100">
32
+ <div class="min-h-screen bg-gray-100 dark:bg-gray-900">
15
33
  <%= yield %>
16
34
  </div>
17
35
  </body>
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/assets/tailwind/application.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/Components",
15
+ "ui": "@/Components/ui",
16
+ "utils": "@/lib/utils",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }
@@ -8,13 +8,26 @@
8
8
  "devDependencies": {
9
9
  "@headlessui/react": "^2.2.10",
10
10
  "@inertiajs/react": "^3.9.1",
11
+ "@radix-ui/react-avatar": "^1.1.3",
12
+ "@radix-ui/react-checkbox": "^1.1.4",
13
+ "@radix-ui/react-dialog": "^1.1.6",
14
+ "@radix-ui/react-dropdown-menu": "^2.1.6",
15
+ "@radix-ui/react-label": "^2.1.2",
16
+ "@radix-ui/react-separator": "^1.1.2",
17
+ "@radix-ui/react-slot": "^1.2.3",
18
+ "@radix-ui/react-tooltip": "^1.1.8",
11
19
  "@types/node": "^26.6.5",
12
20
  "@types/react": "^19.3.0",
13
21
  "@types/react-dom": "^19.3.0",
14
22
  "@vitejs/plugin-react": "^6.1.2",
15
23
  "axios": "^1.6.4",
24
+ "class-variance-authority": "^0.7.1",
25
+ "clsx": "^2.1.1",
26
+ "lucide-react": "^1.61.0",
16
27
  "react": "^19.3.1",
17
28
  "react-dom": "^19.3.1",
29
+ "tailwind-merge": "^3.7.0",
30
+ "tw-animate-css": "^1.4.0",
18
31
  "typescript": "^7.0.2",
19
32
  "vite": "^8.3.4",
20
33
  "vite-plugin-rails": "^0.6.0"