quicksilver_ui 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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/app/assets/tailwind/alert.css +35 -0
  3. data/app/assets/tailwind/badge.css +27 -0
  4. data/app/assets/tailwind/button.css +35 -0
  5. data/app/assets/tailwind/form.css +35 -0
  6. data/app/assets/tailwind/link.css +23 -0
  7. data/app/assets/tailwind/modal.css +43 -0
  8. data/app/assets/tailwind/quicksilver_ui/engine.css +7 -0
  9. data/app/assets/tailwind/typography.css +112 -0
  10. data/app/helpers/app_form_builder.rb +94 -0
  11. data/app/helpers/app_form_helper.rb +7 -0
  12. data/app/javascript/controllers/autogrow_controller.js +19 -0
  13. data/app/javascript/controllers/dismissable_controller.js +35 -0
  14. data/app/javascript/controllers/dropdown_controller.js +59 -0
  15. data/app/javascript/controllers/modal_controller.js +45 -0
  16. data/app/javascript/controllers/tabs_controller.js +62 -0
  17. data/app/javascript/mixins/use_floating_ui.js +104 -0
  18. data/app/views/form/base_tag.rb +42 -0
  19. data/app/views/form/checkbox.rb +62 -0
  20. data/app/views/form/date_field.rb +11 -0
  21. data/app/views/form/email_field.rb +7 -0
  22. data/app/views/form/error.rb +15 -0
  23. data/app/views/form/file_field.rb +12 -0
  24. data/app/views/form/group.rb +97 -0
  25. data/app/views/form/hint.rb +15 -0
  26. data/app/views/form/input.rb +11 -0
  27. data/app/views/form/label.rb +19 -0
  28. data/app/views/form/password_field.rb +7 -0
  29. data/app/views/form/phone_field.rb +7 -0
  30. data/app/views/form/radio_button.rb +37 -0
  31. data/app/views/form/search_field.rb +7 -0
  32. data/app/views/form/select.rb +46 -0
  33. data/app/views/form/text_field.rb +7 -0
  34. data/app/views/form/textarea.rb +27 -0
  35. data/app/views/form/toggle.rb +35 -0
  36. data/app/views/ui/accordion.rb +67 -0
  37. data/app/views/ui/alert.rb +84 -0
  38. data/app/views/ui/avatar.rb +57 -0
  39. data/app/views/ui/badge.rb +35 -0
  40. data/app/views/ui/base.rb +29 -0
  41. data/app/views/ui/dropdown/item.rb +49 -0
  42. data/app/views/ui/dropdown.rb +111 -0
  43. data/app/views/ui/icon.rb +46 -0
  44. data/app/views/ui/modal.rb +96 -0
  45. data/app/views/ui/toast.rb +90 -0
  46. data/lib/generators/quicksilver_ui/affordance/affordance_generator.rb +102 -0
  47. data/lib/generators/quicksilver_ui/component/all_generator.rb +32 -0
  48. data/lib/generators/quicksilver_ui/component/component_generator.rb +194 -0
  49. data/lib/generators/quicksilver_ui/form/all_generator.rb +32 -0
  50. data/lib/generators/quicksilver_ui/form/form_generator.rb +164 -0
  51. data/lib/generators/quicksilver_ui/form/templates/app_form_builder.rb +39 -0
  52. data/lib/generators/quicksilver_ui/form/templates/app_form_helper.rb +7 -0
  53. data/lib/generators/quicksilver_ui/install/install_generator.rb +42 -0
  54. data/lib/generators/quicksilver_ui/install/templates/base.rb +29 -0
  55. data/lib/generators/quicksilver_ui/install/templates/initializer.rb +16 -0
  56. data/lib/quicksilver_ui/dependencies.rb +191 -0
  57. data/lib/quicksilver_ui/engine.rb +18 -0
  58. data/lib/quicksilver_ui/version.rb +5 -0
  59. data/lib/quicksilver_ui.rb +37 -0
  60. metadata +98 -0
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Alert < UI::Base
4
+ prop :icon, _Union?(String, Symbol), predicate: :private, reader: :private
5
+ prop :heading, String, reader: :private
6
+ prop :description, _String?, predicate: :private, reader: :private
7
+ prop :variant, _Union("neutral", "brand", "success", "warning", "danger", "info"),
8
+ default: :info, reader: :private do |value|
9
+ value.to_s.inquiry
10
+ end
11
+ prop :size, _Union("sm", "md"),
12
+ default: :md, reader: :private do |value|
13
+ value.to_s.inquiry
14
+ end
15
+ prop :dismissable, _Boolean, default: false, predicate: :private, reader: :private
16
+
17
+ def view_template
18
+ div(class: classes, data: data_with_defaults, role: alert_role) do
19
+ render Icon(name: icon, size:, class: "shrink-0 mt-1 #{icon_color_classes}") if icon?
20
+ div(class: "w-full") do
21
+ div(class: "flex items-center justify-between gap-2") do
22
+ h5(class: heading_classes) { heading }
23
+
24
+ if dismissable?
25
+ button aria_label: "Dismiss", data: {action: "dismissable#dismiss"} do
26
+ render UI::Icon.new(name: :x_mark, size:, class: "cursor-pointer #{icon_color_classes}")
27
+ end
28
+ end
29
+ end
30
+
31
+ p(class: description_classes) { description } if description?
32
+
33
+ if block_given?
34
+ div(class: description_classes) do
35
+ yield
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def default_classes
45
+ "ui-alert ui-alert-#{variant} ui-alert-#{size}"
46
+ end
47
+
48
+ def icon_color_classes
49
+ class_names(
50
+ "text-turqoise-900": variant.brand?,
51
+ "text-emerald-800": variant.success?,
52
+ "text-amber-800": variant.warning?,
53
+ "text-red-800": variant.danger?,
54
+ "text-blue-800": variant.info?
55
+ )
56
+ end
57
+
58
+ def heading_classes
59
+ class_names(
60
+ "text-gray-950",
61
+ "ui-text-md": size.md?,
62
+ "ui-text-sm": size.sm?,
63
+ "font-medium": description?
64
+ )
65
+ end
66
+
67
+ def description_classes
68
+ class_names(
69
+ "text-gray-950",
70
+ "ui-text-md": size.md?,
71
+ "ui-text-sm": size.sm?
72
+ )
73
+ end
74
+
75
+ def data_with_defaults
76
+ data.with_defaults(
77
+ dismissable? ? {controller: "dismissable"} : {}
78
+ )
79
+ end
80
+
81
+ def alert_role
82
+ (variant.warning? || variant.danger?) ? :alert : :status
83
+ end
84
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Avatar < UI::Base
4
+ include Phlex::Rails::Helpers::ImageTag
5
+
6
+ prop :initials, String, reader: :private
7
+ prop :image_url, _Nilable(String), reader: :private
8
+ prop :size, _Union("xs", "sm", "md", "lg", "xl", "xxl"), default: :md, reader: :private do |value|
9
+ value.to_s.inquiry
10
+ end
11
+
12
+ def view_template
13
+ span(class: classes) do
14
+ if image_url.present?
15
+ image_tag(image_url, class: "size-full rounded-full object-cover")
16
+ else
17
+ span(class: text_classes) { initials }
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def classes
25
+ class_names(
26
+ super,
27
+ size_classes
28
+ )
29
+ end
30
+
31
+ def default_classes
32
+ "inline-flex size-6 items-center justify-center rounded-full bg-primary-100 border border-primary-700"
33
+ end
34
+
35
+ def size_classes
36
+ class_names(
37
+ "size-6": size.xs?,
38
+ "size-8": size.sm?,
39
+ "size-10": size.md?,
40
+ "size-12": size.lg?,
41
+ "size-14": size.xl?,
42
+ "size-16": size.xxl?
43
+ )
44
+ end
45
+
46
+ def text_classes
47
+ class_names(
48
+ "font-medium text-primary-900 uppercase",
49
+ "text-xs": size.xs?,
50
+ "text-sm": size.sm?,
51
+ "text-base": size.md?,
52
+ "text-xl": size.lg? || size.xl?,
53
+ "text-2xl": size.xxl?,
54
+ hidden: image_url.present?
55
+ )
56
+ end
57
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Badge < UI::Base
4
+ prop :text, String, reader: :private
5
+ prop :icon, _Nilable(Symbol), reader: :private
6
+ prop :variant, _Union("neutral", "brand", "danger"),
7
+ default: :neutral, reader: :private do |value|
8
+ value.to_s.inquiry
9
+ end
10
+ prop :size, _Union("sm", "md", "lg"),
11
+ default: :md, reader: :private do |value|
12
+ value.to_s.inquiry
13
+ end
14
+
15
+ def view_template
16
+ div class: classes, data: do
17
+ UI::Icon(name: icon, class: icon_classes, size: :sm) if icon.present?
18
+ span { text }
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def default_classes
25
+ "ui-badge ui-badge-#{variant} ui-badge-#{size}"
26
+ end
27
+
28
+ def icon_classes
29
+ class_names(
30
+ "text-gray-700": variant.neutral?,
31
+ "text-primary-700": variant.brand?,
32
+ "text-red-400": variant.danger?
33
+ )
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Base < Phlex::HTML
4
+ TAILWIND_MERGER = ::TailwindMerge::Merger.new.freeze unless defined?(TAILWIND_MERGER)
5
+
6
+ extend Literal::Properties
7
+ include Phlex::Rails::Helpers::Routes
8
+ include Phlex::Rails::Helpers::ClassNames
9
+ include Phlex::Rails::Helpers::LinkTo
10
+
11
+ if Rails.env.development?
12
+ def before_template
13
+ comment { "Before #{self.class.name}" }
14
+ super
15
+ end
16
+ end
17
+
18
+ prop :class, _Nilable(String)
19
+ prop :data, Hash, default: {}.freeze, reader: :private
20
+
21
+ private
22
+
23
+ def classes
24
+ TAILWIND_MERGER.merge [default_classes, @class].join(" ")
25
+ end
26
+
27
+ def default_classes
28
+ end
29
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Dropdown::Item < UI::Base
4
+ prop :href, String, reader: :private
5
+ prop :icon, _Nilable(Symbol), reader: :private
6
+ prop :trailing_icon, _Nilable(Symbol), reader: :private
7
+ prop :method, _Union(Symbol, String), default: :get, reader: :private
8
+
9
+ def view_template(&)
10
+ tag do
11
+ render Icon(name: icon, class: "text-gray-500") if icon?
12
+
13
+ div class: text_classes do
14
+ yield
15
+ end
16
+
17
+ render Icon(name: trailing_icon, class: "text-gray-500") if trailing_icon?
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def tag(&)
24
+ if method == :get
25
+ a href:, class: classes, &
26
+ else
27
+ button_to href, method:, form_class: classes, class: "flex items-center gap-3 w-full", &
28
+ end
29
+ end
30
+
31
+ def icon?
32
+ icon.present?
33
+ end
34
+
35
+ def trailing_icon?
36
+ trailing_icon.present?
37
+ end
38
+
39
+ def text_classes
40
+ "ui-text-sm flex-1"
41
+ end
42
+
43
+ def default_classes
44
+ class_names(
45
+ "self-stretch flex justify-start items-center flex-1 px-4 py-2 gap-3",
46
+ "hover:bg-gray-100 focus:bg-gray-100 active:bg-gray-200"
47
+ )
48
+ end
49
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Dropdown < UI::Base
4
+ prop :heading, _Nilable(String), reader: :private
5
+ prop :offset, Integer, default: 8, reader: :private
6
+ prop :placement,
7
+ _Union("top", "top-start", "top-end", "right", "right-start", "right-end", "bottom", "bottom-start", "bottom-end", "left", "left-start", "left-end"),
8
+ reader: :private, default: "bottom-end"
9
+
10
+ def initialize(**props)
11
+ super
12
+ @items = []
13
+ @trigger_block = nil
14
+ @action_block = nil
15
+ end
16
+
17
+ def item(href:, icon: nil, trailing_icon: nil, method: :get, &block)
18
+ @items << {href:, icon:, trailing_icon:, method:, block:}
19
+ nil
20
+ end
21
+
22
+ def action(&block)
23
+ @action_block = block if block_given?
24
+ nil
25
+ end
26
+
27
+ def trigger(class: nil, &block)
28
+ @trigger_block = block
29
+ @trigger_classes = grab(class:)
30
+ nil
31
+ end
32
+
33
+ def view_template(&block)
34
+ vanish(&block) if block_given?
35
+
36
+ div(data: {controller: "dropdown", dropdown_offset_value: offset, dropdown_placement_value: placement}, class: classes) do
37
+ render_trigger
38
+
39
+ div(
40
+ data: {
41
+ dropdown_target: "menu",
42
+ transition_enter_from: "opacity-0 scale-95",
43
+ transition_enter_to: "opacity-100 scale-100",
44
+ transition_leave_from: "opacity-100 scale-100",
45
+ transition_leave_to: "opacity-0 scale-95"
46
+ },
47
+ class:
48
+ "hidden transition transform origin-top-left absolute left-0 top-0 z-50"
49
+ ) do
50
+ div(class: "w-64 bg-white shadow-xs border border-gray-300
51
+ inline-flex flex-col justify-start items-start overflow-hidden") do
52
+ render_heading
53
+
54
+ div(class: "w-full flex flex-col justify-start items-start gap-1") do
55
+ @items.each do |item_data|
56
+ render Dropdown::Item.new(
57
+ **item_data.except(:block),
58
+ &item_data[:block]
59
+ )
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def render_trigger
70
+ button(class: trigger_classes, data: {action: "dropdown#toggle click@window->dropdown#hide"}) do
71
+ @trigger_block.call
72
+ end
73
+ end
74
+
75
+ def render_heading
76
+ return unless heading.present? || action?
77
+
78
+ div(class: "w-full px-4 py-2 border-b border-gray-300 flex justify-between
79
+ items-center gap-4") do
80
+ if heading.present?
81
+ div(class: "ui-text-sm-medium") do
82
+ heading
83
+ end
84
+ end
85
+
86
+ render_action
87
+ end
88
+ end
89
+
90
+ def render_action
91
+ div(class: "ui-text-xs text-gray-700") do
92
+ @action_block&.call
93
+ end
94
+ end
95
+
96
+ def default_classes
97
+ "relative"
98
+ end
99
+
100
+ def trigger_classes
101
+ TAILWIND_MERGER.merge [default_trigger_classes, @trigger_classes].join(" ")
102
+ end
103
+
104
+ def default_trigger_classes
105
+ "focus:outline-none active:outline-none"
106
+ end
107
+
108
+ def action?
109
+ @action_block.present?
110
+ end
111
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Icon < UI::Base
4
+ register_output_helper :icon
5
+
6
+ prop :size, _Union("xs", "sm", "md", "lg", "xl", "xxl"), default: :md, reader: :private do |value|
7
+ value.to_s.inquiry
8
+ end
9
+ prop :variant, _Union("micro", "mini", "outline", "solid"), default: :outline, reader: :private do |value|
10
+ value.to_s.inquiry
11
+ end
12
+ prop :name, String, reader: :private do |value|
13
+ value.to_s.dasherize
14
+ end
15
+ prop :fixed_width, _Boolean, default: true, reader: :private
16
+ prop :spin, _Boolean, default: false, reader: :private
17
+
18
+ def view_template
19
+ icon(name, class: classes, variant:)
20
+ end
21
+
22
+ private
23
+
24
+ def size_classes
25
+ class_names(
26
+ "size-2": size.xs?,
27
+ "size-3": size.sm?,
28
+ "size-4": size.md?,
29
+ "size-6": size.lg?,
30
+ "size-8": size.xl?,
31
+ "size-10": size.xxl?
32
+ )
33
+ end
34
+
35
+ def spin_classes
36
+ "animate-spin" if spin
37
+ end
38
+
39
+ def default_classes
40
+ class_names(
41
+ "text-inherit",
42
+ size_classes,
43
+ spin_classes
44
+ )
45
+ end
46
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Modal < UI::Base
4
+ prop :open, _Boolean, default: false, reader: :private
5
+ prop :heading, String, reader: :private
6
+ prop :text, _Nilable(String), reader: :private
7
+ prop :backdrop_closeable, _Boolean, default: true, reader: :private
8
+
9
+ def initialize(**props)
10
+ super
11
+ @trigger_block = nil
12
+ @footer_block = nil
13
+ end
14
+
15
+ def view_template(&block)
16
+ vanish(&block) if block_given?
17
+
18
+ div data: {controller: "modal", modal_open_value: open}.merge(backdrop_closeable_attributes) do
19
+ dialog(data: {modal_target: "dialog"}, class: classes) do
20
+ div(class: "ui-modal-body") do
21
+ div(class: "flex justify-between items-start") do
22
+ render_heading
23
+ render_close_trigger
24
+ end
25
+ render_text
26
+
27
+ yield
28
+ end
29
+
30
+ render_footer
31
+ end
32
+
33
+ render_trigger
34
+ end
35
+ end
36
+
37
+ def trigger(class: nil, &block)
38
+ @trigger_block = block
39
+ @trigger_classes = grab(class:)
40
+ nil
41
+ end
42
+
43
+ def footer(class: nil, &block)
44
+ @footer_block = block
45
+ @footer_classes = grab(class:)
46
+ nil
47
+ end
48
+
49
+ private
50
+
51
+ def render_close_trigger
52
+ button(data: {action: "modal#close"}, class: "cursor-pointer hover:text-gray-700") do
53
+ render UI::Icon.new(name: :x_mark)
54
+ end
55
+ end
56
+
57
+ def render_heading
58
+ h2(class: "ui-display-md") { heading }
59
+ end
60
+
61
+ def render_text
62
+ p(class: "ui-text-md") { text }
63
+ end
64
+
65
+ def render_footer
66
+ return if @footer_block.nil?
67
+
68
+ div(class: footer_classes) do
69
+ @footer_block.call
70
+ end
71
+ end
72
+
73
+ def render_trigger
74
+ button(class: @trigger_classes, data: {action: "modal#open"}) do
75
+ @trigger_block.call
76
+ end
77
+ end
78
+
79
+ def backdrop_closeable_attributes
80
+ return {action: "click->modal#backdropClose"} if backdrop_closeable?
81
+
82
+ {}
83
+ end
84
+
85
+ def backdrop_closeable?
86
+ backdrop_closeable
87
+ end
88
+
89
+ def default_classes
90
+ "ui-modal"
91
+ end
92
+
93
+ def footer_classes
94
+ TAILWIND_MERGER.merge ["ui-modal-footer", @footer_classes].join(" ")
95
+ end
96
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Toast < UI::Base
4
+ AUTO_DISMISS_DELAY = 8000
5
+
6
+ prop :dismissable, _Boolean, default: true, reader: :private
7
+ prop :auto_dismiss, _Boolean, default: true, reader: :private
8
+ prop :variant, _Union("neutral", "danger"),
9
+ default: :neutral, reader: :private do |value|
10
+ value.to_s.inquiry
11
+ end
12
+
13
+ def initialize(**props)
14
+ super
15
+ @action = nil
16
+ end
17
+
18
+ def view_template
19
+ output(
20
+ data: {
21
+ controller: "dismissable",
22
+ dismissable_auto_dismiss_time_value: auto_dismiss_delay
23
+ },
24
+ class: classes
25
+ ) do
26
+ div class: "w-full min-h-8 py-1 px-2 inline-flex items-start justify-between gap-6" do
27
+ span(class: "mt-0.5") { yield }
28
+
29
+ span(class: "flex items-center gap-x-6") do
30
+ render_action
31
+
32
+ if dismissable?
33
+ button data: {action: "dismissable#dismiss"}, class: button_classes do
34
+ Icon(name: :x_mark, variant: :outline, class: "mt-px")
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ def action(href:, text:)
43
+ @action = {href:, text:}
44
+ nil
45
+ end
46
+
47
+ private
48
+
49
+ def render_action
50
+ return if @action.nil?
51
+
52
+ link_to @action[:text], @action[:href], class: action_classes
53
+ end
54
+
55
+ def dismissable?
56
+ dismissable
57
+ end
58
+
59
+ def auto_dismiss?
60
+ dismissable? && auto_dismiss
61
+ end
62
+
63
+ def auto_dismiss_delay
64
+ AUTO_DISMISS_DELAY if auto_dismiss?
65
+ end
66
+
67
+ def default_classes
68
+ class_names(
69
+ "min-w-96 max-w-lg block text-sm font-medium ring shadow-xl",
70
+ "bg-gray-50 text-gray-900 ring-gray-300": variant.neutral?,
71
+ "bg-red-500 text-white ring-red-700": variant.danger?
72
+ )
73
+ end
74
+
75
+ def button_classes
76
+ class_names(
77
+ "p-1 focus:outline-none",
78
+ "text-gray-900 hover:bg-gray-200 focus:bg-gray-200 active:bg-gray-300": variant.neutral?,
79
+ "text-white hover:bg-red-500 focus:bg-red-500 active:bg-red-400": variant.danger?
80
+ )
81
+ end
82
+
83
+ def action_classes
84
+ class_names(
85
+ "py-0.5 px-1 focus:outline-none border text-xs",
86
+ "border-gray-700 hover:border-gray-900 hover:bg-gray-200 focus:border-gray-900 focus:bg-gray-200 active:bg-gray-300": variant.neutral?,
87
+ "border-red-300 hover:bg-red-600 hover:border-red-100 focus:bg-red-600 focus:border-red-100 active:bg-red-700 active:border-red-100": variant.danger?
88
+ )
89
+ end
90
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module QuicksilverUI
6
+ module Generators
7
+ class AffordanceGenerator < Rails::Generators::Base
8
+ namespace "quicksilver_ui:affordance"
9
+
10
+ source_root QuicksilverUI.stylesheets_path.to_s
11
+
12
+ def self.banner
13
+ "rails generate quicksilver_ui:affordance NAME [options]"
14
+ end
15
+
16
+ desc <<~DESC
17
+ Generate a QuicksilverUI affordance stylesheet into your application.
18
+
19
+ Available affordances:
20
+ DESC
21
+
22
+ def self.desc(description = nil)
23
+ return super if description
24
+
25
+ affordances = Dir.glob(File.join(QuicksilverUI.stylesheets_path, "*.css"))
26
+ .map { |f| File.basename(f, ".css") }
27
+ .sort
28
+ .map { |c| " #{c}" }
29
+ .join("\n")
30
+
31
+ "#{super}\n#{affordances}"
32
+ end
33
+
34
+ argument :affordance_name, type: :string, required: true
35
+ class_option :force, type: :boolean, default: false
36
+
37
+ def generate_affordance
38
+ if affordance_not_found?
39
+ say "Affordance not found: #{affordance_name}", :red
40
+ say ""
41
+ say "Available affordances:", :green
42
+ available_affordances.each { |a| say " - #{a}" }
43
+ exit 1
44
+ end
45
+
46
+ say "Generating #{affordance_name} affordance..."
47
+ end
48
+
49
+ def copy_stylesheet
50
+ source = File.join(QuicksilverUI.stylesheets_path, "#{file_name}.css")
51
+ copy_file source, Rails.root.join("app/assets/tailwind", "#{file_name}.css"), force: options["force"]
52
+ add_css_import(file_name)
53
+ end
54
+
55
+ def done
56
+ say ""
57
+ say "#{affordance_name} affordance generated!", :green
58
+ end
59
+
60
+ private
61
+
62
+ def file_name
63
+ affordance_name.underscore
64
+ end
65
+
66
+ def affordance_not_found?
67
+ !File.exist?(File.join(QuicksilverUI.stylesheets_path, "#{file_name}.css"))
68
+ end
69
+
70
+ def available_affordances
71
+ Dir.glob(File.join(QuicksilverUI.stylesheets_path, "*.css"))
72
+ .map { |f| File.basename(f, ".css") }
73
+ .sort
74
+ end
75
+
76
+ def add_css_import(name)
77
+ app_css = Rails.root.join("app/assets/tailwind/application.css")
78
+ import_line = "@import \"./#{name}.css\" layer(affordances);"
79
+
80
+ if File.exist?(app_css)
81
+ content = File.read(app_css)
82
+ return if content.include?(import_line)
83
+
84
+ lines = content.lines
85
+ last_import_index = lines.rindex { |l| l.start_with?("@import") }
86
+
87
+ if last_import_index
88
+ lines.insert(last_import_index + 1, "#{import_line}\n")
89
+ else
90
+ lines.unshift("#{import_line}\n")
91
+ end
92
+
93
+ File.write(app_css, lines.join)
94
+ else
95
+ create_file app_css, "#{import_line}\n"
96
+ end
97
+
98
+ say " Added import for #{name}.css to application.css", :green
99
+ end
100
+ end
101
+ end
102
+ end