ruby_ui_admin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +165 -0
  4. data/app/assets/stylesheets/ruby_ui_admin/application.tailwind.css +111 -0
  5. data/app/components/ruby_ui_admin/ui/base.rb +26 -0
  6. data/app/components/ruby_ui_admin/ui/icon.rb +79 -0
  7. data/app/components/ruby_ui_admin/ui/native_select.rb +37 -0
  8. data/app/components/ruby_ui_admin/ui/native_select_group.rb +18 -0
  9. data/app/components/ruby_ui_admin/ui/native_select_icon.rb +42 -0
  10. data/app/components/ruby_ui_admin/ui/native_select_option.rb +18 -0
  11. data/app/components/ruby_ui_admin/ui/pagination.rb +62 -0
  12. data/app/components/ruby_ui_admin/ui/pagination_content.rb +18 -0
  13. data/app/components/ruby_ui_admin/ui/pagination_ellipsis.rb +45 -0
  14. data/app/components/ruby_ui_admin/ui/pagination_item.rb +32 -0
  15. data/app/components/ruby_ui_admin/ui/select.rb +29 -0
  16. data/app/components/ruby_ui_admin/ui/toast_action.rb +30 -0
  17. data/app/components/ruby_ui_admin/ui/toast_cancel.rb +30 -0
  18. data/app/components/ruby_ui_admin/ui/toast_close.rb +43 -0
  19. data/app/components/ruby_ui_admin/ui/toast_description.rb +21 -0
  20. data/app/components/ruby_ui_admin/ui/toast_icon.rb +69 -0
  21. data/app/components/ruby_ui_admin/ui/toast_item.rb +75 -0
  22. data/app/components/ruby_ui_admin/ui/toast_region.rb +147 -0
  23. data/app/components/ruby_ui_admin/ui/toast_title.rb +21 -0
  24. data/app/components/ruby_ui_admin/ui.rb +12 -0
  25. data/app/components/ruby_ui_admin/views/action.rb +37 -0
  26. data/app/components/ruby_ui_admin/views/action_form.rb +104 -0
  27. data/app/components/ruby_ui_admin/views/action_trigger.rb +148 -0
  28. data/app/components/ruby_ui_admin/views/actions_menu.rb +34 -0
  29. data/app/components/ruby_ui_admin/views/base.rb +301 -0
  30. data/app/components/ruby_ui_admin/views/card_component.rb +69 -0
  31. data/app/components/ruby_ui_admin/views/dashboard.rb +40 -0
  32. data/app/components/ruby_ui_admin/views/docs.rb +91 -0
  33. data/app/components/ruby_ui_admin/views/field_input.rb +156 -0
  34. data/app/components/ruby_ui_admin/views/field_value.rb +301 -0
  35. data/app/components/ruby_ui_admin/views/filter_bar.rb +111 -0
  36. data/app/components/ruby_ui_admin/views/form.rb +151 -0
  37. data/app/components/ruby_ui_admin/views/home.rb +35 -0
  38. data/app/components/ruby_ui_admin/views/index.rb +324 -0
  39. data/app/components/ruby_ui_admin/views/path_helpers.rb +36 -0
  40. data/app/components/ruby_ui_admin/views/rails_helpers.rb +15 -0
  41. data/app/components/ruby_ui_admin/views/show.rb +77 -0
  42. data/app/components/ruby_ui_admin/views/show_fields.rb +37 -0
  43. data/app/components/ruby_ui_admin/views/show_tab.rb +58 -0
  44. data/app/components/ruby_ui_admin/views/structure_renderer.rb +148 -0
  45. data/app/components/ruby_ui_admin/views/translation.rb +12 -0
  46. data/app/controllers/ruby_ui_admin/actions_controller.rb +162 -0
  47. data/app/controllers/ruby_ui_admin/application_controller.rb +81 -0
  48. data/app/controllers/ruby_ui_admin/dashboards_controller.rb +12 -0
  49. data/app/controllers/ruby_ui_admin/docs_controller.rb +166 -0
  50. data/app/controllers/ruby_ui_admin/home_controller.rb +18 -0
  51. data/app/controllers/ruby_ui_admin/resources_controller.rb +259 -0
  52. data/config/locales/ruby_ui_admin.en.yml +59 -0
  53. data/config/locales/ruby_ui_admin.pt-BR.yml +59 -0
  54. data/config/routes/dynamic_routes.rb +11 -0
  55. data/config/routes.rb +23 -0
  56. data/docs/README.md +53 -0
  57. data/docs/actions/overview.md +112 -0
  58. data/docs/authorization/action-policy.md +72 -0
  59. data/docs/authorization/adapters.md +101 -0
  60. data/docs/authorization/explicit-mode.md +45 -0
  61. data/docs/authorization/field-authorization.md +53 -0
  62. data/docs/authorization/policies.md +62 -0
  63. data/docs/authorization/scopes.md +48 -0
  64. data/docs/customization/controllers.md +62 -0
  65. data/docs/customization/ejecting.md +55 -0
  66. data/docs/customization/javascript.md +161 -0
  67. data/docs/customization/theming-rubyui.md +80 -0
  68. data/docs/dashboards/overview.md +65 -0
  69. data/docs/fields/overview.md +94 -0
  70. data/docs/filters/overview.md +73 -0
  71. data/docs/generators-and-tasks/overview.md +42 -0
  72. data/docs/getting-started/authentication.md +98 -0
  73. data/docs/getting-started/configuration.md +174 -0
  74. data/docs/getting-started/installation.md +201 -0
  75. data/docs/getting-started/internationalization.md +50 -0
  76. data/docs/getting-started/practical-guide.md +310 -0
  77. data/docs/resources/associations.md +66 -0
  78. data/docs/resources/index-customization.md +68 -0
  79. data/docs/resources/overview.md +50 -0
  80. data/docs/resources/scopes.md +54 -0
  81. data/docs/resources/tabs-panels.md +65 -0
  82. data/lib/generators/ruby_ui_admin/action/action_generator.rb +21 -0
  83. data/lib/generators/ruby_ui_admin/action/templates/action.rb.tt +24 -0
  84. data/lib/generators/ruby_ui_admin/assets/assets_generator.rb +71 -0
  85. data/lib/generators/ruby_ui_admin/card/card_generator.rb +28 -0
  86. data/lib/generators/ruby_ui_admin/card/templates/chart_card.rb.tt +14 -0
  87. data/lib/generators/ruby_ui_admin/card/templates/metric_card.rb.tt +14 -0
  88. data/lib/generators/ruby_ui_admin/card/templates/partial_card.rb.tt +13 -0
  89. data/lib/generators/ruby_ui_admin/components/components_generator.rb +43 -0
  90. data/lib/generators/ruby_ui_admin/controller/controller_generator.rb +30 -0
  91. data/lib/generators/ruby_ui_admin/controller/templates/controller.rb.tt +24 -0
  92. data/lib/generators/ruby_ui_admin/dashboard/dashboard_generator.rb +18 -0
  93. data/lib/generators/ruby_ui_admin/dashboard/templates/dashboard.rb.tt +14 -0
  94. data/lib/generators/ruby_ui_admin/eject/eject_generator.rb +55 -0
  95. data/lib/generators/ruby_ui_admin/filter/filter_generator.rb +28 -0
  96. data/lib/generators/ruby_ui_admin/filter/templates/boolean_filter.rb.tt +13 -0
  97. data/lib/generators/ruby_ui_admin/filter/templates/select_filter.rb.tt +19 -0
  98. data/lib/generators/ruby_ui_admin/filter/templates/text_filter.rb.tt +13 -0
  99. data/lib/generators/ruby_ui_admin/install/install_generator.rb +62 -0
  100. data/lib/generators/ruby_ui_admin/install/templates/initializer.rb.tt +41 -0
  101. data/lib/generators/ruby_ui_admin/locales/locales_generator.rb +22 -0
  102. data/lib/generators/ruby_ui_admin/policy/policy_generator.rb +18 -0
  103. data/lib/generators/ruby_ui_admin/policy/templates/policy.rb.tt +24 -0
  104. data/lib/generators/ruby_ui_admin/resource/resource_generator.rb +84 -0
  105. data/lib/generators/ruby_ui_admin/resource/templates/resource.rb.tt +15 -0
  106. data/lib/generators/ruby_ui_admin/scope/scope_generator.rb +18 -0
  107. data/lib/generators/ruby_ui_admin/scope/templates/scope.rb.tt +11 -0
  108. data/lib/ruby_ui_admin/authorization/action_policy_adapter.rb +69 -0
  109. data/lib/ruby_ui_admin/authorization/adapter.rb +80 -0
  110. data/lib/ruby_ui_admin/authorization/can_can_can_adapter.rb +70 -0
  111. data/lib/ruby_ui_admin/authorization/pundit_adapter.rb +86 -0
  112. data/lib/ruby_ui_admin/authorization.rb +40 -0
  113. data/lib/ruby_ui_admin/base_action.rb +211 -0
  114. data/lib/ruby_ui_admin/base_dashboard.rb +70 -0
  115. data/lib/ruby_ui_admin/base_policy.rb +38 -0
  116. data/lib/ruby_ui_admin/base_resource.rb +503 -0
  117. data/lib/ruby_ui_admin/cards/base_card.rb +59 -0
  118. data/lib/ruby_ui_admin/cards/chart_card.rb +16 -0
  119. data/lib/ruby_ui_admin/cards/metric_card.rb +22 -0
  120. data/lib/ruby_ui_admin/cards/partial_card.rb +14 -0
  121. data/lib/ruby_ui_admin/configuration.rb +177 -0
  122. data/lib/ruby_ui_admin/current.rb +20 -0
  123. data/lib/ruby_ui_admin/dashboard_manager.rb +52 -0
  124. data/lib/ruby_ui_admin/engine.rb +80 -0
  125. data/lib/ruby_ui_admin/execution_context.rb +36 -0
  126. data/lib/ruby_ui_admin/fields/association_field.rb +67 -0
  127. data/lib/ruby_ui_admin/fields/badge_field.rb +19 -0
  128. data/lib/ruby_ui_admin/fields/base_field.rb +208 -0
  129. data/lib/ruby_ui_admin/fields/belongs_to_field.rb +49 -0
  130. data/lib/ruby_ui_admin/fields/boolean_field.rb +13 -0
  131. data/lib/ruby_ui_admin/fields/boolean_group_field.rb +54 -0
  132. data/lib/ruby_ui_admin/fields/code_field.rb +18 -0
  133. data/lib/ruby_ui_admin/fields/date_field.rb +34 -0
  134. data/lib/ruby_ui_admin/fields/date_time_field.rb +37 -0
  135. data/lib/ruby_ui_admin/fields/field_manager.rb +27 -0
  136. data/lib/ruby_ui_admin/fields/file_field.rb +67 -0
  137. data/lib/ruby_ui_admin/fields/files_field.rb +62 -0
  138. data/lib/ruby_ui_admin/fields/has_and_belongs_to_many_field.rb +9 -0
  139. data/lib/ruby_ui_admin/fields/has_many_field.rb +24 -0
  140. data/lib/ruby_ui_admin/fields/has_one_field.rb +13 -0
  141. data/lib/ruby_ui_admin/fields/hidden_field.rb +14 -0
  142. data/lib/ruby_ui_admin/fields/id_field.rb +19 -0
  143. data/lib/ruby_ui_admin/fields/key_value_field.rb +41 -0
  144. data/lib/ruby_ui_admin/fields/number_field.rb +13 -0
  145. data/lib/ruby_ui_admin/fields/password_field.rb +21 -0
  146. data/lib/ruby_ui_admin/fields/record_link_field.rb +45 -0
  147. data/lib/ruby_ui_admin/fields/select_field.rb +71 -0
  148. data/lib/ruby_ui_admin/fields/status_field.rb +21 -0
  149. data/lib/ruby_ui_admin/fields/text_field.rb +14 -0
  150. data/lib/ruby_ui_admin/fields/textarea_field.rb +13 -0
  151. data/lib/ruby_ui_admin/fields/url_field.rb +21 -0
  152. data/lib/ruby_ui_admin/filters/base_filter.rb +81 -0
  153. data/lib/ruby_ui_admin/filters/boolean_filter.rb +9 -0
  154. data/lib/ruby_ui_admin/filters/multiple_select_filter.rb +11 -0
  155. data/lib/ruby_ui_admin/filters/select_filter.rb +9 -0
  156. data/lib/ruby_ui_admin/filters/text_filter.rb +9 -0
  157. data/lib/ruby_ui_admin/menu/builder.rb +94 -0
  158. data/lib/ruby_ui_admin/resource_manager.rb +82 -0
  159. data/lib/ruby_ui_admin/scopes/base_scope.rb +107 -0
  160. data/lib/ruby_ui_admin/services/authorization_service.rb +38 -0
  161. data/lib/ruby_ui_admin/structure.rb +40 -0
  162. data/lib/ruby_ui_admin/tailwind_source.rb +65 -0
  163. data/lib/ruby_ui_admin/version.rb +5 -0
  164. data/lib/ruby_ui_admin/view.rb +34 -0
  165. data/lib/ruby_ui_admin.rb +110 -0
  166. data/lib/tasks/ruby_ui_admin.rake +71 -0
  167. data/public/ruby-ui-admin-assets/application.css +2 -0
  168. data/public/ruby-ui-admin-assets/controllers/index.js +36 -0
  169. data/public/ruby-ui-admin-assets/controllers/rua--bulk-select_controller.js +19 -0
  170. data/public/ruby-ui-admin-assets/controllers/rua--confirm_controller.js +51 -0
  171. data/public/ruby-ui-admin-assets/controllers/rua--dialog_controller.js +55 -0
  172. data/public/ruby-ui-admin-assets/controllers/rua--row-link_controller.js +16 -0
  173. data/public/ruby-ui-admin-assets/controllers/rua--tabs_controller.js +36 -0
  174. data/public/ruby-ui-admin-assets/controllers/ruby_ui--combobox_controller.js +197 -0
  175. data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_content_controller.js +7 -0
  176. data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_controller.js +15 -0
  177. data/public/ruby-ui-admin-assets/controllers/ruby_ui--sidebar_controller.js +67 -0
  178. data/public/ruby-ui-admin-assets/controllers/ruby_ui--toast_controller.js +151 -0
  179. data/public/ruby-ui-admin-assets/controllers/ruby_ui--toaster_controller.js +314 -0
  180. data/public/ruby-ui-admin-assets/vendor/stimulus.js +2567 -0
  181. data/public/ruby-ui-admin-assets/vendor/turbo.js +7178 -0
  182. metadata +379 -0
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Vendored from RubyUI (github.com/ruby-ui), namespaced under RubyUIAdmin::UI.
4
+ # A page link styled with the Button classes (outline when active, ghost otherwise).
5
+ module RubyUIAdmin
6
+ module UI
7
+ class PaginationItem < Base
8
+ def initialize(href: "#", active: false, **attrs)
9
+ @href = href
10
+ @active = active
11
+ super(**attrs)
12
+ end
13
+
14
+ def view_template(&block)
15
+ li do
16
+ a(href: @href, **attrs, &block)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def default_attrs
23
+ {
24
+ aria: {current: @active ? "page" : nil},
25
+ class: [
26
+ RubyUI::Button.new(variant: @active ? :outline : :ghost).attrs[:class]
27
+ ]
28
+ }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module UI
5
+ # Convenience wrapper around RubyUI's NativeSelect: takes an array of [label, value]
6
+ # `options` (plus `selected:`/`include_blank:`) instead of explicit option components,
7
+ # so call sites stay terse. Renders the styled native select with a custom chevron.
8
+ class Select < Base
9
+ def initialize(options: [], selected: nil, include_blank: false, **attrs)
10
+ @options = options
11
+ @selected = selected
12
+ @include_blank = include_blank
13
+ super(**attrs)
14
+ end
15
+
16
+ def view_template
17
+ render NativeSelect.new(**attrs) do
18
+ render(NativeSelectOption.new(value: "")) { "—" } if @include_blank
19
+
20
+ @options.each do |(label, value)|
21
+ opts = {value: value}
22
+ opts[:selected] = true if value.to_s == @selected.to_s
23
+ render(NativeSelectOption.new(**opts)) { label.to_s }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Vendored from RubyUI (github.com/ruby-ui), namespaced under RubyUIAdmin::UI.
4
+ module RubyUIAdmin
5
+ module UI
6
+ class ToastAction < Base
7
+ def initialize(label:, on: nil, **attrs)
8
+ @label = label
9
+ @on = on
10
+ super(**attrs)
11
+ end
12
+
13
+ def view_template
14
+ button(**attrs) { @label }
15
+ end
16
+
17
+ private
18
+
19
+ def default_attrs
20
+ data = {slot: "action"}
21
+ data[:action] = @on if @on
22
+ {
23
+ type: "button",
24
+ data: data,
25
+ class: "inline-flex h-6 shrink-0 cursor-pointer items-center justify-center rounded px-2 text-xs font-medium bg-foreground text-background border-0 ml-auto hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-ring transition-opacity disabled:pointer-events-none disabled:opacity-50"
26
+ }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Vendored from RubyUI (github.com/ruby-ui), namespaced under RubyUIAdmin::UI.
4
+ module RubyUIAdmin
5
+ module UI
6
+ class ToastCancel < Base
7
+ def initialize(label:, **attrs)
8
+ @label = label
9
+ super(**attrs)
10
+ end
11
+
12
+ def view_template
13
+ button(**attrs) { @label }
14
+ end
15
+
16
+ private
17
+
18
+ def default_attrs
19
+ {
20
+ type: "button",
21
+ data: {
22
+ slot: "cancel",
23
+ action: "click->ruby-ui--toast#dismiss"
24
+ },
25
+ class: "inline-flex h-6 shrink-0 cursor-pointer items-center justify-center rounded px-2 text-xs font-medium bg-foreground/10 text-foreground border-0 ml-auto hover:bg-foreground/15 focus:outline-none focus:ring-2 focus:ring-ring transition-colors"
26
+ }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Vendored from RubyUI (github.com/ruby-ui), namespaced under RubyUIAdmin::UI.
4
+ module RubyUIAdmin
5
+ module UI
6
+ class ToastClose < Base
7
+ def view_template
8
+ button(**attrs) do
9
+ svg(
10
+ xmlns: "http://www.w3.org/2000/svg",
11
+ width: "14",
12
+ height: "14",
13
+ viewbox: "0 0 24 24",
14
+ fill: "none",
15
+ stroke: "currentColor",
16
+ stroke_width: "2",
17
+ stroke_linecap: "round",
18
+ stroke_linejoin: "round",
19
+ class: "size-3.5"
20
+ ) do |s|
21
+ s.path(d: "M18 6 6 18")
22
+ s.path(d: "m6 6 12 12")
23
+ end
24
+ span(class: "sr-only") { "Close" }
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def default_attrs
31
+ {
32
+ type: "button",
33
+ aria_label: "Close toast",
34
+ data: {
35
+ slot: "close",
36
+ action: "click->ruby-ui--toast#dismiss"
37
+ },
38
+ class: "absolute right-2 top-2 size-6 cursor-pointer rounded-md text-foreground/60 p-0 flex items-center justify-center transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
39
+ }
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Vendored from RubyUI (github.com/ruby-ui), namespaced under RubyUIAdmin::UI.
4
+ module RubyUIAdmin
5
+ module UI
6
+ class ToastDescription < Base
7
+ def view_template(&)
8
+ div(**attrs, &)
9
+ end
10
+
11
+ private
12
+
13
+ def default_attrs
14
+ {
15
+ data: {slot: "description"},
16
+ class: "font-normal leading-[1.4] text-muted-foreground"
17
+ }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Vendored from RubyUI (github.com/ruby-ui), namespaced under RubyUIAdmin::UI.
4
+ module RubyUIAdmin
5
+ module UI
6
+ class ToastIcon < Base
7
+ def initialize(variant: nil, **attrs)
8
+ @variant = variant&.to_sym
9
+ super(**attrs)
10
+ end
11
+
12
+ def view_template
13
+ return unless renderable?
14
+
15
+ span(**attrs) do
16
+ svg(
17
+ xmlns: "http://www.w3.org/2000/svg",
18
+ width: "16",
19
+ height: "16",
20
+ viewbox: "0 0 24 24",
21
+ fill: "none",
22
+ stroke: "currentColor",
23
+ stroke_width: "2",
24
+ stroke_linecap: "round",
25
+ stroke_linejoin: "round",
26
+ class: "#{svg_classes} -ml-px"
27
+ ) { |s| paths(s) }
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def renderable?
34
+ %i[success error warning info loading].include?(@variant)
35
+ end
36
+
37
+ def svg_classes
38
+ base = "size-4"
39
+ (@variant == :loading) ? "#{base} animate-spin" : base
40
+ end
41
+
42
+ def paths(s)
43
+ case @variant
44
+ when :success
45
+ s.circle(cx: "12", cy: "12", r: "10")
46
+ s.path(d: "m9 12 2 2 4-4")
47
+ when :error
48
+ s.path(d: "M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z")
49
+ s.path(d: "m15 9-6 6")
50
+ s.path(d: "m9 9 6 6")
51
+ when :warning
52
+ s.path(d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3")
53
+ s.path(d: "M12 9v4")
54
+ s.path(d: "M12 17h.01")
55
+ when :info
56
+ s.circle(cx: "12", cy: "12", r: "10")
57
+ s.path(d: "M12 16v-4")
58
+ s.path(d: "M12 8h.01")
59
+ when :loading
60
+ s.path(d: "M21 12a9 9 0 1 1-6.219-8.56")
61
+ end
62
+ end
63
+
64
+ def default_attrs
65
+ {data: {slot: "icon"}, class: "shrink-0 inline-flex items-center justify-start relative size-4 -ml-[3px] mr-1 text-foreground"}
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Vendored from RubyUI (github.com/ruby-ui), namespaced under RubyUIAdmin::UI.
4
+ module RubyUIAdmin
5
+ module UI
6
+ class ToastItem < Base
7
+ ALERT_VARIANTS = %i[error].freeze
8
+
9
+ def initialize(
10
+ variant: :default,
11
+ id: nil,
12
+ duration: nil,
13
+ dismissible: true,
14
+ invert: false,
15
+ on_dismiss: nil,
16
+ on_auto_close: nil,
17
+ **attrs
18
+ )
19
+ @variant = variant.to_sym
20
+ @id = id
21
+ @duration = duration
22
+ @dismissible = dismissible
23
+ @invert = invert
24
+ @on_dismiss = on_dismiss
25
+ @on_auto_close = on_auto_close
26
+ super(**attrs)
27
+ end
28
+
29
+ def view_template(&)
30
+ li(**attrs, &)
31
+ end
32
+
33
+ private
34
+
35
+ def default_attrs
36
+ a = {
37
+ role: ALERT_VARIANTS.include?(@variant) ? "alert" : "status",
38
+ aria_atomic: "true",
39
+ tabindex: "0",
40
+ data: {
41
+ variant: @variant.to_s,
42
+ state: "pending",
43
+ swipe: "none",
44
+ controller: "ruby-ui--toast",
45
+ ruby_ui__toaster_target: "toast",
46
+ ruby_ui__toast_dismissible_value: @dismissible.to_s,
47
+ ruby_ui__toast_invert_value: @invert.to_s
48
+ },
49
+ class: item_classes
50
+ }
51
+ a[:id] = @id if @id
52
+ a[:data][:ruby_ui__toast_duration_value] = @duration.to_s if @duration
53
+ a[:data][:ruby_ui__toast_on_dismiss_value] = @on_dismiss if @on_dismiss
54
+ a[:data][:ruby_ui__toast_on_auto_close_value] = @on_auto_close if @on_auto_close
55
+ a
56
+ end
57
+
58
+ def item_classes
59
+ <<~CLASSES.tr("\n", " ").squeeze(" ").strip
60
+ group/toast pointer-events-auto absolute left-0 right-0
61
+ flex w-[356px] max-w-full items-center gap-1.5
62
+ rounded-lg border bg-popover text-popover-foreground
63
+ border-border p-4 text-[13px] shadow-[0_4px_12px_rgba(0,0,0,0.1)]
64
+ group-data-[close-button=true]/toaster:pr-10
65
+ transition-[transform,opacity] duration-300 ease-out
66
+ will-change-transform
67
+ opacity-[var(--opacity,1)]
68
+ data-[state=pending]:opacity-0
69
+ data-[state=closing]:opacity-0
70
+ data-[swipe=move]:transition-none
71
+ CLASSES
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Vendored from RubyUI (github.com/ruby-ui), namespaced under RubyUIAdmin::UI. The behaviour
4
+ # is driven by RubyUI's Stimulus controllers (ruby-ui--toaster / ruby-ui--toast), loaded
5
+ # self-hosted via the importmap in the admin layout.
6
+ module RubyUIAdmin
7
+ module UI
8
+ class ToastRegion < Base
9
+ SKELETON_VARIANTS = %i[default success error warning info loading].freeze
10
+
11
+ # Maps Rails flash keys to RubyUI toast variants (admin semantics: notice = success,
12
+ # alert = error), with explicit variant keys passed through.
13
+ FLASH_VARIANTS = {
14
+ "notice" => :success,
15
+ "success" => :success,
16
+ "alert" => :error,
17
+ "error" => :error,
18
+ "warning" => :warning,
19
+ "info" => :info
20
+ }.freeze
21
+
22
+ def initialize(
23
+ position: :bottom_right,
24
+ expand: false,
25
+ max: 3,
26
+ duration: 4000,
27
+ gap: 14,
28
+ offset: 24,
29
+ theme: :system,
30
+ rich_colors: false,
31
+ close_button: false,
32
+ hotkey: %w[alt t],
33
+ dir: :ltr,
34
+ flash: nil,
35
+ **attrs
36
+ )
37
+ @position = position.to_sym
38
+ @expand = expand
39
+ @max = max
40
+ @duration = duration
41
+ @gap = gap
42
+ @offset = offset
43
+ @theme = theme.to_sym
44
+ @rich_colors = rich_colors
45
+ @close_button = close_button
46
+ @hotkey = hotkey
47
+ @dir = dir
48
+ @flash = flash
49
+ super(**attrs)
50
+ end
51
+
52
+ def view_template(&block)
53
+ div(**attrs) do
54
+ ol(id: "ruby-ui-toaster", class: "pointer-events-auto relative m-0 p-0 list-none w-[356px] max-w-full") do
55
+ render_flash if @flash
56
+ yield(self) if block
57
+ end
58
+ SKELETON_VARIANTS.each { |v| skeleton(v) }
59
+ slot_template("actionTpl") { render ToastAction.new(label: "") }
60
+ slot_template("cancelTpl") { render ToastCancel.new(label: "") }
61
+ slot_template("closeTpl") { render ToastClose.new }
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def flash_variant(key)
68
+ FLASH_VARIANTS[key.to_s] || :info
69
+ end
70
+
71
+ def render_flash
72
+ @flash.each do |key, message|
73
+ next if message.nil? || message.to_s.empty?
74
+
75
+ variant = flash_variant(key)
76
+ render ToastItem.new(variant: variant, id: "flash-#{key}", duration: @duration) do
77
+ render ToastIcon.new(variant: variant)
78
+ render ToastTitle.new { message.to_s }
79
+ end
80
+ end
81
+ end
82
+
83
+ def skeleton(variant)
84
+ template(
85
+ data: {
86
+ ruby_ui__toaster_target: "skeleton",
87
+ variant: variant.to_s
88
+ }
89
+ ) do
90
+ render ToastItem.new(variant: variant) do
91
+ render ToastIcon.new(variant: variant)
92
+ div(class: "flex flex-col gap-0.5 flex-1 min-w-0") do
93
+ render ToastTitle.new
94
+ render ToastDescription.new
95
+ end
96
+ render ToastClose.new if @close_button
97
+ end
98
+ end
99
+ end
100
+
101
+ def slot_template(target_name, &)
102
+ template(data: {ruby_ui__toaster_target: target_name}, &)
103
+ end
104
+
105
+ def default_attrs
106
+ {
107
+ id: "ruby-ui-toaster-region",
108
+ role: "region",
109
+ aria_label: "Notifications",
110
+ aria_live: "polite",
111
+ data: {
112
+ controller: "ruby-ui--toaster",
113
+ # NOTE: deliberately NOT `turbo_permanent` (RubyUI ships it permanent). Under Turbo
114
+ # Drive a permanent region is carried over between visits, which would discard the new
115
+ # page's server-rendered flash toast — letting it re-render each visit keeps flash working.
116
+ close_button: @close_button.to_s,
117
+ position: @position.to_s.tr("_", "-"),
118
+ ruby_ui__toaster_position_value: @position.to_s.tr("_", "-"),
119
+ ruby_ui__toaster_expand_value: @expand.to_s,
120
+ ruby_ui__toaster_max_value: @max.to_s,
121
+ ruby_ui__toaster_duration_value: @duration.to_s,
122
+ ruby_ui__toaster_gap_value: @gap.to_s,
123
+ ruby_ui__toaster_offset_value: @offset.to_s,
124
+ ruby_ui__toaster_theme_value: @theme.to_s,
125
+ ruby_ui__toaster_rich_colors_value: @rich_colors.to_s,
126
+ ruby_ui__toaster_close_button_value: @close_button.to_s,
127
+ ruby_ui__toaster_hotkey_value: Array(@hotkey).join("+"),
128
+ ruby_ui__toaster_dir_value: @dir.to_s
129
+ },
130
+ class: region_classes
131
+ }
132
+ end
133
+
134
+ def region_classes
135
+ <<~CLASSES.tr("\n", " ").squeeze(" ").strip
136
+ group/toaster pointer-events-none fixed z-[100] p-4 sm:p-6
137
+ data-[position=top-left]:top-0 data-[position=top-left]:left-0
138
+ data-[position=top-center]:top-0 data-[position=top-center]:left-1/2 data-[position=top-center]:-translate-x-1/2
139
+ data-[position=top-right]:top-0 data-[position=top-right]:right-0
140
+ data-[position=bottom-left]:bottom-0 data-[position=bottom-left]:left-0
141
+ data-[position=bottom-center]:bottom-0 data-[position=bottom-center]:left-1/2 data-[position=bottom-center]:-translate-x-1/2
142
+ data-[position=bottom-right]:bottom-0 data-[position=bottom-right]:right-0
143
+ CLASSES
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Vendored from RubyUI (github.com/ruby-ui), namespaced under RubyUIAdmin::UI.
4
+ module RubyUIAdmin
5
+ module UI
6
+ class ToastTitle < Base
7
+ def view_template(&)
8
+ div(**attrs, &)
9
+ end
10
+
11
+ private
12
+
13
+ def default_attrs
14
+ {
15
+ data: {slot: "title"},
16
+ class: "font-medium leading-normal"
17
+ }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "phlex"
4
+
5
+ module RubyUIAdmin
6
+ # Vendored RubyUI primitives, namespaced under the engine so they never collide
7
+ # with a host app's own `RubyUI` Phlex::Kit. Visual language and
8
+ # Tailwind classes are taken from RubyUI for consistency.
9
+ module UI
10
+ extend Phlex::Kit
11
+ end
12
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Views
5
+ # Full-page confirmation/form for a custom action (no-JS fallback for the modal).
6
+ class Action < Base
7
+ def initialize(resource:, action:, action_id:, record_ids:)
8
+ @resource = resource
9
+ @action = action
10
+ @action_id = action_id
11
+ @record_ids = record_ids
12
+ end
13
+
14
+ def page_title
15
+ "#{@action.name} · #{RubyUIAdmin.configuration.app_name}"
16
+ end
17
+
18
+ def content
19
+ div(class: "mb-6") do
20
+ a(href: resource_index_path(@resource.class), class: "text-sm text-muted-foreground hover:underline") { "← #{@resource.navigation_label}" }
21
+ h1(class: "text-2xl font-semibold tracking-tight mt-1") { @action.name }
22
+ end
23
+
24
+ render RubyUI::Card.new(class: "max-w-2xl") do
25
+ render RubyUI::CardContent.new(class: "pt-6") do
26
+ render RubyUIAdmin::Views::ActionForm.new(
27
+ resource: @resource,
28
+ action: @action,
29
+ action_id: @action_id,
30
+ record_ids: @record_ids
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Views
5
+ # The inner form of a custom action (message + fields + buttons). Shared by the
6
+ # action page (Views::Action) and the inline action dialog (Views::ActionTrigger).
7
+ class ActionForm < Phlex::HTML
8
+ include RubyUIAdmin::UI
9
+ include RailsHelpers
10
+ include PathHelpers
11
+
12
+ def initialize(resource:, action:, action_id:, record_ids: [], frame_id: nil)
13
+ @resource = resource
14
+ @action = action
15
+ @action_id = action_id
16
+ @record_ids = record_ids
17
+ @frame_id = frame_id
18
+ end
19
+
20
+ # When loaded into the modal's `<turbo-frame>`, wrap the form in the matching frame so
21
+ # Turbo swaps it in. `data-turbo-frame="_top"` makes the submit break out into a full
22
+ # visit (POST→303→redirect+flash) instead of just reloading the frame. On the standalone
23
+ # action page (no frame_id) the form renders bare and `_top` is a harmless no-op.
24
+ def view_template
25
+ if @frame_id
26
+ tag(:turbo_frame, id: @frame_id) { render_form }
27
+ else
28
+ render_form
29
+ end
30
+ end
31
+
32
+ def render_form
33
+ form(action: action_path, method: "post", enctype: form_enctype, class: "space-y-5", data: {turbo_frame: "_top"}) do
34
+ input(type: "hidden", name: "authenticity_token", value: form_authenticity_token)
35
+ record_id_fields
36
+ render_message
37
+ @action.get_fields.each { |field| render_field(field) }
38
+ render_actions
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def action_path
45
+ ruby_ui_admin.resource_action_path(resource_name: @resource.route_key, action_id: @action_id)
46
+ end
47
+
48
+ # File-upload fields require a multipart form.
49
+ def form_enctype
50
+ @action.get_fields.any? { |field| %i[file files].include?(field.type) } ? "multipart/form-data" : nil
51
+ end
52
+
53
+ def record_id_fields
54
+ Array(@record_ids).each do |id|
55
+ input(type: "hidden", name: "record_ids[]", value: id)
56
+ end
57
+ end
58
+
59
+ def render_message
60
+ message = @action.message
61
+ return if message.blank?
62
+
63
+ div(class: "text-sm text-muted-foreground") { raw(safe(message.to_s)) }
64
+ end
65
+
66
+ def render_field(field)
67
+ # A lone checkbox reads as "[ ] Label" — render it inline beside the label.
68
+ if field.type == :boolean
69
+ div(class: "space-y-1.5") do
70
+ label(class: "flex items-center gap-2 text-sm font-medium leading-none") do
71
+ render RubyUIAdmin::Views::FieldInput.new(field: field, record: nil, name_prefix: "fields")
72
+ plain field.name
73
+ span(class: "text-destructive ml-0.5") { "*" } if field.required?
74
+ end
75
+ end
76
+ return
77
+ end
78
+
79
+ div(class: "space-y-1.5") do
80
+ label(class: "text-sm font-medium leading-none") do
81
+ plain field.name
82
+ span(class: "text-destructive ml-0.5") { "*" } if field.required?
83
+ end
84
+ render RubyUIAdmin::Views::FieldInput.new(field: field, record: nil, name_prefix: "fields")
85
+ end
86
+ end
87
+
88
+ def render_actions
89
+ div(class: "flex items-center gap-3 pt-2") do
90
+ render(RubyUI::Button.new(type: :submit, variant: :primary)) { @action.class.confirm_button_label }
91
+
92
+ # Closes the dialog when inside one (Stimulus preventDefaults); otherwise falls back to
93
+ # the index. `_top` ensures any non-prevented click escapes the frame (a full visit)
94
+ # rather than rendering the index inside the modal frame.
95
+ a(
96
+ href: resource_index_path(@resource.class),
97
+ data: {action: "click->rua--dialog#close", turbo_frame: "_top"},
98
+ class: "text-sm text-muted-foreground hover:underline"
99
+ ) { @action.class.cancel_button_label }
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end