better_page 2.0.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 (99) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +62 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +357 -0
  5. data/Rakefile +3 -0
  6. data/docs/00-README.md +17 -0
  7. data/docs/01-getting-started.md +137 -0
  8. data/docs/02-component-registry.md +192 -0
  9. data/docs/03-base-pages.md +238 -0
  10. data/docs/04-schema-validation.md +180 -0
  11. data/docs/05-turbo-support.md +220 -0
  12. data/docs/06-compliance-analyzer.md +147 -0
  13. data/docs/07-configuration.md +157 -0
  14. data/guide/00-README.md +32 -0
  15. data/guide/01-quick-start.md +148 -0
  16. data/guide/02-building-index-page.md +258 -0
  17. data/guide/03-building-show-page.md +266 -0
  18. data/guide/04-building-form-page.md +309 -0
  19. data/guide/05-custom-pages.md +325 -0
  20. data/guide/06-best-practices.md +311 -0
  21. data/lib/better_page/base_page.rb +161 -0
  22. data/lib/better_page/compliance/analyzer.rb +409 -0
  23. data/lib/better_page/component_registry.rb +393 -0
  24. data/lib/better_page/config.rb +165 -0
  25. data/lib/better_page/configuration.rb +153 -0
  26. data/lib/better_page/custom_base_page.rb +85 -0
  27. data/lib/better_page/default_components.rb +200 -0
  28. data/lib/better_page/form_base_page.rb +170 -0
  29. data/lib/better_page/index_base_page.rb +69 -0
  30. data/lib/better_page/railtie.rb +34 -0
  31. data/lib/better_page/show_base_page.rb +120 -0
  32. data/lib/better_page/validation_error.rb +7 -0
  33. data/lib/better_page/version.rb +3 -0
  34. data/lib/better_page.rb +80 -0
  35. data/lib/generators/better_page/component_generator.rb +131 -0
  36. data/lib/generators/better_page/install_generator.rb +160 -0
  37. data/lib/generators/better_page/page_generator.rb +101 -0
  38. data/lib/generators/better_page/sync_generator.rb +109 -0
  39. data/lib/generators/better_page/templates/application_page.rb.tt +12 -0
  40. data/lib/generators/better_page/templates/better_page_initializer.rb.tt +53 -0
  41. data/lib/generators/better_page/templates/custom_base_page.rb.tt +83 -0
  42. data/lib/generators/better_page/templates/custom_page.rb.tt +31 -0
  43. data/lib/generators/better_page/templates/edit_page.rb.tt +46 -0
  44. data/lib/generators/better_page/templates/form_base_page.rb.tt +126 -0
  45. data/lib/generators/better_page/templates/index_base_page.rb.tt +65 -0
  46. data/lib/generators/better_page/templates/index_page.rb.tt +56 -0
  47. data/lib/generators/better_page/templates/javascript/controllers/app_nav_controller.js +57 -0
  48. data/lib/generators/better_page/templates/javascript/controllers/drawer_controller.js +99 -0
  49. data/lib/generators/better_page/templates/javascript/controllers/dropdown_controller.js +60 -0
  50. data/lib/generators/better_page/templates/javascript/controllers/index.js +36 -0
  51. data/lib/generators/better_page/templates/javascript/controllers/modal_controller.js +70 -0
  52. data/lib/generators/better_page/templates/javascript/controllers/sidebar_controller.js +152 -0
  53. data/lib/generators/better_page/templates/javascript/controllers/table_controller.js +60 -0
  54. data/lib/generators/better_page/templates/javascript/controllers/tabs_controller.js +89 -0
  55. data/lib/generators/better_page/templates/new_page.rb.tt +46 -0
  56. data/lib/generators/better_page/templates/show_base_page.rb.tt +117 -0
  57. data/lib/generators/better_page/templates/show_page.rb.tt +45 -0
  58. data/lib/generators/better_page/templates/view_components/application_view_component.rb.tt +7 -0
  59. data/lib/generators/better_page/templates/view_components/custom_view_component.html.erb.tt +21 -0
  60. data/lib/generators/better_page/templates/view_components/custom_view_component.rb.tt +21 -0
  61. data/lib/generators/better_page/templates/view_components/form_view_component.html.erb.tt +25 -0
  62. data/lib/generators/better_page/templates/view_components/form_view_component.rb.tt +23 -0
  63. data/lib/generators/better_page/templates/view_components/index_view_component.html.erb.tt +33 -0
  64. data/lib/generators/better_page/templates/view_components/index_view_component.rb.tt +29 -0
  65. data/lib/generators/better_page/templates/view_components/show_view_component.html.erb.tt +29 -0
  66. data/lib/generators/better_page/templates/view_components/show_view_component.rb.tt +25 -0
  67. data/lib/generators/better_page/templates/view_components/ui/alerts_component.html.erb.tt +47 -0
  68. data/lib/generators/better_page/templates/view_components/ui/alerts_component.rb.tt +47 -0
  69. data/lib/generators/better_page/templates/view_components/ui/content_section_component.html.erb.tt +42 -0
  70. data/lib/generators/better_page/templates/view_components/ui/content_section_component.rb.tt +34 -0
  71. data/lib/generators/better_page/templates/view_components/ui/drawer_component.html.erb.tt +73 -0
  72. data/lib/generators/better_page/templates/view_components/ui/drawer_component.rb.tt +78 -0
  73. data/lib/generators/better_page/templates/view_components/ui/errors_component.html.erb.tt +23 -0
  74. data/lib/generators/better_page/templates/view_components/ui/errors_component.rb.tt +18 -0
  75. data/lib/generators/better_page/templates/view_components/ui/field_component.html.erb.tt +65 -0
  76. data/lib/generators/better_page/templates/view_components/ui/field_component.rb.tt +91 -0
  77. data/lib/generators/better_page/templates/view_components/ui/footer_component.html.erb.tt +33 -0
  78. data/lib/generators/better_page/templates/view_components/ui/footer_component.rb.tt +32 -0
  79. data/lib/generators/better_page/templates/view_components/ui/header_component.html.erb.tt +55 -0
  80. data/lib/generators/better_page/templates/view_components/ui/header_component.rb.tt +39 -0
  81. data/lib/generators/better_page/templates/view_components/ui/modal_component.html.erb.tt +70 -0
  82. data/lib/generators/better_page/templates/view_components/ui/modal_component.rb.tt +54 -0
  83. data/lib/generators/better_page/templates/view_components/ui/overview_component.html.erb.tt +22 -0
  84. data/lib/generators/better_page/templates/view_components/ui/overview_component.rb.tt +71 -0
  85. data/lib/generators/better_page/templates/view_components/ui/pagination_component.html.erb.tt +63 -0
  86. data/lib/generators/better_page/templates/view_components/ui/pagination_component.rb.tt +69 -0
  87. data/lib/generators/better_page/templates/view_components/ui/panel_component.html.erb.tt +31 -0
  88. data/lib/generators/better_page/templates/view_components/ui/panel_component.rb.tt +23 -0
  89. data/lib/generators/better_page/templates/view_components/ui/statistics_component.html.erb.tt +33 -0
  90. data/lib/generators/better_page/templates/view_components/ui/statistics_component.rb.tt +51 -0
  91. data/lib/generators/better_page/templates/view_components/ui/table_component.html.erb.tt +112 -0
  92. data/lib/generators/better_page/templates/view_components/ui/table_component.rb.tt +88 -0
  93. data/lib/generators/better_page/templates/view_components/ui/tabs_component.html.erb.tt +52 -0
  94. data/lib/generators/better_page/templates/view_components/ui/tabs_component.rb.tt +76 -0
  95. data/lib/generators/better_page/templates/view_components/ui/widget_component.html.erb.tt +72 -0
  96. data/lib/generators/better_page/templates/view_components/ui/widget_component.rb.tt +34 -0
  97. data/lib/tasks/better_page.rake +70 -0
  98. data/lib/tasks/better_page_tasks.rake +4 -0
  99. metadata +188 -0
@@ -0,0 +1,33 @@
1
+ <div class="better-page better-page--index">
2
+ <%% if alerts? %>
3
+ <%%= render BetterPage::Ui::AlertsComponent.new(alerts: alerts) %>
4
+ <%% end %>
5
+
6
+ <%% if header? %>
7
+ <%%= render BetterPage::Ui::HeaderComponent.new(**header) %>
8
+ <%% end %>
9
+
10
+ <%% if tabs? %>
11
+ <%%= render BetterPage::Ui::TabsComponent.new(**tabs) %>
12
+ <%% end %>
13
+
14
+ <%% if search? %>
15
+ <%%= render BetterPage::Ui::SearchComponent.new(**search) %>
16
+ <%% end %>
17
+
18
+ <%% if statistics? %>
19
+ <%%= render BetterPage::Ui::StatisticsComponent.new(stats: statistics) %>
20
+ <%% end %>
21
+
22
+ <%% if table? %>
23
+ <%%= render BetterPage::Ui::TableComponent.new(**table) %>
24
+ <%% end %>
25
+
26
+ <%% if pagination? %>
27
+ <%%= render BetterPage::Ui::PaginationComponent.new(**pagination) %>
28
+ <%% end %>
29
+
30
+ <%% if footer? %>
31
+ <%%= render BetterPage::Ui::FooterComponent.new(**footer) %>
32
+ <%% end %>
33
+ </div>
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterPage
4
+ class IndexViewComponent < ApplicationViewComponent
5
+ def initialize(config:)
6
+ @config = config
7
+ end
8
+
9
+ # Component accessors
10
+ def header = @config[:header]
11
+ def table = @config[:table]
12
+ def alerts = @config[:alerts]
13
+ def statistics = @config[:statistics]
14
+ def pagination = @config[:pagination]
15
+ def tabs = @config[:tabs]
16
+ def search = @config[:search]
17
+ def footer = @config[:footer]
18
+
19
+ # Presence helpers
20
+ def header? = header.present?
21
+ def table? = table.present?
22
+ def alerts? = alerts.present?
23
+ def statistics? = statistics.present?
24
+ def pagination? = pagination.present?
25
+ def tabs? = tabs.present?
26
+ def search? = search.present?
27
+ def footer? = footer.present?
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ <div class="better-page better-page--show">
2
+ <%% if alerts? %>
3
+ <%%= render BetterPage::Ui::AlertsComponent.new(alerts: alerts) %>
4
+ <%% end %>
5
+
6
+ <%% if header? %>
7
+ <%%= render BetterPage::Ui::HeaderComponent.new(**header) %>
8
+ <%% end %>
9
+
10
+ <%% if statistics? %>
11
+ <%%= render BetterPage::Ui::StatisticsComponent.new(stats: statistics) %>
12
+ <%% end %>
13
+
14
+ <%% if overview? %>
15
+ <%%= render BetterPage::Ui::OverviewComponent.new(**overview) %>
16
+ <%% end %>
17
+
18
+ <%% if content_sections? %>
19
+ <div class="space-y-6">
20
+ <%% content_sections.each do |section| %>
21
+ <%%= render BetterPage::Ui::ContentSectionComponent.new(**section) %>
22
+ <%% end %>
23
+ </div>
24
+ <%% end %>
25
+
26
+ <%% if footer? %>
27
+ <%%= render BetterPage::Ui::FooterComponent.new(**footer) %>
28
+ <%% end %>
29
+ </div>
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterPage
4
+ class ShowViewComponent < ApplicationViewComponent
5
+ def initialize(config:)
6
+ @config = config
7
+ end
8
+
9
+ # Component accessors
10
+ def header = @config[:header]
11
+ def alerts = @config[:alerts]
12
+ def statistics = @config[:statistics]
13
+ def overview = @config[:overview]
14
+ def content_sections = @config[:content_sections]
15
+ def footer = @config[:footer]
16
+
17
+ # Presence helpers
18
+ def header? = header.present?
19
+ def alerts? = alerts.present?
20
+ def statistics? = statistics.present?
21
+ def overview? = overview.present?
22
+ def content_sections? = content_sections.present?
23
+ def footer? = footer.present?
24
+ end
25
+ end
@@ -0,0 +1,47 @@
1
+ <%% if alerts? %>
2
+ <div class="space-y-4 mb-6">
3
+ <%% alerts.each do |alert| %>
4
+ <div class="<%%= alert_classes(alert[:type]) %>">
5
+ <div class="flex">
6
+ <%% if alert[:icon] %>
7
+ <div class="flex-shrink-0">
8
+ <span class="h-5 w-5 <%%= icon_classes(alert[:type]) %>"><%%= alert[:icon] %></span>
9
+ </div>
10
+ <%% end %>
11
+ <div class="<%%= alert[:icon] ? 'ml-3' : '' %>">
12
+ <%% if alert[:title] %>
13
+ <h3 class="text-sm font-medium"><%%= alert[:title] %></h3>
14
+ <%% end %>
15
+ <%% if alert[:message] %>
16
+ <div class="<%%= alert[:title] ? 'mt-2' : '' %> text-sm">
17
+ <p><%%= alert[:message] %></p>
18
+ </div>
19
+ <%% end %>
20
+ <%% if alert[:actions]&.any? %>
21
+ <div class="mt-4">
22
+ <div class="-mx-2 -my-1.5 flex">
23
+ <%% alert[:actions].each do |action| %>
24
+ <%%= link_to action[:label], action[:path],
25
+ class: "px-2 py-1.5 rounded-md text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2" %>
26
+ <%% end %>
27
+ </div>
28
+ </div>
29
+ <%% end %>
30
+ </div>
31
+ <%% if alert[:dismissible] %>
32
+ <div class="ml-auto pl-3">
33
+ <div class="-mx-1.5 -my-1.5">
34
+ <button type="button" class="inline-flex rounded-md p-1.5 focus:outline-none focus:ring-2 focus:ring-offset-2" data-action="click->alert#dismiss">
35
+ <span class="sr-only">Dismiss</span>
36
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
37
+ <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
38
+ </svg>
39
+ </button>
40
+ </div>
41
+ </div>
42
+ <%% end %>
43
+ </div>
44
+ </div>
45
+ <%% end %>
46
+ </div>
47
+ <%% end %>
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterPage
4
+ module Ui
5
+ class AlertsComponent < BetterPage::ApplicationViewComponent
6
+ def initialize(alerts:)
7
+ @alerts = Array(alerts)
8
+ end
9
+
10
+ attr_reader :alerts
11
+
12
+ def alerts? = alerts.any?
13
+
14
+ def alert_classes(type)
15
+ base = "rounded-md p-4 mb-4"
16
+
17
+ case type&.to_sym
18
+ when :success
19
+ "#{base} bg-green-50 text-green-800"
20
+ when :error, :danger
21
+ "#{base} bg-red-50 text-red-800"
22
+ when :warning
23
+ "#{base} bg-yellow-50 text-yellow-800"
24
+ when :info
25
+ "#{base} bg-blue-50 text-blue-800"
26
+ else
27
+ "#{base} bg-gray-50 text-gray-800"
28
+ end
29
+ end
30
+
31
+ def icon_classes(type)
32
+ case type&.to_sym
33
+ when :success
34
+ "text-green-400"
35
+ when :error, :danger
36
+ "text-red-400"
37
+ when :warning
38
+ "text-yellow-400"
39
+ when :info
40
+ "text-blue-400"
41
+ else
42
+ "text-gray-400"
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,42 @@
1
+ <div class="bg-white shadow overflow-hidden rounded-lg mb-6">
2
+ <%% if title? || actions? %>
3
+ <div class="px-4 py-5 sm:px-6 border-b border-gray-200">
4
+ <div class="flex items-center justify-between">
5
+ <div>
6
+ <%% if title? %>
7
+ <h3 class="text-lg font-medium leading-6 text-gray-900"><%%= title %></h3>
8
+ <%% end %>
9
+ <%% if description? %>
10
+ <p class="mt-1 max-w-2xl text-sm text-gray-500"><%%= description %></p>
11
+ <%% end %>
12
+ </div>
13
+ <%% if actions? %>
14
+ <div class="flex space-x-2">
15
+ <%% actions.each do |action| %>
16
+ <%%= link_to action[:path],
17
+ method: action[:method],
18
+ data: action[:confirm] ? { turbo_confirm: action[:confirm] } : {},
19
+ class: action_classes(action[:style]) do %>
20
+ <%%= action[:label] %>
21
+ <%% end %>
22
+ <%% end %>
23
+ </div>
24
+ <%% end %>
25
+ </div>
26
+ </div>
27
+ <%% end %>
28
+
29
+ <%% if content? %>
30
+ <div class="px-4 py-5 sm:p-6">
31
+ <%% if content.is_a?(String) %>
32
+ <div class="prose max-w-none">
33
+ <%%= content.html_safe %>
34
+ </div>
35
+ <%% elsif content.is_a?(Hash) && content[:type] == :table %>
36
+ <%%= render BetterPage::Ui::TableComponent.new(**content.except(:type)) %>
37
+ <%% elsif content.is_a?(Hash) && content[:type] == :overview %>
38
+ <%%= render BetterPage::Ui::OverviewComponent.new(**content.except(:type)) %>
39
+ <%% end %>
40
+ </div>
41
+ <%% end %>
42
+ </div>
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterPage
4
+ module Ui
5
+ class ContentSectionComponent < BetterPage::ApplicationViewComponent
6
+ def initialize(title: nil, description: nil, content: nil, actions: [])
7
+ @title = title
8
+ @description = description
9
+ @content = content
10
+ @actions = actions
11
+ end
12
+
13
+ attr_reader :title, :description, :content, :actions
14
+
15
+ def title? = title.present?
16
+ def description? = description.present?
17
+ def content? = content.present?
18
+ def actions? = actions.any?
19
+
20
+ def action_classes(style)
21
+ base = "inline-flex items-center px-3 py-1.5 border text-xs font-medium rounded focus:outline-none focus:ring-2 focus:ring-offset-2"
22
+
23
+ case style&.to_sym
24
+ when :primary
25
+ "#{base} border-transparent text-white bg-blue-600 hover:bg-blue-700 focus:ring-blue-500"
26
+ when :danger
27
+ "#{base} border-transparent text-white bg-red-600 hover:bg-red-700 focus:ring-red-500"
28
+ else
29
+ "#{base} border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:ring-blue-500"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,73 @@
1
+ <div id="<%%= id %>"
2
+ data-controller="drawer"
3
+ data-drawer-direction-value="<%%= direction %>"
4
+ data-drawer-confirm-close-value="<%%= confirm_close %>">
5
+
6
+ <%%# Trigger slot - bottone per aprire il drawer %>
7
+ <%% if trigger? %>
8
+ <%%= trigger %>
9
+ <%% end %>
10
+
11
+ <%%# Container del drawer (nascosto di default) %>
12
+ <div data-drawer-target="container"
13
+ class="hidden relative z-50"
14
+ aria-modal="true"
15
+ role="dialog">
16
+
17
+ <%%# Backdrop %>
18
+ <div data-drawer-target="backdrop"
19
+ data-action="click->drawer#backdropClick"
20
+ class="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity duration-300 ease-in-out opacity-0">
21
+ </div>
22
+
23
+ <%%# Panel container - pointer-events-none permette ai click di passare al backdrop %>
24
+ <div class="fixed inset-0 overflow-hidden pointer-events-none">
25
+ <div class="absolute inset-0 overflow-hidden pointer-events-none">
26
+ <div class="<%%= panel_position_class %> <%%= panel_size_class %>">
27
+ <div data-drawer-target="panel"
28
+ class="<%%= panel_classes %> flex flex-col transform transition-transform duration-300 ease-in-out <%%= initial_transform_class %>">
29
+
30
+ <%%# Header %>
31
+ <%% if show_header? %>
32
+ <div class="flex items-center justify-between px-4 py-3 border-b border-gray-200 flex-shrink-0">
33
+ <div class="flex items-center gap-4">
34
+ <%% if title? %>
35
+ <h2 class="text-lg font-medium text-gray-900"><%%= title %></h2>
36
+ <%% end %>
37
+ <%% if header_actions? %>
38
+ <div class="flex items-center gap-2">
39
+ <%%= actions %>
40
+ </div>
41
+ <%% end %>
42
+ </div>
43
+ <%% if closable? %>
44
+ <button type="button"
45
+ data-action="click->drawer#requestClose"
46
+ class="rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500">
47
+ <span class="sr-only">Close</span>
48
+ <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
49
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
50
+ </svg>
51
+ </button>
52
+ <%% end %>
53
+ </div>
54
+ <%% end %>
55
+
56
+ <%%# Content %>
57
+ <div class="flex-1 overflow-y-auto p-4">
58
+ <%%= content %>
59
+ </div>
60
+
61
+ <%%# Footer %>
62
+ <%% if footer_actions? %>
63
+ <div class="flex items-center justify-end gap-2 px-4 py-3 border-t border-gray-200 flex-shrink-0 bg-gray-50">
64
+ <%%= actions %>
65
+ </div>
66
+ <%% end %>
67
+
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </div>
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterPage
4
+ module Ui
5
+ class DrawerComponent < BetterPage::ApplicationViewComponent
6
+ renders_one :trigger
7
+ renders_one :actions
8
+
9
+ def initialize(id:, size: :normal, direction: :right, title: nil,
10
+ closable: true, actions_position: :header, confirm_close: false)
11
+ @id = id
12
+ @size = size.to_sym
13
+ @direction = direction.to_sym
14
+ @title = title
15
+ @closable = closable
16
+ @actions_position = actions_position&.to_sym
17
+ @confirm_close = confirm_close
18
+ end
19
+
20
+ attr_reader :id, :size, :direction, :title, :closable, :actions_position, :confirm_close
21
+
22
+ def closable? = closable
23
+ def title? = title.present?
24
+
25
+ def header_actions?
26
+ actions_position == :header && actions?
27
+ end
28
+
29
+ def footer_actions?
30
+ actions_position == :footer && actions?
31
+ end
32
+
33
+ def show_header?
34
+ title? || closable? || header_actions?
35
+ end
36
+
37
+ def panel_position_class
38
+ case direction
39
+ when :right then "pointer-events-none fixed inset-y-0 right-0 pl-10 max-w-full flex"
40
+ when :left then "pointer-events-none fixed inset-y-0 left-0 pr-10 max-w-full flex"
41
+ when :top then "pointer-events-none fixed inset-x-0 top-0 pb-10 max-h-full flex"
42
+ when :bottom then "pointer-events-none fixed inset-x-0 bottom-0 pt-10 max-h-full flex"
43
+ end
44
+ end
45
+
46
+ def panel_size_class
47
+ horizontal = [:left, :right].include?(direction)
48
+ case size
49
+ when :large
50
+ horizontal ? "max-w-[75vw] w-screen" : "max-h-[80vh]"
51
+ else # normal
52
+ horizontal ? "max-w-md w-screen" : "max-h-[50vh]"
53
+ end
54
+ end
55
+
56
+ def panel_classes
57
+ base = "pointer-events-auto bg-white shadow flex flex-col transform transition-transform duration-300 ease-in-out"
58
+ case direction
59
+ when :right
60
+ "#{base} rounded-t-xl md:rounded-l-xl md:rounded-tr-none w-full h-full"
61
+ when :left
62
+ "#{base} rounded-t-xl md:rounded-r-xl md:rounded-tl-none w-full h-full"
63
+ when :top, :bottom
64
+ "#{base} rounded-xl w-full"
65
+ end
66
+ end
67
+
68
+ def initial_transform_class
69
+ case direction
70
+ when :right then "translate-x-full"
71
+ when :left then "-translate-x-full"
72
+ when :top then "-translate-y-full"
73
+ when :bottom then "translate-y-full"
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,23 @@
1
+ <%% if errors? %>
2
+ <div class="rounded-md bg-red-50 p-4 mb-6">
3
+ <div class="flex">
4
+ <div class="flex-shrink-0">
5
+ <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
6
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
7
+ </svg>
8
+ </div>
9
+ <div class="ml-3">
10
+ <h3 class="text-sm font-medium text-red-800">
11
+ <%%= pluralize(error_count, "error") %> prohibited this from being saved:
12
+ </h3>
13
+ <div class="mt-2 text-sm text-red-700">
14
+ <ul role="list" class="list-disc space-y-1 pl-5">
15
+ <%% error_messages.each do |message| %>
16
+ <li><%%= message %></li>
17
+ <%% end %>
18
+ </ul>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </div>
23
+ <%% end %>
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterPage
4
+ module Ui
5
+ class ErrorsComponent < BetterPage::ApplicationViewComponent
6
+ def initialize(resource:)
7
+ @resource = resource
8
+ end
9
+
10
+ attr_reader :resource
11
+
12
+ def errors? = resource.respond_to?(:errors) && resource.errors.any?
13
+ def errors = resource.errors
14
+ def error_count = errors.count
15
+ def error_messages = errors.full_messages
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,65 @@
1
+ <%% unless hidden? %>
2
+ <div class="<%%= span_classes %>">
3
+ <%% if checkbox? %>
4
+ <div class="relative flex items-start">
5
+ <div class="flex h-5 items-center">
6
+ <%%= check_box_tag name, "1", value, class: checkbox_classes, disabled: disabled?, id: name %>
7
+ </div>
8
+ <div class="ml-3 text-sm">
9
+ <%%= label_tag name, label, class: "font-medium text-gray-700" %>
10
+ <%% if hint? %>
11
+ <p class="text-gray-500"><%%= hint %></p>
12
+ <%% end %>
13
+ </div>
14
+ </div>
15
+
16
+ <%% elsif radio? %>
17
+ <fieldset>
18
+ <legend class="text-sm font-medium text-gray-900"><%%= label %></legend>
19
+ <%% if hint? %>
20
+ <p class="text-sm text-gray-500 mb-4"><%%= hint %></p>
21
+ <%% end %>
22
+ <div class="mt-4 space-y-4">
23
+ <%% options.each do |option| %>
24
+ <div class="flex items-center">
25
+ <%%= radio_button_tag name, option[:value], value == option[:value], class: radio_classes, disabled: disabled?, id: "#{name}_#{option[:value]}" %>
26
+ <%%= label_tag "#{name}_#{option[:value]}", option[:label], class: "ml-3 block text-sm font-medium text-gray-700" %>
27
+ </div>
28
+ <%% end %>
29
+ </div>
30
+ </fieldset>
31
+
32
+ <%% else %>
33
+ <%%= label_tag name, class: "block text-sm font-medium text-gray-700" do %>
34
+ <%%= label %>
35
+ <%% if required? %>
36
+ <span class="text-red-500">*</span>
37
+ <%% end %>
38
+ <%% end %>
39
+
40
+ <div class="mt-1">
41
+ <%% if textarea? %>
42
+ <%%= text_area_tag name, value, **input_attributes.merge(rows: 4) %>
43
+
44
+ <%% elsif select? %>
45
+ <%%= select_tag name, options_for_select(options.map { |o| [o[:label], o[:value]] }, value), **input_attributes.except(:placeholder).merge(include_blank: placeholder || true) %>
46
+
47
+ <%% elsif date_field? %>
48
+ <%%= date_field_tag name, value, **input_attributes %>
49
+
50
+ <%% elsif file_field? %>
51
+ <%%= file_field_tag name, **input_attributes.except(:placeholder) %>
52
+
53
+ <%% else %>
54
+ <%%= send("#{type}_field_tag", name, value, **input_attributes) %>
55
+ <%% end %>
56
+ </div>
57
+
58
+ <%% if hint? %>
59
+ <p class="mt-2 text-sm text-gray-500"><%%= hint %></p>
60
+ <%% end %>
61
+ <%% end %>
62
+ </div>
63
+ <%% else %>
64
+ <%%= hidden_field_tag name, value %>
65
+ <%% end %>
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterPage
4
+ module Ui
5
+ class FieldComponent < BetterPage::ApplicationViewComponent
6
+ FIELD_TYPES = %i[text email password number tel url textarea select checkbox radio date datetime file hidden].freeze
7
+
8
+ def initialize(name:, type: :text, label: nil, placeholder: nil, hint: nil, required: false,
9
+ disabled: false, readonly: false, options: [], value: nil, span: nil,
10
+ min: nil, max: nil, step: nil, pattern: nil, autocomplete: nil, data: {})
11
+ @name = name
12
+ @type = type.to_sym
13
+ @label = label || name.to_s.humanize
14
+ @placeholder = placeholder
15
+ @hint = hint
16
+ @required = required
17
+ @disabled = disabled
18
+ @readonly = readonly
19
+ @options = options
20
+ @value = value
21
+ @span = span
22
+ @min = min
23
+ @max = max
24
+ @step = step
25
+ @pattern = pattern
26
+ @autocomplete = autocomplete
27
+ @data = data
28
+ end
29
+
30
+ attr_reader :name, :type, :label, :placeholder, :hint, :required, :disabled, :readonly,
31
+ :options, :value, :span, :min, :max, :step, :pattern, :autocomplete, :data
32
+
33
+ def hint? = hint.present?
34
+ def required? = required
35
+ def disabled? = disabled
36
+ def readonly? = readonly
37
+ def options? = options.any?
38
+
39
+ def text_field? = %i[text email password number tel url].include?(type)
40
+ def textarea? = type == :textarea
41
+ def select? = type == :select
42
+ def checkbox? = type == :checkbox
43
+ def radio? = type == :radio
44
+ def date_field? = %i[date datetime].include?(type)
45
+ def file_field? = type == :file
46
+ def hidden? = type == :hidden
47
+
48
+ def span_classes
49
+ case span
50
+ when 1 then "sm:col-span-1"
51
+ when 2 then "sm:col-span-2"
52
+ when 3 then "sm:col-span-3"
53
+ when 4 then "sm:col-span-4"
54
+ when 5 then "sm:col-span-5"
55
+ when 6 then "sm:col-span-6"
56
+ else "sm:col-span-3"
57
+ end
58
+ end
59
+
60
+ def input_classes
61
+ base = "block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
62
+ base += " bg-gray-50 cursor-not-allowed" if disabled? || readonly?
63
+ base
64
+ end
65
+
66
+ def checkbox_classes
67
+ "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
68
+ end
69
+
70
+ def radio_classes
71
+ "h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
72
+ end
73
+
74
+ def input_attributes
75
+ attrs = {
76
+ class: input_classes,
77
+ placeholder: placeholder,
78
+ required: required?,
79
+ disabled: disabled?,
80
+ readonly: readonly?,
81
+ autocomplete: autocomplete
82
+ }
83
+ attrs[:min] = min if min
84
+ attrs[:max] = max if max
85
+ attrs[:step] = step if step
86
+ attrs[:pattern] = pattern if pattern
87
+ attrs.merge(data)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,33 @@
1
+ <%% if actions? || info? %>
2
+ <footer class="bg-white border-t border-gray-200 px-4 py-4 sm:px-6">
3
+ <div class="flex items-center justify-between">
4
+ <%% if info? %>
5
+ <div class="text-sm text-gray-500">
6
+ <%%= info %>
7
+ </div>
8
+ <%% else %>
9
+ <div></div>
10
+ <%% end %>
11
+
12
+ <%% if actions? %>
13
+ <div class="flex space-x-3">
14
+ <%% actions.each do |action| %>
15
+ <%% if action[:type] == :submit %>
16
+ <%%= submit_tag action[:label], class: action_classes(action[:style]), data: action[:data] || {} %>
17
+ <%% else %>
18
+ <%%= link_to action[:path],
19
+ method: action[:method],
20
+ data: action[:confirm] ? { turbo_confirm: action[:confirm] } : {},
21
+ class: action_classes(action[:style]) do %>
22
+ <%% if action[:icon] %>
23
+ <span class="mr-2"><%%= action[:icon] %></span>
24
+ <%% end %>
25
+ <%%= action[:label] %>
26
+ <%% end %>
27
+ <%% end %>
28
+ <%% end %>
29
+ </div>
30
+ <%% end %>
31
+ </div>
32
+ </footer>
33
+ <%% end %>