plutonium 0.15.6 → 0.15.8

Sign up to get free protection for your applications and to get access to all the features.
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