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,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module QuicksilverUI
6
+ module Generators
7
+ module Component
8
+ class AllGenerator < Rails::Generators::Base
9
+ namespace "quicksilver_ui:component:all"
10
+
11
+ class_option :force, type: :boolean, default: false
12
+
13
+ def generate_all_components
14
+ say "Generating all components..."
15
+
16
+ available_components.each do |component|
17
+ run "bin/rails generate quicksilver_ui:component #{component}#{" --force" if options["force"]}"
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def available_components
24
+ Dir.glob(File.join(QuicksilverUI.ui_path, "*.rb"))
25
+ .map { |f| File.basename(f, ".rb") }
26
+ .reject { |name| name == "base" }
27
+ .sort
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module QuicksilverUI
6
+ module Generators
7
+ class ComponentGenerator < Rails::Generators::Base
8
+ namespace "quicksilver_ui:component"
9
+
10
+ source_root QuicksilverUI.ui_path.to_s
11
+
12
+ def self.banner
13
+ "rails generate quicksilver_ui:component NAME [options]"
14
+ end
15
+
16
+ desc <<~DESC
17
+ Generate a QuicksilverUI component into your application.
18
+
19
+ Available components:
20
+ DESC
21
+
22
+ def self.desc(description = nil)
23
+ return super if description
24
+
25
+ components = Dir.glob(File.join(QuicksilverUI.ui_path, "*.rb"))
26
+ .map { |f| File.basename(f, ".rb") }
27
+ .reject { |n| n == "base" }
28
+ .sort
29
+ .map { |c| " #{c}" }
30
+ .join("\n")
31
+
32
+ "#{super}\n#{components}"
33
+ end
34
+
35
+ argument :component_name, type: :string, required: true
36
+ class_option :force, type: :boolean, default: false
37
+
38
+ def generate_component
39
+ if component_not_found?
40
+ say "Component not found: #{component_name}", :red
41
+ say ""
42
+ say "Available components:", :green
43
+ available_components.each { |c| say " - #{c}" }
44
+ exit 1
45
+ end
46
+
47
+ say "Generating #{component_name} component..."
48
+ end
49
+
50
+ def add_gems
51
+ all_gems.each do |gem_name|
52
+ unless gem_installed?(gem_name)
53
+ say "Adding #{gem_name} to Gemfile...", :yellow
54
+ run "bundle add #{gem_name}"
55
+ end
56
+ end
57
+ end
58
+
59
+ def copy_component_files
60
+ all_components.each do |name|
61
+ paths = file_paths_for(name)
62
+ paths.each do |file_path|
63
+ relative = Pathname.new(file_path).relative_path_from(self.class.source_root)
64
+ copy_file file_path, Rails.root.join("app/views/ui", relative), force: options["force"]
65
+ end
66
+ end
67
+ end
68
+
69
+ def copy_stylesheets
70
+ all_stylesheets.each do |name|
71
+ source = File.join(QuicksilverUI.stylesheets_path, "#{name}.css")
72
+ next unless File.exist?(source)
73
+
74
+ dest = Rails.root.join("app/assets/tailwind", "#{name}.css")
75
+ copy_file source, dest, force: options["force"]
76
+ add_css_import(name)
77
+ end
78
+ end
79
+
80
+ def copy_controllers
81
+ all_controllers.each do |name|
82
+ source = File.join(QuicksilverUI.javascript_controllers_path, "#{name}_controller.js")
83
+ next unless File.exist?(source)
84
+
85
+ copy_file source, Rails.root.join("app/javascript/controllers", "#{name}_controller.js"), force: options["force"]
86
+ end
87
+ end
88
+
89
+ def copy_mixins
90
+ all_mixins.each do |name|
91
+ source = File.join(QuicksilverUI.javascript_mixins_path, "#{name}.js")
92
+ next unless File.exist?(source)
93
+
94
+ copy_file source, Rails.root.join("app/javascript/mixins", "#{name}.js"), force: options["force"]
95
+ end
96
+ end
97
+
98
+ def done
99
+ say ""
100
+ say "#{component_name} component generated!", :green
101
+
102
+ deps = all_components - [component_folder_name]
103
+ if deps.any?
104
+ say " Dependencies copied: #{deps.join(", ")}", :cyan
105
+ end
106
+ end
107
+
108
+ private
109
+
110
+ def all_components
111
+ @all_components ||= QuicksilverUI.resolve_dependencies(component_folder_name)
112
+ end
113
+
114
+ def all_stylesheets
115
+ all_components.flat_map { |name| QuicksilverUI::DEPENDENCIES.dig(name, :stylesheets) || [] }.uniq
116
+ end
117
+
118
+ def all_controllers
119
+ all_components.flat_map { |name| QuicksilverUI::DEPENDENCIES.dig(name, :controllers) || [] }.uniq
120
+ end
121
+
122
+ def all_mixins
123
+ all_components.flat_map { |name| QuicksilverUI::DEPENDENCIES.dig(name, :mixins) || [] }.uniq
124
+ end
125
+
126
+ def all_gems
127
+ all_components.flat_map { |name| QuicksilverUI::DEPENDENCIES.dig(name, :gems) || [] }.uniq
128
+ end
129
+
130
+ def gem_installed?(name)
131
+ Gem::Specification.find_all_by_name(name).any?
132
+ end
133
+
134
+ def add_css_import(name)
135
+ app_css = Rails.root.join("app/assets/tailwind/application.css")
136
+ import_line = "@import \"./#{name}.css\" layer(affordances);"
137
+
138
+ if File.exist?(app_css)
139
+ content = File.read(app_css)
140
+ return if content.include?(import_line)
141
+
142
+ # Insert after the last existing @import line
143
+ lines = content.lines
144
+ last_import_index = lines.rindex { |l| l.start_with?("@import") }
145
+
146
+ if last_import_index
147
+ lines.insert(last_import_index + 1, "#{import_line}\n")
148
+ else
149
+ lines.unshift("#{import_line}\n")
150
+ end
151
+
152
+ File.write(app_css, lines.join)
153
+ else
154
+ create_file app_css, "#{import_line}\n"
155
+ end
156
+
157
+ say " Added import for #{name}.css to application.css", :green
158
+ end
159
+
160
+ def component_not_found?
161
+ !File.exist?(component_file_path) && !Dir.exist?(component_folder_path)
162
+ end
163
+
164
+ def component_folder_name
165
+ component_name.underscore
166
+ end
167
+
168
+ def component_file_path
169
+ File.join(self.class.source_root, "#{component_folder_name}.rb")
170
+ end
171
+
172
+ def component_folder_path
173
+ File.join(self.class.source_root, component_folder_name)
174
+ end
175
+
176
+ def file_paths_for(name)
177
+ paths = []
178
+ file = File.join(self.class.source_root, "#{name}.rb")
179
+ folder = File.join(self.class.source_root, name)
180
+
181
+ paths << file if File.exist?(file)
182
+ paths.concat Dir.glob(File.join(folder, "**/*.rb")) if Dir.exist?(folder)
183
+ paths
184
+ end
185
+
186
+ def available_components
187
+ Dir.glob(File.join(self.class.source_root, "*.rb"))
188
+ .map { |f| File.basename(f, ".rb") }
189
+ .reject { |name| name == "base" }
190
+ .sort
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module QuicksilverUI
6
+ module Generators
7
+ module Form
8
+ class AllGenerator < Rails::Generators::Base
9
+ namespace "quicksilver_ui:form:all"
10
+
11
+ class_option :force, type: :boolean, default: false
12
+
13
+ def generate_all_form_components
14
+ say "Generating all form components..."
15
+
16
+ available_form_components.each do |component|
17
+ run "bin/rails generate quicksilver_ui:form #{component}#{" --force" if options["force"]}"
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def available_form_components
24
+ Dir.glob(File.join(QuicksilverUI.form_path, "*.rb"))
25
+ .map { |f| File.basename(f, ".rb") }
26
+ .reject { |name| name == "base_tag" }
27
+ .sort
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module QuicksilverUI
6
+ module Generators
7
+ class FormGenerator < Rails::Generators::Base
8
+ namespace "quicksilver_ui:form"
9
+
10
+ source_root QuicksilverUI.form_path.to_s
11
+
12
+ def self.banner
13
+ "rails generate quicksilver_ui:form NAME [options]"
14
+ end
15
+
16
+ desc <<~DESC
17
+ Generate a QuicksilverUI form component into your application.
18
+
19
+ Available form components:
20
+ DESC
21
+
22
+ def self.desc(description = nil)
23
+ return super if description
24
+
25
+ components = Dir.glob(File.join(QuicksilverUI.form_path, "*.rb"))
26
+ .map { |f| File.basename(f, ".rb") }
27
+ .reject { |n| n == "base_tag" }
28
+ .sort
29
+ .map { |c| " #{c}" }
30
+ .join("\n")
31
+
32
+ "#{super}\n#{components}"
33
+ end
34
+
35
+ argument :form_component_name, type: :string, required: true
36
+ class_option :force, type: :boolean, default: false
37
+
38
+ def generate_form_component
39
+ if form_component_not_found?
40
+ say "Form component not found: #{form_component_name}", :red
41
+ say ""
42
+ say "Available form components:", :green
43
+ available_form_components.each { |c| say " - #{c}" }
44
+ exit 1
45
+ end
46
+
47
+ say "Generating #{form_component_name} form component..."
48
+ end
49
+
50
+ def add_gems
51
+ all_gems.each do |gem_name|
52
+ unless gem_installed?(gem_name)
53
+ say "Adding #{gem_name} to Gemfile...", :yellow
54
+ run "bundle add #{gem_name}"
55
+ end
56
+ end
57
+ end
58
+
59
+ def copy_form_component_files
60
+ all_form_components.each do |name|
61
+ source = File.join(self.class.source_root, "#{name}.rb")
62
+ next unless File.exist?(source)
63
+
64
+ copy_file source, Rails.root.join("app/views/form", "#{name}.rb"), force: options["force"]
65
+ end
66
+ end
67
+
68
+ def copy_stylesheets
69
+ all_stylesheets.each do |name|
70
+ source = File.join(QuicksilverUI.stylesheets_path, "#{name}.css")
71
+ next unless File.exist?(source)
72
+
73
+ dest = Rails.root.join("app/assets/tailwind", "#{name}.css")
74
+ copy_file source, dest, force: options["force"]
75
+ add_css_import(name)
76
+ end
77
+ end
78
+
79
+ def copy_form_builder
80
+ source = File.join(templates_path, "app_form_builder.rb")
81
+ dest = Rails.root.join("app/helpers/app_form_builder.rb")
82
+ copy_file source, dest, force: options["force"]
83
+ end
84
+
85
+ def copy_form_helper
86
+ source = File.join(templates_path, "app_form_helper.rb")
87
+ dest = Rails.root.join("app/helpers/app_form_helper.rb")
88
+ copy_file source, dest, force: options["force"]
89
+ end
90
+
91
+ def done
92
+ say ""
93
+ say "#{form_component_name} form component generated!", :green
94
+
95
+ deps = all_form_components - [file_name]
96
+ if deps.any?
97
+ say " Dependencies copied: #{deps.join(", ")}", :cyan
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def all_form_components
104
+ @all_form_components ||= QuicksilverUI.resolve_form_dependencies(file_name)
105
+ end
106
+
107
+ def all_stylesheets
108
+ all_form_components.flat_map { |name| QuicksilverUI::FORM_DEPENDENCIES.dig(name, :stylesheets) || [] }.uniq
109
+ end
110
+
111
+ def all_gems
112
+ all_form_components.flat_map { |name| QuicksilverUI::FORM_DEPENDENCIES.dig(name, :gems) || [] }.uniq
113
+ end
114
+
115
+ def gem_installed?(name)
116
+ Gem::Specification.find_all_by_name(name).any?
117
+ end
118
+
119
+ def add_css_import(name)
120
+ app_css = Rails.root.join("app/assets/tailwind/application.css")
121
+ import_line = "@import \"./#{name}.css\" layer(affordances);"
122
+
123
+ if File.exist?(app_css)
124
+ content = File.read(app_css)
125
+ return if content.include?(import_line)
126
+
127
+ lines = content.lines
128
+ last_import_index = lines.rindex { |l| l.start_with?("@import") }
129
+
130
+ if last_import_index
131
+ lines.insert(last_import_index + 1, "#{import_line}\n")
132
+ else
133
+ lines.unshift("#{import_line}\n")
134
+ end
135
+
136
+ File.write(app_css, lines.join)
137
+ else
138
+ create_file app_css, "#{import_line}\n"
139
+ end
140
+
141
+ say " Added import for #{name}.css to application.css", :green
142
+ end
143
+
144
+ def form_component_not_found?
145
+ !File.exist?(File.join(self.class.source_root, "#{file_name}.rb"))
146
+ end
147
+
148
+ def templates_path
149
+ File.join(File.dirname(__FILE__), "templates")
150
+ end
151
+
152
+ def file_name
153
+ form_component_name.underscore
154
+ end
155
+
156
+ def available_form_components
157
+ Dir.glob(File.join(self.class.source_root, "*.rb"))
158
+ .map { |f| File.basename(f, ".rb") }
159
+ .reject { |name| name == "base_tag" }
160
+ .sort
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,39 @@
1
+ class AppFormBuilder < ActionView::Helpers::FormBuilder
2
+ delegate :render, to: :@template
3
+
4
+ def self.with_blank_error_proc(&block)
5
+ old_error_proc = ActionView::Base.field_error_proc
6
+ begin
7
+ ActionView::Base.field_error_proc = proc do |tag, instance|
8
+ tag
9
+ end
10
+ block.call
11
+ ensure
12
+ ActionView::Base.field_error_proc = old_error_proc
13
+ end
14
+ end
15
+
16
+ def group(method, options = {})
17
+ render Form::Group.new(form: self, method:, type: options[:type] || :text, **options.except(:type))
18
+ end
19
+
20
+ def text_field(method, options = {})
21
+ render Form::TextField.new(form: self, method:, **options)
22
+ end
23
+
24
+ def label(method, text = nil, options = {}, &block)
25
+ render Form::Label.new(form: self, method:, text:, **options)
26
+ end
27
+
28
+ def hint(method, text = nil, options = {}, &block)
29
+ render Form::Hint.new(form: self, method:, text:, **options)
30
+ end
31
+
32
+ def error(method, text = nil, options = {}, &block)
33
+ render Form::Error.new(form: self, method:, text:, **options)
34
+ end
35
+
36
+ def submit(value = nil, options = {})
37
+ super(value, options.with_defaults(class: "ui-button ui-button-primary"))
38
+ end
39
+ end
@@ -0,0 +1,7 @@
1
+ module AppFormHelper
2
+ def app_form_with(*, **, &)
3
+ AppFormBuilder.with_blank_error_proc do
4
+ form_with(*, builder: AppFormBuilder, **, &)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module QuicksilverUI
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ namespace "quicksilver_ui:install"
9
+
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ def add_gems
13
+ %w[phlex-rails literal tailwind_merge].each do |gem_name|
14
+ unless gem_installed?(gem_name)
15
+ say "Adding #{gem_name} to Gemfile...", :yellow
16
+ run "bundle add #{gem_name}"
17
+ end
18
+ end
19
+ end
20
+
21
+ def create_initializer
22
+ template "initializer.rb", Rails.root.join("config/initializers/quicksilver_ui.rb")
23
+ end
24
+
25
+ def create_base_component
26
+ template "base.rb", Rails.root.join("app/views/ui/base.rb")
27
+ end
28
+
29
+ def done
30
+ say ""
31
+ say "Quicksilver UI installed successfully!", :green
32
+ say "Run `bin/rails g quicksilver_ui:component Alert` to generate a component.", :green
33
+ end
34
+
35
+ private
36
+
37
+ def gem_installed?(name)
38
+ Gem::Specification.find_all_by_name(name).any?
39
+ end
40
+ end
41
+ end
42
+ 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,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UI
4
+ extend Phlex::Kit
5
+ end
6
+
7
+ # Look for Phlex components in the views folder
8
+ Rails.autoloaders.main.push_dir(
9
+ Rails.root.join("app/views")
10
+ )
11
+
12
+ # Add a folder specifically for UI components, that are generalized and
13
+ # reusable, like modals and accordions.
14
+ Rails.autoloaders.main.push_dir(
15
+ Rails.root.join("app/views/ui"), namespace: UI
16
+ )