kanso 0.1.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/MIT-LICENSE +20 -0
- data/README.md +352 -0
- data/Rakefile +8 -0
- data/app/assets/images/kanso/icons/check-circle.svg +3 -0
- data/app/assets/images/kanso/icons/chevron-down.svg +3 -0
- data/app/assets/images/kanso/icons/exclamation-circle.svg +3 -0
- data/app/assets/images/kanso/icons/exclamation-triangle.svg +3 -0
- data/app/assets/images/kanso/icons/information-circle.svg +3 -0
- data/app/assets/images/kanso/icons/question-circle.svg +3 -0
- data/app/assets/images/kanso/icons/x-circle.svg +3 -0
- data/app/assets/images/kanso/icons/x-mark.svg +3 -0
- data/app/assets/javascripts/kanso/controllers/dropdown_controller.js +52 -0
- data/app/assets/javascripts/kanso/controllers/form_controller.js +17 -0
- data/app/assets/javascripts/kanso/controllers/modal_controller.js +48 -0
- data/app/assets/javascripts/kanso/controllers/notification_controller.js +43 -0
- data/app/assets/javascripts/kanso/helpers/transition.js +49 -0
- data/app/components/kanso/button_component.rb +58 -0
- data/app/components/kanso/class_combinable.rb +25 -0
- data/app/components/kanso/dropdown_component.html.erb +19 -0
- data/app/components/kanso/dropdown_component.rb +10 -0
- data/app/components/kanso/form_field_component.html.erb +24 -0
- data/app/components/kanso/form_field_component.rb +51 -0
- data/app/components/kanso/form_field_skeleton_component.html.erb +7 -0
- data/app/components/kanso/form_field_skeleton_component.rb +11 -0
- data/app/components/kanso/icon_component.rb +30 -0
- data/app/components/kanso/modal_component.html.erb +53 -0
- data/app/components/kanso/modal_component.rb +49 -0
- data/app/components/kanso/notification_component.html.erb +25 -0
- data/app/components/kanso/notification_component.rb +58 -0
- data/app/controllers/kanso/application_controller.rb +4 -0
- data/app/helpers/kanso/application_helper.rb +4 -0
- data/config/importmap.rb +5 -0
- data/config/routes.rb +2 -0
- data/lib/generators/kanso/install/install_generator.rb +209 -0
- data/lib/kanso/engine.rb +11 -0
- data/lib/kanso/version.rb +3 -0
- data/lib/kanso.rb +6 -0
- data/lib/tasks/kanso_tasks.rake +4 -0
- metadata +192 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kanso
|
|
4
|
+
class ButtonComponent < ViewComponent::Base
|
|
5
|
+
include Kanso::ClassCombinable
|
|
6
|
+
|
|
7
|
+
THEME_CLASSES = {
|
|
8
|
+
primary: "bg-indigo-500 text-white hover:bg-indigo-600 active:bg-indigo-700",
|
|
9
|
+
danger: "bg-red-600 text-white hover:bg-red-700 active:bg-red-800",
|
|
10
|
+
default: "bg-white text-indigo-600 border border-indigo-600 focus:ring-indigo-500/50 hover:bg-indigo-50 hover:border-indigo-700 active:bg-indigo-700 active:text-white"
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
def initialize(theme: :default, tag: :button, url: nil, method: nil, **options)
|
|
14
|
+
@theme = theme
|
|
15
|
+
@tag = tag
|
|
16
|
+
@url = url
|
|
17
|
+
@method = method
|
|
18
|
+
@options = options
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call
|
|
22
|
+
if @url
|
|
23
|
+
button_to(@url, method: @method, **html_options) do
|
|
24
|
+
content
|
|
25
|
+
end
|
|
26
|
+
else
|
|
27
|
+
content_tag(@tag, content, html_options)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def html_options
|
|
34
|
+
final_options = @options.dup
|
|
35
|
+
user_provided_class_value = final_options.delete(:class)
|
|
36
|
+
combined_class_string = combine_classes(classes, user_provided_class_value)
|
|
37
|
+
final_options.deep_merge(class: combined_class_string)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def classes
|
|
41
|
+
[ base_classes, theme_classes ].join(" ")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def base_classes
|
|
45
|
+
"inline-flex items-center justify-center gap-x-2 " +
|
|
46
|
+
"w-full sm:w-auto rounded-full font-semibold whitespace-nowrap select-none no-underline text-center px-5 py-2 " +
|
|
47
|
+
"shadow-md " +
|
|
48
|
+
"transform transition duration-200 " +
|
|
49
|
+
"hover:cursor-pointer " +
|
|
50
|
+
"active:translate-y-px active:shadow-none " +
|
|
51
|
+
"disabled:cursor-not-allowed disabled:opacity-50"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def theme_classes
|
|
55
|
+
THEME_CLASSES.fetch(@theme, THEME_CLASSES[:default])
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Kanso
|
|
2
|
+
module ClassCombinable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
def combine_classes(component_classes_string, user_classes_value)
|
|
6
|
+
component_classes_array = component_classes_string.split(" ")
|
|
7
|
+
|
|
8
|
+
user_classes_array = clean_and_split_classes(user_classes_value)
|
|
9
|
+
|
|
10
|
+
(component_classes_array + user_classes_array).compact.uniq.join(" ")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def clean_and_split_classes(value)
|
|
16
|
+
if value.nil?
|
|
17
|
+
[]
|
|
18
|
+
elsif value.kind_of?(Array)
|
|
19
|
+
value
|
|
20
|
+
else
|
|
21
|
+
value.split(" ")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<div data-controller="kanso--dropdown" class="relative inline-block text-left">
|
|
2
|
+
<div data-action="click->kanso--dropdown#toggle">
|
|
3
|
+
<%= trigger %>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div data-kanso--dropdown-target="panel"
|
|
7
|
+
class="hidden absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none transition ease-out duration-100"
|
|
8
|
+
role="menu"
|
|
9
|
+
aria-orientation="vertical"
|
|
10
|
+
tabindex="-1"
|
|
11
|
+
data-action="
|
|
12
|
+
keydown.esc@window->kanso--dropdown#close
|
|
13
|
+
click@window->kanso--dropdown#closeOutside
|
|
14
|
+
">
|
|
15
|
+
|
|
16
|
+
<%= content %>
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<div data-controller="kanso--form" class="mb-4">
|
|
2
|
+
<%= form.label attribute, for: field_id, class: "block text-sm font-medium leading-6 text-slate-900" %>
|
|
3
|
+
|
|
4
|
+
<div class="relative mt-1">
|
|
5
|
+
<%= form.send(type, attribute, field_options) %>
|
|
6
|
+
|
|
7
|
+
<% if has_errors? %>
|
|
8
|
+
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
|
9
|
+
<%= render Kanso::IconComponent.new(name: "exclamation-circle", class: "h-5 w-5 text-red-500") %>
|
|
10
|
+
</div>
|
|
11
|
+
<% end %>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div id="<%= description_id %>" class="mt-2 text-sm min-h-[1.25rem]">
|
|
15
|
+
<% if has_errors? %>
|
|
16
|
+
<p data-kanso--form-target="errorMessage"
|
|
17
|
+
class="text-red-600 opacity-0 transition duration-300 ease-out -translate-y-2">
|
|
18
|
+
<%= form.object.errors.full_messages_for(attribute).to_sentence %>
|
|
19
|
+
</p>
|
|
20
|
+
<% elsif help_text.present? %>
|
|
21
|
+
<p class="text-slate-500"><%= help_text %></p>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kanso
|
|
4
|
+
class FormFieldComponent < ViewComponent::Base
|
|
5
|
+
renders_one :help_text
|
|
6
|
+
|
|
7
|
+
attr_reader :form, :attribute, :type, :options
|
|
8
|
+
|
|
9
|
+
def initialize(form:, attribute:, type: :text_field, **options)
|
|
10
|
+
@form = form
|
|
11
|
+
@attribute = attribute
|
|
12
|
+
@type = type
|
|
13
|
+
@options = options
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def has_errors?
|
|
19
|
+
form.object.errors.include?(attribute)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def field_id
|
|
23
|
+
helpers.dom_id(form.object, attribute)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def description_id
|
|
27
|
+
"#{field_id}_description"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def field_classes
|
|
31
|
+
base_classes = "block w-full rounded-md border-0 py-1.5 px-3 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6"
|
|
32
|
+
|
|
33
|
+
if has_errors?
|
|
34
|
+
error_classes = "text-red-900 ring-red-300 placeholder:text-red-300 focus:ring-red-500"
|
|
35
|
+
"#{base_classes} #{error_classes}"
|
|
36
|
+
else
|
|
37
|
+
normal_classes = "text-slate-900 ring-slate-300 placeholder:text-slate-400 focus:ring-blue-600"
|
|
38
|
+
"#{base_classes} #{normal_classes}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def field_options
|
|
43
|
+
options.deep_merge(
|
|
44
|
+
id: field_id,
|
|
45
|
+
class: field_classes,
|
|
46
|
+
aria: { describedby: description_id },
|
|
47
|
+
data: { kanso__form_target: ("errorField" if has_errors?) }
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kanso
|
|
4
|
+
class IconComponent < ViewComponent::Base
|
|
5
|
+
attr_reader :name, :options
|
|
6
|
+
|
|
7
|
+
def initialize(name:, **options)
|
|
8
|
+
@name = name
|
|
9
|
+
@options = options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
attributes = tag.attributes(
|
|
14
|
+
options.deep_merge(data: { icon_name: name })
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
raw_svg = cached_svg_content
|
|
18
|
+
raw_svg.sub("<svg", "<svg #{attributes}").html_safe
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def cached_svg_content
|
|
24
|
+
Rails.cache.fetch("kanso:icon:#{name}", expires_in: 1.day) do
|
|
25
|
+
file_path = Kanso::Engine.root.join("app/assets/images/kanso/icons/#{name}.svg")
|
|
26
|
+
File.read(file_path) if File.exist?(file_path)
|
|
27
|
+
end.to_s
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<div data-controller="kanso--modal">
|
|
2
|
+
|
|
3
|
+
<div data-action="click->kanso--modal#open">
|
|
4
|
+
<%= trigger %>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div data-kanso--modal-target="container"
|
|
8
|
+
class="fixed inset-0 z-50 hidden"
|
|
9
|
+
data-action="keydown.esc@window->kanso--modal#close">
|
|
10
|
+
|
|
11
|
+
<div data-kanso--modal-target="backdrop"
|
|
12
|
+
data-action="mousedown->kanso--modal#backdropMousedown mouseup->kanso--modal#backdropMouseup"
|
|
13
|
+
class="fixed inset-0 flex w-full items-center justify-center bg-slate-900/70 p-4 transition-opacity duration-300 ease-in-out"
|
|
14
|
+
data-transition-enter-from="opacity-0"
|
|
15
|
+
data-transition-enter-to="opacity-100"
|
|
16
|
+
data-transition-leave-from="opacity-100"
|
|
17
|
+
data-transition-leave-to="opacity-0">
|
|
18
|
+
|
|
19
|
+
<div role="dialog"
|
|
20
|
+
aria-modal="true"
|
|
21
|
+
aria-labelledby="<%= header.title_id if header.present? %>"
|
|
22
|
+
data-kanso--modal-target="panel"
|
|
23
|
+
class="flex w-full <%= size_classes %> max-h-full transform flex-col rounded-lg bg-white text-left shadow-2xl transition-all duration-300 ease-in-out"
|
|
24
|
+
data-transition-enter-from="opacity-0 scale-95"
|
|
25
|
+
data-transition-enter-to="opacity-100 scale-100"
|
|
26
|
+
data-transition-leave-from="opacity-100 scale-100"
|
|
27
|
+
data-transition-leave-to="opacity-0 scale-95">
|
|
28
|
+
|
|
29
|
+
<% if header.present? %>
|
|
30
|
+
<div data-test-id="kanso--modal-header" class="flex flex-shrink-0 items-center justify-between rounded-t-lg border-b border-slate-200 p-4">
|
|
31
|
+
<h2 id="<%= header.title_id %>" class="text-lg font-semibold text-slate-900">
|
|
32
|
+
<%= header.title %>
|
|
33
|
+
</h2>
|
|
34
|
+
<button type="button" data-action="click->kanso--modal#close" class="-m-2 p-2 text-slate-400 hover:cursor-pointer hover:text-slate-600">
|
|
35
|
+
<span class="sr-only">Close</span>
|
|
36
|
+
<%= render Kanso::IconComponent.new(name: "x-mark", class: "h-6 w-6") %>
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
<% end %>
|
|
40
|
+
|
|
41
|
+
<div data-test-id="kanso--modal-body" class="overflow-y-auto p-4">
|
|
42
|
+
<%= content %>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<% if footer.present? %>
|
|
46
|
+
<div data-test-id="kanso--modal-footer" class="flex flex-shrink-0 flex-row-reverse items-center gap-x-2 rounded-b-lg border-t border-slate-200 bg-slate-50 p-4">
|
|
47
|
+
<%= footer %>
|
|
48
|
+
</div>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kanso
|
|
4
|
+
class ModalComponent < ViewComponent::Base
|
|
5
|
+
renders_one :trigger
|
|
6
|
+
renders_one :header, ->(title:) do
|
|
7
|
+
HeaderComponent.new(title: title)
|
|
8
|
+
end
|
|
9
|
+
renders_one :footer
|
|
10
|
+
|
|
11
|
+
attr_reader :size
|
|
12
|
+
|
|
13
|
+
def initialize(size: :lg)
|
|
14
|
+
@size = size
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def size_classes
|
|
20
|
+
case size
|
|
21
|
+
when :sm
|
|
22
|
+
"max-w-sm"
|
|
23
|
+
when :md
|
|
24
|
+
"max-w-md"
|
|
25
|
+
when :lg
|
|
26
|
+
"max-w-lg"
|
|
27
|
+
when :xl
|
|
28
|
+
"max-w-xl"
|
|
29
|
+
when :xxl
|
|
30
|
+
"max-w-2xl"
|
|
31
|
+
else
|
|
32
|
+
"max-w-lg"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class HeaderComponent < ViewComponent::Base
|
|
37
|
+
attr_reader :title, :title_id
|
|
38
|
+
|
|
39
|
+
def initialize(title:)
|
|
40
|
+
@title = title
|
|
41
|
+
@title_id = "modal-title-#{SecureRandom.hex(4)}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def call
|
|
45
|
+
content
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<div data-controller="kanso--notification"
|
|
2
|
+
class="flex w-full max-w-sm items-center rounded-lg bg-white p-4 shadow-lg ring-1 ring-black ring-opacity-5 opacity-0 transform translate-x-full transition-all duration-300 ease-out"
|
|
3
|
+
role="<%= theme_data.aria_role %>">
|
|
4
|
+
<div class="flex-shrink-0">
|
|
5
|
+
<div class="flex h-8 w-8 items-center justify-center rounded-full <%= theme_data.icon_bg_classes %>">
|
|
6
|
+
<%= render Kanso::IconComponent.new(name: theme_data.icon_name, class: "h-5 w-5 #{theme_data.icon_color_classes}") %>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="ml-3 w-0 flex-1 pt-0.5">
|
|
10
|
+
<% if title.present? %>
|
|
11
|
+
<h3 class="text-sm font-medium <%= theme_data.title_text_classes %>"><%= title %></h3>
|
|
12
|
+
<p class="mt-1 text-sm <%= theme_data.body_text_classes %>"><%= message %></p>
|
|
13
|
+
<% else %>
|
|
14
|
+
<p class="text-sm font-medium <%= theme_data.title_text_classes %>"><%= message %></p>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="ml-4 flex flex-shrink-0">
|
|
18
|
+
<button type="button"
|
|
19
|
+
data-action="click->kanso--notification#close"
|
|
20
|
+
class="inline-flex rounded-md bg-white text-gray-400 transition-colors hover:text-gray-500 hover:cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
|
21
|
+
<span class="sr-only">Close</span>
|
|
22
|
+
<%= render Kanso::IconComponent.new(name: "x-mark", class: "h-5 w-5") %>
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kanso
|
|
4
|
+
class NotificationComponent < ViewComponent::Base
|
|
5
|
+
Theme = Struct.new(
|
|
6
|
+
:icon_name,
|
|
7
|
+
:icon_bg_classes,
|
|
8
|
+
:icon_color_classes,
|
|
9
|
+
:title_text_classes,
|
|
10
|
+
:body_text_classes,
|
|
11
|
+
:aria_role,
|
|
12
|
+
keyword_init: true
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
THEMES = {
|
|
16
|
+
success: Theme.new(
|
|
17
|
+
icon_name: "check-circle",
|
|
18
|
+
icon_bg_classes: "bg-green-100",
|
|
19
|
+
icon_color_classes: "text-green-600",
|
|
20
|
+
title_text_classes: "text-green-800",
|
|
21
|
+
body_text_classes: "text-green-700",
|
|
22
|
+
aria_role: "status"
|
|
23
|
+
),
|
|
24
|
+
error: Theme.new(
|
|
25
|
+
icon_name: "x-circle",
|
|
26
|
+
icon_bg_classes: "bg-red-100",
|
|
27
|
+
icon_color_classes: "text-red-600",
|
|
28
|
+
title_text_classes: "text-red-800",
|
|
29
|
+
body_text_classes: "text-red-700",
|
|
30
|
+
aria_role: "alert"
|
|
31
|
+
),
|
|
32
|
+
warning: Theme.new(
|
|
33
|
+
icon_name: "exclamation-triangle",
|
|
34
|
+
icon_bg_classes: "bg-yellow-100",
|
|
35
|
+
icon_color_classes: "text-yellow-600",
|
|
36
|
+
title_text_classes: "text-yellow-800",
|
|
37
|
+
body_text_classes: "text-yellow-700",
|
|
38
|
+
aria_role: "alert"
|
|
39
|
+
),
|
|
40
|
+
info: Theme.new(
|
|
41
|
+
icon_name: "information-circle",
|
|
42
|
+
icon_bg_classes: "bg-blue-100",
|
|
43
|
+
icon_color_classes: "text-blue-600",
|
|
44
|
+
title_text_classes: "text-blue-800",
|
|
45
|
+
body_text_classes: "text-blue-700",
|
|
46
|
+
aria_role: "status"
|
|
47
|
+
)
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
50
|
+
attr_reader :title, :message, :theme_data
|
|
51
|
+
|
|
52
|
+
def initialize(message:, title: nil, theme: :info)
|
|
53
|
+
@title = title
|
|
54
|
+
@message = message
|
|
55
|
+
@theme_data = THEMES[theme.to_sym] || THEMES[:info]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
pin "controllers/kanso--modal_controller", to: "kanso/controllers/modal_controller.js"
|
|
2
|
+
pin "controllers/kanso--form_controller", to: "kanso/controllers/form_controller.js"
|
|
3
|
+
pin "controllers/kanso--notification_controller", to: "kanso/controllers/notification_controller.js"
|
|
4
|
+
pin "controllers/kanso--dropdown_controller", to: "kanso/controllers/dropdown_controller.js"
|
|
5
|
+
pin "transition", to: "kanso/helpers/transition.js"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# lib/generators/kanso/install/install_generator.rb
|
|
2
|
+
|
|
3
|
+
class Kanso::InstallGenerator < Rails::Generators::Base
|
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
|
5
|
+
|
|
6
|
+
# Kanso Philosophy: Public methods are generator tasks.
|
|
7
|
+
# Helper methods must be explicitly declared to avoid being run by Thor.
|
|
8
|
+
no_commands do
|
|
9
|
+
# This method exists to make the generator testable.
|
|
10
|
+
# In a test environment, we can stub this to point to a temporary directory.
|
|
11
|
+
def application_root
|
|
12
|
+
Rails.root
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def install
|
|
17
|
+
say "Kanso: Starting installation...", :cyan
|
|
18
|
+
|
|
19
|
+
# --- Prerequisite Checks based on observable facts ---
|
|
20
|
+
css_path = application_root.join("app/assets/tailwind/application.css")
|
|
21
|
+
root_config_path = application_root.join("tailwind.config.js")
|
|
22
|
+
legacy_config_path = application_root.join("config/tailwind.config.js")
|
|
23
|
+
|
|
24
|
+
unless File.exist?(css_path)
|
|
25
|
+
say "\nKanso Installation Blocked", :red
|
|
26
|
+
say "--------------------------", :red
|
|
27
|
+
say "Could not find the required Tailwind CSS entrypoint at:", :yellow
|
|
28
|
+
say "`app/assets/tailwind/application.css`", :bold
|
|
29
|
+
say "\nThis file is required for Kanso to be installed correctly.", :yellow
|
|
30
|
+
say "Please run `rails tailwindcss:install` to generate it, then re-run this generator.", :cyan
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if File.exist?(legacy_config_path)
|
|
35
|
+
say "\nKanso Installation Blocked", :red
|
|
36
|
+
say "--------------------------", :red
|
|
37
|
+
say "A Tailwind configuration file was found at:", :yellow
|
|
38
|
+
say "`config/tailwind.config.js`", :bold
|
|
39
|
+
say "\nTo ensure a single source of truth, Kanso requires the configuration to be at the project root.", :yellow
|
|
40
|
+
say "Please migrate this file to `tailwind.config.js` at the root of your project, then re-run this generator.", :cyan
|
|
41
|
+
return
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# --- Core Logic ---
|
|
45
|
+
configure_tailwind_config(root_config_path)
|
|
46
|
+
configure_application_css(css_path, root_config_path)
|
|
47
|
+
|
|
48
|
+
say "Kanso: Installation complete! ✨", :green
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# --- Tailwind Configuration Logic ---
|
|
54
|
+
|
|
55
|
+
def configure_tailwind_config(path)
|
|
56
|
+
if File.exist?(path)
|
|
57
|
+
inject_into_tailwind_config(path)
|
|
58
|
+
else
|
|
59
|
+
create_tailwind_config(path)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def inject_into_tailwind_config(path)
|
|
64
|
+
file_content = File.read(path)
|
|
65
|
+
|
|
66
|
+
if file_content.include?("// Kanso: Managed")
|
|
67
|
+
say "Kanso: Tailwind configuration is already Kanso-aware. Skipping.", :yellow
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
if file_content.match?(/export\s+default/m)
|
|
72
|
+
say "Kanso: Your `tailwind.config.js` uses ESM syntax (`export default`).", :yellow
|
|
73
|
+
say " To prevent conflicts, Kanso will not modify this file.", :yellow
|
|
74
|
+
say " Please manually add the Kanso content path to your config:", :cyan
|
|
75
|
+
say " You'll need `path.join` and this helper function:", :bold
|
|
76
|
+
puts find_kanso_gem_path_function.indent(7)
|
|
77
|
+
puts " And add this to your `content` array:".indent(7)
|
|
78
|
+
puts " `path.join(kansoGemPath, \"app/components/**/*.{rb,html.erb}\")`".indent(7)
|
|
79
|
+
return
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
say "Kanso: Found existing `tailwind.config.js`. Safely appending Kanso configuration.", :cyan
|
|
83
|
+
append_to_file path, kanso_scriptlet
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def create_tailwind_config(path)
|
|
87
|
+
say "Kanso: No `tailwind.config.js` found. Creating a new, Kanso-aware configuration.", :cyan
|
|
88
|
+
create_file path, <<~JS
|
|
89
|
+
// Kanso: Managed Tailwind Configuration
|
|
90
|
+
const path = require("path");
|
|
91
|
+
const { execSync } = require("child_process");
|
|
92
|
+
|
|
93
|
+
#{find_kanso_gem_path_function.strip.indent(2)}
|
|
94
|
+
|
|
95
|
+
const kansoGemPath = findKansoGemPath();
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
content: [
|
|
99
|
+
"./app/helpers/**/*.rb",
|
|
100
|
+
"./app/javascript/**/*.js",
|
|
101
|
+
"./app/views/**/*.{html,html.erb,erb}",
|
|
102
|
+
path.join(kansoGemPath, "app/components/**/*.{rb,html.erb}")
|
|
103
|
+
],
|
|
104
|
+
theme: {
|
|
105
|
+
extend: {},
|
|
106
|
+
},
|
|
107
|
+
plugins: [],
|
|
108
|
+
}
|
|
109
|
+
JS
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# --- CSS Configuration Logic ---
|
|
113
|
+
|
|
114
|
+
def configure_application_css(css_path, config_path)
|
|
115
|
+
file_content = File.read(css_path)
|
|
116
|
+
inject_config_bridge(css_path, file_content, config_path)
|
|
117
|
+
inject_base_styles(css_path, file_content)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def inject_config_bridge(css_path, file_content, config_path)
|
|
121
|
+
if file_content.include?("@config")
|
|
122
|
+
say "Kanso: `@config` directive already present in CSS. Skipping.", :yellow
|
|
123
|
+
else
|
|
124
|
+
relative_config_path = config_path.relative_path_from(css_path.dirname).to_s
|
|
125
|
+
config_block = "/* Kanso: Tells Tailwind to use our smart JS config */\n@config \"#{relative_config_path}\";\n\n"
|
|
126
|
+
prepend_to_file css_path, config_block
|
|
127
|
+
say "Kanso: Injected `@config` bridge into `#{relative_path(css_path)}`.", :green
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def inject_base_styles(css_path, file_content)
|
|
132
|
+
if file_content.include?("/* Kanso: Base Styles")
|
|
133
|
+
say "Kanso: Base styles already present. Skipping.", :yellow
|
|
134
|
+
else
|
|
135
|
+
anchor = /@import\s+["']tailwindcss["'];\s*(\r\n|\n)/
|
|
136
|
+
if file_content.match?(anchor)
|
|
137
|
+
insert_into_file css_path, "\n#{base_styles_block.strip}\n", after: anchor
|
|
138
|
+
say "Kanso: Injected base styles.", :green
|
|
139
|
+
else
|
|
140
|
+
say "Kanso: Could not find the `@import \"tailwindcss\";` directive.", :red
|
|
141
|
+
say " Please add the Kanso base styles block manually:", :yellow
|
|
142
|
+
puts base_styles_block
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# --- Helper Methods & Content Blocks ---
|
|
148
|
+
|
|
149
|
+
def kanso_scriptlet
|
|
150
|
+
<<~JS
|
|
151
|
+
|
|
152
|
+
// --- Kanso Configuration (appended by `rails g kanso:install`) ---
|
|
153
|
+
// Kanso: Managed
|
|
154
|
+
try {
|
|
155
|
+
const kansoGemPath = require("child_process").execSync(`bundle exec ruby -e "puts Bundler.rubygems.find_name(%q{kanso}).first.full_gem_path"`).toString().trim();
|
|
156
|
+
if (kansoGemPath) {
|
|
157
|
+
const kansoContentPath = require("path").join(kansoGemPath, "app/components/**/*.{rb,html.erb}");
|
|
158
|
+
if (module.exports.content) {
|
|
159
|
+
if (!module.exports.content.includes(kansoContentPath)) {
|
|
160
|
+
module.exports.content.push(kansoContentPath);
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
module.exports.content = [kansoContentPath];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch (e) {
|
|
167
|
+
console.error("Kanso post-install configuration failed. Please add the Kanso content path to your tailwind.config.js manually.");
|
|
168
|
+
console.error(e);
|
|
169
|
+
}
|
|
170
|
+
// --- End Kanso Configuration ---
|
|
171
|
+
JS
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def find_kanso_gem_path_function
|
|
175
|
+
<<~JS
|
|
176
|
+
function findKansoGemPath() {
|
|
177
|
+
try {
|
|
178
|
+
return execSync(
|
|
179
|
+
`bundle exec ruby -e "puts Bundler.rubygems.find_name(%q{kanso}).first.full_gem_path"`
|
|
180
|
+
).toString().trim();
|
|
181
|
+
} catch (e) {
|
|
182
|
+
console.error("Could not find the 'kanso' gem via Bundler. Please ensure it's in your Gemfile.");
|
|
183
|
+
console.error(e.stderr.toString());
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
JS
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def base_styles_block
|
|
191
|
+
<<~CSS
|
|
192
|
+
/* Kanso: Base Styles injected into the native 'base' cascade layer */
|
|
193
|
+
@layer base {
|
|
194
|
+
input:focus, textarea:focus, select:focus {
|
|
195
|
+
outline: none;
|
|
196
|
+
}
|
|
197
|
+
turbo-frame[busy] {
|
|
198
|
+
opacity: 0.5;
|
|
199
|
+
transition: opacity 0.3s ease-in-out;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/* End Kanso Base Styles */
|
|
203
|
+
CSS
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def relative_path(path)
|
|
207
|
+
path.relative_path_from(Rails.root).to_s
|
|
208
|
+
end
|
|
209
|
+
end
|
data/lib/kanso/engine.rb
ADDED