kern 0.5.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 (84) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +22 -0
  3. data/Gemfile.lock +423 -0
  4. data/README.md +178 -0
  5. data/Rakefile +31 -0
  6. data/app/assets/builds/tailwind.css +2 -0
  7. data/app/controllers/concerns/authentication.rb +55 -0
  8. data/app/controllers/concerns/authentication.rb.tt +55 -0
  9. data/app/controllers/kern/application_controller.rb +7 -0
  10. data/app/controllers/kern/pages_controller.rb +6 -0
  11. data/app/controllers/kern/passwords_controller.rb +40 -0
  12. data/app/controllers/kern/sessions_controller.rb +30 -0
  13. data/app/controllers/kern/settings/users_controller.rb +26 -0
  14. data/app/controllers/kern/settings_controller.rb +6 -0
  15. data/app/controllers/kern/signups_controller.rb +31 -0
  16. data/app/controllers/kern/signups_controller.rb.tt +31 -0
  17. data/app/helpers/kern/component_helper.rb +15 -0
  18. data/app/helpers/kern/turbo_stream_actions_helper.rb +10 -0
  19. data/app/mailers/kern/application_mailer.rb +6 -0
  20. data/app/mailers/kern/passwords_mailer.rb +9 -0
  21. data/app/models/actor.rb +4 -0
  22. data/app/models/application_form.rb +12 -0
  23. data/app/models/current.rb +8 -0
  24. data/app/models/member/acting.rb +28 -0
  25. data/app/models/member/setup.rb +37 -0
  26. data/app/models/member.rb +10 -0
  27. data/app/models/role.rb +5 -0
  28. data/app/models/session.rb +5 -0
  29. data/app/models/signup.rb +38 -0
  30. data/app/models/user/workspace_member.rb +12 -0
  31. data/app/models/user.rb +14 -0
  32. data/app/models/workspace/members.rb +10 -0
  33. data/app/models/workspace/setup.rb +21 -0
  34. data/app/models/workspace.rb +7 -0
  35. data/app/views/components/_container.html.erb +2 -0
  36. data/app/views/components/_flash.html.erb +7 -0
  37. data/app/views/components/_heading.html.erb +12 -0
  38. data/app/views/components/flash/_message.html.erb +4 -0
  39. data/app/views/kern/pages/welcome.html.erb +42 -0
  40. data/app/views/kern/passwords/edit.html.erb +12 -0
  41. data/app/views/kern/passwords/new.html.erb +10 -0
  42. data/app/views/kern/passwords_mailer/reset.html.erb +6 -0
  43. data/app/views/kern/passwords_mailer/reset.html.erb.tt +6 -0
  44. data/app/views/kern/passwords_mailer/reset.text.erb +4 -0
  45. data/app/views/kern/passwords_mailer/reset.text.erb.tt +4 -0
  46. data/app/views/kern/sessions/new.html.erb +16 -0
  47. data/app/views/kern/settings/_cards.html.erb +10 -0
  48. data/app/views/kern/settings/show.html.erb +5 -0
  49. data/app/views/kern/settings/users/show.html.erb +15 -0
  50. data/app/views/kern/signups/new.html.erb +16 -0
  51. data/app/views/layouts/kern/application/_navigation.html.erb +30 -0
  52. data/app/views/layouts/kern/application.html.erb +31 -0
  53. data/app/views/layouts/kern/application.html.erb.tt +31 -0
  54. data/app/views/layouts/kern/auth.html.erb +42 -0
  55. data/bin/dev +4 -0
  56. data/bin/rails +14 -0
  57. data/bin/release +35 -0
  58. data/config/routes.rb +13 -0
  59. data/db/migrate/20250101000001_create_users.rb +13 -0
  60. data/db/migrate/20250101000002_create_sessions.rb +11 -0
  61. data/db/migrate/20250101000003_create_workspaces.rb +12 -0
  62. data/db/migrate/20250101000004_create_members.rb +15 -0
  63. data/db/migrate/20250101000005_create_roles.rb +11 -0
  64. data/db/migrate/20250101000006_create_actors.rb +10 -0
  65. data/kern.gemspec +22 -0
  66. data/lib/generators/kern/feature/USAGE +17 -0
  67. data/lib/generators/kern/feature/feature_generator.rb +77 -0
  68. data/lib/generators/kern/helpers.rb +28 -0
  69. data/lib/generators/kern/install/USAGE +13 -0
  70. data/lib/generators/kern/install/install_generator.rb +48 -0
  71. data/lib/generators/kern/install/templates/configurations/README.md +33 -0
  72. data/lib/generators/kern/install/templates/configurations/urls.yml +9 -0
  73. data/lib/generators/kern/views/USAGE +22 -0
  74. data/lib/generators/kern/views/views_generator.rb +42 -0
  75. data/lib/kern/config.rb +25 -0
  76. data/lib/kern/engine.rb +17 -0
  77. data/lib/kern/form_builder/input.rb +100 -0
  78. data/lib/kern/form_builder/styles.rb +53 -0
  79. data/lib/kern/form_builder.rb +101 -0
  80. data/lib/kern/version.rb +3 -0
  81. data/lib/kern.rb +7 -0
  82. data/lib/sluggable.rb +66 -0
  83. data/lib/tasks/kern_tasks.rake +4 -0
  84. metadata +139 -0
@@ -0,0 +1,12 @@
1
+ <%# locals: (level: :h1, breadcrumbs: [], actions: [], css: class_names("flex items-center gap-x-1 h-8 font-sans font-bold text-gray-950 *:inline-block", {"text-base font-bold tracking-tight md:text-lg": level == :h1, "text-sm font-semibold md:text-base": level == :h2, "text-xs font-medium md:text-sm": level == :h3})) %>
2
+ <%= tag.header id: "heading", class: class_names("flex items-center justify-between", {"px-4 py-5 border-b border-gray-200/50": level == :h1}) do %>
3
+ <%= tag.send(level, class: css) do %>
4
+ <%= safe_join(breadcrumbs.map.with_index {|it, index| tag.span(link_to(it[:label], it[:href], class: "hover:underline", id: "header_part_link_#{index}") + tag.span("/", class: "ml-1 font-normal text-gray-400"), id: "header_part_#{index}")}) if level == :h1 %>
5
+
6
+ <%= breadcrumbs.present? ? tag.span(yield, class: "text-gray-500") : tag.span(yield) %>
7
+ <% end %>
8
+
9
+ <%= tag.ul class: "shrink-0 flex items-center gap-x-2 leading-none" do %>
10
+ <%= safe_join(actions.map {tag.li(it)}) %>
11
+ <% end if actions.any? %>
12
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <%# locals: (type:, message:, icon: "check-circle", icon_css: class_names("size-3.5 shrink-0", {"text-emerald-300": type == "notice", "text-red-400": type == "alert"}), css: class_names("flex items-center gap-x-1.5 bg-gradient-to-b from-gray-900/80 to-gray-900 px-3 py-1.5 text-xs font-normal text-white ring ring-gray-700/90 rounded-md shadow-lg transition-all")) %>
2
+ <li class="translate-x-0 opacity-100 transition-all duration-300 ease-in-out starting:translate-x-4 starting:opacity-0">
3
+ <%= tag.p safe_join([(icon(icon, class: icon_css) rescue nil), message]), aria: {live: :polite}, class: css %>
4
+ </li>
@@ -0,0 +1,42 @@
1
+ <main class="mt-6 px-2 max-w-5xl mx-auto">
2
+ <h1 class="font-sans font-bold text-gray-950 text-base tracking-tight md:text-lg">
3
+ Welcome to Kern
4
+ </h1>
5
+
6
+ <p class="mt-2 text-sm md:text-base text-gray-800">
7
+ SaaS foundation for Ruby on Rails apps by <%= link_to "Rails Designer", "https://railsdesigner.com" %>. You are ready to build your next SaaS.
8
+ </p>
9
+
10
+ <section class="mt-6">
11
+ <h2 class="font-sans font-bold text-gray-950 text-sm tracking-tight md:text-base">
12
+ Get started
13
+ </h2>
14
+
15
+ <ul class="list-disc list-inside marker:text-blue-500">
16
+ <li>
17
+ add your own root route (<code>root to: "dashboard#show"</code>)
18
+ </li>
19
+
20
+ <li>
21
+ install <%= link_to "Rails Icons", "https://github.com/Rails-Designer/rails_icons" %>; Kern uses the <b>Phosphor</b> library
22
+ </li>
23
+
24
+ <li>
25
+ override any views by running <code>bin/rails g kern:views</code>
26
+ </li>
27
+
28
+ <li>
29
+ override any feature by running <code>bin/rals g kern:feature</code>
30
+ </li>
31
+
32
+ <li>
33
+ build your SaaS
34
+ </li>
35
+
36
+ <li>
37
+ profit 🤑
38
+ </li>
39
+ </ul>
40
+
41
+ </section>
42
+ </main>
@@ -0,0 +1,12 @@
1
+ <% content_for :title, "Update password" %>
2
+ <% content_for :page_title, "Update account password" %>
3
+
4
+ <%= form_with url: password_path(params[:token]), method: :put, class: "px-2 pb-2 md:px-4 md:pb-4" do |form| %>
5
+ <%= form.input :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72 %>
6
+
7
+ <%= form.input :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %>
8
+
9
+ <div class="buttons">
10
+ <%= form.submit "Save", class: "btn-primary" %>
11
+ </div>
12
+ <% end %>
@@ -0,0 +1,10 @@
1
+ <% content_for :title, "Reset your password" %>
2
+ <% content_for :page_title, "Reset your password" %>
3
+
4
+ <%= form_with url: passwords_path, class: "px-2 pb-2 md:px-4 md:pb-4" do |form| %>
5
+ <%= form.input :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %>
6
+
7
+ <div class="buttons">
8
+ <%= form.submit "Continue", class: "btn-primary" %>
9
+ </div>
10
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <p>
2
+ You can reset your password on
3
+ <%= link_to "this password reset page", kern.edit_password_url(@user.password_reset_token) %>.
4
+
5
+ This link will expire in <%= distance_of_time_in_words(0, @user.password_reset_token_expires_in) %>.
6
+ </p>
@@ -0,0 +1,6 @@
1
+ <p>
2
+ You can reset your password on
3
+ <%%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>.
4
+
5
+ This link will expire in <%%= distance_of_time_in_words(0, @user.password_reset_token_expires_in) %>.
6
+ </p>
@@ -0,0 +1,4 @@
1
+ You can reset your password on
2
+ <%= kern.edit_password_url(@user.password_reset_token) %>
3
+
4
+ This link will expire in <%= distance_of_time_in_words(0, @user.password_reset_token_expires_in) %>.
@@ -0,0 +1,4 @@
1
+ You can reset your password on
2
+ <%%= edit_password_url(@user.password_reset_token) %>
3
+
4
+ This link will expire in <%%= distance_of_time_in_words(0, @user.password_reset_token_expires_in) %>.
@@ -0,0 +1,16 @@
1
+ <% content_for :title, "Log in" %>
2
+ <% content_for :page_title, "Log in" %>
3
+
4
+ <%= form_with url: session_path, class: "px-2 md:px-4" do |form| %>
5
+ <%= form.input :email_address, type: :email, required: true, autofocus: true, autocomplete: "username", value: params[:email_address] %>
6
+
7
+ <%= form.input :password, required: true, autocomplete: "current-password", maxlength: 72, label_suffix: link_to("Forgot password?", new_password_path, class: "font-normal text-gray-400 hover:text-gray-500") %>
8
+
9
+ <div class="buttons">
10
+ <%= form.submit "Log in", data: {turbo_submits_with: "Logging in…"}, class: "btn-primary" %>
11
+ </div>
12
+ <% end %>
13
+
14
+ <p class="px-2 pt-4 pb-2 border-t border-gray-100 md:pb-4">
15
+ <%= link_to "No account yet?", new_signup_path, class: "block text-center text-sm text-gray-500 hover:underline" %>
16
+ </p>
@@ -0,0 +1,10 @@
1
+ <%# locals: (card_css: "col-span-12 md:col-span-4 lg:col-span-3", link_css: "block px-3 py-2 border border-gray-100 rounded-md transition ease-in-out delay-75 hover:border-gray-200 hover:shadow-lg/5", heading_css: "text-base font-bold tracking-tight text-gray-800", description_css: "mt-0.5 text-sm font-light text-gray-600") %>
2
+ <ul class="grid grid-cols-12">
3
+ <%= tag.li class: card_css do %>
4
+ <%= link_to settings_user_path, class: link_css do %>
5
+ <%= tag.h4 "Account", class: heading_css %>
6
+
7
+ <%= tag.p "Update your email and password", class: description_css %>
8
+ <% end %>
9
+ <% end %>
10
+ </ul>
@@ -0,0 +1,5 @@
1
+ <% content_for :title, "Settings" %>
2
+
3
+ <%= component("heading") { "Settings" } %>
4
+
5
+ <%= component("container") { render "kern/settings/cards" } %>
@@ -0,0 +1,15 @@
1
+ <% content_for :title, "Account" %>
2
+
3
+ <%= component("heading", breadcrumbs: [{label: "Settings", href: settings_path}]) { "Account" } %>
4
+
5
+ <%= component "container" do %>
6
+ <%= form_with model: [:settings, @user], class: "max-w-lg" do |form| %>
7
+ <%= form.input :email_address %>
8
+
9
+ <%= form.input :password %>
10
+
11
+ <div class="buttons">
12
+ <%= form.submit "Save", class: "btn-primary" %>
13
+ </div>
14
+ <% end %>
15
+ <% end %>
@@ -0,0 +1,16 @@
1
+ <% content_for :title, "Create your account" %>
2
+ <% content_for :page_title, "Create your account" %>
3
+
4
+ <%= form_with model: @signup, url: signup_path, class: "px-2 md:px-4" do |form| %>
5
+ <%= form.input :email_address %>
6
+
7
+ <%= form.input :password %>
8
+
9
+ <div class="buttons">
10
+ <%= form.submit "Sign up", data: {turbo_submits_with: "Signing up…"}, class: "btn-primary" %>
11
+ </div>
12
+ <% end %>
13
+
14
+ <p class="px-2 pt-4 pb-2 border-t border-gray-100 md:pb-4">
15
+ <%= link_to "Already have an account?", new_session_path, class: "block text-center text-sm font-medium text-gray-500 hover:underline" %>
16
+ </p>
@@ -0,0 +1,30 @@
1
+ <%# locals: (links: [], link_css: "group/link flex items-center gap-x-2 px-3 py-2 rounded-md hover:bg-gray-200/40") %>
2
+ <nav class="w-16 shrink-0 px-2 bg-gray-50 border-r border-gray-100 lg:px-6 lg:w-60">
3
+ <div class="sticky top-0 h-dvh pb-4 flex flex-col justify-between">
4
+ <div class="mt-7">
5
+ <header class="flex items-center gap-x-1.5 px-3">
6
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class="size-4.5 shrink-0 fill-brand-400 text-brand-600"><path d="M223.85,47.12a16,16,0,0,0-15-15c-12.58-.75-44.73.4-71.41,27.07L132.69,64H74.36A15.91,15.91,0,0,0,63,68.68L28.7,103a16,16,0,0,0,9.07,27.16l38.47,5.37,44.21,44.21,5.37,38.49a15.94,15.94,0,0,0,10.78,12.92,16.11,16.11,0,0,0,5.1.83A15.91,15.91,0,0,0,153,227.3L187.32,193A15.91,15.91,0,0,0,192,181.64V123.31l4.77-4.77C223.45,91.86,224.6,59.71,223.85,47.12ZM74.36,80h42.33L77.16,119.52,40,114.34Zm74.41-9.45a76.65,76.65,0,0,1,59.11-22.47,76.46,76.46,0,0,1-22.42,59.16L128,164.68,91.32,128ZM176,181.64,141.67,216l-5.19-37.17L176,139.31Zm-74.16,9.5C97.34,201,82.29,224,40,224a8,8,0,0,1-8-8c0-42.29,23-57.34,32.86-61.85a8,8,0,0,1,6.64,14.56c-6.43,2.93-20.62,12.36-23.12,38.91,26.55-2.5,36-16.69,38.91-23.12a8,8,0,1,1,14.56,6.64Z"/></svg>
7
+
8
+ <%= tag.small Rails.application.name.humanize, class: "sr-only text-sm font-extrabold tracking-tight text-slate-800 lg:not-sr-only" %>
9
+ </header>
10
+
11
+ <ul class="flex flex-col mt-8 gap-y-0.5">
12
+ <% links.each do |link| %>
13
+ <li>
14
+ <%= link_to link[:href], class: class_names(link_css, {"bg-gray-200/40": current_page?(link[:href])}) do %>
15
+ <%= icon link[:icon], variant: :duotone, class: class_names("size-4 shrink-0 text-current group-hover/link:text-brand-700 group-hover/link:fill-brand-700", {"text-gray-700 fill-brand-600/60": current_page?(link[:href])}) rescue nil %>
16
+
17
+ <%= tag.span link[:label], class: "sr-only text-sm font-medium text-gray-800 group-hover/link:text-brand-950 lg:not-sr-only" %>
18
+ <% end %>
19
+ </li>
20
+ <% end %>
21
+ </ul>
22
+ </div>
23
+
24
+ <%= link_to kern.settings_path, class: link_css do %>
25
+ <%= icon "gear-six", variant: :duotone, class: "size-4 shrink-0 text-current group-hover/link:text-brand-700 group-hover/link:fill-brand-700" rescue nil %>
26
+
27
+ <span class="sr-only shrink-0 text-sm font-medium text-gray-800 group-hover/link:text-gray-950 lg:not-sr-only">Settings</span>
28
+ <% end %>
29
+ </div>
30
+ </nav>
@@ -0,0 +1,31 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>
5
+ <%= content_for?(:title)? "#{yield(:title)} - #{Rails.application.name.humanize}" : Rails.application.name.humanize %>
6
+ </title>
7
+ <meta name="viewport" content="width=device-width,initial-scale=1">
8
+ <%= csrf_meta_tags %>
9
+ <%= csp_meta_tag %>
10
+
11
+ <%= stylesheet_link_tag :tailwind, "data-turbo-track": "reload" %>
12
+ </head>
13
+
14
+ <body class="<%= class_names("antialiased bg-white selection:bg-pink-200/40 selection:text-pink-800/70", yield(:body_class)) %>">
15
+ <div class="flex">
16
+ <%= render partial: "layouts/kern/application/navigation",
17
+ locals: {
18
+ links: [
19
+ {label: "Home", href: main_app.respond_to?(:root_path) ? main_app.root_path : root_path, icon: "house-line"},
20
+ ]
21
+ }
22
+ %>
23
+
24
+ <div class="flex-1">
25
+ <%= yield %>
26
+ </div>
27
+ </div>
28
+
29
+ <%= component "flash" %>
30
+ </body>
31
+ </html>
@@ -0,0 +1,31 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>
5
+ <%%= content_for?(:title)? "#{yield(:title)} - #{Rails.application.name.humanize}" : Rails.application.name.humanize %>
6
+ </title>
7
+ <meta name="viewport" content="width=device-width,initial-scale=1">
8
+ <%%= csrf_meta_tags %>
9
+ <%%= csp_meta_tag %>
10
+
11
+ <%%= stylesheet_link_tag :tailwind, "data-turbo-track": "reload" %>
12
+ </head>
13
+
14
+ <body class="<%%= class_names("antialiased bg-white selection:bg-pink-200/40 selection:text-pink-800/70", yield(:body_class)) %>">
15
+ <div class="flex">
16
+ <%%= render partial: "layouts/kern/application/navigation",
17
+ locals: {
18
+ links: [
19
+ {label: "Home", href: root_path, icon: "house-line"},
20
+ ]
21
+ }
22
+ %>
23
+
24
+ <div class="flex-1">
25
+ <%%= yield %>
26
+ </div>
27
+ </div>
28
+
29
+ <%%= component "flash" %>
30
+ </body>
31
+ </html>
@@ -0,0 +1,42 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>
5
+ <%= content_for?(:title)? "#{yield(:title)} - #{Rails.application.name.humanize}" : Rails.application.name.humanize %>
6
+ </title>
7
+ <meta name="viewport" content="width=device-width,initial-scale=1">
8
+ <meta name="robots" content="noindex, nofollow">
9
+ <%= csrf_meta_tags %>
10
+ <%= csp_meta_tag %>
11
+
12
+ <%= stylesheet_link_tag :tailwind, "data-turbo-track": "reload" %>
13
+ </head>
14
+
15
+ <body class="relative antialiased overflow-clip">
16
+ <svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="absolute -top-[400px] -left-[350px] size-[800px] text-brand-500">
17
+ <path fill="currentColor" d="M42.9,-43.4C60.9,-36,84.8,-27.6,92.2,-12.2C99.8,3.1,91.2,25.4,76.1,37C61.1,48.5,39.7,49.3,21.9,52.7C4,56.1,-10.4,62,-26.6,61.1C-42.9,60.3,-61.1,52.9,-61.9,41C-62.6,28.8,-46,12.1,-42.2,-7C-38.4,-25.9,-47.4,-46.5,-42.5,-56.1C-37.6,-65.8,-18.8,-64.5,-3.2,-60.7C12.4,-57,24.9,-50.8,42.9,-43.4Z" transform="translate(100 100)" />
18
+ </svg>
19
+
20
+ <svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="absolute -bottom-[450px] -right-[300px] size-[800px] rotate-32 text-cyan-400">
21
+ <path fill="currentColor" d="M42.9,-43.4C60.9,-36,84.8,-27.6,92.2,-12.2C99.8,3.1,91.2,25.4,76.1,37C61.1,48.5,39.7,49.3,21.9,52.7C4,56.1,-10.4,62,-26.6,61.1C-42.9,60.3,-61.1,52.9,-61.9,41C-62.6,28.8,-46,12.1,-42.2,-7C-38.4,-25.9,-47.4,-46.5,-42.5,-56.1C-37.6,-65.8,-18.8,-64.5,-3.2,-60.7C12.4,-57,24.9,-50.8,42.9,-43.4Z" transform="translate(100 100)" />
22
+ </svg>
23
+
24
+ <main class="grid place-items-center h-screen">
25
+ <div class="w-full max-w-xl">
26
+ <%= link_to Config::Urls.site, class: "flex justify-center items-center size-6 mx-auto text-brand-500 rounded-full transition ease-in-out hover:text-brand-800 hover:bg-brand-50 hover:scale-105" do %>
27
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class="size-5 shrink-0 fill-brand-400"><path d="M223.85,47.12a16,16,0,0,0-15-15c-12.58-.75-44.73.4-71.41,27.07L132.69,64H74.36A15.91,15.91,0,0,0,63,68.68L28.7,103a16,16,0,0,0,9.07,27.16l38.47,5.37,44.21,44.21,5.37,38.49a15.94,15.94,0,0,0,10.78,12.92,16.11,16.11,0,0,0,5.1.83A15.91,15.91,0,0,0,153,227.3L187.32,193A15.91,15.91,0,0,0,192,181.64V123.31l4.77-4.77C223.45,91.86,224.6,59.71,223.85,47.12ZM74.36,80h42.33L77.16,119.52,40,114.34Zm74.41-9.45a76.65,76.65,0,0,1,59.11-22.47,76.46,76.46,0,0,1-22.42,59.16L128,164.68,91.32,128ZM176,181.64,141.67,216l-5.19-37.17L176,139.31Zm-74.16,9.5C97.34,201,82.29,224,40,224a8,8,0,0,1-8-8c0-42.29,23-57.34,32.86-61.85a8,8,0,0,1,6.64,14.56c-6.43,2.93-20.62,12.36-23.12,38.91,26.55-2.5,36-16.69,38.91-23.12a8,8,0,1,1,14.56,6.64Z"/></svg>
28
+ <% end %>
29
+
30
+ <div class="w-full mt-6 p-2 ring ring-gray-200/50 bg-white/50 backdrop-blur-md md:rounded-xl md:shadow-lg">
31
+ <section class="flex flex-col gap-y-5 bg-white ring-gray-200/60 md:ring md:rounded-md md:shadow-lg">
32
+ <%= tag.h1 yield(:page_title), class: "mx-2 mt-8 font-sans text-center text-base font-bold text-gray-800 md:mx-4" %>
33
+
34
+ <%= yield %>
35
+ </section>
36
+ </div>
37
+ </div>
38
+ </main>
39
+
40
+ <%= component "flash" %>
41
+ </body>
42
+ </html>
data/bin/dev ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+
3
+ cd test/dummy
4
+ exec bin/dev "$@"
data/bin/rails ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path("..", __dir__)
6
+ ENGINE_PATH = File.expand_path("../lib/kern/engine", __dir__)
7
+ APP_PATH = File.expand_path("../test/dummy/config/application", __dir__)
8
+
9
+ # Set up gems listed in the Gemfile.
10
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
11
+ require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
12
+
13
+ require "rails/all"
14
+ require "rails/engine/commands"
data/bin/release ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+
3
+ VERSION=$1
4
+
5
+ if [ -z "$VERSION" ]; then
6
+ echo "Error: Version number or bump type is required."
7
+ echo "Usage: $0 <major|minor|patch|version-number>"
8
+
9
+ exit 1
10
+ fi
11
+
12
+ # If `VERSION` is a keyword, bump the current version
13
+ if [[ "$VERSION" =~ ^(major|minor|patch)$ ]]; then
14
+ CURRENT=$(grep -oP 'VERSION = "\K[^"]+' ./lib/kern/version.rb)
15
+ IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
16
+
17
+ case $VERSION in
18
+ major) VERSION="$((MAJOR + 1)).0.0" ;;
19
+ minor) VERSION="$MAJOR.$((MINOR + 1)).0" ;;
20
+ patch) VERSION="$MAJOR.$MINOR.$((PATCH + 1))" ;;
21
+ esac
22
+ fi
23
+
24
+ printf "module Kern\n VERSION = \"$VERSION\"\nend\n" > ./lib/kern/version.rb
25
+
26
+ bundle
27
+
28
+ git add Gemfile.lock lib/kern/version.rb
29
+ git commit -m "Bump version for $VERSION"
30
+ git push
31
+ git tag v$VERSION
32
+ git push --tags
33
+
34
+ rake build
35
+ gem push "pkg/kern-$VERSION.gem"
data/config/routes.rb ADDED
@@ -0,0 +1,13 @@
1
+ Kern::Engine.routes.draw do
2
+ resource :settings, only: %w[show]
3
+ namespace :settings do
4
+ resource :user, path: "account", only: %w[show update]
5
+ end
6
+
7
+ resource :signup, only: %w[new create]
8
+
9
+ resource :session, only: %w[new create destroy]
10
+ resources :passwords, param: :token, only: %w[new create edit update]
11
+
12
+ root to: "pages#welcome" unless Rails.application.routes.named_routes.key?(:root)
13
+ end
@@ -0,0 +1,13 @@
1
+ class CreateUsers < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :users do |t|
4
+ t.string :email_address, null: false
5
+ t.string :password_digest, null: false
6
+ t.integer :current_workspace_id, null: true
7
+
8
+ t.timestamps
9
+ end
10
+
11
+ add_index :users, :email_address, unique: true
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ class CreateSessions < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :sessions do |t|
4
+ t.references :user, null: false, foreign_key: true
5
+ t.string :ip_address, null: false
6
+ t.string :user_agent, null: false
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ class CreateWorkspaces < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :workspaces do |t|
4
+ t.string :slug, null: false
5
+ t.text :name, null: false
6
+
7
+ t.timestamps
8
+ end
9
+
10
+ add_index :workspaces, :slug, unique: true
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ class CreateMembers < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :members do |t|
4
+ t.belongs_to :user, null: false, foreign_key: true
5
+ t.belongs_to :workspace, null: false, foreign_key: true
6
+ t.string :slug, null: false
7
+ t.datetime :deleted_at, null: true
8
+
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :members, [:slug, :workspace_id], unique: true
13
+ add_index :members, :deleted_at, using: :brin
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ class CreateRoles < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :roles do |t|
4
+ t.string :name, null: false
5
+
6
+ t.timestamps
7
+ end
8
+
9
+ add_index :roles, :name, unique: true
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ class CreateActors < ActiveRecord::Migration[8.1]
2
+ def change
3
+ create_table :actors do |t|
4
+ t.belongs_to :member, null: false, foreign_key: true
5
+ t.belongs_to :role, null: false, foreign_key: true
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
data/kern.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ require_relative "lib/kern/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "kern"
5
+ spec.version = Kern::VERSION
6
+ spec.authors = ["Rails Designer"]
7
+ spec.email = "devs@railsdeigner.com"
8
+
9
+ spec.summary = "Rails engine with auth, billing, and common components for SaaS apps"
10
+ spec.description = "A Rails engine that handles the SaaS essentials: authentication, team invitations, subscription/billing, and common partials. Skip the boilerplate and start shipping the actual product. Launch faster."
11
+ spec.homepage = "https://saas.railsdesigner.com/"
12
+ spec.license = "MIT"
13
+
14
+ spec.metadata["homepage_uri"] = spec.homepage
15
+ spec.metadata["source_code_uri"] = "https://github.com/Rails-Designer/kern/"
16
+
17
+ spec.files = Dir["{bin,app,db,config,lib}/**/*", "Rakefile", "README.md", "kern.gemspec", "Gemfile", "Gemfile.lock"]
18
+
19
+ spec.required_ruby_version = ">= 3.4.0"
20
+
21
+ spec.add_dependency "rails", ">= 8.0.0"
22
+ end
@@ -0,0 +1,17 @@
1
+ Description:
2
+ Generates Kern foundational components (models, controllers, views and routes).
3
+
4
+ Example:
5
+ rails generate kern:feature sessions
6
+
7
+ This will generate the sessions feature with model, controller, views and routes.
8
+
9
+ rails generate kern:feature sessions passwords
10
+
11
+ This will generate sessions and passwords features.
12
+
13
+ Available features:
14
+ passwords Password reset functionality
15
+ sessions User authentication (sign in/out)
16
+ settings User settings (user, workspace settings and subscription/billing)
17
+ signups User registration
@@ -0,0 +1,77 @@
1
+ module Kern
2
+ class FeatureGenerator < Rails::Generators::Base
3
+ source_root File.expand_path("../../../../../", __FILE__)
4
+
5
+ AVAILABLE_FEATURES = %w[passwords sessions settings signups]
6
+
7
+ argument :features, type: :array, required: true, banner: "feature feature"
8
+
9
+ def copy_features
10
+ features.each do |feature|
11
+ verify_exists!(feature)
12
+
13
+ case feature
14
+ when "sessions"
15
+ generate_sessions
16
+ when "signups"
17
+ generate_signups
18
+ when "passwords"
19
+ generate_passwords
20
+ when "settings"
21
+ generate_settings
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def verify_exists!(feature)
29
+ return if AVAILABLE_FEATURES.include?(feature)
30
+
31
+ say "Feature `#{feature}` not found. Available features: #{AVAILABLE_FEATURES.join(", ")}", :red
32
+
33
+ exit(1)
34
+ end
35
+
36
+ def generate_sessions
37
+ directory "app/models", "app/models"
38
+ template "app/controllers/concerns/authentication.rb.tt", "app/controllers/concerns/authentication.rb"
39
+ copy_file "app/controllers/kern/sessions_controller.rb", "app/controllers/sessions_controller.rb"
40
+ directory "app/views/kern/sessions", "app/views/sessions"
41
+
42
+ route "resource :session, only: %w[new create destroy]"
43
+ end
44
+
45
+ def generate_signups
46
+ directory "app/models", "app/models" unless features.include?("sessions")
47
+
48
+ template "app/controllers/kern/signups_controller.rb.tt", "app/controllers/signups_controller.rb"
49
+ directory "app/views/kern/signups", "app/views/signups"
50
+
51
+ route "resource :signup, only: %w[new create]"
52
+ end
53
+
54
+ def generate_passwords
55
+ directory "app/views/kern/passwords", "app/views/passwords"
56
+ copy_file "app/controllers/kern/passwords_controller.rb", "app/controllers/passwords_controller.rb"
57
+ copy_file "app/mailers/kern/passwords_mailer.rb", "app/mailers/passwords_mailer.rb"
58
+ template "app/views/kern/passwords_mailer/reset.text.erb.tt", "app/views/passwords_mailer/reset.text.erb"
59
+ template "app/views/kern/passwords_mailer/reset.html.erb.tt", "app/views/passwords_mailer/reset.html.erb"
60
+
61
+ route "resources :passwords, param: :token, only: %w[new create edit update]"
62
+ end
63
+
64
+ def generate_settings
65
+ directory "app/views/kern/settings", "app/views/settings"
66
+ copy_file "app/controllers/kern/settings_controller.rb", "app/controllers/settings_controller.rb"
67
+ directory "app/controllers/kern/settings", "app/controllers/settings"
68
+
69
+ route <<~RUBY
70
+ resource :settings, only: %w[show]
71
+ namespace :settings do
72
+ resource :user, path: "account", only: %w[show update]
73
+ end
74
+ RUBY
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,28 @@
1
+ module Kern
2
+ module Helpers
3
+ def bundle_command(command, env = {}, params = {})
4
+ say_status :run, "bundle #{command}"
5
+
6
+ # taken from railties/lib/rails/generators/bundle_helper.rb
7
+ bundle_command = Gem.bin_path("bundler", "bundle")
8
+
9
+ require "bundler"
10
+
11
+ Bundler.with_original_env do
12
+ exec_bundle_command(bundle_command, command, env, params)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def exec_bundle_command(bundle_command, command, env, params)
19
+ full_command = %("#{Gem.ruby}" "#{bundle_command}" #{command})
20
+
21
+ if options[:quiet] || params[:quiet]
22
+ system(env, full_command, out: File::NULL)
23
+ else
24
+ system(env, full_command)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,13 @@
1
+ Description:
2
+ Installs Kern into your Rails application.
3
+
4
+ Example:
5
+ rails generate kern:install
6
+
7
+ This will:
8
+ - Run Kern migrations
9
+ - Inject Authentication concern into ApplicationController
10
+ - Set Kern layout in ApplicationController
11
+ - Install Rails Icons (optional)
12
+ - Copy configuration files
13
+ - Mount Kern engine routes