partials_fx 0.0.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0412f26bb891f999fbc9b1f0d168017a52da64f5e6aec18d707dea8802092eea
4
- data.tar.gz: fff0afad8ee5daccc5573ff5b566f1a5dabe9f71965901eaf6f1816d2d773cf2
3
+ metadata.gz: 76058417759f42122463669fea7eff9aae7871f62de129df795641bb592ecaff
4
+ data.tar.gz: 8673f99b6704a51ce92a3bcf7f108c51988695cf77a0b5495310071fa4a6f33e
5
5
  SHA512:
6
- metadata.gz: 6e71768ddf891cef92915b52aa2dd37b368db77458fe4415cfc13e4a1c1684936644a647ac71ee4dbe12504b186744b8fe3f8a981d4cbcffac26ee12795faf4c
7
- data.tar.gz: e82617f39b47d8f46bd9f0e317191c8c662b3434513b8d28fb01eba47221bd16daacd680803bf0b5bb53be8e52173d9bee04b75869596d47ad6869c7c35d8734
6
+ metadata.gz: b23685132aa916fc0ff3632ea3990c05dc2d3bf01e08a14a205674c2a56f26a6834a3b834ec13ab5e56dd84d69dec1a3584e380e947a95d148359360c8d1e976
7
+ data.tar.gz: 20a5b2495d7702b3c27c7a0fe770e70d9cb5dba976333542c27cc628608432f5ce7c9ec2db0637f2ba6e6bc6d5f0b34dd18fd6db06837de963f9c760e297619e
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in partials_fx.gemspec
6
+ gemspec
7
+
8
+ gem "irb"
9
+ gem "rake", "~> 13.0"
10
+
11
+ gem "minitest", "~> 5.25", ">= 5.25.5"
12
+ gem "standard", "~> 1.50"
data/Gemfile.lock ADDED
@@ -0,0 +1,243 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ partials_fx (0.5.0)
5
+ rails (>= 7.2, < 8.1)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ actioncable (8.0.2)
11
+ actionpack (= 8.0.2)
12
+ activesupport (= 8.0.2)
13
+ nio4r (~> 2.0)
14
+ websocket-driver (>= 0.6.1)
15
+ zeitwerk (~> 2.6)
16
+ actionmailbox (8.0.2)
17
+ actionpack (= 8.0.2)
18
+ activejob (= 8.0.2)
19
+ activerecord (= 8.0.2)
20
+ activestorage (= 8.0.2)
21
+ activesupport (= 8.0.2)
22
+ mail (>= 2.8.0)
23
+ actionmailer (8.0.2)
24
+ actionpack (= 8.0.2)
25
+ actionview (= 8.0.2)
26
+ activejob (= 8.0.2)
27
+ activesupport (= 8.0.2)
28
+ mail (>= 2.8.0)
29
+ rails-dom-testing (~> 2.2)
30
+ actionpack (8.0.2)
31
+ actionview (= 8.0.2)
32
+ activesupport (= 8.0.2)
33
+ nokogiri (>= 1.8.5)
34
+ rack (>= 2.2.4)
35
+ rack-session (>= 1.0.1)
36
+ rack-test (>= 0.6.3)
37
+ rails-dom-testing (~> 2.2)
38
+ rails-html-sanitizer (~> 1.6)
39
+ useragent (~> 0.16)
40
+ actiontext (8.0.2)
41
+ actionpack (= 8.0.2)
42
+ activerecord (= 8.0.2)
43
+ activestorage (= 8.0.2)
44
+ activesupport (= 8.0.2)
45
+ globalid (>= 0.6.0)
46
+ nokogiri (>= 1.8.5)
47
+ actionview (8.0.2)
48
+ activesupport (= 8.0.2)
49
+ builder (~> 3.1)
50
+ erubi (~> 1.11)
51
+ rails-dom-testing (~> 2.2)
52
+ rails-html-sanitizer (~> 1.6)
53
+ activejob (8.0.2)
54
+ activesupport (= 8.0.2)
55
+ globalid (>= 0.3.6)
56
+ activemodel (8.0.2)
57
+ activesupport (= 8.0.2)
58
+ activerecord (8.0.2)
59
+ activemodel (= 8.0.2)
60
+ activesupport (= 8.0.2)
61
+ timeout (>= 0.4.0)
62
+ activestorage (8.0.2)
63
+ actionpack (= 8.0.2)
64
+ activejob (= 8.0.2)
65
+ activerecord (= 8.0.2)
66
+ activesupport (= 8.0.2)
67
+ marcel (~> 1.0)
68
+ activesupport (8.0.2)
69
+ base64
70
+ benchmark (>= 0.3)
71
+ bigdecimal
72
+ concurrent-ruby (~> 1.0, >= 1.3.1)
73
+ connection_pool (>= 2.2.5)
74
+ drb
75
+ i18n (>= 1.6, < 2)
76
+ logger (>= 1.4.2)
77
+ minitest (>= 5.1)
78
+ securerandom (>= 0.3)
79
+ tzinfo (~> 2.0, >= 2.0.5)
80
+ uri (>= 0.13.1)
81
+ ast (2.4.3)
82
+ base64 (0.2.0)
83
+ benchmark (0.4.0)
84
+ bigdecimal (3.1.9)
85
+ builder (3.3.0)
86
+ concurrent-ruby (1.3.5)
87
+ connection_pool (2.5.3)
88
+ crass (1.0.6)
89
+ date (3.4.1)
90
+ drb (2.2.1)
91
+ erubi (1.13.1)
92
+ globalid (1.2.1)
93
+ activesupport (>= 6.1)
94
+ i18n (1.14.7)
95
+ concurrent-ruby (~> 1.0)
96
+ io-console (0.8.0)
97
+ irb (1.15.2)
98
+ pp (>= 0.6.0)
99
+ rdoc (>= 4.0.0)
100
+ reline (>= 0.4.2)
101
+ json (2.11.3)
102
+ language_server-protocol (3.17.0.4)
103
+ lint_roller (1.1.0)
104
+ logger (1.7.0)
105
+ loofah (2.24.1)
106
+ crass (~> 1.0.2)
107
+ nokogiri (>= 1.12.0)
108
+ mail (2.8.1)
109
+ mini_mime (>= 0.1.1)
110
+ net-imap
111
+ net-pop
112
+ net-smtp
113
+ marcel (1.0.4)
114
+ mini_mime (1.1.5)
115
+ minitest (5.25.5)
116
+ net-imap (0.5.8)
117
+ date
118
+ net-protocol
119
+ net-pop (0.1.2)
120
+ net-protocol
121
+ net-protocol (0.2.2)
122
+ timeout
123
+ net-smtp (0.5.1)
124
+ net-protocol
125
+ nio4r (2.7.4)
126
+ nokogiri (1.18.8-arm64-darwin)
127
+ racc (~> 1.4)
128
+ parallel (1.27.0)
129
+ parser (3.3.8.0)
130
+ ast (~> 2.4.1)
131
+ racc
132
+ pp (0.6.2)
133
+ prettyprint
134
+ prettyprint (0.2.0)
135
+ prism (1.4.0)
136
+ psych (5.2.3)
137
+ date
138
+ stringio
139
+ racc (1.8.1)
140
+ rack (3.1.14)
141
+ rack-session (2.1.1)
142
+ base64 (>= 0.1.0)
143
+ rack (>= 3.0.0)
144
+ rack-test (2.2.0)
145
+ rack (>= 1.3)
146
+ rackup (2.2.1)
147
+ rack (>= 3)
148
+ rails (8.0.2)
149
+ actioncable (= 8.0.2)
150
+ actionmailbox (= 8.0.2)
151
+ actionmailer (= 8.0.2)
152
+ actionpack (= 8.0.2)
153
+ actiontext (= 8.0.2)
154
+ actionview (= 8.0.2)
155
+ activejob (= 8.0.2)
156
+ activemodel (= 8.0.2)
157
+ activerecord (= 8.0.2)
158
+ activestorage (= 8.0.2)
159
+ activesupport (= 8.0.2)
160
+ bundler (>= 1.15.0)
161
+ railties (= 8.0.2)
162
+ rails-dom-testing (2.2.0)
163
+ activesupport (>= 5.0.0)
164
+ minitest
165
+ nokogiri (>= 1.6)
166
+ rails-html-sanitizer (1.6.2)
167
+ loofah (~> 2.21)
168
+ nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
169
+ railties (8.0.2)
170
+ actionpack (= 8.0.2)
171
+ activesupport (= 8.0.2)
172
+ irb (~> 1.13)
173
+ rackup (>= 1.0.0)
174
+ rake (>= 12.2)
175
+ thor (~> 1.0, >= 1.2.2)
176
+ zeitwerk (~> 2.6)
177
+ rainbow (3.1.1)
178
+ rake (13.2.1)
179
+ rdoc (6.13.1)
180
+ psych (>= 4.0.0)
181
+ regexp_parser (2.10.0)
182
+ reline (0.6.1)
183
+ io-console (~> 0.5)
184
+ rubocop (1.75.8)
185
+ json (~> 2.3)
186
+ language_server-protocol (~> 3.17.0.2)
187
+ lint_roller (~> 1.1.0)
188
+ parallel (~> 1.10)
189
+ parser (>= 3.3.0.2)
190
+ rainbow (>= 2.2.2, < 4.0)
191
+ regexp_parser (>= 2.9.3, < 3.0)
192
+ rubocop-ast (>= 1.44.0, < 2.0)
193
+ ruby-progressbar (~> 1.7)
194
+ unicode-display_width (>= 2.4.0, < 4.0)
195
+ rubocop-ast (1.44.1)
196
+ parser (>= 3.3.7.2)
197
+ prism (~> 1.4)
198
+ rubocop-performance (1.25.0)
199
+ lint_roller (~> 1.1)
200
+ rubocop (>= 1.75.0, < 2.0)
201
+ rubocop-ast (>= 1.38.0, < 2.0)
202
+ ruby-progressbar (1.13.0)
203
+ securerandom (0.4.1)
204
+ standard (1.50.0)
205
+ language_server-protocol (~> 3.17.0.2)
206
+ lint_roller (~> 1.0)
207
+ rubocop (~> 1.75.5)
208
+ standard-custom (~> 1.0.0)
209
+ standard-performance (~> 1.8)
210
+ standard-custom (1.0.2)
211
+ lint_roller (~> 1.0)
212
+ rubocop (~> 1.50)
213
+ standard-performance (1.8.0)
214
+ lint_roller (~> 1.1)
215
+ rubocop-performance (~> 1.25.0)
216
+ stringio (3.1.7)
217
+ thor (1.3.2)
218
+ timeout (0.4.3)
219
+ tzinfo (2.0.6)
220
+ concurrent-ruby (~> 1.0)
221
+ unicode-display_width (3.1.4)
222
+ unicode-emoji (~> 4.0, >= 4.0.4)
223
+ unicode-emoji (4.0.4)
224
+ uri (1.0.3)
225
+ useragent (0.16.11)
226
+ websocket-driver (0.7.7)
227
+ base64
228
+ websocket-extensions (>= 0.1.0)
229
+ websocket-extensions (0.1.5)
230
+ zeitwerk (2.7.2)
231
+
232
+ PLATFORMS
233
+ arm64-darwin-23
234
+
235
+ DEPENDENCIES
236
+ irb
237
+ minitest (~> 5.25, >= 5.25.5)
238
+ partials_fx!
239
+ rake (~> 13.0)
240
+ standard (~> 1.50)
241
+
242
+ BUNDLED WITH
243
+ 2.6.8
data/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # Partials Fx
2
+
3
+ Partials FX extends Rails' partials to make them quack more like components. Each partial, located in `app/views/components/`, is backed by a Ruby class with the same name. It also supports CSS modules, meaning you define CSS in the component class and it gets “scoped” to that component only. It is powering the [self-hosted Community Software, Forge](https://forge.railsdesigner.com/).
4
+
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ bundle add partials_fx
10
+ ```
11
+
12
+
13
+ ## Usage
14
+
15
+ `bin/rails generate component Avatar`
16
+
17
+ This creates two files:
18
+ - `app/views/components/_avatar_component.html.erb`;
19
+ - `app/views/components/avatar_component.rb`.
20
+
21
+ They could look like this:
22
+ ```erb
23
+ <span id="user-avatar" class="<%= component_class %> <%= avatar_size %>">
24
+ <% if avatar.attached? %>
25
+ <%= image_tag avatar.variant(avatar_variant), alt: alt_text %>
26
+ <% else %>
27
+ <%= tag.span name.first.upcase, class: "initial" %>
28
+ <% end %>
29
+ </span>
30
+ ```
31
+
32
+ ```ruby
33
+ # app/views/components/avatar_component.rb
34
+ class AvatarComponent < PartialsFx::Component
35
+ attribute :user, required: true
36
+ attribute :size, :string, default: "md", inquiry: true
37
+ attribute :variant, :string, default: "thumb", inquiry: true
38
+
39
+ def avatar_size
40
+ return "avatar-profile" if variant.profile?
41
+
42
+ class_names(
43
+ "avatar-xs": size.xs?,
44
+ "avatar-sm": size.sm?,
45
+ "avatar-md": size.md?,
46
+ "avatar-lg": size.lg?,
47
+ "avatar-xl": size.xl?
48
+ )
49
+ end
50
+
51
+ def name
52
+ @name ||= user.decorate.name
53
+ end
54
+
55
+ def avatar = profile.avatar
56
+
57
+ def alt_text = "Avatar for #{name}"
58
+
59
+ def avatar_variant
60
+ variant.thumb? ? :thumb : :profile
61
+ end
62
+
63
+ private
64
+
65
+ def profile
66
+ @profile ||= user.profile
67
+ end
68
+ end
69
+ ```
70
+
71
+ In views:
72
+ ```erb
73
+ <%= render AvatarComponent.new user: Current.user %>
74
+ ```
75
+
76
+
77
+ ## CSS modules
78
+
79
+ A powerful feature of Partials FX is support for “CSS modules”. Meaning that styles are locally scoped by default, preventing style conflicts across components. This allows the use of the same class names in different components without worrying about collisions, as Partials FX automatically generates unique class names, for the parent element, during compilation. CSS modules improve maintainability by creating a clear connection between components and their styles, making it easier to manage complex UI systems.
80
+
81
+ It works like this. First define a `styles` block in the class:
82
+ ```ruby
83
+ class AvatarComponent < PartialsFx::Component
84
+ # …
85
+
86
+ styles do
87
+ <<~CSS
88
+ .component {
89
+ }
90
+ CSS
91
+ end
92
+
93
+ # …
94
+ end
95
+ ```
96
+
97
+ Then add the `component_class` method to the the component partial:
98
+ ```erb
99
+ <span id="user-avatar" class="<%= component_class %> <%= avatar_size %>">
100
+ </span>
101
+ ```
102
+
103
+ You can now add any CSS selector in the `.component` selector, which itself will compile to something like:
104
+ ```css
105
+ /* app/assets/stylesheets/components.partialsfx.css */
106
+ .c-9af2b949 {
107
+ --avatar-font-size-fallback:
108
+ clamp(.75rem, calc(var(--avatar-height) * .8), 2rem);
109
+ display: inline flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ height: var(--avatar-height, 1rem);
113
+ /* … */
114
+
115
+ .initial {
116
+ font-size: var(--avatar-font-size, var(--avatar-font-size-fallback));
117
+ font-weight: 600;
118
+ color: var(--avatar-color, var(--base-60));
119
+ }
120
+
121
+ &.avatar-profile {}
122
+
123
+ /* … */
124
+ }
125
+ ```
126
+
127
+ Inside the component partial, `component_class` will be replaced with the unique selector `.c-9af2b949`. Because [nested selectors are available in CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/Nesting_selector) this will work smoothly. Keeping your selectors scoped to the one component. Making your app's CSS way more maintainable.
128
+
129
+ Make sure to import the automatically-created CSS file from Partials FX into your `application.css`, e.g.
130
+ ```css
131
+ @layer reset, components, utilities;
132
+
133
+ /* … */
134
+ @import url("./components.partialsfx.css") layer(components);
135
+ /* … */
136
+ ```
137
+
138
+
139
+ # Contributing
140
+
141
+ This project uses [Standard](https://github.com/testdouble/standard) for formatting Ruby code. Please make sure to run `be standardrb` before submitting pull requests. Run tests via `rails test`.
142
+
143
+
144
+ ## License
145
+
146
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[test standard]
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "partials_fx"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ require "irb"
11
+ IRB.start(__FILE__)
data/bin/release ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+
3
+ VERSION=$1
4
+
5
+ if [ -z "$VERSION" ]; then
6
+ echo "Error: The version number is required."
7
+ echo "Usage: $0 <version-number>"
8
+ exit 1
9
+ fi
10
+
11
+ printf "module PartialsFx\n VERSION = \"$VERSION\"\nend\n" > ./lib/partials_fx/version.rb
12
+ bundle
13
+ git add Gemfile.lock lib/partials_fx/version.rb
14
+ git commit -m "Bump version for $VERSION"
15
+ git push
16
+ git tag v$VERSION
17
+ git push --tags
18
+ gem build partials_fx.gemspec
19
+ gem push "partials_fx-$VERSION.gem"
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Creates a new component with a view template and Ruby class
3
+
4
+ Example:
5
+ rails generate component Avatar
6
+
7
+ This will create:
8
+ app/views/components/_avatar_component.html.erb
9
+ app/views/components/avatar_component.rb
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/named_base"
5
+
6
+ class ComponentGenerator < Rails::Generators::NamedBase
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ check_class_collision suffix: "Component"
10
+
11
+ class_option :component_styles, type: :boolean, default: PartialsFx.configuration.enable_component_styles
12
+
13
+ def copy_component_file
14
+ template "component.rb", File.join(PartialsFx.configuration.components_path, class_path, "#{file_name}_component.rb")
15
+ template "component.html.erb", File.join(PartialsFx.configuration.components_path, class_path, "_#{file_name}_component.html.erb")
16
+ end
17
+
18
+ private
19
+
20
+ def parent_class = defined?(ApplicationComponent) ? ApplicationComponent : PartialsFx::Component
21
+ end
@@ -0,0 +1,4 @@
1
+ <%%# Component class: <%= PartialsFx.configuration.components_path %>/<%= file_path %>_component.rb %>
2
+ <div<%- if options["component_styles"] %> class="<%%= component_class %>"<%- end %>>
3
+ <%%# Your component here %>
4
+ </div>
@@ -0,0 +1,10 @@
1
+ class <%= class_name %>Component < <%= parent_class %>
2
+ <%- if options["component_styles"] -%>
3
+ styles do
4
+ <<~CSS
5
+ .component {
6
+ }
7
+ CSS
8
+ end
9
+ <%- end -%>
10
+ end
@@ -0,0 +1,35 @@
1
+ module PartialsFx
2
+ class Component
3
+ module Attributes
4
+ module ClassMethods
5
+ def attribute(name, *arguments, required: false, inquiry: false, **options)
6
+ track_required_attribute(name) if required
7
+
8
+ super(name, *arguments, **options)
9
+
10
+ return unless inquiry
11
+
12
+ define_method(name) do
13
+ value = super()
14
+
15
+ return value if value.nil?
16
+
17
+ ActiveSupport::StringInquirer.new(value.to_s)
18
+ end
19
+ end
20
+
21
+ def required_attributes
22
+ @required_attributes || []
23
+ end
24
+
25
+ private
26
+
27
+ def track_required_attribute(attribute_name)
28
+ @required_attributes ||= []
29
+
30
+ @required_attributes << attribute_name
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,27 @@
1
+ module PartialsFx
2
+ class Component
3
+ module Render
4
+ def render_in(view_context, &block)
5
+ @view_context = view_context
6
+
7
+ capture_content_if_block_given(&block)
8
+
9
+ partial_path = build_partial_path
10
+
11
+ view_context.render(partial: partial_path, locals: to_locals)
12
+ end
13
+
14
+ private
15
+
16
+ def capture_content_if_block_given(&block)
17
+ return unless block_given?
18
+
19
+ @content = @view_context.capture do
20
+ yield self
21
+ end
22
+ end
23
+
24
+ def build_partial_path = "components/#{self.class.name.underscore}"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ module PartialsFx
2
+ class Component
3
+ module Slots
4
+ module ClassMethods
5
+ def slot(slot_name, default: nil)
6
+ define_method(slot_name) do |&block|
7
+ @slots ||= {}
8
+
9
+ if block
10
+ @slots[slot_name] = view_context.capture(&block)
11
+
12
+ nil # Prevent output during capture
13
+ else
14
+ @slots[slot_name] || (default.is_a?(Symbol) ? send(default) : default)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,63 @@
1
+ module PartialsFx
2
+ class Component
3
+ module Styles
4
+ module ClassMethods
5
+ def styles(&block)
6
+ @styles_block = block
7
+
8
+ register_styles_if_rails_loaded
9
+ end
10
+
11
+ def component_styles
12
+ return @component_styles if defined?(@component_styles)
13
+ return nil unless @styles_block
14
+
15
+ generate_component_styles
16
+ end
17
+
18
+ def register_styles
19
+ return unless @styles_block
20
+
21
+ Stylist.register(self)
22
+ end
23
+
24
+ private
25
+
26
+ def register_styles_if_rails_loaded
27
+ register_styles if defined?(Rails) && Rails.root
28
+ end
29
+
30
+ def generate_component_styles
31
+ styles_content = @styles_block.call
32
+
33
+ @component_id = generate_component_id(styles_content)
34
+ @component_styles = process_styles_with_component_id(styles_content)
35
+ end
36
+
37
+ def generate_component_id(styles_content)
38
+ hash = Digest::MD5.hexdigest(styles_content)[0..7]
39
+
40
+ "c-#{hash}"
41
+ end
42
+
43
+ def process_styles_with_component_id(styles_content)
44
+ styles_content.gsub(/\.component\b/, ".#{@component_id}")
45
+ end
46
+ end
47
+
48
+ def component_class
49
+ return nil unless component_has_styles?
50
+
51
+ self.class.component_styles
52
+ self.class.instance_variable_get(:@component_id)
53
+ end
54
+
55
+ private
56
+
57
+ def component_has_styles?
58
+ self.class.instance_variable_defined?(:@styles_block) &&
59
+ self.class.instance_variable_get(:@styles_block)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require "action_view"
5
+
6
+ require "partials_fx/component/attributes"
7
+ require "partials_fx/component/slots"
8
+ require "partials_fx/component/styles"
9
+ require "partials_fx/component/render"
10
+
11
+ module PartialsFx
12
+ class Component
13
+ include ActiveModel::Model
14
+ include ActionView::Helpers::TagHelper
15
+ include ActiveModel::Attributes
16
+
17
+ include Styles
18
+ include Render
19
+
20
+ extend Attributes::ClassMethods
21
+ extend Slots::ClassMethods
22
+ extend Styles::ClassMethods
23
+
24
+ attr_accessor :content, :view_context
25
+
26
+ class << self
27
+ def inherited(subclass)
28
+ super
29
+
30
+ subclass.instance_eval do
31
+ @pending_registration = true
32
+ end
33
+ end
34
+ end
35
+
36
+ def initialize(**component_attributes)
37
+ validate_required_attributes(component_attributes)
38
+
39
+ super
40
+ end
41
+
42
+ def to_locals
43
+ base_locals = attributes.transform_keys(&:to_sym)
44
+
45
+ base_locals
46
+ .merge(public_methods(false).to_h { [_1, public_send(_1)] })
47
+ .merge(slot_locals)
48
+ .merge(content: content, component_class: component_class)
49
+ end
50
+
51
+ def method_missing(method, *arguments, &block)
52
+ if respond_to_missing?(method)
53
+ public_send(method, *arguments, &block)
54
+ else
55
+ super
56
+ end
57
+ end
58
+
59
+ def respond_to_missing?(method, include_private = false)
60
+ public_methods.include?(method) || super
61
+ end
62
+
63
+ private
64
+
65
+ def validate_required_attributes(component_attributes)
66
+ self.class.required_attributes.each do |required_attribute|
67
+ unless component_attributes.key?(required_attribute)
68
+ raise MissingRequiredAttributeError.new(required_attribute)
69
+ end
70
+ end
71
+ end
72
+
73
+ def slot_locals
74
+ @slots || {}
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PartialsFx
4
+ class Configuration
5
+ attr_accessor :components_path, :enable_component_styles, :components_layer
6
+
7
+ def initialize
8
+ @components_path = File.join("app", "views", "components")
9
+ @enable_component_styles = true
10
+ @components_layer = nil # e.g. "components"
11
+ end
12
+ end
13
+
14
+ class << self
15
+ def configuration
16
+ @configuration ||= Configuration.new
17
+ end
18
+
19
+ def configure = yield(configuration)
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PartialsFx
4
+ class Error < StandardError; end
5
+
6
+ class MissingRequiredAttributeError < Error
7
+ def initialize(attribute_name)
8
+ super("Missing required attribute: #{attribute_name}")
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module PartialsFx
6
+ class Railtie < Rails::Railtie
7
+ initializer "partials_fx.autoload", before: :set_autoload_paths do |app|
8
+ app.config.autoload_paths << app.root.join(PartialsFx.configuration.components_path)
9
+ end
10
+
11
+ config.after_initialize do
12
+ PartialsFx::Railtie.load_and_register_components if PartialsFx.configuration.enable_component_styles
13
+ end
14
+
15
+ if Rails.env.development?
16
+ config.to_prepare do
17
+ PartialsFx::Railtie.register_all_components if PartialsFx.configuration.enable_component_styles
18
+ end
19
+ end
20
+
21
+ class << self
22
+ def components_glob = Rails.root.join(PartialsFx.configuration.components_path, "**/*_component.rb")
23
+
24
+ def load_and_register_components
25
+ Dir[components_glob].each { require_dependency _1 }
26
+
27
+ register_all_components
28
+ end
29
+
30
+ def register_all_components
31
+ ObjectSpace.each_object(Class)
32
+ .select { _1 < PartialsFx::Component }
33
+ .each(&:register_styles)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PartialsFx
4
+ class Stylist
5
+ class Writer
6
+ def self.print(component_class)
7
+ new.print!(component_class)
8
+ end
9
+
10
+ def print!(component_class)
11
+ component_name = component_class.name
12
+ component_class.instance_variable_get(:@component_id)
13
+ styles = component_class.component_styles
14
+
15
+ ensure_stylesheet_exists!
16
+
17
+ update_stylesheet_with(component_name, styles)
18
+ end
19
+
20
+ private
21
+
22
+ def ensure_stylesheet_exists!
23
+ return if File.exist?(stylesheet_path)
24
+
25
+ FileUtils.mkdir_p(File.dirname(stylesheet_path))
26
+ File.write(stylesheet_path, initial_content)
27
+ end
28
+
29
+ def update_stylesheet_with(component_name, styles)
30
+ content = File.read(stylesheet_path)
31
+ updated_content = merge_styles_into(content, component_name, styles)
32
+
33
+ write_atomically(updated_content)
34
+
35
+ notify_asset_pipeline!
36
+ end
37
+
38
+ def merge_styles_into(content, component_name, styles)
39
+ wrapped_styles = wrap_with(component_name, styles)
40
+ pattern = style_pattern_for(component_name)
41
+
42
+ if content.match?(pattern)
43
+ content.gsub(pattern, wrapped_styles.strip)
44
+ else
45
+ append_styles_to(content, wrapped_styles)
46
+ end
47
+ end
48
+
49
+ def write_atomically(content)
50
+ if PartialsFx.configuration.components_layer
51
+ content = content.rstrip + "\n}\n"
52
+ end
53
+
54
+ temp_file = "#{stylesheet_path}.tmp"
55
+
56
+ File.write(temp_file, content)
57
+ FileUtils.mv(temp_file, stylesheet_path)
58
+ end
59
+
60
+ def notify_asset_pipeline! = FileUtils.touch(stylesheet_path)
61
+
62
+ def wrap_with(component_name, styles)
63
+ start_marker = "/* #{component_name} */\n"
64
+ end_marker = "/* /#{component_name} */"
65
+
66
+ "#{start_marker}#{styles}#{end_marker}"
67
+ end
68
+
69
+ def style_pattern_for(component_name)
70
+ /\/\* #{Regexp.escape(component_name)} \*\/\n.*?\/\* \/#{Regexp.escape(component_name)} \*\//m
71
+ end
72
+
73
+ def append_styles_to(content, wrapped_styles)
74
+ [content.chomp, wrapped_styles, "\n"].join("\n")
75
+ end
76
+
77
+ def initial_content
78
+ content = "/* Generated by PartialsFx - Do not edit manually */\n"
79
+
80
+ if (layer = PartialsFx.configuration.components_layer)
81
+ content += "@layer #{layer} {\n"
82
+ end
83
+
84
+ content
85
+ end
86
+
87
+ def stylesheet_path
88
+ @stylesheet_path ||= Rails.root.join("app/assets/stylesheets/components.partialsfx.css")
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "partials_fx/stylist/writer"
4
+
5
+ module PartialsFx
6
+ class Stylist
7
+ def self.register(component_class)
8
+ return if unstyled?(component_class)
9
+
10
+ Writer.print(component_class)
11
+ end
12
+
13
+ private_class_method def self.unstyled?(component_class)
14
+ !component_class.component_styles &&
15
+ !component_class.instance_variable_get(:@component_id)
16
+ end
17
+ end
18
+ end
@@ -1,3 +1,3 @@
1
1
  module PartialsFx
2
- VERSION = "0.0.1"
2
+ VERSION = "0.5.0"
3
3
  end
data/lib/partials_fx.rb CHANGED
@@ -1,4 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "partials_fx/version"
4
+ require "partials_fx/configuration"
5
+ require "generators/component_generator"
6
+ require "partials_fx/errors"
7
+ require "partials_fx/component"
8
+ require "partials_fx/stylist"
9
+ require "partials_fx/railtie"
2
10
 
3
11
  module PartialsFx
4
12
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/partials_fx/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "partials_fx"
7
+ spec.version = PartialsFx::VERSION
8
+ spec.authors = ["Rails Designer"]
9
+ spec.email = ["devs@railsdesigner.com"]
10
+
11
+ spec.summary = "Extends Rails' partials to make them quack more like components"
12
+ spec.description = "Partials FX extends Rails' partials to make them quack more like components. Each partial, located in app/views/components/, is backed by a Ruby class with the same name. It also supports CSS modules, meaning you define CSS in the component class and it gets “scoped” to that component only."
13
+ spec.homepage = "https://railsdesigner.com/partials-fx/"
14
+ spec.license = "MIT"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/Rails-Designer/partials_fx/"
18
+
19
+ spec.files = Dir["{bin,lib}/**/*", "Rakefile", "README.md", "partials_fx.gemspec", "Gemfile", "Gemfile.lock"]
20
+
21
+ spec.required_ruby_version = ">= 3.4.0"
22
+
23
+ spec.add_dependency "rails", ">= 7.2", "< 8.1"
24
+ end
metadata CHANGED
@@ -1,26 +1,74 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: partials_fx
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rails Designer
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.2'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '8.1'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '7.2'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '8.1'
32
+ description: Partials FX extends Rails' partials to make them quack more like components.
33
+ Each partial, located in app/views/components/, is backed by a Ruby class with the
34
+ same name. It also supports CSS modules, meaning you define CSS in the component
35
+ class and it gets “scoped” to that component only.
12
36
  email:
13
37
  - devs@railsdesigner.com
14
38
  executables: []
15
39
  extensions: []
16
40
  extra_rdoc_files: []
17
41
  files:
42
+ - Gemfile
43
+ - Gemfile.lock
44
+ - README.md
45
+ - Rakefile
46
+ - bin/console
47
+ - bin/release
48
+ - bin/setup
49
+ - lib/generators/USAGE
50
+ - lib/generators/component_generator.rb
51
+ - lib/generators/templates/component.html.erb.tt
52
+ - lib/generators/templates/component.rb.tt
18
53
  - lib/partials_fx.rb
54
+ - lib/partials_fx/component.rb
55
+ - lib/partials_fx/component/attributes.rb
56
+ - lib/partials_fx/component/render.rb
57
+ - lib/partials_fx/component/slots.rb
58
+ - lib/partials_fx/component/styles.rb
59
+ - lib/partials_fx/configuration.rb
60
+ - lib/partials_fx/errors.rb
61
+ - lib/partials_fx/railtie.rb
62
+ - lib/partials_fx/stylist.rb
63
+ - lib/partials_fx/stylist/writer.rb
19
64
  - lib/partials_fx/version.rb
20
- homepage: https://railsdesigner.com/
65
+ - partials_fx.gemspec
66
+ homepage: https://railsdesigner.com/partials-fx/
21
67
  licenses:
22
68
  - MIT
23
- metadata: {}
69
+ metadata:
70
+ homepage_uri: https://railsdesigner.com/partials-fx/
71
+ source_code_uri: https://github.com/Rails-Designer/partials_fx/
24
72
  rdoc_options: []
25
73
  require_paths:
26
74
  - lib
@@ -28,14 +76,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
28
76
  requirements:
29
77
  - - ">="
30
78
  - !ruby/object:Gem::Version
31
- version: 3.1.0
79
+ version: 3.4.0
32
80
  required_rubygems_version: !ruby/object:Gem::Requirement
33
81
  requirements:
34
82
  - - ">="
35
83
  - !ruby/object:Gem::Version
36
84
  version: '0'
37
85
  requirements: []
38
- rubygems_version: 3.6.7
86
+ rubygems_version: 3.6.9
39
87
  specification_version: 4
40
- summary: Rails partials enhanced.
88
+ summary: Extends Rails' partials to make them quack more like components
41
89
  test_files: []