uchi 0.1.3 → 0.1.5

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 (283) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +222 -0
  3. data/app/assets/config/uchi_manifest.js +2 -0
  4. data/app/assets/javascripts/uchi/application.js +6095 -0
  5. data/app/assets/javascripts/uchi.js +4 -0
  6. data/app/assets/stylesheets/uchi/application.css +3971 -0
  7. data/app/assets/stylesheets/uchi/uchi.css +17 -0
  8. data/app/assets/tailwind/uchi.css +21 -0
  9. data/app/components/flowbite/breadcrumb.rb +33 -0
  10. data/app/components/flowbite/breadcrumb_home.rb +26 -0
  11. data/app/components/flowbite/breadcrumb_item/current.rb +33 -0
  12. data/app/components/flowbite/breadcrumb_item/first.rb +35 -0
  13. data/app/components/flowbite/breadcrumb_item.rb +48 -0
  14. data/app/components/flowbite/breadcrumb_separator.rb +30 -0
  15. data/app/components/flowbite/button/outline.rb +22 -0
  16. data/app/components/flowbite/button/pill.rb +40 -0
  17. data/app/components/flowbite/button.rb +92 -0
  18. data/app/components/flowbite/card.rb +45 -0
  19. data/app/components/flowbite/input/checkbox.rb +73 -0
  20. data/app/components/flowbite/input/date.rb +11 -0
  21. data/app/components/flowbite/input/date_time.rb +11 -0
  22. data/app/components/flowbite/input/email.rb +12 -0
  23. data/app/components/flowbite/input/field.rb +117 -0
  24. data/app/components/flowbite/input/file.rb +30 -0
  25. data/app/components/flowbite/input/hint.rb +57 -0
  26. data/app/components/flowbite/input/label.rb +82 -0
  27. data/app/components/flowbite/input/number.rb +11 -0
  28. data/app/components/flowbite/input/password.rb +11 -0
  29. data/app/components/flowbite/input/phone.rb +11 -0
  30. data/app/components/flowbite/input/radio_button.rb +50 -0
  31. data/app/components/flowbite/input/select.rb +49 -0
  32. data/app/components/flowbite/input/textarea.rb +42 -0
  33. data/app/components/flowbite/input/url.rb +12 -0
  34. data/app/components/flowbite/input/validation_error.rb +11 -0
  35. data/app/components/flowbite/input_field/checkbox.html.erb +14 -0
  36. data/app/components/flowbite/input_field/checkbox.rb +54 -0
  37. data/app/components/flowbite/input_field/date.rb +13 -0
  38. data/app/components/flowbite/input_field/date_time.rb +13 -0
  39. data/app/components/flowbite/input_field/email.rb +13 -0
  40. data/app/components/flowbite/input_field/file.rb +13 -0
  41. data/app/components/flowbite/input_field/input_field.html.erb +8 -0
  42. data/app/components/flowbite/input_field/number.rb +13 -0
  43. data/app/components/flowbite/input_field/password.rb +13 -0
  44. data/app/components/flowbite/input_field/phone.rb +13 -0
  45. data/app/components/flowbite/input_field/radio_button.html.erb +14 -0
  46. data/app/components/flowbite/input_field/radio_button.rb +86 -0
  47. data/app/components/flowbite/input_field/select.rb +31 -0
  48. data/app/components/flowbite/input_field/text.rb +8 -0
  49. data/app/components/flowbite/input_field/textarea.rb +13 -0
  50. data/app/components/flowbite/input_field/url.rb +13 -0
  51. data/app/components/flowbite/input_field.rb +192 -0
  52. data/app/components/flowbite/link.rb +21 -0
  53. data/app/components/flowbite/style.rb +13 -0
  54. data/app/components/flowbite/toast/icon.html.erb +5 -0
  55. data/app/components/flowbite/toast/icon.rb +57 -0
  56. data/app/components/flowbite/toast/toast.html.erb +11 -0
  57. data/app/components/flowbite/toast.rb +34 -0
  58. data/app/components/uchi/field/base.rb +57 -0
  59. data/app/components/uchi/field/belongs_to/edit.html.erb +1 -0
  60. data/app/components/uchi/field/belongs_to/index.html.erb +1 -0
  61. data/app/components/uchi/field/belongs_to/show.html.erb +5 -0
  62. data/app/components/uchi/field/belongs_to.rb +112 -0
  63. data/app/components/uchi/field/blank/edit.html.erb +1 -0
  64. data/app/components/uchi/field/blank/index.html.erb +1 -0
  65. data/app/components/uchi/field/blank/show.html.erb +1 -0
  66. data/app/components/uchi/field/blank.rb +16 -0
  67. data/app/components/uchi/field/boolean/edit.html.erb +1 -0
  68. data/app/components/uchi/field/boolean/index.html.erb +9 -0
  69. data/app/components/uchi/field/boolean/show.html.erb +9 -0
  70. data/app/components/uchi/field/boolean.rb +27 -0
  71. data/app/components/uchi/field/date/edit.html.erb +1 -0
  72. data/app/components/uchi/field/date/index.html.erb +1 -0
  73. data/app/components/uchi/field/date/show.html.erb +1 -0
  74. data/app/components/uchi/field/date.rb +27 -0
  75. data/app/components/uchi/field/date_time/edit.html.erb +1 -0
  76. data/app/components/uchi/field/date_time/index.html.erb +1 -0
  77. data/app/components/uchi/field/date_time/show.html.erb +1 -0
  78. data/app/components/uchi/field/date_time.rb +27 -0
  79. data/app/components/uchi/field/file/edit.html.erb +1 -0
  80. data/app/components/uchi/field/file/index.html.erb +6 -0
  81. data/app/components/uchi/field/file/show.html.erb +8 -0
  82. data/app/components/uchi/field/file.rb +37 -0
  83. data/app/components/uchi/field/has_and_belongs_to_many/edit.html.erb +9 -0
  84. data/app/components/uchi/field/has_and_belongs_to_many/index.html.erb +1 -0
  85. data/app/components/uchi/field/has_and_belongs_to_many/show.html.erb +28 -0
  86. data/app/components/uchi/field/has_and_belongs_to_many.rb +131 -0
  87. data/app/components/uchi/field/has_many/edit.html.erb +1 -0
  88. data/app/components/uchi/field/has_many/index.html.erb +1 -0
  89. data/app/components/uchi/field/has_many/show.html.erb +28 -0
  90. data/app/components/uchi/field/has_many.rb +107 -0
  91. data/app/components/uchi/field/id/index.html.erb +4 -0
  92. data/app/components/uchi/field/id/show.html.erb +4 -0
  93. data/app/components/uchi/field/id.rb +26 -0
  94. data/app/components/uchi/field/image/edit.html.erb +1 -0
  95. data/app/components/uchi/field/image/index.html.erb +6 -0
  96. data/app/components/uchi/field/image/show.html.erb +6 -0
  97. data/app/components/uchi/field/image.rb +38 -0
  98. data/app/components/uchi/field/number/edit.html.erb +1 -0
  99. data/app/components/uchi/field/number/index.html.erb +1 -0
  100. data/app/components/uchi/field/number/show.html.erb +1 -0
  101. data/app/components/uchi/field/number.rb +32 -0
  102. data/app/components/uchi/field/string/edit.html.erb +1 -0
  103. data/app/components/uchi/field/string/index.html.erb +1 -0
  104. data/app/components/uchi/field/string/show.html.erb +1 -0
  105. data/app/components/uchi/field/string.rb +33 -0
  106. data/app/components/uchi/field/text/edit.html.erb +1 -0
  107. data/app/components/uchi/field/text/index.html.erb +1 -0
  108. data/app/components/uchi/field/text/show.html.erb +1 -0
  109. data/app/components/uchi/field/text.rb +38 -0
  110. data/app/components/uchi/ui/breadcrumb/breadcrumb.html.erb +13 -0
  111. data/app/components/uchi/ui/breadcrumb.rb +14 -0
  112. data/app/components/uchi/ui/form/footer/footer.html.erb +5 -0
  113. data/app/components/uchi/ui/form/footer.rb +15 -0
  114. data/app/components/uchi/ui/form/input/collection_checkboxes.html.erb +32 -0
  115. data/app/components/uchi/ui/form/input/collection_checkboxes.rb +125 -0
  116. data/app/components/uchi/ui/frame/frame.html.erb +3 -0
  117. data/app/components/uchi/ui/frame.rb +10 -0
  118. data/app/components/uchi/ui/index/records_table/records_table.html.erb +67 -0
  119. data/app/components/uchi/ui/index/records_table/search_form/search_form.html.erb +21 -0
  120. data/app/components/uchi/ui/index/records_table/search_form.rb +49 -0
  121. data/app/components/uchi/ui/index/records_table.rb +29 -0
  122. data/app/components/uchi/ui/index/turbo_frame.rb +50 -0
  123. data/app/components/uchi/ui/page_header/page_header.html.erb +24 -0
  124. data/app/components/uchi/ui/page_header.rb +18 -0
  125. data/app/components/uchi/ui/pagination/current_link.html.erb +3 -0
  126. data/app/components/uchi/ui/pagination/current_link.rb +10 -0
  127. data/app/components/uchi/ui/pagination/gap.html.erb +3 -0
  128. data/app/components/uchi/ui/pagination/gap.rb +10 -0
  129. data/app/components/uchi/ui/pagination/item.rb +24 -0
  130. data/app/components/uchi/ui/pagination/link.html.erb +3 -0
  131. data/app/components/uchi/ui/pagination/link.rb +10 -0
  132. data/app/components/uchi/ui/pagination/next_link.html.erb +8 -0
  133. data/app/components/uchi/ui/pagination/next_link.rb +22 -0
  134. data/app/components/uchi/ui/pagination/pagination.html.erb +15 -0
  135. data/app/components/uchi/ui/pagination/previous_link.html.erb +8 -0
  136. data/app/components/uchi/ui/pagination/previous_link.rb +22 -0
  137. data/app/components/uchi/ui/pagination.rb +48 -0
  138. data/app/components/uchi/ui/show/attribute_fields/attribute_fields.html.erb +14 -0
  139. data/app/components/uchi/ui/show/attribute_fields.rb +18 -0
  140. data/app/components/uchi/ui/spinner/spinner.html.erb +7 -0
  141. data/app/components/uchi/ui/spinner.rb +15 -0
  142. data/app/controllers/uchi/application_controller.rb +4 -0
  143. data/app/controllers/uchi/controller.rb +13 -0
  144. data/app/controllers/uchi/repository_controller.rb +166 -0
  145. data/app/helpers/uchi/application_helper.rb +17 -0
  146. data/app/jobs/uchi/application_job.rb +4 -0
  147. data/app/mailers/uchi/application_mailer.rb +6 -0
  148. data/app/views/layouts/uchi/_flash_messages.html.erb +10 -0
  149. data/app/views/layouts/uchi/application.html.erb +33 -0
  150. data/app/views/uchi/repository/edit.html.erb +40 -0
  151. data/app/views/uchi/repository/index.html.erb +49 -0
  152. data/app/views/uchi/repository/new.html.erb +37 -0
  153. data/app/views/uchi/repository/show.html.erb +41 -0
  154. data/lib/generators/uchi/controller/controller_generator.rb +16 -0
  155. data/lib/generators/uchi/controller/templates/controller.rb.tt +11 -0
  156. data/lib/generators/uchi/install/install_generator.rb +13 -0
  157. data/lib/generators/uchi/repository/repository_generator.rb +16 -0
  158. data/lib/generators/uchi/repository/templates/repository.rb.tt +11 -0
  159. data/lib/tasks/uchi_tasks.rake +4 -0
  160. data/lib/uchi/application_record.rb +5 -0
  161. data/lib/uchi/engine.rb +24 -0
  162. data/lib/uchi/field/configuration.rb +142 -0
  163. data/lib/uchi/field.rb +80 -0
  164. data/lib/uchi/i18n.rb +13 -0
  165. data/lib/uchi/pagination/controller.rb +26 -0
  166. data/lib/uchi/pagination/page.rb +20 -0
  167. data/lib/uchi/pagy/LICENSE.txt +21 -0
  168. data/lib/uchi/pagy/classes/exceptions.rb +35 -0
  169. data/lib/uchi/pagy/classes/offset/offset.rb +56 -0
  170. data/lib/uchi/pagy/classes/request.rb +38 -0
  171. data/lib/uchi/pagy/modules/abilities/configurable.rb +38 -0
  172. data/lib/uchi/pagy/modules/abilities/linkable.rb +62 -0
  173. data/lib/uchi/pagy/modules/abilities/rangeable.rb +17 -0
  174. data/lib/uchi/pagy/modules/abilities/shiftable.rb +14 -0
  175. data/lib/uchi/pagy/modules/console.rb +40 -0
  176. data/lib/uchi/pagy/toolbox/helpers/loader.rb +19 -0
  177. data/lib/uchi/pagy/toolbox/helpers/page_url.rb +25 -0
  178. data/lib/uchi/pagy/toolbox/paginators/method.rb +21 -0
  179. data/lib/uchi/pagy/toolbox/paginators/offset.rb +35 -0
  180. data/lib/uchi/pagy.rb +60 -0
  181. data/lib/uchi/repository/routes.rb +62 -0
  182. data/lib/uchi/repository/translate.rb +368 -0
  183. data/lib/uchi/repository.rb +156 -0
  184. data/lib/uchi/sort_order.rb +35 -0
  185. data/lib/uchi/version.rb +5 -0
  186. data/lib/uchi.rb +18 -0
  187. data/uchi.gemspec +35 -0
  188. metadata +189 -97
  189. data/.github/dependabot.yml +0 -17
  190. data/.github/workflows/build.yml +0 -23
  191. data/.github/workflows/lint.yml +0 -30
  192. data/CHANGELOG.md +0 -29
  193. data/docs/fields.md +0 -82
  194. data/docs/repositories.md +0 -63
  195. data/package.json +0 -31
  196. data/sig/uchi.rbs +0 -4
  197. data/test/components/uchi/field/belongs_to_test.rb +0 -134
  198. data/test/components/uchi/field/blank_test.rb +0 -119
  199. data/test/components/uchi/field/boolean_test.rb +0 -163
  200. data/test/components/uchi/field/date_test.rb +0 -163
  201. data/test/components/uchi/field/date_time_test.rb +0 -152
  202. data/test/components/uchi/field/has_and_belongs_to_many_test.rb +0 -144
  203. data/test/components/uchi/field/has_many_test.rb +0 -138
  204. data/test/components/uchi/field/id_test.rb +0 -113
  205. data/test/components/uchi/field/number_test.rb +0 -163
  206. data/test/components/uchi/field/string_test.rb +0 -159
  207. data/test/components/uchi/field/text_test.rb +0 -160
  208. data/test/components/uchi/ui/form/input/collection_checkboxes_test.rb +0 -171
  209. data/test/controllers/uchi/authors_controller_test.rb +0 -120
  210. data/test/controllers/uchi/repository_controller_test.rb +0 -93
  211. data/test/controllers/uchi/scoped_repository_controller_test.rb +0 -73
  212. data/test/dummy/Rakefile +0 -6
  213. data/test/dummy/app/assets/images/.keep +0 -0
  214. data/test/dummy/app/assets/stylesheets/application.css +0 -15
  215. data/test/dummy/app/controllers/application_controller.rb +0 -4
  216. data/test/dummy/app/controllers/concerns/.keep +0 -0
  217. data/test/dummy/app/controllers/uchi/authors_controller.rb +0 -7
  218. data/test/dummy/app/controllers/uchi/books_controller.rb +0 -7
  219. data/test/dummy/app/controllers/uchi/titles_controller.rb +0 -7
  220. data/test/dummy/app/helpers/application_helper.rb +0 -2
  221. data/test/dummy/app/jobs/application_job.rb +0 -7
  222. data/test/dummy/app/mailers/application_mailer.rb +0 -4
  223. data/test/dummy/app/models/application_record.rb +0 -3
  224. data/test/dummy/app/models/author.rb +0 -5
  225. data/test/dummy/app/models/book.rb +0 -4
  226. data/test/dummy/app/models/concerns/.keep +0 -0
  227. data/test/dummy/app/models/title.rb +0 -3
  228. data/test/dummy/app/uchi/repositories/author.rb +0 -20
  229. data/test/dummy/app/uchi/repositories/book.rb +0 -16
  230. data/test/dummy/app/uchi/repositories/title.rb +0 -17
  231. data/test/dummy/app/views/layouts/application.html.erb +0 -27
  232. data/test/dummy/app/views/layouts/mailer.html.erb +0 -13
  233. data/test/dummy/app/views/layouts/mailer.text.erb +0 -1
  234. data/test/dummy/app/views/pwa/manifest.json.erb +0 -22
  235. data/test/dummy/app/views/pwa/service-worker.js +0 -26
  236. data/test/dummy/bin/dev +0 -2
  237. data/test/dummy/bin/rails +0 -4
  238. data/test/dummy/bin/rake +0 -4
  239. data/test/dummy/bin/setup +0 -34
  240. data/test/dummy/config/application.rb +0 -29
  241. data/test/dummy/config/boot.rb +0 -5
  242. data/test/dummy/config/cable.yml +0 -10
  243. data/test/dummy/config/database.yml +0 -32
  244. data/test/dummy/config/environment.rb +0 -5
  245. data/test/dummy/config/environments/development.rb +0 -69
  246. data/test/dummy/config/environments/production.rb +0 -89
  247. data/test/dummy/config/environments/test.rb +0 -53
  248. data/test/dummy/config/initializers/content_security_policy.rb +0 -25
  249. data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -8
  250. data/test/dummy/config/initializers/inflections.rb +0 -16
  251. data/test/dummy/config/locales/da.yml +0 -52
  252. data/test/dummy/config/locales/en.yml +0 -31
  253. data/test/dummy/config/puma.rb +0 -38
  254. data/test/dummy/config/routes.rb +0 -9
  255. data/test/dummy/config/storage.yml +0 -34
  256. data/test/dummy/config.ru +0 -6
  257. data/test/dummy/db/migrate/20251002183635_create_authors.rb +0 -11
  258. data/test/dummy/db/migrate/20251005131726_create_books.rb +0 -9
  259. data/test/dummy/db/migrate/20251005131811_create_titles.rb +0 -11
  260. data/test/dummy/db/migrate/20251031140958_add_author_books_join_table.rb +0 -9
  261. data/test/dummy/db/schema.rb +0 -44
  262. data/test/dummy/log/.keep +0 -0
  263. data/test/dummy/public/400.html +0 -114
  264. data/test/dummy/public/404.html +0 -114
  265. data/test/dummy/public/406-unsupported-browser.html +0 -114
  266. data/test/dummy/public/422.html +0 -114
  267. data/test/dummy/public/500.html +0 -114
  268. data/test/dummy/public/icon.png +0 -0
  269. data/test/dummy/public/icon.svg +0 -3
  270. data/test/dummy/storage/.keep +0 -0
  271. data/test/dummy/test/fixtures/authors.yml +0 -11
  272. data/test/dummy/test/models/author_test.rb +0 -7
  273. data/test/dummy/tmp/.keep +0 -0
  274. data/test/dummy/tmp/pids/.keep +0 -0
  275. data/test/dummy/tmp/storage/.keep +0 -0
  276. data/test/test_helper.rb +0 -15
  277. data/test/uchi/field_test.rb +0 -77
  278. data/test/uchi/i18n_test.rb +0 -18
  279. data/test/uchi/repository/routes_test.rb +0 -49
  280. data/test/uchi/repository/translate_test.rb +0 -272
  281. data/test/uchi/repository_test.rb +0 -137
  282. data/test/uchi/sort_order_test.rb +0 -47
  283. data/test/uchi_test.rb +0 -7
@@ -0,0 +1 @@
1
+ <%= render(Flowbite::InputField::File.new(**options)) %>
@@ -0,0 +1,6 @@
1
+ <% attachment = field.value(record) %>
2
+ <% if attachment.attached? %>
3
+ <%= image_tag attachment.variant(resize_to_limit: [100, 100]), class: "h-12 w-12 object-cover rounded" %>
4
+ <% else %>
5
+ <span class="text-gray-400">—</span>
6
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <% attachment = field.value(record) %>
2
+ <% if attachment.attached? %>
3
+ <%= image_tag attachment, class: "max-w-full h-auto rounded-lg shadow-md" %>
4
+ <% else %>
5
+ <span class="text-gray-400">—</span>
6
+ <% end %>
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Field
5
+ class Image < Field
6
+ class Edit < Uchi::Field::Base::Edit
7
+ private
8
+
9
+ def options
10
+ options = {
11
+ attribute: field.name,
12
+ form: form,
13
+ label: {content: label},
14
+ input: {options: {accept: "image/*"}}
15
+ }
16
+ options[:hint] = {content: hint} if hint.present?
17
+ options
18
+ end
19
+ end
20
+
21
+ class Index < Uchi::Field::Base::Index
22
+ end
23
+
24
+ class Show < Uchi::Field::Base::Show
25
+ end
26
+
27
+ protected
28
+
29
+ def default_searchable?
30
+ false
31
+ end
32
+
33
+ def default_sortable?
34
+ false
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1 @@
1
+ <%= render(Flowbite::InputField::Number.new(**options)) %>
@@ -0,0 +1 @@
1
+ <%= field.value(record) %>
@@ -0,0 +1 @@
1
+ <%= field.value(record) %>
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Field
5
+ class Number < Field
6
+ class Edit < Uchi::Field::Base::Edit
7
+ private
8
+
9
+ def options
10
+ options = {
11
+ attribute: field.name,
12
+ form: form,
13
+ label: {content: label}
14
+ }
15
+ options[:hint] = {content: hint} if hint.present?
16
+ options
17
+ end
18
+ end
19
+
20
+ class Index < Uchi::Field::Base::Index
21
+ class << self
22
+ def classes_for_table_cell
23
+ super + ["text-right"]
24
+ end
25
+ end
26
+ end
27
+
28
+ class Show < Uchi::Field::Base::Show
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1 @@
1
+ <%= render(Flowbite::InputField::Text.new(**options)) %>
@@ -0,0 +1 @@
1
+ <%= field.value(record) %>
@@ -0,0 +1 @@
1
+ <%= field.value(record) %>
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Field
5
+ class String < Field
6
+ class Edit < Uchi::Field::Base::Edit
7
+ private
8
+
9
+ def options
10
+ options = {
11
+ attribute: field.name,
12
+ form: form,
13
+ label: {content: label}
14
+ }
15
+ options[:hint] = {content: hint} if hint.present?
16
+ options
17
+ end
18
+ end
19
+
20
+ class Index < Uchi::Field::Base::Index
21
+ end
22
+
23
+ class Show < Uchi::Field::Base::Show
24
+ end
25
+
26
+ protected
27
+
28
+ def default_searchable?
29
+ true
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1 @@
1
+ <%= render(Flowbite::InputField::Textarea.new(**options)) %>
@@ -0,0 +1 @@
1
+ <%= field.value(record) %>
@@ -0,0 +1 @@
1
+ <%= field.value(record) %>
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Field
5
+ class Text < Field
6
+ class Edit < Uchi::Field::Base::Edit
7
+ private
8
+
9
+ def options
10
+ options = {
11
+ attribute: field.name,
12
+ form: form,
13
+ label: {content: label},
14
+ input: {options: {rows: 8}}
15
+ }
16
+ options[:hint] = {content: hint} if hint.present?
17
+ options
18
+ end
19
+ end
20
+
21
+ class Index < Uchi::Field::Base::Index
22
+ end
23
+
24
+ class Show < Uchi::Field::Base::Show
25
+ end
26
+
27
+ protected
28
+
29
+ def default_on
30
+ [:edit, :show]
31
+ end
32
+
33
+ def default_searchable?
34
+ true
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,13 @@
1
+ <%= render(Flowbite::Breadcrumb.new) do |breadcrumb| %>
2
+ <% items.each.with_index do |item, index| %>
3
+ <% breadcrumb.with_item do %>
4
+ <% if index.zero? %>
5
+ <%= render Flowbite::BreadcrumbItem::First.new(href: item[:href]).with_content(item[:label]) %>
6
+ <% elsif index == items.size - 1 %>
7
+ <%= render Flowbite::BreadcrumbItem::Current.new.with_content(item[:label]) %>
8
+ <% else %>
9
+ <%= render Flowbite::BreadcrumbItem.new(href: item[:href]).with_content(item[:label]) %>
10
+ <% end %>
11
+ <% end %>
12
+ <% end %>
13
+ <% end %>
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ module Ui
5
+ class Breadcrumb < ViewComponent::Base
6
+ attr_reader :items
7
+
8
+ def initialize(items:)
9
+ super()
10
+ @items = items
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ <div class="flex items-center space-x-4">
2
+ <% actions.each do |action| %>
3
+ <%= action %>
4
+ <% end %>
5
+ </div>
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ module Ui
5
+ module Form
6
+ class Footer < ViewComponent::Base
7
+ renders_many :actions
8
+
9
+ def render?
10
+ actions.any?
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,32 @@
1
+ <div>
2
+ <% if render_label? %>
3
+ <%= form.label attribute, label_text, class: label_classes.join(" ") %>
4
+ <% end %>
5
+
6
+ <div class="mt-2 space-y-3">
7
+ <%= form.collection_check_boxes(
8
+ attribute,
9
+ collection,
10
+ value_method,
11
+ text_method,
12
+ collection_check_boxes_options
13
+ ) do |b| %>
14
+ <div class="<%= checkbox_item_classes.join(" ") %>">
15
+ <div class="flex items-center h-5">
16
+ <%= b.check_box(class: checkbox_classes.join(" "), disabled: disabled?) %>
17
+ </div>
18
+ <div class="ms-2 text-sm">
19
+ <%= b.label(class: checkbox_label_classes.join(" ")) %>
20
+ </div>
21
+ </div>
22
+ <% end %>
23
+ </div>
24
+
25
+ <% if render_hint? %>
26
+ <p class="<%= hint_classes.join(" ") %>"><%= hint_text %></p>
27
+ <% end %>
28
+
29
+ <% errors.each do |error| %>
30
+ <%= render(Flowbite::Input::ValidationError.new) { error.upcase_first } %>
31
+ <% end %>
32
+ </div>
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ module Ui
5
+ module Form
6
+ module Input
7
+ # A component that wraps Rails' collection_check_boxes form helper
8
+ # to render multiple checkboxes from a collection using Flowbite styling.
9
+ #
10
+ # Example usage:
11
+ # <%= render Uchi::Ui::Form::Input::CollectionCheckboxes.new(
12
+ # attribute: :tag_ids,
13
+ # collection: @tags,
14
+ # form: form,
15
+ # label: "Select Tags",
16
+ # value_method: :id,
17
+ # text_method: :name
18
+ # ) %>
19
+ class CollectionCheckboxes < ViewComponent::Base
20
+ attr_reader :attribute, :collection, :form, :label_text, :hint_text,
21
+ :value_method, :text_method, :disabled
22
+
23
+ def initialize(
24
+ attribute:,
25
+ collection:,
26
+ form:,
27
+ label: nil,
28
+ hint: nil,
29
+ value_method: :id,
30
+ text_method: :name,
31
+ disabled: false,
32
+ options: {}
33
+ )
34
+ super()
35
+ @attribute = attribute
36
+ @collection = collection
37
+ @form = form
38
+ @label_text = label
39
+ @hint_text = hint
40
+ @value_method = value_method
41
+ @text_method = text_method
42
+ @disabled = disabled
43
+ @options = options || {}
44
+ end
45
+
46
+ def object
47
+ @object ||= form.object
48
+ end
49
+
50
+ def errors
51
+ @errors ||= object.errors[attribute] || []
52
+ end
53
+
54
+ def errors?
55
+ errors.any?
56
+ end
57
+
58
+ def disabled?
59
+ !!disabled
60
+ end
61
+
62
+ def render_label?
63
+ label_text.present?
64
+ end
65
+
66
+ def render_hint?
67
+ hint_text.present?
68
+ end
69
+
70
+ def label_classes
71
+ base = ["block", "mb-2", "text-sm", "font-medium"]
72
+ if disabled?
73
+ base + ["text-gray-400", "dark:text-gray-500"]
74
+ elsif errors?
75
+ base + ["text-red-700", "dark:text-red-500"]
76
+ else
77
+ base + ["text-gray-900", "dark:text-white"]
78
+ end
79
+ end
80
+
81
+ def hint_classes
82
+ base = ["text-xs", "font-normal", "mt-1"]
83
+ if disabled?
84
+ base + ["text-gray-400", "dark:text-gray-500"]
85
+ else
86
+ base + ["text-gray-500", "dark:text-gray-300"]
87
+ end
88
+ end
89
+
90
+ def checkbox_item_classes
91
+ ["flex", "items-start", "mb-3"]
92
+ end
93
+
94
+ def checkbox_classes
95
+ base = ["w-4", "h-4", "rounded-sm", "focus:ring-2", "focus:ring-offset-2"]
96
+ if disabled?
97
+ base + ["text-blue-600", "bg-gray-100", "border-gray-300",
98
+ "dark:bg-gray-700", "dark:border-gray-600"]
99
+ elsif errors?
100
+ base + ["text-red-600", "bg-red-50", "border-red-500",
101
+ "focus:ring-red-500", "dark:focus:ring-red-600",
102
+ "dark:bg-gray-700", "dark:border-red-500"]
103
+ else
104
+ base + ["text-blue-600", "bg-gray-100", "border-gray-300",
105
+ "focus:ring-blue-500", "dark:focus:ring-blue-600",
106
+ "dark:bg-gray-700", "dark:border-gray-600"]
107
+ end
108
+ end
109
+
110
+ def checkbox_label_classes
111
+ if disabled?
112
+ ["ms-2", "text-sm", "font-medium", "text-gray-400", "dark:text-gray-500"]
113
+ else
114
+ ["ms-2", "text-sm", "font-medium", "text-gray-900", "dark:text-gray-300"]
115
+ end
116
+ end
117
+
118
+ def collection_check_boxes_options
119
+ @options
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,3 @@
1
+ <div class="bg-white border border-gray-200 shadow-sm md:rounded-lg dark:bg-gray-800 dark:border-gray-700">
2
+ <%= content %>
3
+ </div>
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ module Ui
5
+ # Renders a styled frame around its content. Rounded corners, border,
6
+ # shadow, no inner whitespace.
7
+ class Frame < ViewComponent::Base
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,67 @@
1
+ <table class="w-full text-left text-gray-500 rtl:text-right dark:text-gray-400">
2
+ <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
3
+ <tr>
4
+ <% columns.each do |field| %>
5
+ <% component_class = field.index_component_class %>
6
+ <th scope="col" class="px-4 py-3 md:px-6 whitespace-nowrap <%= component_class.classes_for_table_cell.join(" ") %>">
7
+ <% if field.sortable? %>
8
+ <% if field.name == sort_order&.name %>
9
+ <% if sort_order&.ascending? %>
10
+ <%= link_to(repository.routes.path_for(:index, query: query, scope: scope, sort: {:by => field.name, :direction => :desc})) do %>
11
+ <%= repository.translate.field_label(field) %>
12
+ <svg class="inline w-4 h-4 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
13
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19V5m0 14-4-4m4 4 4-4"/>
14
+ </svg>
15
+ <% end %>
16
+ <% else %>
17
+ <%= link_to(repository.routes.path_for(:index, query: query, scope: scope, sort: {:by => field.name, :direction => :asc})) do %>
18
+ <%= repository.translate.field_label(field) %>
19
+ <svg class="inline w-4 h-4 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
20
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v13m0-13 4 4m-4-4-4 4"/>
21
+ </svg>
22
+ <% end %>
23
+ <% end %>
24
+ <% else %>
25
+ <%= link_to(repository.routes.path_for(:index, query: query, scope: scope, sort: {:by => field.name, :direction => :asc})) do %>
26
+ <%= repository.translate.field_label(field) %>
27
+ <svg class="inline w-4 h-4 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
28
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 20V7m0 13-4-4m4 4 4-4m4-12v13m0-13 4 4m-4-4-4 4"/>
29
+ </svg>
30
+ <% end %>
31
+ <% end %>
32
+ <% else %>
33
+ <%= repository.translate.field_label(field) %>
34
+ <% end %>
35
+ </th>
36
+ <% end %>
37
+ <th scope="col" class="px-4 py-3 md:px-6"></th>
38
+ </tr>
39
+ </thead>
40
+ <tbody>
41
+ <% records.each do |record| %>
42
+ <tr class="bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700">
43
+ <% columns.each do |field| %>
44
+ <% component_class = field.index_component_class %>
45
+ <td class="px-4 py-4 md:px-6 <%= component_class.classes_for_table_cell.join(" ") %>">
46
+ <%= render(field.index_component(record: record, repository: repository)) %>
47
+ </td>
48
+ <% end %>
49
+ <td class="px-4 py-4 text-right md:px-6">
50
+ <div class="flex justify-end space-x-2">
51
+ <%= link_to(repository.routes.path_for(:show, id: record.id), class: "inline-block hover:text-blue-600 hover:dark:text-blue-500", :data => {:"turbo-frame" => "_top"}) do %>
52
+ <svg class="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
53
+ <path stroke="currentColor" stroke-width="2" d="M21 12c0 1.2-4.03 6-9 6s-9-4.8-9-6c0-1.2 4.03-6 9-6s9 4.8 9 6Z"/>
54
+ <path stroke="currentColor" stroke-width="2" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/>
55
+ </svg>
56
+ <% end %>
57
+ <%= link_to(path_for_edit(record), class: "inline-block hover:text-blue-600 hover:dark:text-blue-500", :data => {:"turbo-frame" => "_top"}) do %>
58
+ <svg class="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
59
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m14.304 4.844 2.852 2.852M7 7H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-4.5m2.409-9.91a2.017 2.017 0 0 1 0 2.853l-6.844 6.844L8 14l.713-3.565 6.844-6.844a2.015 2.015 0 0 1 2.852 0Z"/>
60
+ </svg>
61
+ <% end %>
62
+ </div>
63
+ </td>
64
+ </tr>
65
+ <% end %>
66
+ </tbody>
67
+ </table>
@@ -0,0 +1,21 @@
1
+ <%= form_tag(repository.routes.path_for(:index), class: "flex items-center max-w-sm", method: :get) do %>
2
+ <%= label_tag(:query, repository.translate.search_label, class: "sr-only") %>
3
+ <div class="relative w-full">
4
+ <%= search_field_tag(:query, query, class: "bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500") %>
5
+ </div>
6
+ <%= button_tag(class: "p-2.5 ms-2 text-sm font-medium text-white bg-blue-700 rounded-lg border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800") do %>
7
+ <svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
8
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
9
+ </svg>
10
+ <span class="sr-only"><%= repository.translate.search_button %></span>
11
+ <% end %>
12
+
13
+ <% if scoped? %>
14
+ <%= hidden_field_tag("scope[field]", scope[:field]) %>
15
+ <%= hidden_field_tag("scope[id]", scope[:id]) %>
16
+ <%= hidden_field_tag("scope[inverse_of]", scope[:inverse_of]) %>
17
+ <%= hidden_field_tag("scope[model]", scope[:model]) %>
18
+ <% end %>
19
+ <%= hidden_field_tag("sort[by]", sort_by) if sort_by? %>
20
+ <%= hidden_field_tag("sort[direction]", sort_direction) if sort_direction? %>
21
+ <% end %>
@@ -0,0 +1,49 @@
1
+ module Uchi
2
+ module Ui
3
+ module Index
4
+ class RecordsTable
5
+ # Renders a search for the records table.
6
+ #
7
+ # Based on Flowbites Simple search input
8
+ # (https://flowbite.com/docs/forms/search-input/#simple-search-input)
9
+ class SearchForm < ViewComponent::Base
10
+ attr_reader :params, :repository
11
+
12
+ def initialize(params:, repository:)
13
+ super()
14
+ @params = params
15
+ @repository = repository
16
+ end
17
+
18
+ def query
19
+ params[:query]
20
+ end
21
+
22
+ def scope
23
+ params[:scope]
24
+ end
25
+
26
+ def scoped?
27
+ scope.present?
28
+ end
29
+
30
+ def sort_by
31
+ params.dig(:sort, :by)
32
+ end
33
+
34
+ def sort_by?
35
+ sort_by.present?
36
+ end
37
+
38
+ def sort_direction
39
+ params.dig(:sort, :direction)
40
+ end
41
+
42
+ def sort_direction?
43
+ sort_direction.present?
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ module Ui
5
+ module Index
6
+ class RecordsTable < ViewComponent::Base
7
+ # Returns the columns to be displayed in this table. Each column is a
8
+ # representation of a Field from repository. Defaults to all fields.
9
+ attr_reader :columns
10
+
11
+ attr_reader :query, :sort_order, :records, :repository, :scope
12
+
13
+ def initialize(columns:, records:, repository:, query: nil, scope: nil, sort_order: nil)
14
+ super()
15
+ @columns = columns
16
+ @query = query
17
+ @sort_order = sort_order
18
+ @records = records
19
+ @repository = repository
20
+ @scope = scope
21
+ end
22
+
23
+ def path_for_edit(record)
24
+ repository.routes.path_for(:edit, id: record.id, scope: scope)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ module Ui
5
+ module Index
6
+ class TurboFrame < ViewComponent::Base
7
+ attr_reader :repository, :scope, :src
8
+
9
+ def call
10
+ helpers
11
+ .turbo_frame_tag(turbo_frame_id, **options) {
12
+ content
13
+ }
14
+ end
15
+
16
+ def initialize(repository:, scope: nil, src: nil)
17
+ super()
18
+ @repository = repository
19
+ @scope = scope
20
+ @src = src
21
+ end
22
+
23
+ protected
24
+
25
+ def options
26
+ options = {}
27
+ options[:src] = src if src.present?
28
+ options
29
+ end
30
+
31
+ def scoped?
32
+ scope.present?
33
+ end
34
+
35
+ def turbo_frame_id
36
+ parts = if scoped?
37
+ [
38
+ scope[:model],
39
+ scope[:id],
40
+ scope[:field]
41
+ ]
42
+ else
43
+ [repository.controller_name]
44
+ end
45
+ parts.compact.join("_")
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,24 @@
1
+ <header class="px-4 mb-6 space-y-6 md:px-0">
2
+ <div>
3
+ <% if breadcrumb? %>
4
+ <%= breadcrumb %>
5
+ <% end %>
6
+ </div>
7
+
8
+ <div class="items-start justify-between space-x-6 md:flex md:px-0">
9
+ <div>
10
+ <h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white group"><%= title %></h1>
11
+ <% if description.present? %>
12
+ <div class="text-lg text-gray-500 lg:mb-0 dark:text-gray-400 lg:max-w-2xl"><%= description %></div>
13
+ <% end %>
14
+ </div>
15
+
16
+ <% if actions.any? %>
17
+ <div class="flex items-center space-x-2">
18
+ <% actions.each do |action| %>
19
+ <%= action %>
20
+ <% end %>
21
+ </div>
22
+ <% end %>
23
+ </div>
24
+ </header>