fluxbit_view_components 0.3.0 → 0.4.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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -0
  3. data/app/assets/javascripts/fluxbit_view_components/assigner_controller.js +49 -0
  4. data/app/assets/javascripts/fluxbit_view_components/auto_submit_controller.js +39 -0
  5. data/app/assets/javascripts/fluxbit_view_components/drawer_controller.js +135 -0
  6. data/app/assets/javascripts/fluxbit_view_components/index.js +56 -0
  7. data/app/assets/javascripts/fluxbit_view_components/method_link_controller.js +143 -0
  8. data/app/assets/javascripts/fluxbit_view_components/modal_controller.js +118 -0
  9. data/app/assets/javascripts/fluxbit_view_components/password_controller.js +170 -0
  10. data/app/assets/javascripts/fluxbit_view_components/progress_controller.js +374 -0
  11. data/app/assets/javascripts/fluxbit_view_components/row_click_controller.js +32 -0
  12. data/app/assets/javascripts/fluxbit_view_components/select_all_controller.js +122 -0
  13. data/app/assets/javascripts/fluxbit_view_components/spinner_percent_controller.js +174 -0
  14. data/app/assets/javascripts/fluxbit_view_components/theme_button_controller.js +90 -0
  15. data/app/assets/javascripts/fluxbit_view_components.js +1175 -0
  16. data/app/components/fluxbit/accordion_component.rb +125 -0
  17. data/app/components/fluxbit/alert_component.rb +8 -8
  18. data/app/components/fluxbit/avatar_component.rb +11 -12
  19. data/app/components/fluxbit/avatar_group_component.rb +1 -1
  20. data/app/components/fluxbit/badge_component.rb +8 -7
  21. data/app/components/fluxbit/banner_component.rb +139 -0
  22. data/app/components/fluxbit/bottom_navigation_component.rb +437 -0
  23. data/app/components/fluxbit/breadcrumb_component.rb +66 -0
  24. data/app/components/fluxbit/button_component.rb +39 -11
  25. data/app/components/fluxbit/button_group_component.rb +1 -1
  26. data/app/components/fluxbit/card_component.rb +26 -23
  27. data/app/components/fluxbit/carousel_component.rb +154 -0
  28. data/app/components/fluxbit/component.rb +24 -3
  29. data/app/components/fluxbit/drawer_component.html.erb +30 -0
  30. data/app/components/fluxbit/drawer_component.rb +125 -0
  31. data/app/components/fluxbit/dropdown_component.rb +41 -0
  32. data/app/components/fluxbit/dropdown_item_component.rb +68 -0
  33. data/app/components/fluxbit/flex_component.rb +1 -1
  34. data/app/components/fluxbit/form/component.rb +15 -8
  35. data/app/components/fluxbit/form/dropzone_component.rb +3 -3
  36. data/app/components/fluxbit/form/field_component.rb +4 -2
  37. data/app/components/fluxbit/form/help_text_component.rb +1 -1
  38. data/app/components/fluxbit/form/label_component.rb +10 -3
  39. data/app/components/fluxbit/form/password_component.rb +247 -0
  40. data/app/components/fluxbit/form/radio_group_button_component.rb +126 -0
  41. data/app/components/fluxbit/form/select_component.rb +108 -11
  42. data/app/components/fluxbit/form/text_field_component.rb +40 -23
  43. data/app/components/fluxbit/form/toggle_component.rb +2 -2
  44. data/app/components/fluxbit/form/upload_image_component.html.erb +3 -3
  45. data/app/components/fluxbit/form/upload_image_component.rb +12 -1
  46. data/app/components/fluxbit/gravatar_component.rb +7 -0
  47. data/app/components/fluxbit/icon_helpers.rb +167 -0
  48. data/app/components/fluxbit/link_component.rb +42 -0
  49. data/app/components/fluxbit/modal_component.rb +28 -31
  50. data/app/components/fluxbit/pagination_component.rb +206 -0
  51. data/app/components/fluxbit/popover_component.rb +14 -14
  52. data/app/components/fluxbit/progress_component.rb +196 -0
  53. data/app/components/fluxbit/skeleton_component.rb +237 -0
  54. data/app/components/fluxbit/speed_dial_action_component.html.erb +30 -0
  55. data/app/components/fluxbit/speed_dial_action_component.rb +59 -0
  56. data/app/components/fluxbit/speed_dial_component.html.erb +33 -0
  57. data/app/components/fluxbit/speed_dial_component.rb +73 -0
  58. data/app/components/fluxbit/spinner_component.rb +71 -0
  59. data/app/components/fluxbit/spinner_percent_component.rb +174 -0
  60. data/app/components/fluxbit/stepper_component.rb +223 -0
  61. data/app/components/fluxbit/tab_component.rb +44 -25
  62. data/app/components/fluxbit/table_component.rb +186 -0
  63. data/app/components/fluxbit/table_group_component.rb +28 -0
  64. data/app/components/fluxbit/theme_button_component.rb +64 -0
  65. data/app/components/fluxbit/timeline_component.rb +63 -0
  66. data/app/components/fluxbit/timeline_item_component.html.erb +64 -0
  67. data/app/components/fluxbit/timeline_item_component.rb +78 -0
  68. data/app/components/fluxbit/tooltip_component.rb +2 -2
  69. data/app/helpers/fluxbit/components_helper.rb +74 -4
  70. data/app/helpers/fluxbit/form_builder.rb +64 -15
  71. data/app/helpers/fluxbit/view_helper.rb +71 -0
  72. data/config/locales/en.yml +37 -4
  73. data/config/locales/pt-BR.yml +36 -0
  74. data/lib/fluxbit/config/accordion_component.rb +73 -0
  75. data/lib/fluxbit/config/avatar_component.rb +11 -11
  76. data/lib/fluxbit/config/badge_component.rb +14 -11
  77. data/lib/fluxbit/config/banner_component.rb +60 -0
  78. data/lib/fluxbit/config/bottom_navigation_component.rb +74 -0
  79. data/lib/fluxbit/config/breadcrumb_component.rb +24 -0
  80. data/lib/fluxbit/config/button_component.rb +6 -4
  81. data/lib/fluxbit/config/card_component.rb +23 -12
  82. data/lib/fluxbit/config/carousel_component.rb +33 -0
  83. data/lib/fluxbit/config/drawer_component.rb +48 -0
  84. data/lib/fluxbit/config/dropdown_component.rb +29 -0
  85. data/lib/fluxbit/config/form/check_box_component.rb +1 -1
  86. data/lib/fluxbit/config/form/dropzone_component.rb +1 -1
  87. data/lib/fluxbit/config/form/help_text_component.rb +1 -1
  88. data/lib/fluxbit/config/form/label_component.rb +3 -2
  89. data/lib/fluxbit/config/form/password_component.rb +19 -0
  90. data/lib/fluxbit/config/form/radio_group_button_component.rb +24 -0
  91. data/lib/fluxbit/config/form/text_field_component.rb +11 -11
  92. data/lib/fluxbit/config/form/toggle_component.rb +5 -5
  93. data/lib/fluxbit/config/link_component.rb +24 -0
  94. data/lib/fluxbit/config/modal_component.rb +1 -1
  95. data/lib/fluxbit/config/pagination_component.rb +31 -0
  96. data/lib/fluxbit/config/popover_component.rb +1 -1
  97. data/lib/fluxbit/config/progress_component.rb +63 -0
  98. data/lib/fluxbit/config/skeleton_component.rb +82 -0
  99. data/lib/fluxbit/config/speed_dial_component.rb +50 -0
  100. data/lib/fluxbit/config/spinner_component.rb +30 -0
  101. data/lib/fluxbit/config/spinner_percent_component.rb +61 -0
  102. data/lib/fluxbit/config/stepper_component.rb +299 -0
  103. data/lib/fluxbit/config/tab_component.rb +6 -0
  104. data/lib/fluxbit/config/table_component.rb +75 -0
  105. data/lib/fluxbit/config/theme_button_component.rb +19 -0
  106. data/lib/fluxbit/config/timeline_component.rb +77 -0
  107. data/lib/fluxbit/view_components/engine.rb +11 -3
  108. data/lib/fluxbit/view_components/version.rb +1 -1
  109. data/lib/fluxbit/view_components.rb +20 -0
  110. data/lib/generators/fluxbit/devise_views_generator.rb +116 -0
  111. data/lib/generators/fluxbit/pagy_generator.rb +39 -0
  112. data/lib/generators/fluxbit/scaffold_generator.rb +165 -0
  113. data/lib/generators/fluxbit/templates/_alert.html.erb.tt +1 -0
  114. data/lib/generators/fluxbit/templates/_flash.html.erb.tt +15 -0
  115. data/lib/generators/fluxbit/templates/_form.html.erb.tt +38 -0
  116. data/lib/generators/fluxbit/templates/_metadata.html.erb.tt +44 -0
  117. data/lib/generators/fluxbit/templates/controller.rb.tt +406 -0
  118. data/lib/generators/fluxbit/templates/create.turbo_stream.erb.tt +7 -0
  119. data/lib/generators/fluxbit/templates/destroy.turbo_stream.erb.tt +3 -0
  120. data/lib/generators/fluxbit/templates/destroy_all.turbo_stream.erb.tt +9 -0
  121. data/lib/generators/fluxbit/templates/devise_views/confirmations/new.html.erb +11 -0
  122. data/lib/generators/fluxbit/templates/devise_views/layouts/devise.html.erb +64 -0
  123. data/lib/generators/fluxbit/templates/devise_views/mailer/confirmation_instructions.html.erb +5 -0
  124. data/lib/generators/fluxbit/templates/devise_views/mailer/email_changed.html.erb +7 -0
  125. data/lib/generators/fluxbit/templates/devise_views/mailer/password_changed.html.erb +3 -0
  126. data/lib/generators/fluxbit/templates/devise_views/mailer/reset_password_instructions.html.erb +8 -0
  127. data/lib/generators/fluxbit/templates/devise_views/mailer/unlock_instructions.html.erb +7 -0
  128. data/lib/generators/fluxbit/templates/devise_views/passwords/edit.html.erb +29 -0
  129. data/lib/generators/fluxbit/templates/devise_views/passwords/new.html.erb +11 -0
  130. data/lib/generators/fluxbit/templates/devise_views/registrations/edit.html.erb +43 -0
  131. data/lib/generators/fluxbit/templates/devise_views/registrations/new.html.erb +34 -0
  132. data/lib/generators/fluxbit/templates/devise_views/sessions/new.html.erb +15 -0
  133. data/lib/generators/fluxbit/templates/devise_views/shared/_error_messages.html.erb +14 -0
  134. data/lib/generators/fluxbit/templates/devise_views/shared/_links.html.erb +25 -0
  135. data/lib/generators/fluxbit/templates/devise_views/unlocks/new.html.erb +11 -0
  136. data/lib/generators/fluxbit/templates/edit.html.erb.tt +47 -0
  137. data/lib/generators/fluxbit/templates/fluxbit_pagy.css +27 -0
  138. data/lib/generators/fluxbit/templates/i18n.en.yml.tt +121 -0
  139. data/lib/generators/fluxbit/templates/i18n.pt-BR.yml.tt +121 -0
  140. data/lib/generators/fluxbit/templates/index.html.erb.tt +254 -0
  141. data/lib/generators/fluxbit/templates/index.json.jbuilder.tt +33 -0
  142. data/lib/generators/fluxbit/templates/new.html.erb.tt +47 -0
  143. data/lib/generators/fluxbit/templates/partial.html.erb.tt +61 -0
  144. data/lib/generators/fluxbit/templates/policy.rb.tt +36 -0
  145. data/lib/generators/fluxbit/templates/send_alert_via_drawer.erb.tt +10 -0
  146. data/lib/generators/fluxbit/templates/show.html.erb.tt +44 -0
  147. data/lib/generators/fluxbit/templates/show.json.jbuilder.tt +6 -0
  148. data/lib/generators/fluxbit/templates/update.turbo_stream.erb.tt +10 -0
  149. data/lib/generators/fluxbit/templates/update_all.turbo_stream.erb.tt +20 -0
  150. data/lib/install/install.rb +58 -0
  151. metadata +107 -18
  152. data/app/helpers/fluxbit/classes_helper.rb +0 -9
@@ -0,0 +1,254 @@
1
+ <% singular = file_name.singularize; plural = file_name.pluralize -%>
2
+ <div data-controller="fx-select-all">
3
+ <div id="notice" class="fixed top-4 right-4 z-50 max-w-md w-auto">
4
+ <%%= render "shared/flash" %>
5
+ </div>
6
+ <% if options[:turbo] -%>
7
+ <% if options[:ui] == "drawer" -%>
8
+ <turbo-frame id="drawer"></turbo-frame>
9
+ <% elsif options[:ui] == "modal" -%>
10
+ <turbo-frame id="modal"></turbo-frame>
11
+ <% end -%>
12
+ <% end -%>
13
+ <%%= fx_heading(class: "m-4", size: 2) { t("<%= plural %>.titles.index") } %>
14
+
15
+ <%%= fx_breadcrumb(class: "m-4") do |breadcrumb| %>
16
+ <%% breadcrumb.with_item(href: root_path, icon: "heroicons_solid:home") %>
17
+ <% if namespaced? -%>
18
+ <% namespace_path.split("/").each do |ns| -%>
19
+ <%% breadcrumb.with_item { "<%= ns.camelize %>" } %>
20
+ <% end -%>
21
+ <% end -%>
22
+ <%% breadcrumb.with_item(href: <%= path_prefix %><%= plural %>_path) { t("<%= plural %>.titles.index") } %>
23
+ <%% breadcrumb.with_item(current_page: true) { t("<%= plural %>.titles.index_name") } %>
24
+ <%% end %>
25
+
26
+ <%%= fx_card(class: "m-4") do |card| %>
27
+ <%% card.with_section do %>
28
+ <%%= form_with(id: "<%= singular %>_index", url: <%= path_prefix %><%= plural %>_path, method: :get, local: true,
29
+ data: { controller: "fx-auto-submit" }) do |form| %>
30
+ <div class="flex justify-between">
31
+ <div class="flex gap-2 items-end">
32
+ <% if options[:paginator] -%>
33
+ <input type="hidden" id="perPageParam" name="per_page" value="<%%= params["per_page"] %>">
34
+ <% end -%>
35
+ <%%= fx_text_field name: :q, value: params[:q], type: :search,
36
+ placeholder: t("<%= plural %>.helpers.search"),
37
+ addon: "heroicons_solid:magnifying-glass",
38
+ data: { action: 'keydown.enter->fx-auto-submit#submit' } %>
39
+ <%% if @has_filters %>
40
+ <%%= fx_button as: :a, href: <%= path_prefix %><%= plural %>_path(<% if options[:paginator] %>per_page: params["per_page"]<% end %>),
41
+ color: :light, class: "w-auto px-4 h-[42px]", remove_class: "w-full",
42
+ title: t("<%= plural %>.actions.remove_filters") do %>
43
+ <%%= anyicon("heroicons_solid:x-mark", class: "size-3") %>
44
+ <%% end #button %>
45
+ <%% end %>
46
+ </div>
47
+ <div class="flex gap-2">
48
+ <%%= fx_button as: :a, href: new_<%= path_prefix %><%= singular %>_path,
49
+ class: "w-auto px-4", remove_class: "w-full"<% if options[:turbo] && options[:ui] != "none" %>, data: { turbo_frame: "<%= options[:ui] == "modal" ? "modal" : "drawer" %>" }<% end %> do %>
50
+ <%%= anyicon("heroicons_solid:plus", class: "size-4") %>
51
+ <span class="pl-2 pr-4 hidden md:inline"><%%= t("<%= plural %>.actions.new") %></span>
52
+ <%% end #button %>
53
+
54
+ <%%= fx_button id: "actionButton", as: :button, type: "button", color: :light, class: "w-auto px-4",
55
+ title: "Actions", data: { "fx-select-all-target": "showOnEmptySelect" },
56
+ remove_class: "w-full" do |actions_button| %>
57
+ <%%= anyicon("heroicons_solid:ellipsis-vertical", class: "size-4") %>
58
+ <span class="pl-2 pr-4 hidden md:inline">
59
+ <%%== t("<%= plural %>.actions.actions") %>
60
+ </span>
61
+ <%% actions_button.with_dropdown(id: "dropdownActionButton") do |dropdown| %>
62
+ <% attributes
63
+ .select { |a| %i[boolean].include?(a.type) }
64
+ .each do |attr|
65
+ -%>
66
+ <%% dropdown.with_item do %>
67
+ <%%= link_to "#",
68
+ class: "inline-flex px-2",
69
+ data: {
70
+ controller: "fx-method-link",
71
+ fx_method_link_method_value: "patch",
72
+ fx_method_link_url_value: update_all_<%= path_prefix %><%= plural %>_path,
73
+ fx_method_link_params_value: { <%= singular %>: { <%= attr.name %>: true } },
74
+ fx_method_link_form_data_id_value: "<%= singular %>_edit"
75
+ },
76
+ title: t("<%= plural %>.messages.click_to_change_<%= attr.name %>") do %>
77
+ <%%= anyicon("heroicons_solid:check-circle", class: "size-5") %>
78
+ <%%= t("<%= plural %>.actions.activate_<%= attr.name %>_selected") %>
79
+ <%% end # link_to %>
80
+ <%% end # dropdown item %>
81
+
82
+ <%% dropdown.with_item do %>
83
+ <%%= link_to "#",
84
+ class: "inline-flex px-2",
85
+ data: {
86
+ controller: "fx-method-link",
87
+ fx_method_link_method_value: "patch",
88
+ fx_method_link_url_value: update_all_<%= path_prefix %><%= plural %>_path,
89
+ fx_method_link_params_value: { <%= singular %>: { <%= attr.name %>: false } },
90
+ fx_method_link_form_data_id_value: "<%= singular %>_edit"
91
+ },
92
+ title: t("<%= plural %>.messages.click_to_change_<%= attr.name %>") do %>
93
+ <%%= anyicon("heroicons_solid:x-circle", class: "size-5") %>
94
+ <%%= t("<%= plural %>.actions.deactivate_<%= attr.name %>_selected") %>
95
+ <%% end # link_to %>
96
+ <%% end # dropdown item %>
97
+ <% end -%>
98
+ <%% dropdown.with_item do %>
99
+ <%%= link_to "#",
100
+ onclick: "event.preventDefault();",
101
+ class: "inline-flex px-2",
102
+ data: {
103
+ "modal-target": "confirm-mass-deletion-modal",
104
+ "modal-toggle": "confirm-mass-deletion-modal"
105
+ },
106
+ title: t("<%= plural %>.messages.click_to_destroy_selected") do %>
107
+ <%%= anyicon("heroicons_solid:trash", class: "size-5") %>
108
+ <%%= t("<%= plural %>.actions.delete_selected") %>
109
+ <%% end # link_to %>
110
+ <%% end # dropdown item %>
111
+ <%% end # dropdown %>
112
+ <%% end # button %>
113
+
114
+ <%%= fx_button id: "filterButton", as: :button, type: "button", color: :light, class: "w-auto px-4",
115
+ title: t("<%= plural %>.helpers.filter"), remove_class: "w-full" do |filter_button| %>
116
+ <%%= anyicon("heroicons_solid:funnel", class: "size-4") %>
117
+ <span class="pl-2 pr-4 hidden md:inline"><%%= t("<%= plural %>.helpers.filter") %></span>
118
+ <%% filter_button.with_dropdown(id: "dropdownFilterButton") do |dropdown| %>
119
+ <div class="px-2">
120
+ <% attributes
121
+ .select { |a| %i[integer decimal text string].include?(a.type) }
122
+ .first(5)
123
+ .each do |attr|
124
+ -%>
125
+ <%%= fx_text_field id: "<%= attr.name %>Filter", label: t("<%= plural %>.fields.<%= attr.name -%>"), name: "<%= attr.name -%>",
126
+ value: @<%= attr.name %>, placeholder: t("<%= plural %>.helpers.filter_<%= attr.name -%>"), class: "mb-2",
127
+ data: { action: "keydown.enter->fx-auto-submit#submit" }
128
+ %>
129
+ <% end -%>
130
+ <%% if @has_filters %>
131
+ <%%= fx_link href: <%= path_prefix %><%= plural -%>_path(<% if options[:paginator] %>per_page: params["per_page"]<% end %>) { t("<%= plural -%>.actions.remove_filters") } %>
132
+ <%% end %>
133
+ </div>
134
+ <%% end #dropdown %>
135
+ <%% end # button %>
136
+
137
+ <%%= fx_button as: :a, href: <%= path_prefix %><%= plural -%>_path(request.query_parameters.merge(format: :csv)), type: "button",
138
+ color: :light, class: "w-auto px-4", title: t("<%= plural %>.helpers.export"), remove_class: "w-full" do %>
139
+ <%%= anyicon("heroicons_solid:arrow-down-tray", class: "size-4") %>
140
+ <span class="pl-2 pr-4 hidden md:inline"><%%= t("<%= plural %>.actions.export") %></span>
141
+ <%% end # button %>
142
+ </div>
143
+ </div>
144
+ <%% end #form %>
145
+ <%% end # section %>
146
+
147
+ <%% card.with_section(no_class: true) do %>
148
+ <%%= form_with(id: "<%= singular %>_edit", name: "<%= singular %>_edit", url: <%= path_prefix %><%= plural %>_path, method: :get, local: true) do |form| %>
149
+ <%%= fx_table(striped: true, bordered: true, hover: true, tbody_html: {id: "<%= plural %>"}) do |table| %>
150
+ <%% table.with_header do |header| %>
151
+ <%% header.with_cell(title: "Select All/Deselect All") {
152
+ fx_checkbox(id: "<%= singular %>_select_all", data: { "fx-select-all-target": "selectAll" }) } %>
153
+ <% attributes.reject { |attr| attr.type == :password_digest }.first(5).each do |attr| -%>
154
+ <%% header.with_cell { fx_sort_field(:<%= attr.name %>, :<%= path_prefix %><%= plural %>_path, t("<%= plural %>.fields.<%= attr.name %>")) } %>
155
+ <% end -%>
156
+ <%% header.with_cell { "" } %>
157
+ <%% end %>
158
+ <%% if @<%= plural %>.count > 0 %>
159
+ <%% render "<%= plural %>", <%= plural %>: @<%= plural %> %>
160
+ <%% else %>
161
+ <tr id="not_found"><td colspan=7>
162
+ <div class="text-center text-gray-500 dark:text-gray-400 py-20 flex flex-col items-center justify-center">
163
+ <%%= anyicon("heroicons_solid:exclamation-circle", class: "size-12 mb-2") %>
164
+ <p><%%= t("<%= plural %>.messages.no_records") %></p>
165
+ <p><%%= t("<%= plural %>.messages.no_records_sub") %></p>
166
+ </div>
167
+ </td></tr>
168
+ <%% end %>
169
+ <%% end # table %>
170
+ <%% end # form %>
171
+ <%% end #section %>
172
+
173
+ <% if options[:paginator] -%>
174
+ <%% if @<%= plural %>.count > 1 %>
175
+ <%% card.with_section do %>
176
+ <%%= fx_flex(gap: 2, align_items: :center, justify_content: :space_between) do %>
177
+ <div class="text-sm text-gray-700 dark:text-gray-400 m-0 p-2">
178
+ <%%== t("<%= plural %>.messages.pagination_info", start: @pagy.from, end: @pagy.to, total: @pagy.count) %>
179
+ </div>
180
+ <%%= fx_flex(gap: 2, align_items: :center) do %>
181
+ <%%= fx_pagination(@pagy, id: 'nav', sizing: 1, show_first_last: true) if @<%= plural %>.count > @page_size%>
182
+ <%%= fx_select id: "perPage", name:"per_page", class: "pr-4", data: {
183
+ controller: "fx-assigner fx-auto-submit",
184
+ fx_assigner_change_param: {
185
+ "#perPageParam" => {
186
+ "value": { element: "#per_page", attribute: "value" }
187
+ }
188
+ }.to_json,
189
+ fx_assigner_prevent_default_param: "true",
190
+ fx_auto_submit_form_id_param: "<%= singular %>_index",
191
+ action: "change->fx-assigner#assign change->fx-auto-submit#submit",
192
+ },
193
+ options: options_for_select([
194
+ [t("<%= plural %>.actions.per_page", count: 5), "5"],
195
+ [t("<%= plural %>.actions.per_page", count: 10), "10"],
196
+ [t("<%= plural %>.actions.per_page", count: 20), "20"],
197
+ [t("<%= plural %>.actions.per_page", count: 50), "50"],
198
+ [t("<%= plural %>.actions.per_page", count: 100), "100"]
199
+ ], selected: params["per_page"] || "5") %>
200
+ <%% end # flex %>
201
+ <%% end # flex %>
202
+ <%% end # section %>
203
+ <%% end # if count %>
204
+ <% end -%>
205
+ <%% end # card %>
206
+
207
+ <%%= fx_modal id: "confirm-deletion-modal", size: 1 do |modal| %>
208
+ <%%= fx_flex(vertical: true, align_items: :center) do %>
209
+ <%%= anyicon("heroicons_solid:trash", class: "size-11 text-red-600 pb-2") %>
210
+ <%%= t("<%= plural %>.messages.confirm_destroy") %>
211
+ <%%= fx_flex(gap: 2, class: "mt-4") do %>
212
+ <%%= fx_button(as: :button, color: :light, class: "w-auto px-4", remove_class: "w-full",
213
+ data: {
214
+ "modal-target": "confirm-deletion-modal",
215
+ "modal-toggle": "confirm-deletion-modal"
216
+ }) { t("<%= plural %>.actions.cancel_destroy") } %>
217
+ <%%= fx_button(as: :a, href: "#", id: "confirm-deletion-button",
218
+ color: :danger, class: "w-auto text-nowrap px-4", remove_class: "w-full",
219
+ data: {
220
+ controller: "fx-method-link",
221
+ fx_method_link_method_value: "delete",
222
+ fx_method_link_url_value: <%= path_prefix %><%= singular %>_path(0),
223
+ "modal-target": "confirm-deletion-modal",
224
+ "modal-toggle": "confirm-deletion-modal"
225
+ }) { t("<%= plural %>.actions.confirm_destroy") } %>
226
+ <%% end #flex %>
227
+ <%% end #flex %>
228
+ <%% end # modal %>
229
+
230
+ <%%= fx_modal id: "confirm-mass-deletion-modal", size: 1 do |modal| %>
231
+ <%%= fx_flex(vertical: true, align_items: :center) do %>
232
+ <%%= anyicon("heroicons_solid:trash", class: "size-11 text-red-600 pb-2") %>
233
+ <div><%%== t("<%= plural %>.messages.confirm_bulk_destroy") %></div>
234
+ <%%= fx_flex(gap: 2, class: "mt-4") do %>
235
+ <%%= fx_button(as: :button, color: :light, class: "w-auto px-4", remove_class: "w-full",
236
+ data: {
237
+ "modal-target": "confirm-mass-deletion-modal",
238
+ "modal-toggle": "confirm-mass-deletion-modal"
239
+ }) { t("<%= plural %>.actions.cancel_destroy") } %>
240
+ <%%= fx_button(as: :a, href: "#", id: "confirm-deletion-button",
241
+ color: :danger, class: "w-auto text-nowrap px-4", remove_class: "w-full",
242
+ data: {
243
+ "modal-target": "confirm-mass-deletion-modal",
244
+ "modal-toggle": "confirm-mass-deletion-modal",
245
+
246
+ controller: "fx-method-link",
247
+ fx_method_link_method_value: "delete",
248
+ fx_method_link_url_value: destroy_all_<%= path_prefix %><%= plural %>_path,
249
+ fx_method_link_form_data_id_value: "<%= singular %>_edit"
250
+ }) { t("<%= plural %>.actions.confirm_destroy") } %>
251
+ <%% end #flex %>
252
+ <%% end #flex %>
253
+ <%% end # modal %>
254
+ </div>
@@ -0,0 +1,33 @@
1
+ <% singular = file_name.singularize; plural = file_name.pluralize -%>
2
+ # app/views/<%= plural %>/index.json.jbuilder
3
+ json.<%= plural %> @<%= plural %> do |<%= singular %>|
4
+ json.id <%= singular %>.id
5
+ <% attributes.each do |att|
6
+ name = att.name
7
+ next if att.type.to_s == 'password_digest' # handle password fields separately
8
+ -%>
9
+ json.<%= name %> <%= singular %>.<%= name %>
10
+ <% end -%>
11
+ json.created_at <%= singular %>.created_at
12
+ json.updated_at <%= singular %>.updated_at
13
+ json.url <%= path_prefix %><%= singular %>_url(<%= singular %>, format: :json)
14
+ end
15
+
16
+ json.pagination do
17
+ json.current_page @pagy.page
18
+ json.total_pages @pagy.pages
19
+ json.total_count @pagy.count
20
+ json.per_page @pagy.limit
21
+ json.next_page @pagy.next
22
+ json.prev_page @pagy.prev
23
+ end
24
+
25
+ json.filters do
26
+ json.search @q
27
+ <% attributes
28
+ .select { |a| %i[integer decimal text string].include?(a.type) }
29
+ .each do |att|
30
+ -%>
31
+ json.<%= att.name %> @<%= att.name %>
32
+ <% end -%>
33
+ end
@@ -0,0 +1,47 @@
1
+ <% singular = file_name.singularize; plural = file_name.pluralize -%>
2
+ <% if options[:ui] == "drawer" -%>
3
+ <% if options[:turbo] -%>
4
+ <%% if turbo_frame_request? %>
5
+ <turbo-frame id="drawer">
6
+ <%%= fx_drawer(id: "new-drawer", placement: :right, sizing: :xl, data: {controller: "fx-drawer"}) do |drawer| %>
7
+ <%% drawer.with_header { t("<%= plural %>.titles.new") } %>
8
+ <%%= render "form", <%= singular %>: @<%= singular %> %>
9
+ <%% end %>
10
+ <script>document.dispatchEvent(new CustomEvent("showDrawer:new-drawer"));</script>
11
+ </turbo-frame>
12
+ <%% else %>
13
+ <%%= fx_heading(class: "m-4", size: 2) { t("<%= plural %>.titles.new") } %>
14
+ <%%= fx_card(class: "m-4") do |card| %>
15
+ <%% card.with_section do %>
16
+ <%%= render "form", <%= singular %>: @<%= singular %> %>
17
+ <%% end %>
18
+ <%% end %>
19
+ <%% end %>
20
+ <% end -%>
21
+ <% elsif options[:ui] == "modal" -%>
22
+ <% if options[:turbo] -%>
23
+ <%% if turbo_frame_request? %>
24
+ <turbo-frame id="modal">
25
+ <%%= fx_modal(id: "new-modal", size: :xl, data: {controller: "fx-modal"}) do |modal| %>
26
+ <%% modal.with_header { t("<%= plural %>.titles.new") } %>
27
+ <%%= render "form", <%= singular %>: @<%= singular %> %>
28
+ <%% end %>
29
+ <script>document.dispatchEvent(new CustomEvent("showModal:new-modal"));</script>
30
+ </turbo-frame>
31
+ <%% else %>
32
+ <%%= fx_heading(class: "m-4", size: 2) { t("<%= plural %>.titles.new") } %>
33
+ <%%= fx_card(class: "m-4") do |card| %>
34
+ <%% card.with_section do %>
35
+ <%%= render "form", <%= singular %>: @<%= singular %> %>
36
+ <%% end %>
37
+ <%% end %>
38
+ <%% end %>
39
+ <% end -%>
40
+ <% else -%>
41
+ <%%= fx_heading(class: "m-4", size: 2) { t("<%= plural %>.titles.new") } %>
42
+ <%%= fx_card(class: "m-4") do |card| %>
43
+ <%% card.with_section do %>
44
+ <%%= render "form", <%= singular %>: @<%= singular %> %>
45
+ <%% end %>
46
+ <%% end %>
47
+ <% end -%>
@@ -0,0 +1,61 @@
1
+ <% singular = file_name.singularize; plural = file_name.pluralize -%>
2
+ <%%= fx_table(striped: true, only_rows: true, hover: true) do |table| %>
3
+ <%% <%= plural %>.each do |<%= singular %>| %>
4
+ <%% table.with_row(
5
+ id: dom_id(<%= singular %>),
6
+ class: "cursor-pointer hover:bg-gray-50",
7
+ title: <%= %{t("#{plural}.helpers.click_to_edit")} %>,
8
+ <% if options[:turbo] && options[:ui] != "none" -%>data: {
9
+ controller: "fx-row-click",
10
+ fx_row_click_url_value: edit_<%= path_prefix %><%= singular %>_path(<%= singular %>),
11
+ fx_row_click_frame_value: "<%= options[:ui] == "modal" ? "modal" : "drawer" %>"
12
+ }<% end -%>) do |row| %>
13
+ <%% row.with_cell(width: "1%") { fx_checkbox(id: dom_id(<%= singular %>, "select"), value: <%= singular %>.id, name: "<%= singular %>_ids[]", data: { "fx-select-all-target": "select" }) } %>
14
+
15
+ <% attributes.reject { |attr| attr.type == :password_digest }.first(5).each do |attr|
16
+ attr_name = attr.name
17
+ attr_type = attr.type -%>
18
+
19
+ <% if attr_type == :boolean -%>
20
+ <%% row.with_cell(width: "1%", class: "text-center") do %>
21
+ <%%= link_to "#",
22
+ class: "inline-flex items-center justify-center w-full h-full",
23
+ data: {
24
+ controller: "fx-method-link",
25
+ fx_method_link_method_value: "patch",
26
+ fx_method_link_url_value: <%= path_prefix %><%= singular %>_path(<%= singular %>),
27
+ fx_method_link_params_value: { <%= singular %>: { <%= attr_name %>: !<%= singular %>.<%= attr_name %> } }
28
+ },
29
+ title: <%= %{t("#{plural}.helpers.click_to_change_#{attr_name}")} %> do %>
30
+ <%%= anyicon(
31
+ "heroicons_solid:#{<%= singular %>.<%= attr_name %>? ? 'check-circle' : 'x-circle'}",
32
+ class: "size-5 #{<%= singular %>.<%= attr_name %>? ? 'text-green-600' : 'text-red-600'}") %>
33
+ <%% end %>
34
+ <%% end %>
35
+ <% else -%>
36
+ <%% row.with_cell { <%= singular %>.<%= attr_name %>.to_s } %>
37
+ <% end -%>
38
+ <% end -%>
39
+
40
+ <%% row.with_cell(width: "1%") do %>
41
+ <%%= fx_button color: :danger_outline,
42
+ onclick: "event.preventDefault();",
43
+ data: {
44
+ href: <%= path_prefix %><%= singular %>_path(<%= singular %>),
45
+ controller: "fx-assigner",
46
+ fx_assigner_change_param: {
47
+ "#confirm-deletion-button" => {
48
+ "data-fx-method-link-url-value" => <%= path_prefix %><%= singular %>_path(<%= singular %>),
49
+ }
50
+ }.to_json,
51
+ fx_assigner_prevent_default_param: "true",
52
+ action: "fx-assigner#assign",
53
+ "modal-target": "confirm-deletion-modal",
54
+ "modal-toggle": "confirm-deletion-modal"
55
+ }, remove_class: "w-full" do %>
56
+ <%%= anyicon("heroicons_solid:trash", class: "size-4") %>
57
+ <%% end %>
58
+ <%% end %>
59
+ <%% end %>
60
+ <%% end %>
61
+ <%% end %>
@@ -0,0 +1,36 @@
1
+ class <%= file_name.singularize.camelize %>Policy < ApplicationPolicy
2
+ def index?
3
+ true
4
+ end
5
+
6
+ def show?
7
+ true
8
+ end
9
+
10
+ def create?
11
+ true
12
+ end
13
+
14
+ def update?
15
+ true
16
+ end
17
+
18
+ def destroy?
19
+ false
20
+ end
21
+
22
+ def update_all?
23
+ update?
24
+ end
25
+
26
+ def destroy_all?
27
+ destroy?
28
+ end
29
+
30
+ class Scope < ApplicationPolicy::Scope
31
+ # NOTE: Be explicit about which records you allow access to!
32
+ def resolve
33
+ scope.all
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,10 @@
1
+ <turbo-frame id="drawer">
2
+ <script>
3
+ alertsContainer = document.getElementById("notice");
4
+ if (alertsContainer) {
5
+ const alertElement = document.createElement("div");
6
+ alertElement.innerHTML = `<%%= j fx_alert(color: color) { sanitize(message.to_s, tags: [], attributes: []) } %>`;
7
+ alertsContainer.appendChild(alertElement);
8
+ }
9
+ </script>
10
+ </turbo-frame>
@@ -0,0 +1,44 @@
1
+ <% singular = file_name.singularize; plural = file_name.pluralize -%>
2
+ <p id="notice"><%%= notice %></p>
3
+
4
+ <%%= fx_heading(class: "m-4", size: 2) { t("<%= plural %>.titles.show") } %>
5
+
6
+ <%%= fx_card(class: "m-4") do |card| %>
7
+ <%% card.with_section do %>
8
+ <div class="grid gap-4 sm:grid-cols-2">
9
+
10
+ <% attributes.reject { |attr| attr.type == :password_digest }.each do |attr| -%>
11
+ <% attr_name = attr.name -%>
12
+ <% if attr.type == :boolean -%>
13
+ <div>
14
+ <p class="text-xs uppercase tracking-wide text-gray-500"><%%= t("<%= plural %>.fields.<%= attr_name %>") %></p>
15
+ <%%= fx_badge(pill: true, color: @<%= singular %>.<%= attr_name %>? ? :success_bordered : :danger_bordered) { t(@<%= singular %>.<%= attr_name %>?, scope: "<%= plural %>.values.<%= attr_name %>") } %>
16
+ </div>
17
+ <% elsif attr.type == :text -%>
18
+ <div class="sm:col-span-2">
19
+ <p class="text-xs uppercase tracking-wide text-gray-500"><%%= t("<%= plural %>.fields.<%= attr_name %>") %></p>
20
+ <p class="mt-1 leading-relaxed text-gray-700 whitespace-pre-line"><%%= @<%= singular %>.<%= attr_name %> || t("<%= plural %>.values.no_value") %></p>
21
+ </div>
22
+ <% else -%>
23
+ <div>
24
+ <p class="text-xs uppercase tracking-wide text-gray-500"><%%= t("<%= plural %>.fields.<%= attr_name %>") %></p>
25
+ <p class="mt-1 font-medium text-gray-900"><%%= @<%= singular %>.<%= attr_name %> || t("<%= plural %>.values.no_value") %></p>
26
+ </div>
27
+ <% end -%>
28
+ <% end -%>
29
+ </div>
30
+
31
+ <%%= render "metadata", <%= singular %>: @<%= singular %> %>
32
+ <%% end %>
33
+ <%% end %>
34
+
35
+ <div class="m-4 flex gap-2">
36
+ <%%= fx_button as: :a, href: edit_<%= path_prefix %><%= singular %>_path(@<%= singular %>), data: { turbo_frame: "drawer" }, remove_class: "w-full", class: "p-2 px-4" do %>
37
+ <%%= anyicon("heroicons_solid:pencil", class: "size-4") %>
38
+ <span class="pl-2"><%%= t("<%= plural %>.actions.edit") %></span>
39
+ <%% end %>
40
+ <%%= fx_button as: :a, href: <%= path_prefix %><%= plural %>_path, remove_class: "w-full", class: "p-2 px-4" do %>
41
+ <%%= anyicon("heroicons_solid:arrow-left", class: "size-4") %>
42
+ <span class="pl-2"><%%= t("<%= plural %>.actions.back") %></span>
43
+ <%% end %>
44
+ </div>
@@ -0,0 +1,6 @@
1
+ <% singular = file_name.singularize; plural = file_name.pluralize -%>
2
+ # app/views/<%= plural %>s/show.json.jbuilder
3
+ json.extract! @<%= singular %>, <%= attributes.reject { |a|
4
+ a.type.to_s == "password_digest" }.map { |a|
5
+ ":#{a.name}" }.join(', ') %>, :created_at, :updated_at
6
+ json.url <%= path_prefix %><%= singular %>_url(@<%= singular %>, format: :json)
@@ -0,0 +1,10 @@
1
+ <% singular = file_name.singularize; plural = file_name.pluralize -%>
2
+ <%% if @<%= singular %>.errors.any? %>
3
+ <%%# On failure: re-render the form (with errors still in drawer) %>
4
+ <%%= turbo_stream.replace "<%= singular %>_form", partial: "<%= namespace_path %>/<%= plural %>/form", locals: { <%= plural %>: [@<%= singular %>] } %>
5
+ <%% else %>
6
+ <%%# On success: replace the existing row, clear drawer, and show notice %>
7
+ <%%= turbo_stream.replace dom_id(@<%= singular %>), partial: "<%= namespace_path %>/<%= plural %>/<%= plural %>", locals: { <%= plural %>: [@<%= singular %>] } %>
8
+ <%%= turbo_stream.update "drawer", "" %>
9
+ <%%= turbo_stream.append "notice", fx_alert(with_content: @message, color: :success) %>
10
+ <%% end %>
@@ -0,0 +1,20 @@
1
+ <% singular = file_name.singularize; plural = file_name.pluralize -%>
2
+ <%% if @errors.empty? %>
3
+ <%%# On success: replace the existing row, clear drawer, and show notice %>
4
+ <%% @<%= plural %>.each do |<%= singular %>| %>
5
+ <%%= turbo_stream.replace dom_id(<%= singular %>),
6
+ partial: "<%= namespace_path %>/<%= plural %>/<%= plural %>",
7
+ locals: { <%= plural %>: [<%= singular %>] } %>
8
+ <%% end %>
9
+ <%% end %>
10
+ <%%= turbo_stream.append "notice" do %>
11
+ <%%= fx_alert(color: @errors.any? ? :danger : :success) { @message } %>
12
+ <%% end %>
13
+ <%%= turbo_stream.after "notice" do %>
14
+ <script id="temp-script-<%= Time.current.to_i %>">
15
+ (function() {
16
+ document.getElementById("dropdownActionButton").classList.add("hidden");
17
+ document.currentScript.remove();
18
+ })();
19
+ </script>
20
+ <%% end %>
@@ -7,6 +7,7 @@ layout_path = Rails.root.join("app/views/layouts/application.html.erb")
7
7
  importmap_binstub = Rails.root.join("bin/importmap")
8
8
  importmap_config = Rails.root.join("config/importmap.rb")
9
9
  stimulus_path = Rails.root.join("app/javascript/controllers/index.js")
10
+ application_js_path = Rails.root.join("app/javascript/application.js")
10
11
  package_json_path = Rails.root.join("package.json")
11
12
  tailwind_config_path = Rails.root.join("tailwind.config.js")
12
13
  stylesheets_path = Rails.root.join("app/assets/stylesheets/application.tailwind.css")
@@ -25,6 +26,12 @@ system "#{RbConfig.ruby} ./bin/rails tailwindcss:install"
25
26
  say.call "- Installing Flowbite via npm..."
26
27
  system "npm install flowbite --save"
27
28
 
29
+ say.call "- Installing Fluxbit ViewComponents JavaScript package via npm..."
30
+ system "npm install fluxbit_view_components --save"
31
+
32
+ say.call "- Installing flowbite-datepicker via npm..."
33
+ system "npm install flowbite-datepicker --save"
34
+
28
35
  say.call "- Copying tailwind.config.js from template..."
29
36
  if File.exist?(template_path)
30
37
  FileUtils.cp(template_path, tailwind_config_path)
@@ -43,6 +50,57 @@ else
43
50
  say.call "⚠️ Couldn't find application.tailwind.css, skipping CSS modifications", :red
44
51
  end
45
52
 
53
+ if application_js_path.exist?
54
+ say.call "- Updating application.js with Fluxbit imports..."
55
+ app_js_content = File.read(application_js_path)
56
+
57
+ unless app_js_content.include?("flowbite/dist/flowbite.turbo.js")
58
+ # Find the line with "./controllers" import and add before it
59
+ if app_js_content.include?("./controllers")
60
+ gsub_file application_js_path.to_s, 'import "./controllers"',
61
+ 'import "flowbite/dist/flowbite.turbo.js"' + "\n" +
62
+ 'import "flowbite-datepicker"' + "\n" +
63
+ 'import "./controllers"' + "\n" +
64
+ 'import "fluxbit-view-components"'
65
+ say.call " Added Flowbite and Fluxbit imports to application.js"
66
+ else
67
+ say.call " ⚠️ Couldn't find './controllers' import in application.js", :red
68
+ say.call " Add these lines manually:", :red
69
+ say.call ' import "flowbite/dist/flowbite.turbo.js"', :red
70
+ say.call ' import "flowbite-datepicker"', :red
71
+ say.call ' import "fluxbit-view-components"', :red
72
+ end
73
+ else
74
+ say.call " Flowbite imports already exist in application.js, skipping..."
75
+ end
76
+ else
77
+ say.call "⚠️ Couldn't find application.js, skipping JavaScript modifications", :red
78
+ say.call " Add these lines to your application.js:", :red
79
+ say.call ' import "flowbite/dist/flowbite.turbo.js"', :red
80
+ say.call ' import "flowbite-datepicker"', :red
81
+ say.call ' import "fluxbit-view-components"', :red
82
+ end
83
+
84
+ if stimulus_path.exist?
85
+ say.call "- Updating controllers/index.js with Fluxbit controllers..."
86
+ controllers_content = File.read(stimulus_path)
87
+
88
+ unless controllers_content.include?("registerFluxbitControllers")
89
+ # Add the Fluxbit controller registration at the end
90
+ controllers_content << "\nimport { registerFluxbitControllers } from \"fluxbit-view-components\"\n"
91
+ controllers_content << "registerFluxbitControllers(Stimulus)\n"
92
+ File.write(stimulus_path, controllers_content)
93
+ say.call " Added Fluxbit controller registration to controllers/index.js"
94
+ else
95
+ say.call " Fluxbit controllers already registered, skipping..."
96
+ end
97
+ else
98
+ say.call "⚠️ Couldn't find controllers/index.js, skipping Stimulus controller setup", :red
99
+ say.call " Add these lines to your controllers/index.js:", :red
100
+ say.call ' import { registerFluxbitControllers } from "fluxbit-view-components"', :red
101
+ say.call ' registerFluxbitControllers(Stimulus)', :red
102
+ end
103
+
46
104
  if layout_path.exist?
47
105
  say.call "- Updating layout to include Fluxbit styles..."
48
106