swage 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1eb99b2b002dccde7c9d242952b711e4d0fc2432b8c46c792e529b22d83e3486
4
+ data.tar.gz: f4eabce375c4ffe258ee251297915ba55916eeafbac9e120b1929e5c87701ae4
5
+ SHA512:
6
+ metadata.gz: d409d145064d845053c9a91db02fd017877c6d745918f95ce5d59627b4ec588ce6cc6ef6414aa66050169384a10add1f694a9b20c992ac3639e62c6dd44985bf
7
+ data.tar.gz: 7f16fbe0f753870da630dfc455507bac3734204c0da4efc7fd420a170028ea7af8372227adf39962fdc0e2e0da8a0fbd655758c46834a72e14f3f23ced1cc24f
data/LICENSE.txt ADDED
@@ -0,0 +1,19 @@
1
+ The MIT License (MIT)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # Swage
2
+ Basically a different implementation of [Superview](https://github.com/rubymonolith/superview), but with a few extra goodies and generators baked in. Use [Phlex](https://github.com/yippee-fun/phlex-rails/) for rendering views and [Superform](https://github.com/rubymonolith/superform/) for the form DSL and strong parameters. Uses [RubyUI](https://github.com/ruby-ui/ruby_ui) for the base UI components and [Phlexible](https://github.com/joelmoss/phlexible) to glue everything together (along with a lot of monkeypatching).
3
+
4
+ ## Installation
5
+ Simply run `rails g swage:install`, and all of the necessary components will be generated. Please not this might take a while to run.
6
+
7
+ ## Usage
8
+ By default Swage will hook into the rails scaffold engine. However, it is a drop-in replacement for the erb scaffold_controller, meaning it can be used in the exact same way.
9
+
10
+ ## Modification
11
+ If you wish to modify the generators, use `rails g swage:generators` to generate all of the scaffold and install generators/templates.
12
+
13
+
14
+ ## TODO
15
+ * Consolidate the Tailwind classes down for the non-RubyUI components
@@ -0,0 +1,5 @@
1
+ Description:
2
+ Replaces the default rails scaffold controller to generate phlex views
3
+
4
+ Example:
5
+ bin/rails generate flex Thing
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Swage::Generators
4
+ class BaseGenerator < ::Rails::Generators::NamedBase
5
+ include Rails::Generators::ResourceHelpers
6
+ def self.set_source_root(file_name, dir)
7
+ if File.exist?(Rails.root.join("lib/generators/swage", file_name))
8
+ source_root Rails.root.join("lib/generators/swage/templates")
9
+ else
10
+ source_root File.expand_path("templates", dir)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Swage::Generators
4
+ class GeneratorsGenerator < ::Rails::Generators::Base
5
+ source_root File.expand_path __dir__
6
+
7
+ # there's probably a better way to do this, but this should probably work well enough?
8
+ def create_scaffold_files
9
+ base = File.join "generators", "swage", "scaffold"
10
+
11
+ Dir.chdir("#{source_paths.first}/../scaffold") do
12
+ lib File.join(base, "scaffold_generator.rb"), File.open("scaffold_generator.rb", "r").read
13
+ end
14
+
15
+ Dir.chdir("#{source_paths.first}/../scaffold/templates") do
16
+ Dir.glob("*.rb.tt").each do |template|
17
+ lib File.join(base, "templates", template), File.open(template, "r").read
18
+ end
19
+ end
20
+ end
21
+
22
+ def create_install_files
23
+ base = File.join "generators", "swage", "install"
24
+ Dir.chdir("#{source_paths.first}/../install") do
25
+ lib File.join(base, "install_generator.rb"), File.open("install_generator.rb", "r").read
26
+ end
27
+
28
+ Dir.chdir("#{source_paths.first}/../install/templates") do
29
+ Dir.glob("*.rb.tt").each do |template|
30
+ lib File.join(base, "templates", template), File.open(template, "r").read
31
+ end
32
+ end
33
+ end
34
+
35
+ def create_base_generator
36
+ base = File.join "generators", "swage"
37
+ Dir.chdir("#{source_paths.first}/..") do
38
+ lib File.join(base, "base_generator.rb"), File.open("base_generator.rb", "r").read
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Swage::Generators
4
+ class InstallGenerator < ::Rails::Generators::Base
5
+ source_root File.expand_path("templates", __dir__)
6
+
7
+ class_option :engine, type: :boolean, default: false
8
+ class_option :override, type: :boolean, default: false
9
+
10
+ @name = nil
11
+
12
+ def check_for_engine
13
+ @engine = options["engine"] # set use instance variables because they look nicer
14
+
15
+ if probably_engine? && !@engine
16
+ if yes?("This appears to be an engine. do you wish to insatll swage as such?")
17
+ say "Installing in engine mode. Hint: use `rails g swage:install --engine`"
18
+ @engine = true
19
+ end
20
+ end
21
+ end
22
+
23
+ def check_for_override
24
+ if @engine
25
+ @override = options["override"]
26
+ else
27
+ @override = true
28
+ end
29
+ end
30
+
31
+ def check_for_js
32
+ return if @engine # engines don't have their own js ecosystem
33
+ unless using_importmap? || using_bun? || using_yarn? || using_npm? || using_pnpm?
34
+ if yes?("No javascript package manager was detected. Do you wish to exit now, or continue and install missing requirements manually?")
35
+ exit 0
36
+ end
37
+ end
38
+ end
39
+
40
+ def add_dependencies
41
+ deps = %W[ tailwindcss-rails phlex-rails phlexible superform ruby_ui ]
42
+ missing = []
43
+ deps.each do |d|
44
+ unless Gem.loaded_specs.has_key?(d)
45
+ say "missing #{d} in gemfile"
46
+ missing << d
47
+ end
48
+ end
49
+
50
+ if missing.any?
51
+ return if @engine && missing.length == 1 && missing[0] == "ruby_ui"
52
+ if yes?("would you like to automatically install the missing dependencies?")
53
+ add_source "https://rubygems.org" do
54
+ deps.each do |d|
55
+ gem d
56
+ end
57
+ end
58
+ else
59
+ exit 1
60
+ end
61
+ end
62
+
63
+ # because rails is silly and won't let you do stuff unless you have everything as a gem already
64
+ return if @engine && !@override
65
+ add_source "https://rubygems.org" do
66
+ gem "tailwindcss-rails"
67
+ gem "phlex"
68
+ gem "superform"
69
+ # gem "rogue"
70
+ end
71
+ end
72
+
73
+ def install_tailwind
74
+ if !@engine || (@engine && @override)
75
+ say "install tailwind"
76
+ execute_command "rake", "tailwindcss:install"
77
+ end
78
+ end
79
+
80
+ def install_phlex
81
+ return if @engine
82
+ say "install phlex"
83
+ generate "phlex:install"
84
+ end
85
+
86
+ def install_superform
87
+ return if @engine
88
+ say "install superform"
89
+ generate "superform:install"
90
+ end
91
+
92
+ def insatll_ruby_ui
93
+ if !@engine || (@engine && @override)
94
+ say "install rubyui"
95
+ generate "ruby_ui:install"
96
+ generate "ruby_ui:component:all"
97
+ end
98
+ end
99
+
100
+ def install_tw_animate_css # because this file is usually missing while installing ruby_ui
101
+ template "tw-animate-css.js.tt", File.join(destination_root, "vendor/javascript/tw-animate-css.js") unless @engine
102
+ end
103
+
104
+ def create_initializer
105
+ say "install swage"
106
+ template "swage.rb.tt", File.join(destination_root, "config/initializers/swage.rb"), force: true
107
+ end
108
+
109
+ def create_base_component
110
+ template "base_component.rb.tt", File.join(destination_root, "app/components/base.rb"), force: true if @override
111
+ end
112
+
113
+ def create_base_view
114
+ template "base_view.rb.tt", File.join(destination_root, "app/views/base.rb"), force: true if @override
115
+ end
116
+
117
+ def create_base_form
118
+ template "form.rb.tt", File.join(destination_root, "app/components/form.rb"), force: true if @override
119
+ end
120
+
121
+ def modify_application_controller
122
+ template "application_controller.rb.tt", File.join(destination_root, "app/controllers/application_controller.rb") if @override
123
+ end
124
+
125
+ def create_application_view
126
+ remove_file File.join(destination_root, "app/views/layouts/application.html.erb")
127
+ template "application_view.rb.tt", File.join(destination_root, "app/views/layouts/application.rb") if @override
128
+ end
129
+
130
+ def modify_engine
131
+ return unless @engine
132
+ @name ||= get_name
133
+ template "engine.rb.tt", File.join(destination_root, "lib", get_name, "engine.rb")
134
+ end
135
+
136
+ private # copied from ruby_ui installer since that's what it uses
137
+ def using_importmap?
138
+ File.exist?(Rails.root.join("config/importmap.rb")) && File.exist?(Rails.root.join("bin/importmap"))
139
+ end
140
+
141
+ def using_bun?
142
+ File.exist?(Rails.root.join("bun.lock"))
143
+ end
144
+
145
+ def using_npm?
146
+ File.exist?(Rails.root.join("package-lock.json"))
147
+ end
148
+
149
+ def using_pnpm?
150
+ File.exist?(Rails.root.join("pnpm-lock.yaml"))
151
+ end
152
+
153
+ def using_yarn?
154
+ File.exist?(Rails.root.join("yarn.lock"))
155
+ end
156
+
157
+ def probably_engine?
158
+ @name = get_name
159
+
160
+ # Dir.glob(File.join(destination_root, "test", "dummy")).empty? ||
161
+ # Dir.glob(File.join(destination_root, "*.gemspec")).empty? ||
162
+ File.exist?(File.join(destination_root, "lib", @name, "engine.rb")) ||
163
+ File.exist?(File.join(destination_root, "lib", @name, "railtie.rb"))
164
+ end
165
+
166
+ def get_name
167
+ destination_root.match(/\w+$/)[0] # this is gross but there seems to be no better way
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,14 @@
1
+ class ApplicationController < ActionController::Base
2
+ include Phlexible::Rails::ActionController::ImplicitRender
3
+ include Superform::Rails::StrongParameters
4
+
5
+ # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
6
+ allow_browser versions: :modern
7
+
8
+ # Changes to the importmap will invalidate the etag for HTML responses
9
+ stale_when_importmap_changes
10
+
11
+ def phlex_view_path(action_name)
12
+ "views/#{controller_path}/#{action_name}"
13
+ end
14
+ end
@@ -0,0 +1,44 @@
1
+ module Views
2
+ module Layouts
3
+ class Application < Phlex::HTML
4
+ include Phlex::Rails::Helpers::ContentFor
5
+ include Phlex::Rails::Helpers::CSPMetaTag
6
+ include Phlex::Rails::Helpers::CSRFMetaTags
7
+ include Phlex::Rails::Helpers::StyleSheetLinkTag
8
+ include Phlex::Rails::Helpers::JavaScriptImportmapTags
9
+
10
+ def initialize(view)
11
+ @view = view
12
+ end
13
+
14
+ def view_template(&block)
15
+ html do
16
+ head do
17
+ title { content_for(:title) || "Asdf" }
18
+ meta(name: "viewport", content: "width=device-width,initial-scale=1")
19
+ meta(name: "apple-mobile-web-app-capable", content: "yes")
20
+ meta(name: "application-name", content: "Asdf")
21
+ meta(name: "mobile-web-app-capable", content: "yes")
22
+
23
+ csrf_meta_tags
24
+ csp_meta_tag
25
+
26
+ # Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!)
27
+ # = tag.link rel: "manifest", href: pwa_manifest_path(format: :json)
28
+ link(rel: "icon", href: "/icon.png", type: "image/png")
29
+ link(rel: "icon", href: "/icon.svg", type: "image/svg+xml")
30
+ link(rel: "apple-touch-icon", href: "/icon.png")
31
+
32
+ # Includes all stylesheet files in app/assets/stylesheets
33
+ stylesheet_link_tag :app, "data-turbo-track": "reload"
34
+ stylesheet_link_tag "tailwind", "data-turbo-track": "reload"
35
+ javascript_importmap_tags
36
+ end
37
+ body do
38
+ div id: "body", &block
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Components::Base < Phlex::HTML
4
+ include RubyUI
5
+ include Phlex::Rails::Helpers::Routes
6
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Views::Base < Phlex::HTML
4
+ include Phlexible::Rails::AElement
5
+ include Phlexible::Rails::ControllerVariables
6
+ include Phlexible::Callbacks
7
+ include RubyUI
8
+ include Rails.application.routes.url_helpers
9
+
10
+ # More caching options at https://www.phlex.fun/components/caching
11
+ def cache_store = Rails.cache
12
+
13
+ # set up layout paths
14
+ def self.auto_layout_view_prefix = "Views::"
15
+ def self.auto_layout_namespace = "Views::Layouts::"
16
+ def self.auto_layout_default = "Views::Layouts::Application"
17
+ end
@@ -0,0 +1,25 @@
1
+ module <%= @name.capitalize %>
2
+ class Engine < ::Rails::Engine
3
+ initializer "<%= @name %>.set_autoloading" do
4
+ module ::Views; end
5
+
6
+ Rails.autoloaders.main.push_dir root.join("app/views"), namespace: ::Views
7
+ end
8
+
9
+ # Uncomment to automatically include migrations into the application's migration path
10
+ # This avoids copying migrations to the main application
11
+ # initializer "<%= @name %>.add_migrations" do |app|
12
+ # unless app.root.to_s == root.to_s
13
+ # config.paths["db/migrate"].expanded.each do |expanded_path|
14
+ # app.config.paths["db/migrate"] << expanded_path
15
+ # end
16
+ # end
17
+ # end
18
+
19
+ config.generators do |g|
20
+ g.scaffold_controller :swage
21
+
22
+ g.fallbacks[:swage] = :scaffold_controller
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,209 @@
1
+ module Components
2
+ class Form < Superform::Rails::Form
3
+ include Phlexible::Callbacks
4
+ include Phlex::Rails::Helpers::FormAuthenticityToken
5
+ include RubyUI
6
+
7
+ # monkeypatch superform here because editing superform itself is gross
8
+ class Field < self::Field
9
+ include RubyUI
10
+ def input(**attributes)
11
+ attributes.merge! dom_hash(field.dom)
12
+ RubyUI::Input.new(**attributes)
13
+ end
14
+
15
+ def checkbox(index: nil, **attributes)
16
+ attributes.merge! dom_hash(field.dom)
17
+ RubyUI::Checkbox.new(**attributes)
18
+ end
19
+
20
+ def textarea(**attributes)
21
+ attributes.merge! dom_hash(field.dom)
22
+ RubyUI::Textarea.new(**attributes) { attributes[:value] }
23
+ end
24
+
25
+ def select(*options, multiple: false, **attributes, &)
26
+ attributes.merge! dom_hash(field.dom)
27
+ if mutliple
28
+ CheckboxGroup do
29
+ options.each do |opt|
30
+ div class: "flex flex-col gap-2" do
31
+ div class: "flex flex-row items-center gap-2" do
32
+ RubyUI::Checkbox(*attributes, id: "select_#{opt}")
33
+ FormFieldLabel(for: "select_#{opt}") { opt.capitalize }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ else
39
+ RubyUI::Select(**attributes) do
40
+ SelectGroup do
41
+ options.each do |opt|
42
+ SelectItem(value: opt) { opt.capitalize }
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ # not sure if this is actually needed or if we can just use a combobox or something similar
50
+ # def datalist(*options, **attributes, &block)
51
+ # Components::Datalist.new(field, options:, **attributes, &block)
52
+ # end
53
+
54
+ def errors
55
+ object.errors[key]
56
+ end
57
+
58
+ def invalid?
59
+ errors.any?
60
+ end
61
+
62
+ def valid?
63
+ not invalid?
64
+ end
65
+
66
+ def human_attribute_name
67
+ object.class.human_attribute_name key
68
+ end
69
+
70
+ def text(**attributes)
71
+ attributes.merge! dom_hash(field.dom).merge(type: :text)
72
+ RubyUI::Input(**attributes)
73
+ end
74
+
75
+ def hidden(**attributes)
76
+ attributes.merge! dom_hash(field.dom).merge(type: :hidden)
77
+ RubyUI::Input(**attributes)
78
+ end
79
+
80
+ def password(**attributes)
81
+ attributes.merge! dom_hash(field.dom).merge(type: :password)
82
+ RubyUI::Input(**attributes)
83
+ end
84
+
85
+ def email(**attributes)
86
+ attributes.merge! dom_hash(field.dom).merge(type: :email)
87
+ RubyUI::Input(**attributes)
88
+ end
89
+
90
+ def url(**attributes)
91
+ attributes.merge! dom_hash(field.dom).merge(type: :url)
92
+ RubyUI::Input(**attributes)
93
+ end
94
+
95
+ def tel(**attributes)
96
+ attributes.merge! dom_hash(field.dom).merge(type: :tel)
97
+ RubyUI::Input(**attributes)
98
+ end
99
+ alias_method :phone, :tel
100
+
101
+ def number(**attributes)
102
+ RubyUI::Input(**attributes)
103
+ end
104
+
105
+ def range(**attributes)
106
+ RubyUI::Input(**attributes)
107
+ end
108
+
109
+ def date(**attributes)
110
+ attributes.merge! dom_hash(field.dom)
111
+ RubyUI::DatePicker(**attributes)
112
+ end
113
+
114
+ def time(**attributes)
115
+ attributes.merge! dom_hash(field.dom).merge(type: :time)
116
+ RubyUI::Input(**attributes)
117
+ end
118
+
119
+ def datetime(**attributes)
120
+ attributes.merge! dom_hash(field.dom).merge(type: :"datetime-local")
121
+ RubyUI::Input(**attributes)
122
+ end
123
+
124
+ def month(**attributes)
125
+ attributes.merge! dom_hash(field.dom).merge(type: :month)
126
+ RubyUI::Input(**attributes)
127
+ end
128
+
129
+ def week(**attributes)
130
+ attributes.merge! dom_hash(field.dom).merge(type: :week)
131
+ RubyUI::Input(**attributes)
132
+ end
133
+
134
+ def color(**attributes)
135
+ attributes.merge! dom_hash(field.dom).merge(type: :color)
136
+ RubyUI::Input(**attributes)
137
+ end
138
+
139
+ def search(**attributes)
140
+ attributes.merge! dom_hash(field.dom).merge(type: :search)
141
+ RubyUI::Input(**attributes)
142
+ end
143
+
144
+ def file(**attributes)
145
+ attributes.merge! dom_hash(field.dom).merge(type: :file)
146
+ RubyUI::Input(**attributes)
147
+ end
148
+
149
+ def radio(value, index: value, **attributes)
150
+ attributes.merge! dom_hash(field.dom)
151
+ div class: "clex items-center space-x-2" do
152
+ RubyUI::RadioButton(id: index)
153
+ FormFieldLabel(for: index) { value }
154
+ end
155
+ end
156
+
157
+ def radios(*options, **attributes, &block)
158
+ options = enum_options if options.empty?
159
+ Components::Radios.new(field, options:, **attributes, &block)
160
+ end
161
+
162
+ def checkboxes(*options, **attributes, &block)
163
+ options = enum_options if options.empty?
164
+ attributes.merge! dom_hash(field.dom)
165
+
166
+ CheckboxGroup do
167
+ options.each do |opt|
168
+ div class: "flex flex-col gap-2" do
169
+ div class: "flex flex-row items-center gap-2" do
170
+ RubyUI::Checkbox(*attributes, id: "select_#{opt}")
171
+ FormFieldLabel(for: "select_#{opt}") { opt.capitalize }
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ # Rails compatibility aliases
179
+ alias_method :check_box, :checkbox
180
+ alias_method :text_area, :textarea
181
+
182
+ def title
183
+ key.to_s.titleize
184
+ end
185
+
186
+ private
187
+ # superform has a field with a dom that stores useful values... must convert it to usable attributes
188
+ def dom_hash(dom)
189
+ {
190
+ id: dom.id,
191
+ name: dom.name,
192
+ value: dom.value
193
+ }
194
+ end
195
+ end
196
+
197
+ def around_template(&)
198
+ super do
199
+ yield if block_given?
200
+ end
201
+ end
202
+
203
+ def submit(value = submit_value, **attributes)
204
+ div do
205
+ RubyUI::Button(name: "commit", type: "submit", **attributes) { submit_value }
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "phlexible"
4
+
5
+ Rails.application.config.generators do |g|
6
+ g.scaffold_controller :swage
7
+
8
+ g.fallbacks[:swage] = :scaffold_controller
9
+ end
10
+
11
+ module Phlexible::Rails::ButtonToConcerns
12
+ # use consistent ui components
13
+ def view_template(&block)
14
+ action = url_for(@url)
15
+ @options = DEFAULT_OPTIONS.merge((@options || {}).symbolize_keys)
16
+
17
+ method = (@options.delete(:method).presence || method_for_options(@options)).to_s
18
+ form_method = method == "get" ? "get" : "post"
19
+
20
+ form action: action, method: form_method, **form_attributes do
21
+ method_tag method
22
+ form_method == "post" && token_input(action, method.empty? ? "post" : method)
23
+ param_inputs
24
+
25
+ block_given? ? RubyUI::Button(**button_attrs, &block) : RubyUI::Button(**button_attrs) { @name }
26
+ end
27
+ end
28
+ end