proscenium 0.19.0.beta4-x86_64-darwin → 0.19.0.beta5-x86_64-darwin

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/proscenium/builder.rb +9 -13
  4. data/lib/proscenium/ext/proscenium +0 -0
  5. data/lib/proscenium/importer.rb +13 -13
  6. data/lib/proscenium/middleware/base.rb +0 -2
  7. data/lib/proscenium/middleware/engines.rb +5 -9
  8. data/lib/proscenium/middleware/esbuild.rb +13 -8
  9. data/lib/proscenium/middleware.rb +2 -4
  10. data/lib/proscenium/railtie.rb +6 -3
  11. data/lib/proscenium/react_componentable.rb +1 -1
  12. data/lib/proscenium/resolver.rb +3 -8
  13. data/lib/proscenium/side_load.rb +1 -1
  14. data/lib/proscenium/ui/flash/index.css +1 -0
  15. data/lib/proscenium/ui/flash/index.js +73 -0
  16. data/lib/proscenium/ui/flash.rb +15 -0
  17. data/lib/proscenium/ui/form/field_methods.rb +88 -0
  18. data/lib/proscenium/ui/form/fields/base.rb +188 -0
  19. data/lib/proscenium/ui/form/fields/checkbox/index.jsx +48 -0
  20. data/lib/proscenium/ui/form/fields/checkbox/index.module.css +9 -0
  21. data/lib/proscenium/ui/form/fields/checkbox/previews/basic.jsx +8 -0
  22. data/lib/proscenium/ui/form/fields/checkbox.rb +32 -0
  23. data/lib/proscenium/ui/form/fields/date.module.css +27 -0
  24. data/lib/proscenium/ui/form/fields/datetime.rb +15 -0
  25. data/lib/proscenium/ui/form/fields/hidden.rb +9 -0
  26. data/lib/proscenium/ui/form/fields/input/index.jsx +71 -0
  27. data/lib/proscenium/ui/form/fields/input/index.module.css +13 -0
  28. data/lib/proscenium/ui/form/fields/input/previews/basic.jsx +8 -0
  29. data/lib/proscenium/ui/form/fields/input.rb +14 -0
  30. data/lib/proscenium/ui/form/fields/radio_group.rb +173 -0
  31. data/lib/proscenium/ui/form/fields/radio_input/index.jsx +44 -0
  32. data/lib/proscenium/ui/form/fields/radio_input/index.module.css +13 -0
  33. data/lib/proscenium/ui/form/fields/radio_input/previews/basic.jsx +8 -0
  34. data/lib/proscenium/ui/form/fields/radio_input.rb +17 -0
  35. data/lib/proscenium/ui/form/fields/rich_textarea.css +23 -0
  36. data/lib/proscenium/ui/form/fields/rich_textarea.js +6 -0
  37. data/lib/proscenium/ui/form/fields/rich_textarea.rb +18 -0
  38. data/lib/proscenium/ui/form/fields/select.jsx +47 -0
  39. data/lib/proscenium/ui/form/fields/select.module.css +46 -0
  40. data/lib/proscenium/ui/form/fields/select.rb +300 -0
  41. data/lib/proscenium/ui/form/fields/tel.css +297 -0
  42. data/lib/proscenium/ui/form/fields/tel.js +83 -0
  43. data/lib/proscenium/ui/form/fields/tel.rb +54 -0
  44. data/lib/proscenium/ui/form/fields/textarea/index.jsx +50 -0
  45. data/lib/proscenium/ui/form/fields/textarea/index.module.css +13 -0
  46. data/lib/proscenium/ui/form/fields/textarea/previews/basic.jsx +8 -0
  47. data/lib/proscenium/ui/form/fields/textarea.rb +18 -0
  48. data/lib/proscenium/ui/form/translation.rb +71 -0
  49. data/lib/proscenium/ui/form.css +52 -0
  50. data/lib/proscenium/ui/form.rb +213 -0
  51. data/lib/proscenium/ui/props.css +7 -0
  52. data/lib/proscenium/ui/react-manager/index.jsx +1 -1
  53. data/lib/proscenium/ui/test.js +1 -1
  54. data/lib/proscenium/ui/ujs/index.js +1 -1
  55. data/lib/proscenium/ui.rb +3 -0
  56. data/lib/proscenium/utils.rb +33 -0
  57. data/lib/proscenium/version.rb +1 -1
  58. data/lib/proscenium/view_component.rb +0 -2
  59. data/lib/proscenium.rb +12 -2
  60. metadata +61 -10
  61. data/lib/proscenium/middleware/runtime.rb +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d337128cd3145af4fa669a65928b7dce82c27eb20e659ab2e547982779c9d5e5
4
- data.tar.gz: 93f32b35727ac31e4191050ded38e6132c36f9c49393d866ad0497cc92a48315
3
+ metadata.gz: c7d5e8e2b9deefe7c722dc78c0ad12651401380329e1bb6ca605db1eb71e3069
4
+ data.tar.gz: ac844bc778e8e57136c998a0524adb669b89ccbef103ad2b66ae60fdaa035ae0
5
5
  SHA512:
6
- metadata.gz: 8956e0f943e104d3276bab91e97b5f9b4ff4e3b914152e07bcfe6f39cf57827a8843c38ad22cfa9f034d68b4c5613211e4410c15a9c998f774f55f7152fb7f82
7
- data.tar.gz: ffe4c4900cab2ff8e78a5e606e7fdbce3368f9a01026d33b7826ccf89afc8d95e558cde6a04e80c3866e9f60e87596f127e6f91bb5e75cd95694fe0902ec8bf5
6
+ metadata.gz: a56e2457d22054e28f1fac57e9411c571654fbbac6bf19ada0818845057cf3f6e39fa03d9e10cfda02b9f112ccfd05ab69795e28cada92911cb09017f26d017a
7
+ data.tar.gz: cba5c32379b284930b29c13b26fe3f4fed22a0dd3bee2fd4c4bb98a778aa24709360efd6d9932a84cfe42e88024d909ac4773996d77604bd483bdb1cfb61ee93
data/README.md CHANGED
@@ -413,7 +413,7 @@ if (typeof proscenium.env?.UNKNOWN !== "undefined") {
413
413
  Basic support is provided for importing your Rails locale files from `config/locales/*.yml`, exporting them as JSON.
414
414
 
415
415
  ```js
416
- import translations from "@proscenium/i18n";
416
+ import translations from "proscenium/i18n";
417
417
  // translations.en.*
418
418
  ```
419
419
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'ffi'
4
- require 'oj'
5
4
 
6
5
  module Proscenium
7
6
  class Builder
@@ -42,7 +41,7 @@ module Proscenium
42
41
  attr_reader :error
43
42
 
44
43
  def initialize(error)
45
- @error = Oj.load(error, mode: :strict).deep_transform_keys(&:underscore)
44
+ @error = JSON.parse(error, strict: true).deep_transform_keys(&:underscore)
46
45
 
47
46
  msg = @error['text']
48
47
  if (location = @error['location'])
@@ -65,8 +64,8 @@ module Proscenium
65
64
  new(root:).build_to_path(path)
66
65
  end
67
66
 
68
- def self.build_to_string(path, root: nil)
69
- new(root:).build_to_string(path)
67
+ def self.build_to_string(path, root: nil, bundle: nil)
68
+ new(root:, bundle:).build_to_string(path)
70
69
  end
71
70
 
72
71
  def self.resolve(path, root: nil)
@@ -78,15 +77,18 @@ module Proscenium
78
77
  Request.reset_config
79
78
  end
80
79
 
81
- def initialize(root: nil)
80
+ def initialize(root: nil, bundle: nil)
81
+ bundle = Proscenium.config.bundle if bundle.nil?
82
+
82
83
  @request_config = FFI::MemoryPointer.from_string({
83
84
  RootPath: (root || Rails.root).to_s,
84
85
  GemPath: gem_root,
85
86
  Environment: ENVIRONMENTS.fetch(Rails.env.to_sym, 2),
86
- Engines: engines,
87
+ Engines: Proscenium.config.engines,
87
88
  EnvVars: env_vars,
88
89
  CodeSplitting: Proscenium.config.code_splitting,
89
- Bundle: Proscenium.config.bundle,
90
+ ExternalNodeModules: Proscenium.config.external_node_modules,
91
+ Bundle: bundle,
90
92
  Debug: Proscenium.config.debug
91
93
  }.to_json)
92
94
  end
@@ -139,12 +141,6 @@ module Proscenium
139
141
  q ? "--cache-query-string #{q}" : nil
140
142
  end
141
143
 
142
- def engines
143
- Proscenium.config.engines.to_h { |e| [e.engine_name, e.root.to_s] }.tap do |x|
144
- x['proscenium/ui'] = Proscenium.ui_path.to_s
145
- end
146
- end
147
-
148
144
  def gem_root
149
145
  Pathname.new(__dir__).join('..', '..').to_s
150
146
  end
Binary file
@@ -67,28 +67,28 @@ module Proscenium
67
67
  sideload_css(filepath, **options) unless options[:css] == false
68
68
  end
69
69
 
70
- def sideload_js(filepath, **options)
71
- return unless Proscenium.config.side_load
72
-
73
- filepath = Rails.root.join(filepath) unless filepath.is_a?(Pathname)
74
- filepath = filepath.sub_ext('')
70
+ def sideload_js(filepath, **)
71
+ _sideload(filepath, JS_EXTENSIONS, **)
72
+ end
75
73
 
76
- JS_EXTENSIONS.find do |x|
77
- if (fp = filepath.sub_ext(x)).exist?
78
- import(Resolver.resolve(fp.to_s), sideloaded: true, **options)
79
- end
80
- end
74
+ def sideload_css(filepath, **)
75
+ _sideload(filepath, CSS_EXTENSIONS, **)
81
76
  end
82
77
 
83
- def sideload_css(filepath, **options)
78
+ private def _sideload(filepath, extensions, **options) # rubocop:disable Style/AccessModifierDeclarations
84
79
  return unless Proscenium.config.side_load
85
80
 
86
81
  filepath = Rails.root.join(filepath) unless filepath.is_a?(Pathname)
87
82
  filepath = filepath.sub_ext('')
88
83
 
89
- CSS_EXTENSIONS.find do |x|
84
+ extensions.find do |x|
90
85
  if (fp = filepath.sub_ext(x)).exist?
91
- import(Resolver.resolve(fp.to_s), sideloaded: true, **options)
86
+ if (fp = fp.to_s).start_with?(Proscenium.ui_path.to_s)
87
+ fp.sub!(Proscenium.ui_path_regex, 'proscenium/')
88
+ import(Resolver.resolve(fp), sideloaded: true, **options)
89
+ else
90
+ import(Resolver.resolve(fp.to_s), sideloaded: true, **options)
91
+ end
92
92
  end
93
93
  end
94
94
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'oj'
4
-
5
3
  module Proscenium
6
4
  class Middleware
7
5
  class Base
@@ -10,7 +10,7 @@ module Proscenium
10
10
  #
11
11
  # module Gem1
12
12
  # class Engine < ::Rails::Engine
13
- # config.proscenium.engines << self
13
+ # config.proscenium.engines['gem1'] = root
14
14
  # end
15
15
  # end
16
16
  #
@@ -24,21 +24,17 @@ module Proscenium
24
24
  end
25
25
 
26
26
  def root_for_readable
27
- ui? ? Proscenium.ui_path : engine.root
27
+ engine.last
28
28
  end
29
29
 
30
30
  def engine
31
- @engine ||= Proscenium.config.engines.find do |x|
32
- @request.path.start_with?("/#{x.engine_name}")
31
+ @engine ||= Proscenium.config.engines.find do |k, _|
32
+ @request.path.start_with?("/#{k}")
33
33
  end
34
34
  end
35
35
 
36
36
  def engine_name
37
- ui? ? 'proscenium/ui' : engine.engine_name
38
- end
39
-
40
- def ui?
41
- @request.path.start_with?('/proscenium/ui/')
37
+ engine.first
42
38
  end
43
39
  end
44
40
  end
@@ -6,21 +6,26 @@ module Proscenium
6
6
  class CompileError < Base::CompileError
7
7
  def initialize(args)
8
8
  detail = args[:detail]
9
- detail = ActiveSupport::HashWithIndifferentAccess.new(Oj.load(detail, mode: :strict))
9
+ detail = JSON.parse(detail, mode: :strict)
10
10
 
11
- args[:detail] = if detail[:location]
12
- "#{detail[:text]} in #{detail[:location][:file]}:" +
13
- detail[:location][:line].to_s
14
- else
15
- detail[:text]
16
- end
11
+ args['detail'] = if detail['location']
12
+ "#{detail['text']} in #{detail['location']['file']}:" +
13
+ detail['location']['line'].to_s
14
+ else
15
+ detail['text']
16
+ end
17
17
 
18
18
  super
19
19
  end
20
20
  end
21
21
 
22
22
  def attempt
23
- render_response Builder.build_to_string(path_to_build)
23
+ bundle = nil
24
+ if Proscenium.config.external_node_modules && path_to_build.start_with?('node_modules/')
25
+ bundle = false
26
+ end
27
+
28
+ render_response Builder.build_to_string(path_to_build, bundle:)
24
29
  rescue Builder::CompileError => e
25
30
  raise self.class::CompileError, { file: @request.fullpath, detail: e.message }, caller
26
31
  end
@@ -10,7 +10,6 @@ module Proscenium
10
10
  autoload :Base
11
11
  autoload :Esbuild
12
12
  autoload :Engines
13
- autoload :Runtime
14
13
 
15
14
  def initialize(app)
16
15
  @app = app
@@ -40,7 +39,6 @@ module Proscenium
40
39
  end
41
40
 
42
41
  def find_type(request)
43
- return Runtime if request.path.match?(%r{^/@proscenium/})
44
42
  return Esbuild if Pathname.new(request.path).fnmatch?(app_path_glob, File::FNM_EXTGLOB)
45
43
 
46
44
  pathname = Pathname.new(request.path)
@@ -53,12 +51,12 @@ module Proscenium
53
51
  end
54
52
 
55
53
  def engines_path_glob
56
- names = Proscenium.config.engines.map(&:engine_name)
54
+ names = Proscenium.config.engines.keys
57
55
  "/{#{names.join(',')}}/{#{Proscenium::ALLOWED_DIRECTORIES}}/**.{#{file_extensions}}"
58
56
  end
59
57
 
60
58
  def ui_path_glob
61
- "/proscenium/ui/**.{#{file_extensions}}"
59
+ "/proscenium/**.{#{file_extensions}}"
62
60
  end
63
61
 
64
62
  def file_extensions
@@ -14,6 +14,7 @@ module Proscenium
14
14
  config.proscenium.bundle = true
15
15
  config.proscenium.side_load = true
16
16
  config.proscenium.code_splitting = true
17
+ config.proscenium.external_node_modules = false
17
18
 
18
19
  # Cache asset paths when building to path. Enabled by default in production.
19
20
  # @see Proscenium::Builder#build_to_path
@@ -36,9 +37,11 @@ module Proscenium
36
37
  #
37
38
  # Example:
38
39
  # class Gem1::Engine < ::Rails::Engine
39
- # config.proscenium.engines << self
40
+ # config.proscenium.engines[:gem1] = root
40
41
  # end
41
- config.proscenium.engines = Set.new
42
+ config.proscenium.engines = {
43
+ proscenium: Proscenium.ui_path
44
+ }
42
45
 
43
46
  config.action_dispatch.rescue_templates = {
44
47
  'Proscenium::Builder::BuildError' => 'build_error'
@@ -64,7 +67,7 @@ module Proscenium
64
67
  end
65
68
 
66
69
  initializer 'proscenium.middleware' do |app|
67
- app.middleware.insert_after ActionDispatch::Static, Middleware
70
+ app.middleware.insert_after ActionDispatch::Static, Proscenium::Middleware
68
71
  app.middleware.insert_after ActionDispatch::Static, Rack::ETag, 'no-cache'
69
72
  app.middleware.insert_after ActionDispatch::Static, Rack::ConditionalGet
70
73
  end
@@ -40,7 +40,7 @@ module Proscenium
40
40
  class_attribute :loader
41
41
 
42
42
  # @return [String] the URL path to the component manager.
43
- class_attribute :manager, default: '/@proscenium/react-manager/index.jsx'
43
+ class_attribute :manager, default: '/proscenium/react-manager/index.jsx'
44
44
  end
45
45
 
46
46
  class_methods do
@@ -11,8 +11,6 @@ module Proscenium
11
11
  #
12
12
  # @param path [String] Can be URL path, file system path, or bare specifier (ie. NPM package).
13
13
  # @return [String] URL path.
14
- #
15
- # rubocop:disable Metrics/*
16
14
  def self.resolve(path)
17
15
  self.resolved ||= {}
18
16
 
@@ -21,12 +19,10 @@ module Proscenium
21
19
  raise ArgumentError, 'path must be an absolute file system or URL path'
22
20
  end
23
21
 
24
- if path.start_with?('@proscenium/')
22
+ if path.start_with?('proscenium/')
25
23
  "/#{path}"
26
- elsif path.start_with?(Proscenium.ui_path.to_s)
27
- path.delete_prefix Proscenium.root.join('lib').to_s
28
- elsif (engine = Proscenium.config.engines.find { |e| path.start_with? "#{e.root}/" })
29
- path.sub(/^#{engine.root}/, "/#{engine.engine_name}")
24
+ elsif (engine = Proscenium.config.engines.find { |_, v| path.start_with? "#{v}/" })
25
+ path.sub(/^#{engine.last}/, "/#{engine.first}")
30
26
  elsif path.start_with?("#{Rails.root}/")
31
27
  path.delete_prefix Rails.root.to_s
32
28
  else
@@ -34,6 +30,5 @@ module Proscenium
34
30
  end
35
31
  end
36
32
  end
37
- # rubocop:enable Metrics/*
38
33
  end
39
34
  end
@@ -172,7 +172,7 @@ module Proscenium
172
172
  end
173
173
 
174
174
  # The reason why we sideload CSS after JS is because the order of CSS is important.
175
- # Basically, the layout should be loaded before the view so that CSS cascading works i9n the
175
+ # Basically, the layout should be loaded before the view so that CSS cascading works in the
176
176
  # right direction.
177
177
  css_imports.reverse_each do |it|
178
178
  Importer.sideload_css it, **options
@@ -0,0 +1 @@
1
+ @import "https://cdn.jsdelivr.net/npm/sourdough-toast/src/sourdough-toast.css";
@@ -0,0 +1,73 @@
1
+ import domMutations from "https://esm.run/dom-mutations";
2
+ import { Sourdough, toast } from "https://esm.run/sourdough-toast";
3
+
4
+ class HueFlash extends HTMLElement {
5
+ static observedAttributes = ["data-flash-alert", "data-flash-notice"];
6
+
7
+ connectedCallback() {
8
+ this.#initSourdough();
9
+ }
10
+
11
+ async #initSourdough() {
12
+ if ("sourdoughBooted" in window) return;
13
+
14
+ const sourdough = new Sourdough({
15
+ richColors: true,
16
+ yPosition: "bottom",
17
+ xPosition: "center",
18
+ });
19
+ sourdough.boot();
20
+ window.sourdoughBooted = true;
21
+
22
+ // Watch for changes to htl:flashes meta tag
23
+ const flashesSelector = "meta[name='rails:flashes']";
24
+ for await (const mutation of domMutations(document.head, {
25
+ childList: true,
26
+ subtree: true,
27
+ attributes: true,
28
+ })) {
29
+ let $ele = null;
30
+
31
+ if (
32
+ mutation.type === "attributes" &&
33
+ mutation.target.nodeName == "META" &&
34
+ mutation.attributeName == "content"
35
+ ) {
36
+ $ele = mutation.target;
37
+ } else if (mutation.type === "childList") {
38
+ for (const node of mutation.addedNodes) {
39
+ if (node.matches(flashesSelector)) {
40
+ $ele = node;
41
+ break;
42
+ }
43
+ }
44
+ }
45
+
46
+ if ($ele) {
47
+ const flashes = JSON.parse($ele.getAttribute("content"));
48
+ for (const [type, message] of Object.entries(flashes)) {
49
+ if (type === "alert") {
50
+ toast.error(message);
51
+ } else if (type === "notice") {
52
+ toast.success(message);
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
58
+
59
+ attributeChangedCallback(name, _oldValue, newValue) {
60
+ this.#initSourdough();
61
+
62
+ if (newValue === null) return;
63
+
64
+ if (name === "data-flash-alert") {
65
+ toast.warning(newValue);
66
+ } else if (name === "data-flash-notice") {
67
+ toast.success(newValue);
68
+ }
69
+ }
70
+ }
71
+
72
+ !customElements.get("pui-flash") &&
73
+ customElements.define("pui-flash", HueFlash);
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium::UI
4
+ class Flash < Component
5
+ register_element :pui_flash
6
+
7
+ def self.source_path
8
+ super / '../flash/index.rb'
9
+ end
10
+
11
+ def view_template
12
+ pui_flash data: { flash: helpers.flash.to_hash }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Proscenium::UI::Form
4
+ module FieldMethods
5
+ # Renders a hidden input field.
6
+ #
7
+ # @param args [Array<Symbol>] name or nested names of model attribute
8
+ # @param attributes [Hash] passed through to each input
9
+ def hidden_field(*args, **)
10
+ render Fields::Hidden.new(args, @model, self, **)
11
+ end
12
+
13
+ # @param args [Array<Symbol>] name or nested names of model attribute
14
+ # @param attributes [Hash] passed through to each input
15
+ def rich_textarea_field(*args, **attributes)
16
+ merge_bang_attributes! args, attributes
17
+ render Fields::RichTextarea.new(args, @model, self, **attributes)
18
+ end
19
+
20
+ # @param args [Array<Symbol>] name or nested names of model attribute
21
+ # @param attributes [Hash] passed through to each input
22
+ def datetime_local_field(*args, **attributes)
23
+ merge_bang_attributes! args, attributes
24
+ render Fields::Datetime.new(args, @model, self, **attributes)
25
+ end
26
+
27
+ # @param args [Array<Symbol>] name or nested names of model attribute
28
+ # @param attributes [Hash] passed through to each input
29
+ def checkbox_field(*args, **attributes)
30
+ merge_bang_attributes! args, attributes
31
+ render Fields::Checkbox.new(args, @model, self, **attributes)
32
+ end
33
+
34
+ # @param args [Array<Symbol>] name or nested names of model attribute
35
+ # @param attributes [Hash] passed through to each input
36
+ def tel_field(*args, **attributes)
37
+ merge_bang_attributes! args, attributes
38
+ render Fields::Tel.new(args, @model, self, **attributes)
39
+ end
40
+
41
+ # @param args [Array<Symbol>] name or nested names of model attribute
42
+ # @param attributes [Hash] passed through to each input
43
+ def select_field(*args, **attributes, &)
44
+ merge_bang_attributes! args, attributes, additional_bang_attrs: [:typeahead]
45
+ render Fields::Select.new(args, @model, self, **attributes, &)
46
+ end
47
+
48
+ # @see #select_field
49
+ def select_country_field(*args, **attributes)
50
+ merge_bang_attributes! args, attributes
51
+ attributes[:typeahead] = true
52
+ attributes[:options] = '/countries'
53
+ attributes[:component_props] = {
54
+ items_on_search: true,
55
+ input_props: { required: attributes.delete(:required) }
56
+ }
57
+
58
+ select_field(*args, **attributes)
59
+ end
60
+
61
+ # Renders a <textarea> field for the given `attribute`.
62
+ #
63
+ # @param args [Array<Symbol>] name or nested names of model attribute
64
+ # @param attributes [Hash] passed through to each input
65
+ def textarea_field(*args, **attributes)
66
+ merge_bang_attributes! args, attributes
67
+ render Fields::Textarea.new(args, @model, self, **attributes)
68
+ end
69
+
70
+ # Renders a group of radio inputs for each option of the given `field`.
71
+ #
72
+ # @param args [Array<Symbol>] name or nested names of model attribute
73
+ # @param attributes [Hash] passed through to each input
74
+ def radio_group(*args, **attributes)
75
+ attributes[:options] = args.pop if args.last.is_a?(Array)
76
+
77
+ render Fields::RadioGroup.new(args, @model, self, **attributes)
78
+ end
79
+
80
+ def radio_field(...)
81
+ div { radio_input(...) }
82
+ end
83
+
84
+ def radio_input(*args, **)
85
+ render Fields::RadioInput.new(args, @model, self, **)
86
+ end
87
+ end
88
+ end