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.
- checksums.yaml +7 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +423 -0
- data/README.md +178 -0
- data/Rakefile +31 -0
- data/app/assets/builds/tailwind.css +2 -0
- data/app/controllers/concerns/authentication.rb +55 -0
- data/app/controllers/concerns/authentication.rb.tt +55 -0
- data/app/controllers/kern/application_controller.rb +7 -0
- data/app/controllers/kern/pages_controller.rb +6 -0
- data/app/controllers/kern/passwords_controller.rb +40 -0
- data/app/controllers/kern/sessions_controller.rb +30 -0
- data/app/controllers/kern/settings/users_controller.rb +26 -0
- data/app/controllers/kern/settings_controller.rb +6 -0
- data/app/controllers/kern/signups_controller.rb +31 -0
- data/app/controllers/kern/signups_controller.rb.tt +31 -0
- data/app/helpers/kern/component_helper.rb +15 -0
- data/app/helpers/kern/turbo_stream_actions_helper.rb +10 -0
- data/app/mailers/kern/application_mailer.rb +6 -0
- data/app/mailers/kern/passwords_mailer.rb +9 -0
- data/app/models/actor.rb +4 -0
- data/app/models/application_form.rb +12 -0
- data/app/models/current.rb +8 -0
- data/app/models/member/acting.rb +28 -0
- data/app/models/member/setup.rb +37 -0
- data/app/models/member.rb +10 -0
- data/app/models/role.rb +5 -0
- data/app/models/session.rb +5 -0
- data/app/models/signup.rb +38 -0
- data/app/models/user/workspace_member.rb +12 -0
- data/app/models/user.rb +14 -0
- data/app/models/workspace/members.rb +10 -0
- data/app/models/workspace/setup.rb +21 -0
- data/app/models/workspace.rb +7 -0
- data/app/views/components/_container.html.erb +2 -0
- data/app/views/components/_flash.html.erb +7 -0
- data/app/views/components/_heading.html.erb +12 -0
- data/app/views/components/flash/_message.html.erb +4 -0
- data/app/views/kern/pages/welcome.html.erb +42 -0
- data/app/views/kern/passwords/edit.html.erb +12 -0
- data/app/views/kern/passwords/new.html.erb +10 -0
- data/app/views/kern/passwords_mailer/reset.html.erb +6 -0
- data/app/views/kern/passwords_mailer/reset.html.erb.tt +6 -0
- data/app/views/kern/passwords_mailer/reset.text.erb +4 -0
- data/app/views/kern/passwords_mailer/reset.text.erb.tt +4 -0
- data/app/views/kern/sessions/new.html.erb +16 -0
- data/app/views/kern/settings/_cards.html.erb +10 -0
- data/app/views/kern/settings/show.html.erb +5 -0
- data/app/views/kern/settings/users/show.html.erb +15 -0
- data/app/views/kern/signups/new.html.erb +16 -0
- data/app/views/layouts/kern/application/_navigation.html.erb +30 -0
- data/app/views/layouts/kern/application.html.erb +31 -0
- data/app/views/layouts/kern/application.html.erb.tt +31 -0
- data/app/views/layouts/kern/auth.html.erb +42 -0
- data/bin/dev +4 -0
- data/bin/rails +14 -0
- data/bin/release +35 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20250101000001_create_users.rb +13 -0
- data/db/migrate/20250101000002_create_sessions.rb +11 -0
- data/db/migrate/20250101000003_create_workspaces.rb +12 -0
- data/db/migrate/20250101000004_create_members.rb +15 -0
- data/db/migrate/20250101000005_create_roles.rb +11 -0
- data/db/migrate/20250101000006_create_actors.rb +10 -0
- data/kern.gemspec +22 -0
- data/lib/generators/kern/feature/USAGE +17 -0
- data/lib/generators/kern/feature/feature_generator.rb +77 -0
- data/lib/generators/kern/helpers.rb +28 -0
- data/lib/generators/kern/install/USAGE +13 -0
- data/lib/generators/kern/install/install_generator.rb +48 -0
- data/lib/generators/kern/install/templates/configurations/README.md +33 -0
- data/lib/generators/kern/install/templates/configurations/urls.yml +9 -0
- data/lib/generators/kern/views/USAGE +22 -0
- data/lib/generators/kern/views/views_generator.rb +42 -0
- data/lib/kern/config.rb +25 -0
- data/lib/kern/engine.rb +17 -0
- data/lib/kern/form_builder/input.rb +100 -0
- data/lib/kern/form_builder/styles.rb +53 -0
- data/lib/kern/form_builder.rb +101 -0
- data/lib/kern/version.rb +3 -0
- data/lib/kern.rb +7 -0
- data/lib/sluggable.rb +66 -0
- data/lib/tasks/kern_tasks.rake +4 -0
- 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,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,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
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,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
|
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
|