plutonium 0.15.6 → 0.15.8

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/plutonium.css +1 -1
  3. data/app/assets/plutonium.js +25 -11
  4. data/app/assets/plutonium.js.map +2 -2
  5. data/app/assets/plutonium.min.js +4 -4
  6. data/app/assets/plutonium.min.js.map +3 -3
  7. data/app/views/layouts/rodauth.html.erb +2 -2
  8. data/docs/guide/getting-started/installation.md +2 -1
  9. data/docs/guide/getting-started/resources.md +8 -12
  10. data/docs/public/templates/plutonium.rb +8 -0
  11. data/lib/generators/pu/core/assets/assets_generator.rb +1 -1
  12. data/lib/generators/pu/core/assets/templates/tailwind.config.js +11 -1
  13. data/lib/generators/pu/eject/layout/layout_generator.rb +3 -3
  14. data/lib/generators/pu/eject/shell/shell_generator.rb +3 -3
  15. data/lib/generators/pu/extra/colorized_logger/colorized_logger_generator.rb +21 -0
  16. data/lib/generators/pu/extra/colorized_logger/templates/config/initializers/colorized_logger.rb +22 -0
  17. data/lib/generators/pu/gem/dotenv/dotenv_generator.rb +1 -1
  18. data/lib/generators/pu/gem/letter_opener/letter_opener_generator.rb +21 -0
  19. data/lib/generators/pu/gem/redis/redis_generator.rb +0 -2
  20. data/lib/generators/pu/gem/standard/standard_generator.rb +19 -0
  21. data/lib/generators/pu/lib/plutonium_generators/generator.rb +1 -1
  22. data/lib/generators/pu/res/conn/conn_generator.rb +1 -1
  23. data/lib/plutonium/core/controllers/authorizable.rb +1 -1
  24. data/lib/plutonium/definition/actions.rb +6 -2
  25. data/lib/plutonium/definition/base.rb +1 -0
  26. data/lib/plutonium/definition/nested_inputs.rb +19 -0
  27. data/lib/plutonium/resource/controller.rb +13 -9
  28. data/lib/plutonium/resource/controllers/authorizable.rb +2 -2
  29. data/lib/plutonium/resource/controllers/interactive_actions.rb +1 -1
  30. data/lib/plutonium/resource/controllers/presentable.rb +15 -8
  31. data/lib/plutonium/ui/block.rb +13 -0
  32. data/lib/plutonium/ui/component/kit.rb +10 -0
  33. data/lib/plutonium/ui/display/resource.rb +29 -11
  34. data/lib/plutonium/ui/display/theme.rb +1 -1
  35. data/lib/plutonium/ui/dyna_frame/content.rb +2 -2
  36. data/lib/plutonium/ui/dyna_frame/host.rb +20 -0
  37. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +285 -0
  38. data/lib/plutonium/ui/form/resource.rb +39 -29
  39. data/lib/plutonium/ui/form/theme.rb +1 -1
  40. data/lib/plutonium/ui/frame_navigator_panel.rb +53 -0
  41. data/lib/plutonium/ui/panel.rb +63 -0
  42. data/lib/plutonium/ui/skeleton_table.rb +29 -0
  43. data/lib/plutonium/ui/table/resource.rb +1 -1
  44. data/lib/plutonium/version.rb +1 -1
  45. data/package-lock.json +2 -2
  46. data/package.json +1 -1
  47. data/src/js/controllers/frame_navigator_controller.js +25 -8
  48. data/src/js/controllers/nested_resource_form_fields_controller.js +2 -2
  49. data/tailwind.config.js +11 -1
  50. data/tailwind.options.js +7 -1
  51. metadata +13 -3
  52. data/lib/generators/pu/gem/redis/templates/.keep +0 -0
@@ -0,0 +1,285 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module UI
5
+ module Form
6
+ module Concerns
7
+ # Handles rendering of nested resource fields in forms
8
+ # TODO: further decompose this into components
9
+ # @api private
10
+ module RendersNestedResourceFields
11
+ extend ActiveSupport::Concern
12
+
13
+ DEFAULT_NESTED_LIMIT = 10
14
+ NESTED_OPTION_KEYS = [:allow_destroy, :update_only, :macro, :class].freeze
15
+ SINGULAR_MACROS = %i[belongs_to has_one].freeze
16
+
17
+ class NestedInputsDefinition
18
+ include Plutonium::Definition::DefineableProps
19
+
20
+ defineable_props :field, :input
21
+ end
22
+
23
+ class NestedFieldContext
24
+ attr_reader :name, :definition, :options, :permitted_fields
25
+
26
+ def initialize(name:, definition:, resource_class:, resource_definition:, object_class:)
27
+ @name = name
28
+ @definition = definition
29
+ @resource_definition = resource_definition
30
+ @resource_class = resource_class
31
+ @options = build_options
32
+ @permitted_fields = build_permitted_fields
33
+ @object_class = object_class
34
+ end
35
+
36
+ def nested_attribute_options
37
+ @nested_attribute_options ||= @resource_class.all_nested_attributes_options[@name] || {}
38
+ end
39
+
40
+ def nested_fields_input_param
41
+ @options[:as] || :"#{@name}_attributes"
42
+ end
43
+
44
+ def nested_fields_multiple?
45
+ @options[:multiple]
46
+ end
47
+
48
+ def blank_object
49
+ (@object_class || nested_attribute_options[:class])&.new
50
+ end
51
+
52
+ private
53
+
54
+ def build_options
55
+ options = @resource_definition.defined_nested_inputs[@name][:options].dup || {}
56
+ merge_nested_fields_options(options)
57
+ set_nested_fields_limits(options)
58
+ options
59
+ end
60
+
61
+ def merge_nested_fields_options(options)
62
+ NESTED_OPTION_KEYS.each do |key|
63
+ options.fetch(key) { options[key] = nested_attribute_options[key] }
64
+ end
65
+ end
66
+
67
+ def set_nested_fields_limits(options)
68
+ options.fetch(:limit) do
69
+ options[:limit] = if SINGULAR_MACROS.include?(nested_attribute_options[:macro])
70
+ 1
71
+ else
72
+ nested_attribute_options[:limit] || DEFAULT_NESTED_LIMIT
73
+ end
74
+ end
75
+
76
+ options.fetch(:multiple) do
77
+ options[:multiple] = !SINGULAR_MACROS.include?(nested_attribute_options[:macro])
78
+ end
79
+ end
80
+
81
+ def build_permitted_fields
82
+ @options[:fields] || @definition.defined_inputs.keys
83
+ end
84
+ end
85
+
86
+ # Template object for new nested records
87
+
88
+ private
89
+
90
+ # Renders a nested resource field with associated inputs
91
+ # @param [Symbol] name The name of the nested resource field
92
+ # @raise [ArgumentError] if the nested input definition is missing required configuration
93
+ def render_nested_resource_field(name)
94
+ # debugger if $extracting_input
95
+ context = NestedFieldContext.new(
96
+ name: name,
97
+ definition: build_nested_fields_definition(name),
98
+ resource_class: resource_class,
99
+ resource_definition: resource_definition,
100
+ object_class: resource_definition.defined_nested_inputs[name][:options]&.fetch(:object_class, nil)
101
+ )
102
+
103
+ render_nested_field_container(context) do
104
+ render_nested_field_header(context)
105
+ render_nested_field_content(context)
106
+ render_nested_fields_add_button(context)
107
+ end
108
+ end
109
+
110
+ def build_nested_fields_definition(name)
111
+ nested_input_definition = resource_definition.defined_nested_inputs[name]
112
+
113
+ if nested_input_definition[:options]&.fetch(:using, nil)
114
+ nested_input_definition[:options][:using]
115
+ elsif nested_input_definition[:block]
116
+ build_nested_fields_definition_from_block(nested_input_definition[:block])
117
+ else
118
+ raise_missing_nested_definition_error(name)
119
+ end
120
+ end
121
+
122
+ def build_nested_fields_definition_from_block(block)
123
+ definition = NestedInputsDefinition.new
124
+ block.call(definition)
125
+ definition
126
+ end
127
+
128
+ def render_nested_field_container(context, &)
129
+ div(
130
+ class: "col-span-full space-y-2 my-4",
131
+ data: {
132
+ controller: "nested-resource-form-fields",
133
+ nested_resource_form_fields_limit_value: context.options[:limit]
134
+ },
135
+ &
136
+ )
137
+ end
138
+
139
+ def render_nested_field_header(context)
140
+ div do
141
+ h2(class: "text-lg font-semibold text-gray-900 dark:text-white") { context.name.to_s.humanize }
142
+ render_nested_fields_header_description(context.options[:description]) if context.options[:description]
143
+ end
144
+ end
145
+
146
+ def render_nested_fields_header_description(description)
147
+ p(class: "text-md font-normal text-gray-500 dark:text-gray-400") { description }
148
+ end
149
+
150
+ def render_nested_field_content(context)
151
+ if context.nested_fields_multiple?
152
+ render_multiple_nested_fields(context)
153
+ else
154
+ render_single_nested_field(context)
155
+ end
156
+
157
+ div(data_nested_resource_form_fields_target: :target, hidden: true)
158
+ end
159
+
160
+ def render_multiple_nested_fields(context)
161
+ nesting_method = :nest_many
162
+ options = {default: {NEW_RECORD: context.blank_object}}
163
+ render_template_for_nested_fields(context, options, nesting_method:)
164
+ render_existing_nested_fields(context, options, nesting_method:)
165
+ end
166
+
167
+ def render_single_nested_field(context)
168
+ nesting_method = :nest_one
169
+ options = {default: context.blank_object}
170
+ render_template_for_nested_fields(context, options, nesting_method:)
171
+ render_existing_nested_fields(context, options, nesting_method:)
172
+ end
173
+
174
+ def render_template_for_nested_fields(context, options, nesting_method:)
175
+ template_tag data_nested_resource_form_fields_target: "template" do
176
+ send(nesting_method, context.name, as: context.nested_fields_input_param, **options, template: true) do |nested|
177
+ render_nested_fields_fieldset(nested, context)
178
+ end
179
+ end
180
+ end
181
+
182
+ def render_existing_nested_fields(context, options, nesting_method:)
183
+ send(nesting_method, context.name, as: context.nested_fields_input_param, **options) do |nested|
184
+ render_nested_fields_fieldset(nested, context)
185
+ end
186
+ end
187
+
188
+ def render_nested_fields_fieldset(nested, context)
189
+ fieldset(
190
+ data_new_record: !nested.object&.persisted?,
191
+ class: "nested-resource-form-fields border border-gray-200 dark:border-gray-700 rounded-lg p-4 space-y-4 relative"
192
+ ) do
193
+ render_nested_fields_fieldset_content(nested, context)
194
+ render_nested_fields_delete_button(nested, context.options)
195
+ end
196
+ end
197
+
198
+ def render_nested_fields_fieldset_content(nested, context)
199
+ div(class: "grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-4 gap-4 grid-flow-row-dense") do
200
+ render_nested_fields_hidden_fields(nested, context)
201
+ render_nested_fields_visible_fields(nested, context)
202
+ end
203
+ end
204
+
205
+ def render_nested_fields_hidden_fields(nested, context)
206
+ if !context.options[:update_only] && context.options[:class]&.respond_to?(:primary_key)
207
+ render nested.field(context.options[:class].primary_key).hidden_tag
208
+ end
209
+ render nested.field(:_destroy).hidden_tag if context.options[:allow_destroy]
210
+ end
211
+
212
+ def render_nested_fields_visible_fields(nested, context)
213
+ context.permitted_fields.each do |input|
214
+ render_simple_resource_field(input, context.definition, nested)
215
+ end
216
+ end
217
+
218
+ def render_nested_fields_delete_button(nested, options)
219
+ return unless !nested.object&.persisted? || options[:allow_destroy]
220
+
221
+ render_nested_fields_delete_button_content
222
+ end
223
+
224
+ def render_nested_fields_delete_button_content
225
+ div(class: "flex items-center justify-end") do
226
+ label(class: "inline-flex items-center text-md font-medium text-red-900 cursor-pointer") do
227
+ plain "Delete"
228
+ render_nested_fields_delete_checkbox
229
+ end
230
+ end
231
+ end
232
+
233
+ def render_nested_fields_delete_checkbox
234
+ input(
235
+ type: :checkbox,
236
+ class: "w-4 h-4 ms-2 text-red-600 bg-red-100 border-red-300 rounded focus:ring-red-500 dark:focus:ring-red-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 cursor-pointer",
237
+ data_action: "nested-resource-form-fields#remove"
238
+ )
239
+ end
240
+
241
+ def render_nested_fields_add_button(context)
242
+ div do
243
+ button(
244
+ type: :button,
245
+ class: "inline-block",
246
+ data: {
247
+ action: "nested-resource-form-fields#add",
248
+ nested_resource_form_fields_target: "addButton"
249
+ }
250
+ ) do
251
+ render_nested_fields_add_button_content(context.name)
252
+ end
253
+ end
254
+ end
255
+
256
+ def render_nested_fields_add_button_content(name)
257
+ span(class: "bg-secondary-700 text-white hover:bg-secondary-800 focus:ring-secondary-300 dark:bg-secondary-600 dark:hover:bg-secondary-700 dark:focus:ring-secondary-800 flex items-center justify-center px-4 py-1.5 text-sm font-medium rounded-lg focus:outline-none focus:ring-4") do
258
+ render Phlex::TablerIcons::Plus.new(class: "w-4 h-4 mr-1")
259
+ span { "Add #{name.to_s.singularize.humanize}" }
260
+ end
261
+ end
262
+
263
+ def raise_missing_nested_definition_error(name)
264
+ raise ArgumentError, %(
265
+ `nested_input :#{name}` is missing a definition
266
+
267
+ you can either pass in a block:
268
+ ```ruby
269
+ nested_input :#{name} do |definition|
270
+ input :city
271
+ input :country
272
+ end
273
+ ```
274
+
275
+ or pass in options:
276
+ ```ruby
277
+ nested_input :#{name}, using: #{name.to_s.classify}Definition, fields: %i[city country]
278
+ ```
279
+ )
280
+ end
281
+ end
282
+ end
283
+ end
284
+ end
285
+ end
@@ -4,6 +4,8 @@ module Plutonium
4
4
  module UI
5
5
  module Form
6
6
  class Resource < Base
7
+ include Plutonium::UI::Form::Concerns::RendersNestedResourceFields
8
+
7
9
  attr_reader :resource_fields, :resource_definition
8
10
 
9
11
  def initialize(*, resource_fields:, resource_definition:, **, &)
@@ -35,37 +37,45 @@ module Plutonium
35
37
 
36
38
  def render_resource_field(name)
37
39
  when_permitted(name) do
38
- # field :name, as: :string
39
- # input :name, as: :string
40
- # input :description, class: "col-span-full"
41
- # input :age, tag: {class: "max-h-fit"}
42
- # input :dob do |f|
43
- # f.date_tag
44
- # end
45
-
46
- field_options = resource_definition.defined_fields[name] ? resource_definition.defined_fields[name][:options] : {}
47
-
48
- input_definition = resource_definition.defined_inputs[name] || {}
49
- input_options = input_definition[:options] || {}
50
-
51
- tag = field_options[:as] || input_options[:as]
52
- tag_attributes = input_options[:tag] || {}
53
- tag_block = input_definition[:block] || ->(f) {
54
- tag ||= f.inferred_field_component
55
- f.send(:"#{tag}_tag", **tag_attributes)
56
- }
57
-
58
- field_options = field_options.except(:as)
59
- wrapper_options = input_options.except(:tag, :as)
60
- if !wrapper_options[:class] || !wrapper_options[:class].include?("col-span")
61
- # temp hack to allow col span overrides
62
- # TODO: remove once we complete theming, which will support merges
63
- wrapper_options[:class] = tokens("col-span-full", wrapper_options[:class])
40
+ if resource_definition.defined_nested_inputs[name]
41
+ render_nested_resource_field(name)
42
+ else
43
+ render_simple_resource_field(name, resource_definition, self)
64
44
  end
45
+ end
46
+ end
65
47
 
66
- render field(name, **field_options).wrapped(**wrapper_options) do |f|
67
- render tag_block.call(f)
68
- end
48
+ def render_simple_resource_field(name, definition, form)
49
+ # field :name, as: :string
50
+ # input :name, as: :string
51
+ # input :description, class: "col-span-full"
52
+ # input :age, tag: {class: "max-h-fit"}
53
+ # input :dob do |f|
54
+ # f.date_tag
55
+ # end
56
+
57
+ field_options = definition.defined_fields[name] ? definition.defined_fields[name][:options] : {}
58
+
59
+ input_definition = definition.defined_inputs[name] || {}
60
+ input_options = input_definition[:options] || {}
61
+
62
+ tag = field_options[:as] || input_options[:as]
63
+ tag_attributes = input_options[:tag] || {}
64
+ tag_block = input_definition[:block] || ->(f) {
65
+ tag ||= f.inferred_field_component
66
+ f.send(:"#{tag}_tag", **tag_attributes)
67
+ }
68
+
69
+ field_options = field_options.except(:as)
70
+ wrapper_options = input_options.except(:tag, :as)
71
+ if !wrapper_options[:class] || !wrapper_options[:class].include?("col-span")
72
+ # temp hack to allow col span overrides
73
+ # TODO: remove once we complete theming, which will support merges
74
+ wrapper_options[:class] = tokens("col-span-full", wrapper_options[:class])
75
+ end
76
+
77
+ render form.field(name, **field_options).wrapped(**wrapper_options) do |f|
78
+ render tag_block.call(f)
69
79
  end
70
80
  end
71
81
 
@@ -7,7 +7,7 @@ module Plutonium
7
7
  def self.theme
8
8
  super.merge({
9
9
  base: "relative bg-white dark:bg-gray-800 shadow-md sm:rounded-lg my-3 p-6 space-y-6",
10
- fields_wrapper: "grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-4 gap-6 grid-flow-row-dense",
10
+ fields_wrapper: "grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-4 gap-4 grid-flow-row-dense",
11
11
  actions_wrapper: "flex justify-end space-x-2",
12
12
  wrapper: nil,
13
13
  inner_wrapper: "w-full",
@@ -0,0 +1,53 @@
1
+ module Plutonium
2
+ module UI
3
+ class FrameNavigatorPanel < Plutonium::UI::Component::Base
4
+ class PanelItem < Plutonium::UI::Component::Base
5
+ def initialize(label:, icon:, **attributes)
6
+ @label = label
7
+ @icon = icon
8
+ @attributes = attributes
9
+ end
10
+
11
+ def view_template
12
+ button(
13
+ title: @label,
14
+ style: "display: none",
15
+ class: "text-gray-600 dark:text-gray-300",
16
+ **@attributes
17
+ ) {
18
+ render @icon.new(class: "w-6 h-6")
19
+ }
20
+ end
21
+ end
22
+
23
+ class PanelContent < Plutonium::UI::Component::Base
24
+ def initialize(src:)
25
+ @src = src
26
+ end
27
+
28
+ def view_template
29
+ DynaFrameHost src: @src, loading: :lazy, data: {"frame-navigator-target": "frame"} do
30
+ SkeletonTable()
31
+ end
32
+ end
33
+ end
34
+
35
+ def initialize(title:, src:)
36
+ @title = title
37
+ @src = src
38
+ end
39
+
40
+ def view_template
41
+ div(data: {controller: %w[has-many-panel frame-navigator]}) do
42
+ Panel do |panel|
43
+ panel.with_title @title
44
+ panel.with_item PanelItem.new(label: "Home", icon: Phlex::TablerIcons::Home2, data_frame_navigator_target: %(homeButton))
45
+ panel.with_item PanelItem.new(label: "Back", icon: Phlex::TablerIcons::ChevronLeft, data_frame_navigator_target: %(backButton))
46
+ panel.with_item PanelItem.new(label: "Refresh", icon: Phlex::TablerIcons::RefreshDot, data_frame_navigator_target: %(refreshButton))
47
+ panel.with_content PanelContent.new(src: @src)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,63 @@
1
+ module Plutonium
2
+ module UI
3
+ class Panel < Plutonium::UI::Component::Base
4
+ include Phlex::DeferredRender
5
+
6
+ def initialize
7
+ @items = []
8
+ end
9
+
10
+ def with_title(title)
11
+ @title = title
12
+ end
13
+
14
+ def with_item(item)
15
+ @items << item
16
+ end
17
+
18
+ def with_content(content)
19
+ @content = content
20
+ end
21
+
22
+ def view_template
23
+ wrapped do
24
+ render_toolbar if render_toolbar?
25
+ render_content if render_content?
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def wrapped(&)
32
+ div(class: "mt-6", &)
33
+ end
34
+
35
+ def render_toolbar
36
+ div(class: %(flex justify-between items-center mb-4)) do
37
+ if @title.present?
38
+ h5(class: %(text-2xl font-bold tracking-tight text-gray-900 dark:text-white)) do
39
+ @title
40
+ end
41
+ end
42
+ div(class: "flex space-x-4") do
43
+ @items.each do |item|
44
+ render item
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def render_content
51
+ render @content
52
+ end
53
+
54
+ def render_toolbar?
55
+ @title || @items.present?
56
+ end
57
+
58
+ def render_content?
59
+ @content
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,29 @@
1
+ module Plutonium
2
+ module UI
3
+ class SkeletonTable < Plutonium::UI::Component::Base
4
+ def view_template
5
+ div(
6
+ role: "status",
7
+ class:
8
+ "p-4 space-y-4 border border-gray-200 divide-y divide-gray-200 rounded shadow motion-safe:animate-pulse dark:divide-gray-700 md:p-6 dark:border-gray-700"
9
+ ) do
10
+ div(class: "flex items-center justify-between") do
11
+ div do
12
+ div(class: "h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5")
13
+ div(class: "w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700")
14
+ end
15
+ div(class: "h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12")
16
+ end
17
+ div(class: "flex items-center justify-between pt-4") do
18
+ div do
19
+ div(class: "h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5")
20
+ div(class: "w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700")
21
+ end
22
+ div(class: "h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12")
23
+ end
24
+ span(class: "sr-only") { "Loading..." }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -94,7 +94,7 @@ module Plutonium
94
94
  end
95
95
 
96
96
  def render_footer
97
- div(class: "sticky bottom-[-2px] p-4 pb-6 w-full z-50 bg-gray-50 dark:bg-gray-900") {
97
+ div(class: "sticky dyna:static bottom-[-2px] mt-1 p-4 pb-6 w-full z-50 bg-gray-50 dark:bg-gray-900") {
98
98
  TableInfo(pagy_instance)
99
99
  TablePagination(pagy_instance)
100
100
  }
@@ -1,5 +1,5 @@
1
1
  module Plutonium
2
- VERSION = "0.15.6"
2
+ VERSION = "0.15.8"
3
3
  NEXT_MAJOR_VERSION = VERSION.split(".").tap { |v|
4
4
  v[1] = v[1].to_i + 1
5
5
  v[2] = 0
data/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@radioactive-labs/plutonium",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@radioactive-labs/plutonium",
9
- "version": "0.1.5",
9
+ "version": "0.1.6",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@hotwired/stimulus": "^3.2.2",
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radioactive-labs/plutonium",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Core assets for the Plutonium gem",
5
5
  "type": "module",
6
6
  "main": "src/js/core.js",
@@ -6,6 +6,7 @@ export default class extends Controller {
6
6
 
7
7
  connect() {
8
8
  console.log(`frame-navigator connected: ${this.element}`)
9
+ this.#loadingStarted()
9
10
 
10
11
  this.srcHistory = []
11
12
  this.originalFrameSrc = this.frameTarget.src
@@ -32,6 +33,9 @@ export default class extends Controller {
32
33
  this.frameLoading = this.frameLoading.bind(this);
33
34
  this.frameTarget.addEventListener("turbo:click", this.frameLoading);
34
35
  this.frameTarget.addEventListener("turbo:submit-start", this.frameLoading);
36
+
37
+ this.frameFailed = this.frameFailed.bind(this);
38
+ this.frameTarget.addEventListener("turbo:fetch-request-error", this.frameFailed);
35
39
  }
36
40
 
37
41
  disconnect() {
@@ -42,16 +46,19 @@ export default class extends Controller {
42
46
  this.frameTarget.removeEventListener("turbo:frame-load", this.frameLoaded);
43
47
  this.frameTarget.removeEventListener("turbo:click", this.frameLoading);
44
48
  this.frameTarget.removeEventListener("turbo:submit-start", this.frameLoading);
49
+ this.frameTarget.removeEventListener("turbo:fetch-request-error", this.frameFailed);
45
50
  }
46
51
 
47
52
  frameLoading(event) {
48
- if (this.hasRefreshButtonTarget) this.refreshButtonTarget.classList.add("motion-safe:animate-spin")
49
- this.frameTarget.classList.add("motion-safe:animate-pulse")
53
+ this.#loadingStarted()
54
+ }
55
+
56
+ frameFailed(event) {
57
+ this.#loadingStopped()
50
58
  }
51
59
 
52
60
  frameLoaded(event) {
53
- if (this.hasRefreshButtonTarget) this.refreshButtonTarget.classList.remove("motion-safe:animate-spin")
54
- this.frameTarget.classList.remove("motion-safe:animate-pulse")
61
+ this.#loadingStopped()
55
62
 
56
63
  let src = event.target.src
57
64
  if (src == this.currentSrc) {
@@ -63,7 +70,7 @@ export default class extends Controller {
63
70
  else
64
71
  this.srcHistory.push(src)
65
72
 
66
- this.updateNavigationButtonsDisplay()
73
+ this.#updateNavigationButtonsDisplay()
67
74
  }
68
75
 
69
76
  refreshButtonClicked(event) {
@@ -87,13 +94,23 @@ export default class extends Controller {
87
94
 
88
95
  get currentSrc() { return this.srcHistory[this.srcHistory.length - 1] }
89
96
 
90
- updateNavigationButtonsDisplay() {
97
+ #loadingStarted() {
98
+ if (this.hasRefreshButtonTarget) this.refreshButtonTarget.classList.add("motion-safe:animate-spin")
99
+ this.frameTarget.classList.add("motion-safe:animate-pulse")
100
+ }
101
+
102
+ #loadingStopped() {
103
+ if (this.hasRefreshButtonTarget) this.refreshButtonTarget.classList.remove("motion-safe:animate-spin")
104
+ this.frameTarget.classList.remove("motion-safe:animate-pulse")
105
+ }
106
+
107
+ #updateNavigationButtonsDisplay() {
91
108
  if (this.hasHomeButtonTarget) {
92
- this.homeButtonTarget.style.display = this.srcHistory.length > 1 ? '' : 'none'
109
+ this.homeButtonTarget.style.display = this.srcHistory.length > 2 ? '' : 'none'
93
110
  }
94
111
 
95
112
  if (this.hasBackButtonTarget) {
96
- this.backButtonTarget.style.display = this.srcHistory.length > 2 ? '' : 'none'
113
+ this.backButtonTarget.style.display = this.srcHistory.length > 1 ? '' : 'none'
97
114
  }
98
115
  }
99
116
  }
@@ -33,11 +33,11 @@ export default class extends Controller {
33
33
  e.preventDefault()
34
34
 
35
35
  const wrapper = e.target.closest(this.wrapperSelectorValue)
36
-
37
- if (wrapper.dataset.newRecord === "true") {
36
+ if (wrapper.dataset.newRecord !== undefined) {
38
37
  wrapper.remove()
39
38
  } else {
40
39
  wrapper.style.display = "none"
40
+ wrapper.classList.remove(...wrapper.classList)
41
41
 
42
42
  const input = wrapper.querySelector("input[name*='_destroy']")
43
43
  input.value = "1"
data/tailwind.config.js CHANGED
@@ -1,9 +1,19 @@
1
1
  /** @type {import('tailwindcss').Config} */
2
2
 
3
+ const tailwindPlugin = require('tailwindcss/plugin')
3
4
  const options = require("./tailwind.options.js")
4
5
 
5
6
  export const content = options.content
6
7
  export const darkMode = options.darkMode
7
- export const plugins = options.plugins.map((plugin) => require(plugin))
8
+ export const plugins = options.plugins.map(function (plugin) {
9
+ switch (typeof plugin) {
10
+ case "function":
11
+ return tailwindPlugin(plugin)
12
+ case "string":
13
+ return require(plugin)
14
+ default:
15
+ throw Error(`unsupported plugin: ${plugin}: ${(typeof plugin)}`)
16
+ }
17
+ })
8
18
  export const theme = options.theme
9
19
  export const safelist = options.safelist