uchi 0.1.2 → 0.1.4

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 (276) 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 +3 -0
  62. data/app/components/uchi/field/belongs_to.rb +105 -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 +284 -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 +197 -107
  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/package.json +0 -31
  193. data/sig/uchi.rbs +0 -4
  194. data/test/components/uchi/field/belongs_to_test.rb +0 -134
  195. data/test/components/uchi/field/blank_test.rb +0 -119
  196. data/test/components/uchi/field/boolean_test.rb +0 -163
  197. data/test/components/uchi/field/date_test.rb +0 -163
  198. data/test/components/uchi/field/date_time_test.rb +0 -152
  199. data/test/components/uchi/field/has_many_test.rb +0 -138
  200. data/test/components/uchi/field/id_test.rb +0 -113
  201. data/test/components/uchi/field/number_test.rb +0 -163
  202. data/test/components/uchi/field/string_test.rb +0 -159
  203. data/test/controllers/uchi/authors_controller_test.rb +0 -119
  204. data/test/controllers/uchi/repository_controller_test.rb +0 -93
  205. data/test/controllers/uchi/scoped_repository_controller_test.rb +0 -73
  206. data/test/dummy/Rakefile +0 -6
  207. data/test/dummy/app/assets/images/.keep +0 -0
  208. data/test/dummy/app/assets/stylesheets/application.css +0 -15
  209. data/test/dummy/app/controllers/application_controller.rb +0 -4
  210. data/test/dummy/app/controllers/concerns/.keep +0 -0
  211. data/test/dummy/app/controllers/uchi/authors_controller.rb +0 -7
  212. data/test/dummy/app/controllers/uchi/books_controller.rb +0 -7
  213. data/test/dummy/app/controllers/uchi/titles_controller.rb +0 -7
  214. data/test/dummy/app/helpers/application_helper.rb +0 -2
  215. data/test/dummy/app/jobs/application_job.rb +0 -7
  216. data/test/dummy/app/mailers/application_mailer.rb +0 -4
  217. data/test/dummy/app/models/application_record.rb +0 -3
  218. data/test/dummy/app/models/author.rb +0 -3
  219. data/test/dummy/app/models/book.rb +0 -3
  220. data/test/dummy/app/models/concerns/.keep +0 -0
  221. data/test/dummy/app/models/title.rb +0 -3
  222. data/test/dummy/app/uchi/repositories/author.rb +0 -20
  223. data/test/dummy/app/uchi/repositories/book.rb +0 -16
  224. data/test/dummy/app/uchi/repositories/title.rb +0 -17
  225. data/test/dummy/app/views/layouts/application.html.erb +0 -27
  226. data/test/dummy/app/views/layouts/mailer.html.erb +0 -13
  227. data/test/dummy/app/views/layouts/mailer.text.erb +0 -1
  228. data/test/dummy/app/views/pwa/manifest.json.erb +0 -22
  229. data/test/dummy/app/views/pwa/service-worker.js +0 -26
  230. data/test/dummy/bin/dev +0 -2
  231. data/test/dummy/bin/rails +0 -4
  232. data/test/dummy/bin/rake +0 -4
  233. data/test/dummy/bin/setup +0 -34
  234. data/test/dummy/config/application.rb +0 -29
  235. data/test/dummy/config/boot.rb +0 -5
  236. data/test/dummy/config/cable.yml +0 -10
  237. data/test/dummy/config/database.yml +0 -32
  238. data/test/dummy/config/environment.rb +0 -5
  239. data/test/dummy/config/environments/development.rb +0 -69
  240. data/test/dummy/config/environments/production.rb +0 -89
  241. data/test/dummy/config/environments/test.rb +0 -53
  242. data/test/dummy/config/initializers/content_security_policy.rb +0 -25
  243. data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -8
  244. data/test/dummy/config/initializers/inflections.rb +0 -16
  245. data/test/dummy/config/locales/da.yml +0 -51
  246. data/test/dummy/config/locales/en.yml +0 -31
  247. data/test/dummy/config/puma.rb +0 -38
  248. data/test/dummy/config/routes.rb +0 -9
  249. data/test/dummy/config/storage.yml +0 -34
  250. data/test/dummy/config.ru +0 -6
  251. data/test/dummy/db/migrate/20251002183635_create_authors.rb +0 -11
  252. data/test/dummy/db/migrate/20251005131726_create_books.rb +0 -9
  253. data/test/dummy/db/migrate/20251005131811_create_titles.rb +0 -11
  254. data/test/dummy/db/schema.rb +0 -38
  255. data/test/dummy/log/.keep +0 -0
  256. data/test/dummy/public/400.html +0 -114
  257. data/test/dummy/public/404.html +0 -114
  258. data/test/dummy/public/406-unsupported-browser.html +0 -114
  259. data/test/dummy/public/422.html +0 -114
  260. data/test/dummy/public/500.html +0 -114
  261. data/test/dummy/public/icon.png +0 -0
  262. data/test/dummy/public/icon.svg +0 -3
  263. data/test/dummy/storage/.keep +0 -0
  264. data/test/dummy/test/fixtures/authors.yml +0 -11
  265. data/test/dummy/test/models/author_test.rb +0 -7
  266. data/test/dummy/tmp/.keep +0 -0
  267. data/test/dummy/tmp/pids/.keep +0 -0
  268. data/test/dummy/tmp/storage/.keep +0 -0
  269. data/test/test_helper.rb +0 -15
  270. data/test/uchi/field_test.rb +0 -63
  271. data/test/uchi/i18n_test.rb +0 -18
  272. data/test/uchi/repository/routes_test.rb +0 -49
  273. data/test/uchi/repository/translate_test.rb +0 -263
  274. data/test/uchi/repository_test.rb +0 -137
  275. data/test/uchi/sort_order_test.rb +0 -47
  276. data/test/uchi_test.rb +0 -7
@@ -0,0 +1,14 @@
1
+ <div class="flex">
2
+ <div class="flex items-center h-5">
3
+ <%= input %>
4
+ </div>
5
+
6
+ <div class="ms-2 text-sm">
7
+ <%= label %>
8
+ <%= hint %>
9
+ </div>
10
+
11
+ <% errors.each do |error| %>
12
+ <%= render(Flowbite::Input::ValidationError.new) { error.upcase_first } %>
13
+ <% end %>
14
+ </div>
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ class InputField
5
+ class RadioButton < InputField
6
+ def initialize(attribute:, form:, value:, disabled: false, hint: nil, input: {}, label: {})
7
+ super(attribute: attribute, form: form, disabled: disabled, hint: hint, input: input, label: label)
8
+ @value = value
9
+ end
10
+
11
+ protected
12
+
13
+ def default_input
14
+ args = {
15
+ attribute: @attribute,
16
+ disabled: disabled?,
17
+ form: @form,
18
+ options: default_input_options.merge(@input[:options] || {}),
19
+ value: @value
20
+ }
21
+
22
+ input_component.new(**args)
23
+ end
24
+
25
+ # Returns options for the default label element. This includes CSS classes
26
+ # since they are specific to RadioButton labels (and Checkbox ones).
27
+ def default_label_options
28
+ super.merge({
29
+ options: {
30
+ class: label_classes,
31
+ for: id_for_input_element
32
+ }
33
+ })
34
+ end
35
+
36
+ # Returns the HTML to use for the hint element if any
37
+ def hint
38
+ return unless hint?
39
+
40
+ component = Flowbite::Input::Hint.new(
41
+ attribute: @attribute,
42
+ form: @form,
43
+ options: {
44
+ class: hint_classes,
45
+ id: id_for_hint_element
46
+ }
47
+ ).with_content(@hint)
48
+ render(component)
49
+ end
50
+
51
+ def input_component
52
+ ::Flowbite::Input::RadioButton
53
+ end
54
+
55
+ private
56
+
57
+ def hint_classes
58
+ if disabled?
59
+ "text-xs font-normal text-gray-400 dark:text-gray-500"
60
+ else
61
+ "text-xs font-normal text-gray-500 dark:text-gray-300"
62
+ end
63
+ end
64
+
65
+ def id_for_input_element
66
+ [
67
+ @form.object_name,
68
+ @attribute,
69
+ @value
70
+ ].join("_")
71
+ end
72
+
73
+ def id_for_hint_element
74
+ [id_for_input_element, "hint"].join("_")
75
+ end
76
+
77
+ def label_classes
78
+ if disabled?
79
+ "font-medium text-gray-400 dark:text-gray-500"
80
+ else
81
+ "font-medium text-gray-900 dark:text-gray-300"
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ class InputField
5
+ class Select < InputField
6
+ def initialize(attribute:, form:, collection: [], disabled: false, hint: nil, input: {}, label: {}, size: :default)
7
+ super(attribute: attribute, disabled: disabled, form: form, hint: hint, input: input, label: label, size: size)
8
+ @collection = collection
9
+ end
10
+
11
+ def input
12
+ render(
13
+ input_component.new(
14
+ attribute: @attribute,
15
+ collection: @collection,
16
+ disabled: @disabled,
17
+ form: @form,
18
+ options: input_options,
19
+ size: @size
20
+ )
21
+ )
22
+ end
23
+
24
+ private
25
+
26
+ def input_component
27
+ ::Flowbite::Input::Select
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ class InputField
5
+ class Text < InputField
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ class InputField
5
+ class Textarea < InputField
6
+ protected
7
+
8
+ def input_component
9
+ ::Flowbite::Input::Textarea
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ class InputField
5
+ class Url < InputField
6
+ protected
7
+
8
+ def input_component
9
+ ::Flowbite::Input::Url
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ # A form element for a single field, containing label, input field, error
5
+ # messages, helper text and whatever else is needed for a user friendly input
6
+ # experience.
7
+ #
8
+ # @see https://flowbite.com/docs/forms/input-field/
9
+ #
10
+ # The input field is an important part of the form element that can be used to
11
+ # create interactive controls to accept data from the user based on multiple
12
+ # input types, such as text, email, number, password, URL, phone number, and
13
+ # more.
14
+ #
15
+ # Usually you'd use one of the subclasses of this class which implement the
16
+ # different input types, like `Flowbite::InputField::Text`,
17
+ # `Flowbite::InputField::Email`, etc.
18
+ #
19
+ # Expects 2 arguments:
20
+ #
21
+ # @param attribute [Symbol] The name of the attribute to render in this input
22
+ # field.
23
+ #
24
+ # @param form [ActionView::Helpers::FormBuilder] The form builder object that
25
+ # will be used to generate the input field.
26
+ #
27
+ # Supports additional arguments:
28
+ #
29
+ # @param hint [String] A hint to display below the input field, providing
30
+ # additional context or instructions for the user. This is optional. See
31
+ # https://flowbite.com/docs/forms/input-field/#helper-text
32
+ #
33
+ # @param label [Hash] A hash with options for the label. These are passed to
34
+ # Flowbite::Input::Label, see that for details. Can contain:
35
+ # - `content`: The content of the label. If not provided, the label will
36
+ # default to the attribute name.
37
+ # - `options`: A hash of additional options to pass to the label component.
38
+ # This can be used to set the class, for example.
39
+ #
40
+ # @param disabled [Boolean] Whether the input field should be disabled.
41
+ # Defaults to `false`.
42
+ #
43
+ # @param input [Hash] A hash with options for the default input component.
44
+ # These are passed to the input components constructor, so see whatever
45
+ # component is being used for details. Can contain:
46
+ # - `options`: Additional HTML attributes to pass to the input element.
47
+ #
48
+ # @param size [Symbol] The size of the input field. Can be one of `:sm`,
49
+ # `:md`, or `:lg`. Defaults to `:md`.
50
+ #
51
+ # Sample usage
52
+ #
53
+ # <% form_for @person do |form| %>
54
+ # <%= render(
55
+ # Flowbite::InputField::Number.new(
56
+ # :attribute => :name,
57
+ # :form => form
58
+ # )
59
+ # ) %>
60
+ # <% end %>
61
+ #
62
+ # To render an input without labels or error messages etc, use
63
+ # `Flowbite::Input::Field` instead.
64
+ class InputField < ViewComponent::Base
65
+ renders_one :hint
66
+ renders_one :input
67
+ renders_one :label
68
+
69
+ # Returns the errors for attribute
70
+ def errors
71
+ @object.errors[@attribute] || []
72
+ end
73
+
74
+ def initialize(attribute:, form:, disabled: false, hint: nil, input: {}, label: {}, size: :default)
75
+ @attribute = attribute
76
+ @disabled = disabled
77
+ @form = form
78
+ @hint = hint
79
+ @input = input
80
+ @label = label
81
+ @object = form.object
82
+ @size = size
83
+ end
84
+
85
+ def input_component
86
+ ::Flowbite::Input::Field
87
+ end
88
+
89
+ protected
90
+
91
+ # Returns the HTML to use for the hint element if any
92
+ def default_hint
93
+ return unless hint?
94
+
95
+ component = Flowbite::Input::Hint.new(
96
+ attribute: @attribute,
97
+ form: @form,
98
+ options: default_hint_options
99
+ ).with_content(default_hint_content)
100
+ render(component)
101
+ end
102
+
103
+ def default_hint_content
104
+ return nil unless @hint
105
+
106
+ @hint[:content]
107
+ end
108
+
109
+ # Returns a Hash with the default attributes to apply to the hint element.
110
+ #
111
+ # The default attributes can be overriden by passing the `hint[options]`
112
+ # argument to the constructor.
113
+ def default_hint_options
114
+ return {} unless @hint
115
+
116
+ {
117
+ id: id_for_hint_element
118
+ }.merge(@hint[:options] || {})
119
+ end
120
+
121
+ # Returns a Hash with the default attributes to apply to the input element.
122
+ #
123
+ # The default attributes can be overriden by passing the `input[options]`
124
+ # argument to the constructor.
125
+ def default_input_options
126
+ if hint?
127
+ {
128
+ "aria-describedby": id_for_hint_element
129
+ }
130
+ else
131
+ {}
132
+ end
133
+ end
134
+
135
+ # Returns the HTML to use for the default input element.
136
+ def default_input
137
+ render(input_component.new(**input_arguments))
138
+ end
139
+
140
+ def default_label
141
+ component = Flowbite::Input::Label.new(**default_label_options)
142
+ if default_label_content
143
+ component.with_content(default_label_content)
144
+ else
145
+ component
146
+ end
147
+ end
148
+
149
+ def default_label_content
150
+ @label[:content]
151
+ end
152
+
153
+ def default_label_options
154
+ label_options = @label.dup
155
+ label_options.delete(:content)
156
+
157
+ {
158
+ attribute: @attribute,
159
+ form: @form
160
+ }.merge(label_options)
161
+ end
162
+
163
+ # Returns true if the input field is disabled, false otherwise.
164
+ def disabled?
165
+ !!@disabled
166
+ end
167
+
168
+ # Returns true if the input field has a hint, false otherwise.
169
+ def hint?
170
+ @hint.present?
171
+ end
172
+
173
+ def id_for_hint_element
174
+ "#{@form.object_name}_#{@attribute}_hint"
175
+ end
176
+
177
+ # @return [Hash] The keyword arguments for the input component.
178
+ def input_arguments
179
+ {
180
+ attribute: @attribute,
181
+ disabled: @disabled,
182
+ form: @form,
183
+ options: input_options,
184
+ size: @size
185
+ }
186
+ end
187
+
188
+ def input_options
189
+ default_input_options.merge(@input[:options] || {})
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,21 @@
1
+ module Flowbite
2
+ class Link < ViewComponent::Base
3
+ attr_reader :href, :text, :options
4
+
5
+ class << self
6
+ def classes
7
+ ["font-medium", "text-blue-600", "dark:text-blue-500", "hover:underline"].join(" ")
8
+ end
9
+ end
10
+
11
+ def initialize(href:, **options)
12
+ super()
13
+ @href = href
14
+ @options = options
15
+ end
16
+
17
+ def call
18
+ link_to(content, href, {class: self.class.classes}.merge(options))
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ class Style
5
+ attr_reader :classes
6
+
7
+ delegate :fetch, to: :classes
8
+
9
+ def initialize(classes)
10
+ @classes = classes
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ <div class="<%= container_classes.join(" ") %>">
2
+ <svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
3
+ <path d="<%= svg_path %>"/>
4
+ </svg>
5
+ </div>
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ class Toast
5
+ # Renders an icon for a toast notification.
6
+ #
7
+ # @param style [Symbol] The color style of the icon (:default, :success, :danger, :warning).
8
+ class Icon < ViewComponent::Base
9
+ class << self
10
+ def classes(style: :default)
11
+ styles.fetch(style).fetch(:classes)
12
+ end
13
+
14
+ def svg_path(style: :default)
15
+ styles.fetch(style).fetch(:svg_path)
16
+ end
17
+
18
+ # rubocop:disable Layout/LineLength
19
+ def styles
20
+ {
21
+ default: {
22
+ classes: ["inline-flex", "items-center", "justify-center", "shrink-0", "w-8", "h-8", "text-blue-500", "bg-blue-100", "rounded-lg", "dark:bg-blue-800", "dark:text-blue-200"],
23
+ svg_path: "M15.147 15.085a7.159 7.159 0 0 1-6.189 3.307A6.713 6.713 0 0 1 3.1 15.444c-2.679-4.513.287-8.737.888-9.548A4.373 4.373 0 0 0 5 1.608c1.287.953 6.445 3.218 5.537 10.5 1.5-1.122 2.706-3.01 2.853-6.14 1.433 1.049 3.993 5.395 1.757 9.117Z"
24
+ },
25
+ success: {
26
+ classes: ["inline-flex", "items-center", "justify-center", "shrink-0", "w-8", "h-8", "text-green-500", "bg-green-100", "rounded-lg", "dark:bg-green-800", "dark:text-green-200"],
27
+ svg_path: "M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"
28
+ },
29
+ danger: {
30
+ classes: ["inline-flex", "items-center", "justify-center", "shrink-0", "w-8", "h-8", "text-red-500", "bg-red-100", "rounded-lg", "dark:bg-red-800", "dark:text-red-200"],
31
+ svg_path: "M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 11.793a1 1 0 1 1-1.414 1.414L10 11.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L8.586 10 6.293 7.707a1 1 0 0 1 1.414-1.414L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414L11.414 10l2.293 2.293Z"
32
+ },
33
+ warning: {
34
+ classes: ["inline-flex", "items-center", "justify-center", "shrink-0", "w-8", "h-8", "text-orange-500", "bg-orange-100", "rounded-lg", "dark:bg-orange-700", "dark:text-orange-200"],
35
+ svg_path: "M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM10 15a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-4a1 1 0 0 1-2 0V6a1 1 0 0 1 2 0v5Z"
36
+ }
37
+ }.freeze
38
+ end
39
+ # rubocop:enable Layout/LineLength
40
+ end
41
+
42
+ attr_reader :style
43
+
44
+ def initialize(style: :default)
45
+ @style = style
46
+ end
47
+
48
+ def container_classes
49
+ self.class.classes(style: style)
50
+ end
51
+
52
+ def svg_path
53
+ self.class.svg_path(style: style)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,11 @@
1
+ <div class="<%= container_classes.join(" ") %>" role="alert" <%= options.map { |k, v| "#{k}=\"#{v}\"" }.join(" ").html_safe %>>
2
+ <%= render Flowbite::Toast::Icon.new(style: style) %>
3
+ <div class="ms-3 text-sm font-normal"><%= message %></div>
4
+ <% if dismissible %>
5
+ <button type="button" class="ms-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" aria-label="Close">
6
+ <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
7
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
8
+ </svg>
9
+ </button>
10
+ <% end %>
11
+ </div>
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ # Renders a toast notification element.
5
+ #
6
+ # See https://flowbite.com/docs/components/toast/
7
+ #
8
+ # @param message [String] The message to display in the toast.
9
+ # @param style [Symbol] The color style of the toast (:default, :success, :danger, :warning).
10
+ # @param dismissible [Boolean] Whether the toast can be dismissed (default: true).
11
+ # @param class [Array<String>] Additional CSS classes for the toast container.
12
+ # @param options [Hash] Additional HTML options for the toast container.
13
+ class Toast < ViewComponent::Base
14
+ class << self
15
+ def classes
16
+ ["flex", "items-center", "w-full", "max-w-xs", "p-4", "text-gray-500", "bg-white", "rounded-lg", "shadow-sm", "dark:text-gray-400", "dark:bg-gray-800"]
17
+ end
18
+ end
19
+
20
+ attr_reader :message, :style, :dismissible, :additional_classes, :options
21
+
22
+ def initialize(message:, style: :default, dismissible: true, class: [], **options)
23
+ @message = message
24
+ @style = style
25
+ @dismissible = dismissible
26
+ @additional_classes = Array(binding.local_variable_get(:class)) || []
27
+ @options = options
28
+ end
29
+
30
+ def container_classes
31
+ self.class.classes + additional_classes
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Field
5
+ class Base < Field
6
+ # Uchi::Field::Base::Edit components render fields in the edit view.
7
+ class Edit < ViewComponent::Base
8
+ attr_reader :field, :form, :record, :repository, :label, :hint
9
+
10
+ def initialize(field:, form:, repository:, label: nil, hint: nil)
11
+ super()
12
+
13
+ @field = field
14
+ @form = form
15
+ @label = label
16
+ @hint = hint
17
+ @record = form.object
18
+ @repository = repository
19
+ end
20
+ end
21
+
22
+ # Uchi::Field::Base::Show components render fields in the show view.
23
+ class Show < ViewComponent::Base
24
+ attr_reader :field, :record, :repository
25
+
26
+ def initialize(field:, record:, repository:)
27
+ super()
28
+
29
+ @field = field
30
+ @record = record
31
+ @repository = repository
32
+ end
33
+ end
34
+
35
+ # Uchi::Field::Base::Index components render fields in the index view.
36
+ class Index < ViewComponent::Base
37
+ attr_reader :field, :record, :repository
38
+
39
+ class << self
40
+ # Returns the CSS classes to apply to the td or th of the table where
41
+ # this field is rendered.
42
+ def classes_for_table_cell
43
+ []
44
+ end
45
+ end
46
+
47
+ def initialize(field:, record:, repository:)
48
+ super()
49
+
50
+ @field = field
51
+ @record = record
52
+ @repository = repository
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1 @@
1
+ <%= render(Flowbite::InputField::Select.new(**options)) %>
@@ -0,0 +1 @@
1
+ <%= field.value(record) %>
@@ -0,0 +1,3 @@
1
+ <%= render(Flowbite::Link.new(
2
+ href: associated_repository.routes.path_for(:show, id: associated_record.id)
3
+ ).with_content(associated_record.to_s)) %>
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi
4
+ class Field
5
+ class BelongsTo < Field
6
+ DEFAULT_COLLECTION_QUERY = ->(query) { query }.freeze
7
+
8
+ class Edit < Uchi::Field::Base::Edit
9
+ def associated_repository
10
+ model = reflection.klass
11
+ repository_class = Uchi::Repository.for_model(model)
12
+ repository_class.new
13
+ end
14
+
15
+ def attribute_name
16
+ reflection.foreign_key
17
+ end
18
+
19
+ def collection
20
+ query = associated_repository.find_all
21
+ field.collection_query.call(query)
22
+ end
23
+
24
+ private
25
+
26
+ def collection_for_select
27
+ repository = associated_repository
28
+ collection.map do |item|
29
+ [repository.title(item), item.id]
30
+ end
31
+ end
32
+
33
+ def options
34
+ options = {
35
+ attribute: attribute_name,
36
+ collection: collection_for_select,
37
+ form: form,
38
+ label: {content: label}
39
+ }
40
+ options[:hint] = {content: hint} if hint.present?
41
+ options
42
+ end
43
+
44
+ def reflection
45
+ @reflection ||= record.class.reflect_on_association(field.name)
46
+ end
47
+ end
48
+
49
+ class Index < Uchi::Field::Base::Index
50
+ end
51
+
52
+ class Show < Uchi::Field::Base::Show
53
+ def associated_record
54
+ field.value(record)
55
+ end
56
+
57
+ def associated_repository
58
+ reflection = record.class.reflect_on_association(field.name)
59
+ model = reflection.klass
60
+ repository_class = Uchi::Repository.for_model(model)
61
+ repository_class.new
62
+ end
63
+ end
64
+
65
+ def initialize(name)
66
+ super
67
+ @collection_query = DEFAULT_COLLECTION_QUERY
68
+ end
69
+
70
+ # Sets or gets a custom query for filtering the collection of associated records.
71
+ #
72
+ # When called with an argument, sets the query and returns self for chaining.
73
+ # When called without arguments, returns the current query.
74
+ #
75
+ # @param query_proc [Proc, Symbol] A callable that receives an ActiveRecord query
76
+ # and returns a modified query.
77
+ # @return [self, Proc] Returns self for method chaining when setting,
78
+ # or the query proc when getting
79
+ #
80
+ # @example Setting
81
+ # Field::BelongsTo.new(:company).collection_query(->(query) {
82
+ # query.where(active: true)
83
+ # })
84
+ #
85
+ # @example Getting
86
+ # field.collection_query # => #<Proc...>
87
+ def collection_query(query_proc = Configuration::Unset)
88
+ return @collection_query if query_proc == Configuration::Unset
89
+
90
+ @collection_query = query_proc
91
+ self
92
+ end
93
+
94
+ def group_as(_action)
95
+ :attributes
96
+ end
97
+
98
+ def param_key
99
+ # TODO: This is too naive. We need to match this to the actual foreign
100
+ # key of the model.
101
+ :"#{name}_id"
102
+ end
103
+ end
104
+ end
105
+ end