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,25 @@
1
+ # A row in the sidebar: icon plus label, where the label hides when the rail is
2
+ # collapsed. The Inertia stacks get this from shadcn's sidebar; here the class
3
+ # strings are carried by hand, as with the other hotwire components.
4
+ class SidebarMenuButtonComponent < ViewComponent::Base
5
+ BASE = 'flex h-8 w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ' \
6
+ 'outline-none transition-[width,height,padding] hover:bg-sidebar-accent ' \
7
+ 'hover:text-sidebar-accent-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring ' \
8
+ 'disabled:pointer-events-none disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0'
9
+
10
+ ACTIVE = 'bg-sidebar-accent font-medium text-sidebar-accent-foreground'
11
+
12
+ erb_template <<~ERB
13
+ <a href="<%= @href %>" class="<%= @classes %>"<%= @external_attributes %>>
14
+ <%= render(IconComponent.new(@icon)) if @icon %>
15
+ <span x-show="sidebarOpen || sidebarOpenMobile" class="truncate"><%= content %></span>
16
+ </a>
17
+ ERB
18
+
19
+ def initialize(href:, icon: nil, active: false, external: false, **attributes)
20
+ @href = href
21
+ @icon = icon
22
+ @classes = [ BASE, (ACTIVE if active), attributes[:class] ].compact.join(' ')
23
+ @external_attributes = (' target="_blank" rel="noopener noreferrer"'.html_safe if external)
24
+ end
25
+ end
@@ -5,7 +5,7 @@ class TextInputComponent < ViewComponent::Base
5
5
 
6
6
  def initialize(attributes = {})
7
7
  @disabled = attributes[:disabled] || false
8
- attributes[:class] = "border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-xs#{" #{attributes[:class]}" if attributes[:class]}"
8
+ attributes[:class] = "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive#{" #{attributes[:class]}" if attributes[:class]}"
9
9
  @attributes = attributes.without(:disabled).map { |key, attribute| "#{key}=\"#{attribute}\"" }
10
10
  end
11
11
  end
@@ -0,0 +1,27 @@
1
+ # The initials circle beside a user's name. The Inertia stacks build this from
2
+ # shadcn's avatar and a useInitials hook; here it is one component.
3
+ class UserAvatarComponent < ViewComponent::Base
4
+ erb_template <<~ERB
5
+ <span class="<%= @classes %>"><%= @initials %></span>
6
+ ERB
7
+
8
+ def initialize(name, classes: nil)
9
+ @initials = initials(name)
10
+ @classes = [
11
+ 'flex size-8 shrink-0 items-center justify-center rounded-full bg-neutral-200 text-xs',
12
+ 'font-medium text-black dark:bg-neutral-700 dark:text-white',
13
+ classes
14
+ ].compact.join(' ')
15
+ end
16
+
17
+ private
18
+ def initials(name)
19
+ parts = name.to_s.strip.split(/\s+/).reject(&:empty?)
20
+
21
+ case parts.length
22
+ when 0 then ''
23
+ when 1 then parts.first[0].upcase
24
+ else "#{parts.first[0]}#{parts.last[0]}".upcase
25
+ end
26
+ end
27
+ end
@@ -1,4 +1,5 @@
1
1
  class ApplicationController < ActionController::Base
2
2
  include SetCurrentAuth
3
3
  include Authenticate
4
+ include SidebarState
4
5
  end
@@ -0,0 +1,14 @@
1
+ module SidebarState
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ helper_method :sidebar_open?
6
+ end
7
+
8
+ private
9
+ # The sidebar writes this cookie as it is toggled. Reading it here keeps the
10
+ # first paint in step with the last choice instead of flashing open.
11
+ def sidebar_open?
12
+ cookies[:sidebar_state].nil? || cookies[:sidebar_state] == 'true'
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ class Settings::AppearanceController < ApplicationController
2
+ skip_ensure_email_is_verified
3
+
4
+ def edit
5
+ render 'settings/appearance/edit'
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ class Settings::PasswordController < ApplicationController
2
+ skip_ensure_email_is_verified
3
+
4
+ def edit
5
+ @update_password_form = UpdatePasswordForm.new
6
+
7
+ render 'settings/password/edit'
8
+ end
9
+
10
+ def update
11
+ @update_password_form = UpdatePasswordForm.new(params.permit(:current_password, :password, :password_confirmation))
12
+
13
+ return render partial: 'settings/password/update_password_form', status: :unprocessable_entity if @update_password_form.invalid?
14
+
15
+ Current.auth.user.update(password: @update_password_form.password)
16
+
17
+ redirect_back_or_to password_edit_path, flash: { status: 'password-updated' }
18
+ end
19
+ end
@@ -1,18 +1,17 @@
1
- class ProfileController < ApplicationController
1
+ class Settings::ProfileController < ApplicationController
2
2
  skip_ensure_email_is_verified
3
3
 
4
4
  def edit
5
5
  @update_profile_information_form = UpdateProfileInformationForm.new(name: Current.auth.user.name, email: Current.auth.user.email)
6
- @update_password_form = UpdatePasswordForm.new
7
6
  @delete_user_form = DeleteUserForm.new
8
7
 
9
- render 'profile/edit'
8
+ render 'settings/profile/edit'
10
9
  end
11
10
 
12
11
  def update
13
12
  @update_profile_information_form = UpdateProfileInformationForm.new(params.permit(:name, :email))
14
13
 
15
- return render partial: 'profile/partials/update_profile_information_form', status: :unprocessable_entity if @update_profile_information_form.invalid?
14
+ return render partial: 'settings/profile/update_profile_information_form', status: :unprocessable_entity if @update_profile_information_form.invalid?
16
15
 
17
16
  @update_profile_information_form.update
18
17
 
@@ -16,7 +16,7 @@
16
16
  <!-- Remember Me -->
17
17
  <div class="block mt-4">
18
18
  <label for="remember_me" class="inline-flex items-center">
19
- <input id="remember_me" type="checkbox" class="rounded-sm dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-xs focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800" name="remember">
19
+ <input id="remember_me" type="checkbox" class="size-4 shrink-0 rounded-[4px] border border-input shadow-xs transition-shadow outline-none accent-primary focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50" name="remember">
20
20
  <span class="ms-2 text-sm text-gray-600 dark:text-gray-400">Remember me</span>
21
21
  </label>
22
22
  </div>
@@ -1,14 +1,9 @@
1
- <% content_for(:header) do %>
2
- <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
3
- Dashboard
4
- </h2>
1
+ <% content_for(:title, 'Dashboard') %>
2
+ <% content_for(:breadcrumbs) do %>
3
+ <%= render(BreadcrumbsComponent.new([ { title: 'Dashboard', href: dashboard_path } ])) %>
5
4
  <% end %>
6
- <div class="py-12">
7
- <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
8
- <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-xs sm:rounded-lg">
9
- <div class="p-6 text-gray-900 dark:text-gray-100">
10
- You're logged in!
11
- </div>
12
- </div>
5
+ <div class="flex h-full flex-1 flex-col gap-4 rounded-xl p-4">
6
+ <div class="relative min-h-[60vh] flex-1 overflow-hidden rounded-xl border border-sidebar-border/70 p-6 dark:border-sidebar-border">
7
+ You're logged in!
13
8
  </div>
14
9
  </div>
@@ -0,0 +1,18 @@
1
+ <%# Applies the stored appearance before first paint, so the page never flashes
2
+ the wrong theme. Also exposed as window.applyAppearance so the preference
3
+ control can re-apply it after a change. %>
4
+ <%= javascript_tag nonce: true do %>
5
+ window.applyAppearance = function () {
6
+ var preference = localStorage.getItem('appearance') || 'system'
7
+ var dark = preference === 'dark' ||
8
+ (preference === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
9
+
10
+ document.documentElement.classList.toggle('dark', dark)
11
+ }
12
+
13
+ window.applyAppearance()
14
+
15
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
16
+ window.applyAppearance()
17
+ })
18
+ <% end %>
@@ -0,0 +1,26 @@
1
+ <%# The settings shell: a heading, the sub-nav down the side, and the page's own
2
+ sections in the content column. %>
3
+ <div class="px-4 py-6">
4
+ <%= render(HeadingComponent.new(title: 'Settings', description: 'Manage your profile and account settings')) %>
5
+ <div class="flex flex-col lg:flex-row lg:space-x-12">
6
+ <aside class="w-full max-w-xl lg:w-48">
7
+ <nav class="flex flex-col space-y-1 space-x-0" aria-label="Settings">
8
+ <% [
9
+ { title: 'Profile', href: profile_edit_path },
10
+ { title: 'Password', href: password_edit_path },
11
+ { title: 'Appearance', href: appearance_edit_path }
12
+ ].each do |item| %>
13
+ <a href="<%= item[:href] %>" class="inline-flex h-8 w-full items-center justify-start gap-2 rounded-md px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground <%= 'bg-muted' if current_page?(item[:href]) %>">
14
+ <%= item[:title] %>
15
+ </a>
16
+ <% end %>
17
+ </nav>
18
+ </aside>
19
+ <div class="my-6 h-px w-full bg-border lg:hidden"></div>
20
+ <div class="flex-1 md:max-w-2xl">
21
+ <section class="max-w-xl space-y-12">
22
+ <%= yield %>
23
+ </section>
24
+ </div>
25
+ </div>
26
+ </div>
@@ -0,0 +1,56 @@
1
+ <%# Drawer backdrop. Only ever visible below md, where the sidebar overlays the
2
+ page rather than sitting beside it. %>
3
+ <div x-show="sidebarOpenMobile"
4
+ x-transition:enter="transition-opacity ease-linear duration-200"
5
+ x-transition:enter-start="opacity-0"
6
+ x-transition:leave="transition-opacity ease-linear duration-200"
7
+ x-transition:leave-end="opacity-0"
8
+ x-on:click="sidebarOpenMobile = false"
9
+ class="fixed inset-0 z-40 bg-black/50 md:hidden"
10
+ style="display: none;"></div>
11
+ <%# Holds the rail's width in the flow so the page content is never covered.
12
+ The sidebar itself is fixed, so without this it would overlap. %>
13
+ <div class="hidden shrink-0 transition-[width] duration-200 ease-linear md:block"
14
+ :class="sidebarOpen ? 'w-64' : 'w-16'"></div>
15
+ <%# One sidebar for both breakpoints: a drawer that slides in below md, a rail
16
+ that collapses to icons at md and up. %>
17
+ <div class="fixed inset-y-0 left-0 z-50 flex w-72 flex-col gap-2 bg-sidebar p-2 text-sidebar-foreground transition-all duration-200 ease-linear md:z-10 md:translate-x-0"
18
+ :class="[sidebarOpenMobile ? 'translate-x-0' : '-translate-x-full', sidebarOpen ? 'md:w-64' : 'md:w-16']">
19
+ <div class="flex flex-col gap-2">
20
+ <a href="<%= dashboard_path %>" class="flex h-12 items-center gap-2 overflow-hidden rounded-md p-2 text-left hover:bg-sidebar-accent hover:text-sidebar-accent-foreground">
21
+ <div class="flex aspect-square size-8 shrink-0 items-center justify-center rounded-md bg-sidebar-primary text-sidebar-primary-foreground">
22
+ <%= render(ApplicationLogoComponent.new({ class: "size-5 fill-current text-white dark:text-black" })) %>
23
+ </div>
24
+ <div x-show="sidebarOpen || sidebarOpenMobile" class="ml-1 grid flex-1 text-left text-sm">
25
+ <span class="mb-0.5 truncate leading-tight font-semibold">Rails</span>
26
+ </div>
27
+ </a>
28
+ </div>
29
+ <div class="flex min-h-0 flex-1 flex-col gap-2 overflow-auto">
30
+ <div class="relative flex w-full min-w-0 flex-col p-2">
31
+ <div x-show="sidebarOpen || sidebarOpenMobile" class="px-2 py-1 text-xs font-medium text-sidebar-foreground/70">Platform</div>
32
+ <ul class="flex w-full min-w-0 flex-col gap-1">
33
+ <li>
34
+ <%= render(SidebarMenuButtonComponent.new(href: dashboard_path, icon: "layout-grid", active: current_page?(dashboard_path))) do %>
35
+ Dashboard
36
+ <% end %>
37
+ </li>
38
+ </ul>
39
+ </div>
40
+ </div>
41
+ <div class="flex flex-col gap-2">
42
+ <ul class="flex w-full min-w-0 flex-col gap-1">
43
+ <li>
44
+ <%= render(SidebarMenuButtonComponent.new(href: "https://github.com/cuonggt/kaze", icon: "folder-git-2", external: true, class: "text-neutral-600 hover:text-neutral-800 dark:text-neutral-300 dark:hover:text-neutral-100")) do %>
45
+ Repository
46
+ <% end %>
47
+ </li>
48
+ <li>
49
+ <%= render(SidebarMenuButtonComponent.new(href: "https://github.com/cuonggt/kaze#readme", icon: "book-open", external: true, class: "text-neutral-600 hover:text-neutral-800 dark:text-neutral-300 dark:hover:text-neutral-100")) do %>
50
+ Documentation
51
+ <% end %>
52
+ </li>
53
+ </ul>
54
+ <%= render "layouts/sidebar_user" %>
55
+ </div>
56
+ </div>
@@ -0,0 +1,11 @@
1
+ <%# The bar above the page content: the rail trigger, then the page's own
2
+ breadcrumbs. %>
3
+ <header class="flex h-16 shrink-0 items-center gap-2 border-b border-sidebar-border/50 px-6 transition-[width,height] ease-linear md:px-4">
4
+ <div class="flex items-center gap-2">
5
+ <button x-on:click="toggle()" class="-ml-1 inline-flex size-7 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground" aria-label="Toggle sidebar">
6
+ <span x-show="sidebarOpen"><%= render(IconComponent.new('panel-left-close')) %></span>
7
+ <span x-show="! sidebarOpen" style="display: none;"><%= render(IconComponent.new('panel-left-open')) %></span>
8
+ </button>
9
+ <%= yield :breadcrumbs %>
10
+ </div>
11
+ </header>
@@ -0,0 +1,34 @@
1
+ <%# The user row at the foot of the sidebar. %>
2
+ <%= render DropdownComponent.new(align: 'left', side: 'top', width: 'w-[15rem]') do |component| %>
3
+ <% component.with_trigger do %>
4
+ <button class="flex h-12 w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm hover:bg-sidebar-accent hover:text-sidebar-accent-foreground">
5
+ <%= render(UserAvatarComponent.new(Current.auth.user.name)) %>
6
+ <span x-show="sidebarOpen || sidebarOpenMobile" class="grid flex-1 text-left text-sm leading-tight">
7
+ <span class="truncate font-medium"><%= Current.auth.user.name %></span>
8
+ </span>
9
+ <%= render(IconComponent.new('chevrons-up-down', classes: 'ml-auto size-4 shrink-0')) %>
10
+ </button>
11
+ <% end %>
12
+ <div class="px-1 py-1.5">
13
+ <div class="flex items-center gap-2 text-left text-sm">
14
+ <%= render(UserAvatarComponent.new(Current.auth.user.name)) %>
15
+ <div class="grid flex-1 leading-tight">
16
+ <span class="truncate font-medium"><%= Current.auth.user.name %></span>
17
+ <span class="truncate text-xs text-muted-foreground"><%= Current.auth.user.email %></span>
18
+ </div>
19
+ </div>
20
+ </div>
21
+ <div class="-mx-1 my-1 h-px bg-border"></div>
22
+ <%= render(DropdownLinkComponent.new({ href: profile_edit_path })) do %>
23
+ <%= render(IconComponent.new('settings')) %>
24
+ Settings
25
+ <% end %>
26
+ <div class="-mx-1 my-1 h-px bg-border"></div>
27
+ <form method="POST" action="<%= logout_path %>">
28
+ <input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
29
+ <%= render(DropdownLinkComponent.new({ href: logout_path, onclick: "event.preventDefault();this.closest('form').submit()" })) do %>
30
+ <%= render(IconComponent.new('log-out')) %>
31
+ Log Out
32
+ <% end %>
33
+ </form>
34
+ <% end %>
@@ -3,30 +3,40 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <title><%= content_for(:title) || "Rails" %></title>
6
+ <title><%= content_for?(:title) ? "#{content_for(:title)} - Rails" : "Rails" %></title>
7
7
  <%= csrf_meta_tags %>
8
8
  <%= csp_meta_tag %>
9
9
  <%= stylesheet_link_tag "tailwind", "data-turbo-track": Rails.env.production? ? "reload" : "" %>
10
10
  <%= stylesheet_link_tag "application", "data-turbo-track": Rails.env.production? ? "reload" : "" %>
11
+ <%= render "layouts/appearance" %>
11
12
  <%= javascript_importmap_tags %>
12
13
  <%= turbo_refreshes_with method: :morph, scroll: :preserve %>
13
14
  <%= yield :head %>
14
15
  </head>
15
16
  <body class="font-sans antialiased">
16
- <div class="min-h-screen bg-gray-100">
17
- <%= render "layouts/navigation" %>
18
- <!-- Page Heading -->
19
- <% if content_for?(:header) %>
20
- <header class="bg-white dark:bg-gray-800 shadow-sm">
21
- <div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
22
- <%= yield :header %>
23
- </div>
24
- </header>
25
- <% end %>
26
- <!-- Page Content -->
27
- <main>
28
- <%= yield %>
29
- </main>
17
+ <%# `sidebarOpen` is the rail's state at md and up, `sidebarOpenMobile` the drawer's below
18
+ it. One trigger drives whichever applies, and the rail's state is kept in
19
+ a cookie so the next page renders the way this one was left. %>
20
+ <div
21
+ x-data="{
22
+ sidebarOpen: <%= sidebar_open? %>,
23
+ sidebarOpenMobile: false,
24
+ toggle() {
25
+ if (window.matchMedia('(max-width: 767px)').matches) {
26
+ this.sidebarOpenMobile = ! this.sidebarOpenMobile
27
+ return
28
+ }
29
+ this.sidebarOpen = ! this.sidebarOpen
30
+ document.cookie = 'sidebar_state=' + this.sidebarOpen + '; path=/; max-age=604800; samesite=lax'
31
+ }
32
+ }"
33
+ class="flex min-h-svh w-full bg-sidebar"
34
+ >
35
+ <%= render "layouts/sidebar" %>
36
+ <div class="relative flex min-h-svh flex-1 flex-col overflow-hidden bg-background md:m-2 md:ml-0 md:rounded-xl md:shadow-sm">
37
+ <%= render "layouts/sidebar_header" %>
38
+ <main class="min-w-0 flex-1"><%= yield %></main>
39
+ </div>
30
40
  </div>
31
41
  </body>
32
42
  </html>
@@ -8,6 +8,7 @@
8
8
  <%= csp_meta_tag %>
9
9
  <%= stylesheet_link_tag "tailwind", "data-turbo-track": Rails.env.production? ? "reload" : "" %>
10
10
  <%= stylesheet_link_tag "application", "data-turbo-track": Rails.env.production? ? "reload" : "" %>
11
+ <%= render "layouts/appearance" %>
11
12
  <%= javascript_importmap_tags %>
12
13
  <%= turbo_refreshes_with method: :morph, scroll: :preserve %>
13
14
  </head>
@@ -0,0 +1,28 @@
1
+ <%# Alpine holds the preference; the buttons themselves are rendered here so the
2
+ icons come through the same component the rest of the layout uses. %>
3
+ <div
4
+ x-data="{
5
+ preference: localStorage.getItem('appearance') || 'system',
6
+ select(value) {
7
+ this.preference = value
8
+ localStorage.setItem('appearance', value)
9
+ window.applyAppearance()
10
+ }
11
+ }"
12
+ class="inline-flex gap-1 rounded-lg bg-neutral-100 p-1 dark:bg-neutral-800"
13
+ >
14
+ <% [ [ 'light', 'Light', 'sun' ], [ 'dark', 'Dark', 'moon' ], [ 'system', 'System', 'monitor' ] ].each do |value, label, icon| %>
15
+ <button
16
+ type="button"
17
+ @click="select('<%= value %>')"
18
+ :aria-pressed="preference === '<%= value %>'"
19
+ class="flex items-center rounded-md px-3.5 py-1.5 transition-colors outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
20
+ :class="preference === '<%= value %>'
21
+ ? 'bg-white shadow-xs dark:bg-neutral-700 dark:text-neutral-100'
22
+ : 'text-neutral-500 hover:bg-neutral-200/60 hover:text-black dark:text-neutral-400 dark:hover:bg-neutral-700/60'"
23
+ >
24
+ <%= render(IconComponent.new(icon, classes: '-ml-1 h-4 w-4')) %>
25
+ <span class="ml-1.5 text-sm"><%= label %></span>
26
+ </button>
27
+ <% end %>
28
+ </div>
@@ -0,0 +1,10 @@
1
+ <% content_for(:title, 'Appearance settings') %>
2
+ <% content_for(:breadcrumbs) do %>
3
+ <%= render(BreadcrumbsComponent.new([ { title: 'Appearance settings', href: appearance_edit_path } ])) %>
4
+ <% end %>
5
+ <%= render layout: 'layouts/settings' do %>
6
+ <div class="space-y-6">
7
+ <%= render(HeadingComponent.new(title: 'Appearance settings', description: 'Update the appearance settings for your account', variant: 'small')) %>
8
+ <%= render 'settings/appearance/update_appearance_form' %>
9
+ </div>
10
+ <% end %>
@@ -1,14 +1,6 @@
1
1
  <%= turbo_frame_tag 'update_password_form' do %>
2
2
  <section>
3
- <header>
4
- <h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
5
- Update Password
6
- </h2>
7
- <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
8
- Ensure your account is using a long, random password to stay secure.
9
- </p>
10
- </header>
11
- <%= form_with model: @update_password_form, url: password_update_path, method: "put", class: "mt-6 space-y-6" do |update_password_form| %>
3
+ <%= form_with model: @update_password_form, url: password_update_path, method: "put", class: "mt-6 space-y-6" do |update_password_form| %>
12
4
  <!-- Current Password -->
13
5
  <div>
14
6
  <%= render(InputLabelComponent.new({ for: "update_password_current_password", value: "Current Password" })) %>
@@ -37,7 +29,7 @@
37
29
  x-show="show"
38
30
  x-transition
39
31
  x-init="setTimeout(() => show = false, 2000)"
40
- class="text-sm text-gray-600 dark:text-gray-400">Saved.</p>
32
+ class="text-sm text-muted-foreground">Saved.</p>
41
33
  <% end %>
42
34
  </div>
43
35
  <% end %>
@@ -0,0 +1,10 @@
1
+ <% content_for(:title, 'Password settings') %>
2
+ <% content_for(:breadcrumbs) do %>
3
+ <%= render(BreadcrumbsComponent.new([ { title: 'Password settings', href: password_edit_path } ])) %>
4
+ <% end %>
5
+ <%= render layout: 'layouts/settings' do %>
6
+ <div class="space-y-6">
7
+ <%= render(HeadingComponent.new(title: 'Update password', description: 'Ensure your account is using a long, random password to stay secure', variant: 'small')) %>
8
+ <%= render 'settings/password/update_password_form' %>
9
+ </div>
10
+ <% end %>
@@ -1,10 +1,10 @@
1
1
  <div id="confirm_user_deletion_modal">
2
2
  <%= render(ModalComponent.new({ name: "confirm-user-deletion", show: @delete_user_form.error_messages.has_key?(:password), focusable: true })) do %>
3
3
  <%= form_with model: @delete_user_form, url: profile_destroy_path, method: "delete", class: "p-6" do %>
4
- <h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
4
+ <h2 class="text-lg font-medium text-foreground">
5
5
  Are you sure you want to delete your account?
6
6
  </h2>
7
- <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
7
+ <p class="mt-1 text-sm text-muted-foreground">
8
8
  Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.
9
9
  </p>
10
10
  <div class="mt-6">
@@ -0,0 +1,6 @@
1
+ <section class="space-y-6">
2
+ <%= render(DangerButtonComponent.new({"x-data": "", "x-on:click.prevent": "$dispatch('open-modal', 'confirm-user-deletion')"})) do %>
3
+ Delete Account
4
+ <% end %>
5
+ </section>
6
+ <%= render 'settings/profile/confirm_user_deletion_modal' %>
@@ -1,14 +1,6 @@
1
1
  <%= turbo_frame_tag 'update_profile_information_form' do %>
2
2
  <section>
3
- <header>
4
- <h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
5
- Profile Information
6
- </h2>
7
- <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
8
- Update your account's profile information and email address.
9
- </p>
10
- </header>
11
- <form id="send-verification" method="post" action="<%= verification_send_path %>">
3
+ <form id="send-verification" method="post" action="<%= verification_send_path %>">
12
4
  <input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
13
5
  </form>
14
6
  <%= form_with model: @update_profile_information_form, url: profile_update_path, method: "patch", class: "mt-6 space-y-6" do %>
@@ -25,9 +17,9 @@
25
17
  <%= render(InputErrorComponent.new({ class: "mt-2", message: @update_profile_information_form.error_messages[:email] })) %>
26
18
  <% if User.include?(MustVerifyEmail) && !Current.auth.user.has_verified_email? %>
27
19
  <div>
28
- <p class="text-sm mt-2 text-gray-800 dark:text-gray-200">
20
+ <p class="mt-2 text-sm text-foreground">
29
21
  Your email address is unverified.
30
- <button form="send-verification" class="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">
22
+ <button form="send-verification" class="rounded-md text-sm text-muted-foreground underline outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50">
31
23
  Click here to re-send the verification email.
32
24
  </button>
33
25
  </p>
@@ -49,7 +41,7 @@
49
41
  x-show="show"
50
42
  x-transition
51
43
  x-init="setTimeout(() => show = false, 2000)"
52
- class="text-sm text-gray-600 dark:text-gray-400">Saved.</p>
44
+ class="text-sm text-muted-foreground">Saved.</p>
53
45
  <% end %>
54
46
  </div>
55
47
  <% end %>
@@ -1,3 +1,3 @@
1
1
  <%= turbo_stream.replace 'confirm_user_deletion_modal' do %>
2
- <%= render 'profile/partials/confirm_user_deletion_modal' %>
2
+ <%= render 'settings/profile/confirm_user_deletion_modal' %>
3
3
  <% end %>
@@ -0,0 +1,14 @@
1
+ <% content_for(:title, 'Profile settings') %>
2
+ <% content_for(:breadcrumbs) do %>
3
+ <%= render(BreadcrumbsComponent.new([ { title: 'Profile settings', href: profile_edit_path } ])) %>
4
+ <% end %>
5
+ <%= render layout: 'layouts/settings' do %>
6
+ <div class="space-y-6">
7
+ <%= render(HeadingComponent.new(title: 'Profile information', description: 'Update your name and email address', variant: 'small')) %>
8
+ <%= render 'settings/profile/update_profile_information_form' %>
9
+ </div>
10
+ <div class="space-y-6">
11
+ <%= render(HeadingComponent.new(title: 'Delete account', description: 'Delete your account and all of its resources', variant: 'small')) %>
12
+ <%= render 'settings/profile/delete_user_form' %>
13
+ </div>
14
+ <% end %>
@@ -8,6 +8,7 @@
8
8
  <%= csp_meta_tag %>
9
9
  <%= stylesheet_link_tag "tailwind", "data-turbo-track": Rails.env.production? ? "reload" : "" %>
10
10
  <%= stylesheet_link_tag "application", "data-turbo-track": Rails.env.production? ? "reload" : "" %>
11
+ <%= render "layouts/appearance" %>
11
12
  <%= javascript_importmap_tags %>
12
13
  <%= turbo_refreshes_with method: :morph, scroll: :preserve %>
13
14
  </head>
@@ -3,7 +3,10 @@ module HandleInertiaRequests
3
3
 
4
4
  included do
5
5
  inertia_share do
6
- { auth: { user: Current.auth.get_user } }
6
+ {
7
+ auth: { user: Current.auth.get_user },
8
+ sidebar_open: cookies[:sidebar_state].nil? || cookies[:sidebar_state] == "true"
9
+ }
7
10
  end
8
11
  end
9
12
  end
@@ -0,0 +1,7 @@
1
+ class Settings::AppearanceController < ApplicationController
2
+ skip_ensure_email_is_verified
3
+
4
+ def edit
5
+ render inertia: 'Settings/Appearance'
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ class Settings::PasswordController < ApplicationController
2
+ skip_ensure_email_is_verified
3
+
4
+ def edit
5
+ render inertia: 'Settings/Password'
6
+ end
7
+
8
+ def update
9
+ form = UpdatePasswordForm.new(params.permit(:current_password, :password, :password_confirmation))
10
+
11
+ return redirect_to password_edit_path, inertia: { errors: form.error_messages } if form.invalid?
12
+
13
+ Current.auth.user.update(password: form.password)
14
+
15
+ redirect_back_or_to password_edit_path
16
+ end
17
+ end
@@ -1,8 +1,8 @@
1
- class ProfileController < ApplicationController
1
+ class Settings::ProfileController < ApplicationController
2
2
  skip_ensure_email_is_verified
3
3
 
4
4
  def edit
5
- render inertia: 'Profile/Edit', props: {
5
+ render inertia: 'Settings/Profile', props: {
6
6
  mustVerifyEmail: User.include?(MustVerifyEmail),
7
7
  status: flash[:status]
8
8
  }
@@ -0,0 +1,11 @@
1
+ require 'test_helper'
2
+
3
+ class AppearanceTest < ActionDispatch::IntegrationTest
4
+ test 'appearance page is displayed' do
5
+ user = FactoryBot.create(:user)
6
+
7
+ acting_as(user).get appearance_edit_path
8
+
9
+ assert_response :success
10
+ end
11
+ end