panda-core 0.12.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +53 -0
  4. data/app/assets/builds/panda_core_admin.css +2 -0
  5. data/app/assets/tailwind/application.css +413 -131
  6. data/app/builders/panda/core/form_builder.rb +129 -27
  7. data/app/components/panda/core/UI/badge.html.erb +10 -0
  8. data/app/components/panda/core/UI/badge.rb +14 -30
  9. data/app/components/panda/core/UI/button.html.erb +5 -0
  10. data/app/components/panda/core/UI/button.rb +15 -19
  11. data/app/components/panda/core/UI/card.html.erb +3 -0
  12. data/app/components/panda/core/UI/card.rb +7 -6
  13. data/app/components/panda/core/admin/attribute_diff_component.html.erb +21 -0
  14. data/app/components/panda/core/admin/attribute_diff_component.rb +29 -0
  15. data/app/components/panda/core/admin/breadcrumb_component.html.erb +36 -0
  16. data/app/components/panda/core/admin/breadcrumb_component.rb +12 -64
  17. data/app/components/panda/core/admin/breadcrumbs_component.html.erb +16 -0
  18. data/app/components/panda/core/admin/breadcrumbs_component.rb +26 -0
  19. data/app/components/panda/core/admin/button_component.html.erb +9 -0
  20. data/app/components/panda/core/admin/button_component.rb +37 -32
  21. data/app/components/panda/core/admin/callout_component.html.erb +17 -0
  22. data/app/components/panda/core/admin/callout_component.rb +57 -0
  23. data/app/components/panda/core/admin/code_block_component.html.erb +9 -0
  24. data/app/components/panda/core/admin/code_block_component.rb +23 -0
  25. data/app/components/panda/core/admin/container_component.html.erb +27 -0
  26. data/app/components/panda/core/admin/container_component.rb +11 -77
  27. data/app/components/panda/core/admin/contributors_summary_component.html.erb +15 -0
  28. data/app/components/panda/core/admin/contributors_summary_component.rb +23 -0
  29. data/app/components/panda/core/admin/dashboard_default_content_component.html.erb +70 -0
  30. data/app/components/panda/core/admin/dashboard_default_content_component.rb +32 -0
  31. data/app/components/panda/core/admin/delete_button_component.html.erb +4 -0
  32. data/app/components/panda/core/admin/delete_button_component.rb +41 -0
  33. data/app/components/panda/core/admin/dropdown_button_component.html.erb +28 -0
  34. data/app/components/panda/core/admin/dropdown_button_component.rb +100 -0
  35. data/app/components/panda/core/admin/dropdown_component.html.erb +32 -0
  36. data/app/components/panda/core/admin/dropdown_component.rb +65 -0
  37. data/app/components/panda/core/admin/empty_state_component.html.erb +16 -0
  38. data/app/components/panda/core/admin/empty_state_component.rb +25 -0
  39. data/app/components/panda/core/admin/file_gallery_component.html.erb +58 -0
  40. data/app/components/panda/core/admin/file_gallery_component.rb +26 -98
  41. data/app/components/panda/core/admin/filter_pills_component.html.erb +6 -0
  42. data/app/components/panda/core/admin/filter_pills_component.rb +29 -0
  43. data/app/components/panda/core/admin/flash_message_component.html.erb +11 -0
  44. data/app/components/panda/core/admin/flash_message_component.rb +33 -60
  45. data/app/components/panda/core/admin/form_error_component.html.erb +7 -0
  46. data/app/components/panda/core/admin/form_error_component.rb +8 -16
  47. data/app/components/panda/core/admin/form_footer_component.html.erb +16 -0
  48. data/app/components/panda/core/admin/form_footer_component.rb +65 -0
  49. data/app/components/panda/core/admin/form_input_component.html.erb +1 -0
  50. data/app/components/panda/core/admin/form_input_component.rb +21 -13
  51. data/app/components/panda/core/admin/form_section_component.html.erb +16 -0
  52. data/app/components/panda/core/admin/form_section_component.rb +64 -0
  53. data/app/components/panda/core/admin/form_select_component.html.erb +5 -0
  54. data/app/components/panda/core/admin/form_select_component.rb +25 -26
  55. data/app/components/panda/core/admin/heading_component.html.erb +26 -0
  56. data/app/components/panda/core/admin/heading_component.rb +23 -60
  57. data/app/components/panda/core/admin/login_form_component.html.erb +38 -0
  58. data/app/components/panda/core/admin/login_form_component.rb +28 -0
  59. data/app/components/panda/core/admin/main_layout_component.html.erb +88 -0
  60. data/app/components/panda/core/admin/main_layout_component.rb +19 -0
  61. data/app/components/panda/core/admin/my_profile/connected_account_component.html.erb +41 -0
  62. data/app/components/panda/core/admin/my_profile/connected_account_component.rb +27 -0
  63. data/app/components/panda/core/admin/navigation/item_component.html.erb +34 -0
  64. data/app/components/panda/core/admin/navigation/item_component.rb +69 -0
  65. data/app/components/panda/core/admin/navigation/sub_item_component.html.erb +9 -0
  66. data/app/components/panda/core/admin/navigation/sub_item_component.rb +44 -0
  67. data/app/components/panda/core/admin/navigation/user_menu_component.html.erb +30 -0
  68. data/app/components/panda/core/admin/navigation/user_menu_component.rb +36 -0
  69. data/app/components/panda/core/admin/page_header_component.html.erb +28 -0
  70. data/app/components/panda/core/admin/page_header_component.rb +19 -35
  71. data/app/components/panda/core/admin/pagination_component.html.erb +33 -0
  72. data/app/components/panda/core/admin/pagination_component.rb +56 -0
  73. data/app/components/panda/core/admin/panel_component.html.erb +19 -0
  74. data/app/components/panda/core/admin/panel_component.rb +4 -29
  75. data/app/components/panda/core/admin/prose_panel_component.html.erb +6 -0
  76. data/app/components/panda/core/admin/prose_panel_component.rb +43 -0
  77. data/app/components/panda/core/admin/search_bar_component.html.erb +19 -0
  78. data/app/components/panda/core/admin/search_bar_component.rb +31 -0
  79. data/app/components/panda/core/admin/search_filter_bar_component.html.erb +22 -0
  80. data/app/components/panda/core/admin/search_filter_bar_component.rb +27 -0
  81. data/app/components/panda/core/admin/secret_field_component.html.erb +24 -0
  82. data/app/components/panda/core/admin/secret_field_component.rb +31 -0
  83. data/app/components/panda/core/admin/sidebar_component.html.erb +160 -0
  84. data/app/components/panda/core/admin/sidebar_component.rb +55 -0
  85. data/app/components/panda/core/admin/slideover_component.html.erb +67 -0
  86. data/app/components/panda/core/admin/slideover_component.rb +31 -70
  87. data/app/components/panda/core/admin/statistics_component.html.erb +4 -0
  88. data/app/components/panda/core/admin/statistics_component.rb +6 -8
  89. data/app/components/panda/core/admin/tab_bar_component.html.erb +48 -0
  90. data/app/components/panda/core/admin/tab_bar_component.rb +34 -69
  91. data/app/components/panda/core/admin/table_component.html.erb +82 -0
  92. data/app/components/panda/core/admin/table_component.rb +54 -87
  93. data/app/components/panda/core/admin/tag_badge_component.html.erb +7 -0
  94. data/app/components/panda/core/admin/tag_badge_component.rb +21 -0
  95. data/app/components/panda/core/admin/tag_component.html.erb +1 -0
  96. data/app/components/panda/core/admin/tag_component.rb +51 -20
  97. data/app/components/panda/core/admin/tag_input_component.html.erb +37 -0
  98. data/app/components/panda/core/admin/tag_input_component.rb +22 -0
  99. data/app/components/panda/core/admin/tags_panel_component.html.erb +8 -0
  100. data/app/components/panda/core/admin/tags_panel_component.rb +33 -0
  101. data/app/components/panda/core/admin/user_activity_component.html.erb +15 -0
  102. data/app/components/panda/core/admin/user_activity_component.rb +9 -22
  103. data/app/components/panda/core/admin/user_display_component.html.erb +8 -0
  104. data/app/components/panda/core/admin/user_display_component.rb +19 -22
  105. data/app/components/panda/core/base.rb +35 -42
  106. data/app/components/panda/core/shared/footer_component.html.erb +2 -0
  107. data/app/components/panda/core/shared/footer_component.rb +11 -0
  108. data/app/components/panda/core/shared/header_component.html.erb +32 -0
  109. data/app/components/panda/core/shared/header_component.rb +19 -0
  110. data/app/constraints/panda/core/admin_constraint.rb +2 -2
  111. data/app/controllers/concerns/panda/core/admin_authorization.rb +58 -0
  112. data/app/controllers/concerns/panda/core/authorizable.rb +107 -0
  113. data/app/controllers/panda/core/admin/base_controller.rb +62 -3
  114. data/app/controllers/panda/core/admin/editor/search_controller.rb +17 -0
  115. data/app/controllers/panda/core/admin/feature_flags_controller.rb +35 -0
  116. data/app/controllers/panda/core/admin/file_categories_controller.rb +84 -0
  117. data/app/controllers/panda/core/admin/files_controller.rb +230 -0
  118. data/app/controllers/panda/core/admin/import_sessions_controller.rb +120 -0
  119. data/app/controllers/panda/core/admin/my_profile/logins_controller.rb +24 -0
  120. data/app/controllers/panda/core/admin/my_profile_controller.rb +31 -0
  121. data/app/controllers/panda/core/admin/search_controller.rb +15 -0
  122. data/app/controllers/panda/core/admin/sessions_controller.rb +85 -8
  123. data/app/controllers/panda/core/admin/tags_controller.rb +77 -0
  124. data/app/controllers/panda/core/admin/test_sessions_controller.rb +5 -6
  125. data/app/controllers/panda/core/admin/users_controller.rb +184 -0
  126. data/app/controllers/panda/core/application_controller.rb +13 -1
  127. data/app/helpers/panda/core/active_storage_helper.rb +24 -0
  128. data/app/helpers/panda/core/feature_flag_helper.rb +11 -0
  129. data/app/helpers/panda/core/form_helper.rb +15 -0
  130. data/app/javascript/panda/core/controllers/avatar_upload_controller.js +73 -0
  131. data/app/javascript/panda/core/controllers/clipboard_controller.js +119 -0
  132. data/app/javascript/panda/core/controllers/collapsible_item_controller.js +45 -0
  133. data/app/javascript/panda/core/controllers/custom_select_controller.js +256 -0
  134. data/app/javascript/panda/core/controllers/datepicker_controller.js +121 -0
  135. data/app/javascript/panda/core/controllers/daterange_controller.js +193 -0
  136. data/app/javascript/panda/core/controllers/file_gallery_controller.js +134 -0
  137. data/app/javascript/panda/core/controllers/file_upload_controller.js +165 -0
  138. data/app/javascript/panda/core/controllers/global_search_controller.js +184 -0
  139. data/app/javascript/panda/core/controllers/index.js +36 -0
  140. data/app/javascript/panda/core/controllers/mobile_sidebar_controller.js +58 -0
  141. data/app/javascript/panda/core/controllers/navigation_toggle_controller.js +49 -24
  142. data/app/javascript/panda/core/controllers/row_link_controller.js +27 -0
  143. data/app/javascript/panda/core/controllers/tag_input_controller.js +183 -0
  144. data/app/javascript/panda/core/tailwindcss-stimulus-components.js +2 -3
  145. data/app/javascript/panda/core/vendor/@fortawesome--fontawesome-free@7.2.0.js +8129 -0
  146. data/app/javascript/panda/core/vendor/@hotwired--turbo.js +7197 -100
  147. data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +511 -2
  148. data/app/javascript/panda/core/vendor/@tailwindplus--elements@1.0.22.js +65 -0
  149. data/app/javascript/panda/core/vendor/cropperjs@2.1.0.js +3 -0
  150. data/app/javascript/panda/core/vendor/vanilla-calendar-pro.js +2 -0
  151. data/app/models/concerns/panda/core/has_metadata.rb +164 -0
  152. data/app/models/concerns/panda/core/importable.rb +79 -0
  153. data/app/models/concerns/panda/core/searchable.rb +66 -0
  154. data/app/models/concerns/panda/core/taggable.rb +37 -0
  155. data/app/models/panda/core/feature_flag.rb +56 -0
  156. data/app/models/panda/core/file_categorization.rb +14 -0
  157. data/app/models/panda/core/file_category.rb +45 -0
  158. data/app/models/panda/core/import_session.rb +79 -0
  159. data/app/models/panda/core/presence.rb +24 -0
  160. data/app/models/panda/core/tag.rb +33 -0
  161. data/app/models/panda/core/tagging.rb +14 -0
  162. data/app/models/panda/core/user.rb +96 -18
  163. data/app/models/panda/core/user_activity.rb +42 -0
  164. data/app/models/panda/core/user_session.rb +35 -0
  165. data/app/services/panda/core/attach_avatar_service.rb +15 -3
  166. data/app/services/panda/core/csv_import_service.rb +65 -0
  167. data/app/services/panda/core/file_categorizer.rb +52 -0
  168. data/app/services/panda/core/file_parser.rb +76 -0
  169. data/app/services/panda/core/invite_user_service.rb +44 -0
  170. data/app/services/panda/core/presence_service.rb +49 -0
  171. data/app/views/layouts/panda/core/admin.html.erb +6 -67
  172. data/app/views/layouts/panda/core/admin_simple.html.erb +2 -2
  173. data/app/views/panda/core/admin/dashboard/_default_content.html.erb +1 -73
  174. data/app/views/panda/core/admin/dashboard/show.html.erb +12 -15
  175. data/app/views/panda/core/admin/feature_flags/index.html.erb +56 -0
  176. data/app/views/panda/core/admin/file_categories/_form.html.erb +20 -0
  177. data/app/views/panda/core/admin/file_categories/edit.html.erb +5 -0
  178. data/app/views/panda/core/admin/file_categories/index.html.erb +55 -0
  179. data/app/views/panda/core/admin/file_categories/new.html.erb +5 -0
  180. data/app/views/panda/core/admin/files/_file_details.html.erb +156 -0
  181. data/app/views/panda/core/admin/files/index.html.erb +61 -0
  182. data/app/views/panda/core/admin/import_sessions/column_map.html.erb +43 -0
  183. data/app/views/panda/core/admin/import_sessions/index.html.erb +48 -0
  184. data/app/views/panda/core/admin/import_sessions/new.html.erb +37 -0
  185. data/app/views/panda/core/admin/import_sessions/preview.html.erb +51 -0
  186. data/app/views/panda/core/admin/import_sessions/show.html.erb +87 -0
  187. data/app/views/panda/core/admin/my_profile/edit.html.erb +51 -39
  188. data/app/views/panda/core/admin/my_profile/logins/show.html.erb +72 -0
  189. data/app/views/panda/core/admin/sessions/new.html.erb +1 -38
  190. data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +1 -18
  191. data/app/views/panda/core/admin/shared/_sidebar.html.erb +1 -99
  192. data/app/views/panda/core/admin/shared/_slideover.html.erb +3 -3
  193. data/app/views/panda/core/admin/tags/_form.html.erb +21 -0
  194. data/app/views/panda/core/admin/tags/edit.html.erb +4 -0
  195. data/app/views/panda/core/admin/tags/index.html.erb +41 -0
  196. data/app/views/panda/core/admin/tags/new.html.erb +4 -0
  197. data/app/views/panda/core/admin/users/activity.html.erb +90 -0
  198. data/app/views/panda/core/admin/users/edit.html.erb +48 -0
  199. data/app/views/panda/core/admin/users/index.html.erb +161 -0
  200. data/app/views/panda/core/admin/users/sessions.html.erb +103 -0
  201. data/app/views/panda/core/admin/users/show.html.erb +224 -0
  202. data/app/views/panda/core/shared/_footer.html.erb +1 -2
  203. data/app/views/panda/core/shared/_header.html.erb +4 -22
  204. data/config/brakeman.ignore +68 -31
  205. data/config/importmap.rb +13 -11
  206. data/config/routes.rb +49 -2
  207. data/db/migrate/20260202171614_create_panda_core_file_categories.rb +30 -0
  208. data/db/migrate/20260203000001_add_user_management_fields_to_panda_core_users.rb +21 -0
  209. data/db/migrate/20260203000002_create_panda_core_user_activities.rb +24 -0
  210. data/db/migrate/20260203000003_create_panda_core_user_sessions.rb +25 -0
  211. data/db/migrate/20260207100000_create_panda_core_presences.rb +21 -0
  212. data/db/migrate/20260213000001_fix_active_storage_attachments_record_id_type.rb +44 -0
  213. data/db/migrate/20260217000001_create_panda_core_feature_flags.rb +14 -0
  214. data/db/migrate/20260303000001_add_metadata_to_panda_core_users.rb +11 -0
  215. data/db/migrate/20260310000001_create_panda_core_tags.rb +27 -0
  216. data/db/migrate/20260310000002_create_panda_core_import_sessions.rb +29 -0
  217. data/lib/generators/panda/core/install_generator.rb +32 -0
  218. data/lib/generators/panda/core/templates/panda.rb +63 -0
  219. data/lib/panda/core/asset_loader.rb +6 -3
  220. data/lib/panda/core/authentication.rb +3 -3
  221. data/lib/panda/core/configuration.rb +195 -1
  222. data/lib/panda/core/engine/admin_controller_config.rb +4 -3
  223. data/lib/panda/core/engine/autoload_config.rb +21 -9
  224. data/lib/panda/core/engine/omniauth_config.rb +114 -32
  225. data/lib/panda/core/engine/pending_migrations_check.rb +46 -0
  226. data/lib/panda/core/engine/route_config.rb +1 -1
  227. data/lib/panda/core/engine/sqlite_schema_compatibility.rb +36 -0
  228. data/lib/panda/core/engine/{phlex_config.rb → view_component_config.rb} +5 -7
  229. data/lib/panda/core/engine.rb +90 -25
  230. data/lib/panda/core/middleware.rb +35 -0
  231. data/lib/panda/core/module_registry.rb +21 -13
  232. data/lib/panda/core/navigation_registry.rb +455 -0
  233. data/lib/panda/core/oauth_providers.rb +27 -0
  234. data/lib/panda/core/permission_registry.rb +89 -0
  235. data/lib/panda/core/search_registry.rb +69 -0
  236. data/lib/panda/core/seeds/file_categories.rb +30 -0
  237. data/lib/panda/core/shared/inflections_config.rb +1 -5
  238. data/lib/panda/core/testing/rails_helper.rb +87 -0
  239. data/lib/panda/core/testing/support/authentication_test_helpers.rb +22 -18
  240. data/lib/panda/core/testing/support/system/cuprite_helpers.rb +4 -4
  241. data/lib/panda/core/testing/view_component_test_controller.rb +19 -0
  242. data/lib/panda/core/version.rb +1 -1
  243. data/lib/panda/core/widget_registry.rb +50 -0
  244. data/lib/panda/core.rb +20 -0
  245. data/lib/tasks/panda/core/file_categories.rake +41 -0
  246. data/public/panda-core-assets/panda-core-0.12.2.css +2 -0
  247. data/public/panda-core-assets/panda-core-0.12.3.css +2 -0
  248. data/public/panda-core-assets/panda-core-0.12.5.css +2 -0
  249. data/public/panda-core-assets/panda-core-0.13.0.css +2 -0
  250. data/public/panda-core-assets/panda-core-0.14.0.css +2 -0
  251. data/public/panda-core-assets/panda-core-0.14.4.css +2 -0
  252. data/public/panda-core-assets/panda-core.css +2 -2
  253. data/public/panda-core-assets/vanilla-calendar.css +47 -0
  254. metadata +205 -26
  255. data/lib/panda/core/engine/inflections_config.rb +0 -23
@@ -10,6 +10,12 @@ module Panda
10
10
  super(attribute, text, options.reverse_merge(class: label_styles))
11
11
  end
12
12
 
13
+ # Override field_id to match FormBuilder's signature, since the included
14
+ # FormTagHelper defines a field_id with a different arity that shadows it.
15
+ def field_id(method, *suffixes, namespace: @options[:namespace], index: @options[:index])
16
+ @template.field_id(@object_name, method, *suffixes, namespace: namespace, index: index)
17
+ end
18
+
13
19
  def text_field(attribute, options = {})
14
20
  # Extract custom label if provided
15
21
  custom_label = options.delete(:label)
@@ -48,6 +54,22 @@ module Panda
48
54
  end
49
55
  end
50
56
 
57
+ def telephone_field(method, options = {})
58
+ custom_label = options.delete(:label)
59
+
60
+ content_tag :div, class: container_styles do
61
+ label(method, custom_label) + meta_text(options) + super(method, options.reverse_merge(class: input_styles)) + error_message(method)
62
+ end
63
+ end
64
+
65
+ def url_field(method, options = {})
66
+ custom_label = options.delete(:label)
67
+
68
+ content_tag :div, class: container_styles do
69
+ label(method, custom_label) + meta_text(options) + super(method, options.reverse_merge(class: input_styles)) + error_message(method)
70
+ end
71
+ end
72
+
51
73
  def datetime_field(method, options = {})
52
74
  # Extract custom label if provided
53
75
  custom_label = options.delete(:label)
@@ -57,6 +79,31 @@ module Panda
57
79
  end
58
80
  end
59
81
 
82
+ def datetime_split_field(method, options = {})
83
+ custom_label = options.delete(:label)
84
+ value = object.respond_to?(method) ? object.send(method) : nil
85
+
86
+ content_tag :div, class: container_styles, data: {controller: "datetime"} do
87
+ label(method, custom_label) +
88
+ meta_text(options) +
89
+ hidden_field(method, data: {datetime_target: "combinedField"}, value: value&.iso8601) +
90
+ content_tag(:div, class: "flex gap-3") do
91
+ @template.tag.input(
92
+ type: "date",
93
+ class: input_styles,
94
+ data: {datetime_target: "dateField", action: "change->datetime#update"}
95
+ ) +
96
+ @template.tag.input(
97
+ type: "time",
98
+ step: "60",
99
+ class: input_styles,
100
+ data: {datetime_target: "timeField", action: "change->datetime#update"}
101
+ )
102
+ end +
103
+ error_message(method)
104
+ end
105
+ end
106
+
60
107
  def text_area(method, options = {})
61
108
  # Extract custom label if provided
62
109
  custom_label = options.delete(:label)
@@ -75,12 +122,24 @@ module Panda
75
122
  end
76
123
  end
77
124
 
78
- def select(method, choices = nil, options = {}, html_options = {})
125
+ def number_field(method, options = {})
79
126
  # Extract custom label if provided
80
127
  custom_label = options.delete(:label)
81
128
 
82
129
  content_tag :div, class: container_styles do
83
- label(method, custom_label) + meta_text(options) + super(method, choices, options, html_options.reverse_merge(class: select_styles)) + select_svg + error_message(method)
130
+ label(method, custom_label) + meta_text(options) + super(method, options.reverse_merge(class: input_styles)) + error_message(method)
131
+ end
132
+ end
133
+
134
+ def select(method, choices = nil, options = {}, html_options = {})
135
+ # Extract custom label if provided
136
+ custom_label = options.delete(:label)
137
+
138
+ html_options = html_options.reverse_merge(class: select_styles)
139
+ html_options[:data] = (html_options[:data] || {}).merge("custom-select-target": "select")
140
+
141
+ content_tag :div, class: container_styles, data: {controller: "custom-select"} do
142
+ label(method, custom_label) + meta_text(options) + super(method, choices, options, html_options) + select_svg + error_message(method)
84
143
  end
85
144
  end
86
145
 
@@ -88,19 +147,25 @@ module Panda
88
147
  # Extract custom label if provided
89
148
  custom_label = options.delete(:label)
90
149
 
91
- content_tag :div, class: container_styles do
92
- label(method, custom_label) + meta_text(options) + super(method, collection, value_method, text_method, options, html_options.reverse_merge(class: input_styles)) + error_message(method)
150
+ html_options = html_options.reverse_merge(class: input_styles)
151
+ html_options[:data] = (html_options[:data] || {}).merge("custom-select-target": "select")
152
+
153
+ content_tag :div, class: container_styles, data: {controller: "custom-select"} do
154
+ label(method, custom_label) + meta_text(options) + super(method, collection, value_method, text_method, options, html_options) + error_message(method)
93
155
  end
94
156
  end
95
157
 
96
158
  def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
97
- wrap_field(method, options) do
98
- super(
159
+ html_options = html_options.reverse_merge(class: select_styles)
160
+ html_options[:data] = (html_options[:data] || {}).merge("custom-select-target": "select")
161
+
162
+ content_tag :div, class: container_styles, data: {controller: "custom-select"} do
163
+ label(method) + meta_text(options) + super(
99
164
  method,
100
165
  priority_zones,
101
166
  options,
102
- html_options.reverse_merge(class: select_styles)
103
- )
167
+ html_options
168
+ ) + error_message(method)
104
169
  end
105
170
  end
106
171
 
@@ -125,8 +190,7 @@ module Panda
125
190
  content_tag :div, class: container_styles do
126
191
  label(method, custom_label) +
127
192
  meta_text(options) +
128
- # Cropper stylesheet
129
- @template.content_tag(:link, nil, rel: "stylesheet", href: "https://cdn.jsdelivr.net/npm/cropperjs@1.6.2/dist/cropper.min.css") +
193
+ # Note: cropperjs 2.x uses Web Components with built-in styles — no external CSS needed
130
194
  # File input
131
195
  content_tag(:div, class: "mt-2") do
132
196
  super(method, options.reverse_merge(
@@ -149,7 +213,7 @@ module Panda
149
213
  @template.image_tag("", alt: "Crop preview", data: {image_cropper_target: "preview"}, class: "max-w-full") +
150
214
  # Cropper controls
151
215
  content_tag(:div, class: "mt-4 flex gap-2 flex-wrap") do
152
- @template.button_tag("Crop & Save", type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500", data: {action: "click->image-cropper#crop"}) +
216
+ @template.button_tag("Crop & Save", type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-primary-500 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-primary-600", data: {action: "click->image-cropper#crop"}) +
153
217
  @template.button_tag("Cancel", type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50", data: {action: "click->image-cropper#cancel"}) +
154
218
  @template.button_tag(type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50", data: {action: "click->image-cropper#reset"}) do
155
219
  @template.content_tag(:i, "", class: "fa-solid fa-rotate-left") +
@@ -200,7 +264,7 @@ module Panda
200
264
  end +
201
265
  # Upload area
202
266
  content_tag(:div, class: "mt-4 flex items-baseline justify-center text-sm leading-6 text-gray-600 dark:text-gray-400") do
203
- content_tag(:label, for: field_id, class: "relative cursor-pointer rounded-md bg-transparent font-semibold text-indigo-600 focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:focus-within:outline-indigo-500 dark:hover:text-indigo-300") do
267
+ content_tag(:label, for: field_id, class: "relative cursor-pointer rounded-md bg-transparent font-semibold text-primary-600 focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-primary-600 hover:text-primary-500 dark:text-primary-400 dark:focus-within:outline-primary-500 dark:hover:text-primary-300") do
204
268
  content_tag(:span, "Upload a file") +
205
269
  super(method, options.reverse_merge(
206
270
  id: field_id,
@@ -261,11 +325,11 @@ module Panda
261
325
  def submit(value = nil, options = {})
262
326
  value ||= submit_default_value
263
327
 
264
- # Use the primary mid color for save/create actions
265
- action = object.persisted? ? :save : :create
328
+ # Use the primary color for save/create actions
329
+ action = object&.persisted? ? :save : :create
266
330
  button_classes = case action
267
331
  when :save, :create
268
- "text-white bg-mid hover:bg-mid/80"
332
+ "text-white bg-primary-500 hover:bg-primary-600"
269
333
  when :save_inactive
270
334
  "text-white bg-gray-400"
271
335
  when :secondary
@@ -275,7 +339,7 @@ module Panda
275
339
  end
276
340
 
277
341
  # Combine with common button classes
278
- classes = "inline-flex items-center rounded-md font-medium shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 px-3 py-2 #{button_classes}"
342
+ classes = "inline-flex items-center rounded-md font-medium shadow-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 px-3 py-2 #{button_classes}"
279
343
 
280
344
  options[:class] = options[:class] ? "#{options[:class]} #{classes}" : classes
281
345
 
@@ -287,16 +351,48 @@ module Panda
287
351
  custom_label = options.delete(:label)
288
352
 
289
353
  content_tag :div, class: container_styles do
290
- label(method, custom_label) + meta_text(options) + super(method, options.reverse_merge(class: "border-gray-300 ml-2"), checked_value, unchecked_value)
354
+ label(method, custom_label) + meta_text(options) + super(method, options.reverse_merge(class: "border-gray-300 text-primary-600 focus:ring-primary-500 ml-2"), checked_value, unchecked_value)
291
355
  end
292
356
  end
293
357
 
294
358
  def date_field(method, options = {})
295
- # Extract custom label if provided
296
359
  custom_label = options.delete(:label)
360
+ date_min = options.delete(:min)
361
+ date_max = options.delete(:max)
297
362
 
298
- content_tag :div, class: container_styles do
299
- label(method, custom_label) + meta_text(options) + super(method, options.reverse_merge(class: input_styles))
363
+ # Format current value for display
364
+ raw_value = object.respond_to?(method) ? object.send(method) : nil
365
+ iso_value = if raw_value.respond_to?(:strftime)
366
+ raw_value.strftime("%Y-%m-%d")
367
+ else
368
+ raw_value
369
+ end
370
+ display_value = if raw_value.respond_to?(:strftime)
371
+ raw_value.strftime("%-d %b %Y")
372
+ elsif raw_value.is_a?(String) && raw_value.present?
373
+ raw_value
374
+ else
375
+ ""
376
+ end
377
+
378
+ controller_data = {controller: "datepicker"}
379
+ controller_data[:datepicker_date_min_value] = date_min.to_s if date_min
380
+ controller_data[:datepicker_date_max_value] = date_max.to_s if date_max
381
+
382
+ content_tag :div, class: "#{container_styles} relative", data: controller_data do
383
+ label(method, custom_label) +
384
+ meta_text(options) +
385
+ hidden_field(method, value: iso_value, data: {datepicker_target: "hidden"}) +
386
+ @template.text_field_tag(
387
+ nil,
388
+ display_value,
389
+ class: "#{input_styles} cursor-pointer",
390
+ readonly: true,
391
+ placeholder: options[:placeholder] || "Select date...",
392
+ data: {datepicker_target: "display", action: "click->datepicker#toggle focus->datepicker#toggle"}
393
+ ) +
394
+ content_tag(:div, "", class: "hidden absolute z-50 mt-1 left-0", data: {datepicker_target: "calendar"}) +
395
+ error_message(method)
300
396
  end
301
397
  end
302
398
 
@@ -317,7 +413,7 @@ module Panda
317
413
  is_checked = (current_value.to_s == choice_value.to_s)
318
414
 
319
415
  content_tag(:label, class: "flex items-center gap-x-3 rounded-lg border border-gray-300 px-3 py-3 text-sm/6 font-medium cursor-pointer hover:bg-gray-50 dark:border-white/10 dark:hover:bg-white/5") do
320
- radio_button(method, choice_value, {id: choice_id, checked: is_checked, class: "size-4 border-gray-300 text-indigo-600 focus:ring-indigo-600 dark:border-white/10 dark:bg-white/5"}) +
416
+ radio_button(method, choice_value, {id: choice_id, checked: is_checked, class: "size-4 border-gray-300 text-primary-600 focus:ring-primary-600 dark:border-white/10 dark:bg-white/5"}) +
321
417
  content_tag(:span, choice_label, class: "text-gray-900 dark:text-white")
322
418
  end
323
419
  end.join.html_safe
@@ -332,8 +428,8 @@ module Panda
332
428
  end
333
429
 
334
430
  def section_heading(text, options = {})
335
- @template.content_tag(:div, class: "-mx-4 sm:-mx-6 px-4 sm:px-6 py-4 bg-gray-200 dark:bg-gray-700 mb-6") do
336
- @template.content_tag(:h3, text, class: "text-base font-semibold text-gray-900 dark:text-white")
431
+ @template.content_tag(:div, class: "mt-6 pt-4 border-t border-gray-200 mb-4") do
432
+ @template.content_tag(:h3, text, class: "text-sm font-semibold text-gray-700")
337
433
  end
338
434
  end
339
435
 
@@ -344,7 +440,7 @@ module Panda
344
440
  end
345
441
 
346
442
  def base_input_styles
347
- "block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
443
+ "block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-primary-500"
348
444
  end
349
445
 
350
446
  def input_styles
@@ -360,7 +456,7 @@ module Panda
360
456
  end
361
457
 
362
458
  def select_styles
363
- "block w-full rounded-md bg-white px-3 py-1.5 pr-8 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 appearance-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:focus:outline-indigo-500"
459
+ "block w-full rounded-md bg-white px-3 py-1.5 pr-8 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 appearance-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:focus:outline-primary-500"
364
460
  end
365
461
 
366
462
  def select_svg
@@ -383,7 +479,13 @@ module Panda
383
479
  end
384
480
 
385
481
  def submit_default_value
386
- object.persisted? ? "Update #{object.class.name.demodulize}" : "Create #{object.class.name.demodulize}"
482
+ if object.nil?
483
+ "Submit"
484
+ elsif object.persisted?
485
+ "Update #{object.class.name.demodulize}"
486
+ else
487
+ "Create #{object.class.name.demodulize}"
488
+ end
387
489
  end
388
490
 
389
491
  def wrap_field(method, options = {}, &block)
@@ -401,7 +503,7 @@ module Panda
401
503
  def error_message(attribute)
402
504
  return unless object.respond_to?(:errors) && object.errors[attribute]&.any?
403
505
 
404
- content_tag(:p, class: "mt-2 text-sm text-red-600") do
506
+ content_tag(:p, class: "mt-2 text-sm text-error-600") do
405
507
  object.errors[attribute].join(", ")
406
508
  end
407
509
  end
@@ -0,0 +1,10 @@
1
+ <%= content_tag :span, @attrs do %>
2
+ <%= text %>
3
+ <% if removable? %>
4
+ <%= content_tag :button, type: "button", class: "inline-flex items-center ml-1 hover:opacity-70", aria: { label: "Remove" } do %>
5
+ <%= content_tag :svg, class: "h-3 w-3", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 20 20", fill: "currentColor" do %>
6
+ <%= tag.path d: "M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" %>
7
+ <% end %>
8
+ <% end %>
9
+ <% end %>
10
+ <% end %>
@@ -27,45 +27,29 @@ module Panda
27
27
  # )
28
28
  #
29
29
  class Badge < Panda::Core::Base
30
- prop :text, String
31
- prop :variant, Symbol, default: :default
32
- prop :size, Symbol, default: :medium
33
- prop :removable, _Boolean, default: false
34
- prop :rounded, _Boolean, default: false
30
+ def initialize(text:, variant: :default, size: :medium, removable: false, rounded: false, **attrs)
31
+ @text = text
32
+ @variant = variant
33
+ @size = size
34
+ @removable = removable
35
+ @rounded = rounded
36
+ super(**attrs)
37
+ end
35
38
 
36
- def view_template
37
- span(**@attrs) do
38
- plain text
39
- if removable
40
- whitespace
41
- button(
42
- type: "button",
43
- class: "inline-flex items-center ml-1 hover:opacity-70",
44
- aria: {label: "Remove"}
45
- ) do
46
- svg(
47
- class: "h-3 w-3",
48
- xmlns: "http://www.w3.org/2000/svg",
49
- viewBox: "0 0 20 20",
50
- fill: "currentColor"
51
- ) do |s|
52
- s.path(
53
- d: "M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
54
- )
55
- end
56
- end
57
- end
58
- end
39
+ attr_reader :text, :variant, :size, :removable, :rounded
40
+
41
+ def removable?
42
+ removable
59
43
  end
60
44
 
45
+ private
46
+
61
47
  def default_attrs
62
48
  {
63
49
  class: badge_classes
64
50
  }
65
51
  end
66
52
 
67
- private
68
-
69
53
  def badge_classes
70
54
  base = "inline-flex items-center font-medium"
71
55
  base += " #{size_classes}"
@@ -0,0 +1,5 @@
1
+ <% if @href %>
2
+ <%= content_tag :a, @text, @attrs %>
3
+ <% else %>
4
+ <%= content_tag :button, @text, @attrs %>
5
+ <% end %>
@@ -3,7 +3,7 @@
3
3
  module Panda
4
4
  module Core
5
5
  module UI
6
- # Modern Phlex-based button component with type-safe props.
6
+ # Modern ViewComponent-based button component with type-safe props.
7
7
  #
8
8
  # Supports both <button> and <a> elements based on whether an href is provided.
9
9
  # Follows Tailwind UI Plus styling patterns with dark mode support.
@@ -34,22 +34,18 @@ module Panda
34
34
  # )
35
35
  #
36
36
  class Button < Panda::Core::Base
37
- # Type-safe properties using Literal
38
- prop :text, String
39
- prop :variant, Symbol, default: :default
40
- prop :size, Symbol, default: :medium
41
- prop :disabled, _Boolean, default: false
42
- prop :type, String, default: "button"
43
- prop :href, _Nilable(String), default: -> {}
44
-
45
- def view_template
46
- if @href
47
- a(**@attrs) { @text }
48
- else
49
- button(**@attrs) { @text }
50
- end
37
+ def initialize(text: "", variant: :default, size: :medium, disabled: false, type: "button", href: nil, **attrs)
38
+ @text = text
39
+ @variant = variant
40
+ @size = size
41
+ @disabled = disabled
42
+ @type = type
43
+ @href = href
44
+ super(**attrs)
51
45
  end
52
46
 
47
+ attr_reader :text, :variant, :size, :disabled, :type, :href
48
+
53
49
  def default_attrs
54
50
  base = {
55
51
  class: button_classes
@@ -89,15 +85,15 @@ module Panda
89
85
  def variant_classes
90
86
  case @variant
91
87
  when :primary
92
- # Blue primary button with dark mode support
93
- "bg-blue-600 text-white shadow-xs hover:bg-blue-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:bg-blue-500 dark:shadow-none dark:hover:bg-blue-400 dark:focus-visible:outline-blue-500"
88
+ # Primary button with dark mode support
89
+ "bg-primary-500 text-white shadow-xs hover:bg-primary-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 dark:bg-primary-400 dark:shadow-none dark:hover:bg-primary-500 dark:focus-visible:outline-primary-500"
94
90
  when :secondary
95
91
  # White/gray secondary button with ring and dark mode support
96
92
  "bg-white text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20"
97
93
  when :success
98
- "bg-green-600 text-white shadow-xs hover:bg-green-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600 dark:bg-green-500 dark:shadow-none dark:hover:bg-green-400 dark:focus-visible:outline-green-500"
94
+ "bg-success-600 text-white shadow-xs hover:bg-success-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-success-600 dark:bg-success-500 dark:shadow-none dark:hover:bg-success-600 dark:focus-visible:outline-success-500"
99
95
  when :danger
100
- "bg-red-600 text-white shadow-xs hover:bg-red-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 dark:bg-red-500 dark:shadow-none dark:hover:bg-red-400 dark:focus-visible:outline-red-500"
96
+ "bg-error-600 text-white shadow-xs hover:bg-error-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-error-600 dark:bg-error-500 dark:shadow-none dark:hover:bg-error-600 dark:focus-visible:outline-error-500"
101
97
  when :ghost
102
98
  "bg-transparent text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
103
99
  else # :default
@@ -0,0 +1,3 @@
1
+ <%= content_tag :div, @attrs do %>
2
+ <%= content %>
3
+ <% end %>
@@ -29,14 +29,15 @@ module Panda
29
29
  # end
30
30
  #
31
31
  class Card < Panda::Core::Base
32
- prop :padding, Symbol, default: :medium
33
- prop :elevation, Symbol, default: :low
34
- prop :border, _Boolean, default: true
35
-
36
- def view_template(&block)
37
- div(**@attrs, &block)
32
+ def initialize(padding: :medium, elevation: :low, border: true, **attrs)
33
+ @padding = padding
34
+ @elevation = elevation
35
+ @border = border
36
+ super(**attrs)
38
37
  end
39
38
 
39
+ attr_reader :padding, :elevation, :border
40
+
40
41
  def default_attrs
41
42
  {
42
43
  class: card_classes
@@ -0,0 +1,21 @@
1
+ <%= render Panda::Core::Admin::PanelComponent.new do |panel| %>
2
+ <% panel.with_heading_slot do %>
3
+ <%= heading %>
4
+ <span class="text-sm font-normal text-gray-500 dark:text-gray-400 ml-1">(<%= changes.size %> changed)</span>
5
+ <% end %>
6
+ <% panel.with_body_slot do %>
7
+ <div class="divide-y divide-gray-200 dark:divide-gray-700 -mx-4 -mb-4">
8
+ <% changes.each do |attr, values| %>
9
+ <div class="p-4 grid grid-cols-3 gap-4">
10
+ <div class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= attr.humanize %></div>
11
+ <div class="text-sm">
12
+ <span class="px-1 bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200 line-through"><%= display_value(values[:old]) %></span>
13
+ </div>
14
+ <div class="text-sm">
15
+ <span class="px-1 bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200"><%= display_value(values[:new]) %></span>
16
+ </div>
17
+ </div>
18
+ <% end %>
19
+ </div>
20
+ <% end %>
21
+ <% end %>
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class AttributeDiffComponent < Panda::Core::Base
7
+ def initialize(changes:, heading: "Attribute Changes", **attrs)
8
+ @changes = changes
9
+ @heading = heading
10
+ super(**attrs)
11
+ end
12
+
13
+ attr_reader :changes, :heading
14
+
15
+ def render?
16
+ changes.any?
17
+ end
18
+
19
+ def display_value(value)
20
+ if value.is_a?(FalseClass) || value.is_a?(TrueClass)
21
+ value.to_s
22
+ else
23
+ value.presence || "(empty)"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,36 @@
1
+ <div>
2
+ <%# Mobile back link %>
3
+ <% if @show_back && @items.any? %>
4
+ <nav aria-label="Back" class="sm:hidden">
5
+ <%= content_tag :a, href: back_link_href, class: "flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300" do %>
6
+ <%= render_chevron_left_icon %>
7
+ Back
8
+ <% end %>
9
+ </nav>
10
+ <% end %>
11
+
12
+ <%# Desktop breadcrumb trail %>
13
+ <nav aria-label="Breadcrumb" class="hidden sm:flex">
14
+ <ol role="list" class="flex items-center space-x-4">
15
+ <% @items.each_with_index do |item, index| %>
16
+ <li>
17
+ <% if index.zero? %>
18
+ <%# First item (no separator) %>
19
+ <div class="flex">
20
+ <%= content_tag :a, item[:text], href: item[:href], class: breadcrumb_link_classes(index) %>
21
+ </div>
22
+ <% else %>
23
+ <%# Subsequent items (with separator) %>
24
+ <div class="flex items-center">
25
+ <%= render_chevron_right_icon %>
26
+ <%= content_tag :a, item[:text],
27
+ href: item[:href],
28
+ aria: ((index == @items.length - 1) ? {current: "page"} : nil),
29
+ class: breadcrumb_link_classes(index) %>
30
+ </div>
31
+ <% end %>
32
+ </li>
33
+ <% end %>
34
+ </ol>
35
+ </nav>
36
+ </div>
@@ -26,64 +26,14 @@ module Panda
26
26
  # )
27
27
  #
28
28
  class BreadcrumbComponent < Panda::Core::Base
29
- prop :items, Array, default: -> { [] }
30
- prop :show_back, _Boolean, default: true
31
-
32
- def view_template
33
- div do
34
- # Mobile back link
35
- if @show_back && @items.any?
36
- nav(
37
- aria: {label: "Back"},
38
- class: "sm:hidden"
39
- ) do
40
- a(
41
- href: back_link_href,
42
- class: "flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
43
- ) do
44
- render_chevron_left_icon
45
- plain "Back"
46
- end
47
- end
48
- end
49
-
50
- # Desktop breadcrumb trail
51
- nav(
52
- aria: {label: "Breadcrumb"},
53
- class: "hidden sm:flex"
54
- ) do
55
- ol(
56
- role: "list",
57
- class: "flex items-center space-x-4"
58
- ) do
59
- @items.each_with_index do |item, index|
60
- li do
61
- if index.zero?
62
- # First item (no separator)
63
- div(class: "flex") do
64
- a(
65
- href: item[:href],
66
- class: breadcrumb_link_classes(index)
67
- ) { item[:text] }
68
- end
69
- else
70
- # Subsequent items (with separator)
71
- div(class: "flex items-center") do
72
- render_chevron_right_icon
73
- a(
74
- href: item[:href],
75
- aria: ((index == @items.length - 1) ? {current: "page"} : nil),
76
- class: breadcrumb_link_classes(index)
77
- ) { item[:text] }
78
- end
79
- end
80
- end
81
- end
82
- end
83
- end
84
- end
29
+ def initialize(items: [], show_back: true, **attrs)
30
+ @items = items
31
+ @show_back = show_back
32
+ super(**attrs)
85
33
  end
86
34
 
35
+ attr_reader :items, :show_back
36
+
87
37
  private
88
38
 
89
39
  def back_link_href
@@ -97,14 +47,13 @@ module Panda
97
47
  end
98
48
 
99
49
  def render_chevron_left_icon
100
- svg(
50
+ content_tag(:svg,
101
51
  viewBox: "0 0 20 20",
102
52
  fill: "currentColor",
103
53
  data: {slot: "icon"},
104
54
  aria: {hidden: "true"},
105
- class: "mr-1 -ml-1 size-5 shrink-0 text-gray-400 dark:text-gray-500"
106
- ) do |s|
107
- s.path(
55
+ class: "mr-1 -ml-1 size-5 shrink-0 text-gray-400 dark:text-gray-500") do
56
+ tag.path(
108
57
  d: "M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z",
109
58
  clip_rule: "evenodd",
110
59
  fill_rule: "evenodd"
@@ -113,14 +62,13 @@ module Panda
113
62
  end
114
63
 
115
64
  def render_chevron_right_icon
116
- svg(
65
+ content_tag(:svg,
117
66
  viewBox: "0 0 20 20",
118
67
  fill: "currentColor",
119
68
  data: {slot: "icon"},
120
69
  aria: {hidden: "true"},
121
- class: "size-5 shrink-0 text-gray-400 dark:text-gray-500"
122
- ) do |s|
123
- s.path(
70
+ class: "size-5 shrink-0 text-gray-400 dark:text-gray-500") do
71
+ tag.path(
124
72
  d: "M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z",
125
73
  clip_rule: "evenodd",
126
74
  fill_rule: "evenodd"