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,112 @@
1
+ <div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 rounded-lg"
2
+ <%% if selectable? %>data-controller="table"<%% end %>>
3
+ <%% if items? %>
4
+ <table class="min-w-full divide-y divide-gray-300">
5
+ <thead class="bg-gray-50">
6
+ <tr>
7
+ <%% if selectable? %>
8
+ <th scope="col" class="relative px-6 py-3">
9
+ <input type="checkbox"
10
+ class="absolute left-4 top-1/2 -mt-2 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
11
+ data-table-target="selectAll"
12
+ data-action="change->table#selectAll">
13
+ </th>
14
+ <%% end %>
15
+ <%% columns.each do |column| %>
16
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider <%%= column[:class] %>">
17
+ <%%= column[:label] %>
18
+ </th>
19
+ <%% end %>
20
+ <%% if row_actions? %>
21
+ <th scope="col" class="relative px-6 py-3">
22
+ <span class="sr-only">Actions</span>
23
+ </th>
24
+ <%% end %>
25
+ </tr>
26
+ </thead>
27
+ <tbody class="divide-y divide-gray-200 bg-white">
28
+ <%% items.each do |item| %>
29
+ <tr class="<%%= row_link? ? 'hover:bg-gray-50 cursor-pointer' : 'hover:bg-gray-50' %>"
30
+ <%% if row_link? %>
31
+ data-turbo-frame="_top"
32
+ onclick="Turbo.visit('<%%= link_for(item) %>')"
33
+ <%% end %>>
34
+ <%% if selectable? %>
35
+ <td class="relative px-6 py-4" onclick="event.stopPropagation()">
36
+ <input type="checkbox"
37
+ class="absolute left-4 top-1/2 -mt-2 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
38
+ value="<%%= item_id(item) %>"
39
+ data-table-target="row"
40
+ data-action="change->table#rowChanged">
41
+ </td>
42
+ <%% end %>
43
+ <%% columns.each do |column| %>
44
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 <%%= column[:cell_class] %>">
45
+ <%%= format_value(item, column) %>
46
+ </td>
47
+ <%% end %>
48
+ <%% if row_actions? %>
49
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium" onclick="event.stopPropagation()">
50
+ <%% if dropdown_actions? %>
51
+ <div data-controller="dropdown" class="relative inline-block text-left">
52
+ <button type="button"
53
+ data-action="click->dropdown#toggle"
54
+ class="p-2 rounded-full hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
55
+ <svg class="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
56
+ <path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z"/>
57
+ </svg>
58
+ <span class="sr-only">Open options</span>
59
+ </button>
60
+
61
+ <div data-dropdown-target="menu"
62
+ class="hidden w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
63
+ <div class="py-1" role="menu">
64
+ <%% actions_for(item).each do |action| %>
65
+ <%%= link_to action[:path],
66
+ data: { turbo_method: action[:method], turbo_confirm: action[:confirm] }.compact,
67
+ class: action_dropdown_class(action[:style]),
68
+ role: "menuitem" do %>
69
+ <%%= action[:label] %>
70
+ <%% end %>
71
+ <%% end %>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ <%% else %>
76
+ <div class="flex justify-end space-x-2">
77
+ <%% actions_for(item).each do |action| %>
78
+ <%%= link_to action[:path],
79
+ data: { turbo_method: action[:method], turbo_confirm: action[:confirm] }.compact,
80
+ class: action_link_class(action[:style]) do %>
81
+ <%%= action[:label] %>
82
+ <%% end %>
83
+ <%% end %>
84
+ </div>
85
+ <%% end %>
86
+ </td>
87
+ <%% end %>
88
+ </tr>
89
+ <%% end %>
90
+ </tbody>
91
+ </table>
92
+ <%% elsif empty_state? %>
93
+ <div class="text-center py-12">
94
+ <%% if empty_state[:icon] %>
95
+ <div class="mx-auto h-12 w-12 text-gray-400">
96
+ <%%= empty_state[:icon] %>
97
+ </div>
98
+ <%% end %>
99
+ <h3 class="mt-2 text-sm font-medium text-gray-900"><%%= empty_state[:title] %></h3>
100
+ <%% if empty_state[:description] %>
101
+ <p class="mt-1 text-sm text-gray-500"><%%= empty_state[:description] %></p>
102
+ <%% end %>
103
+ <%% if empty_state[:action] %>
104
+ <%%= link_to empty_state[:action][:path],
105
+ data: { turbo_frame: "_top" },
106
+ class: "mt-4 inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" do %>
107
+ <%%= empty_state[:action][:label] %>
108
+ <%% end %>
109
+ <%% end %>
110
+ </div>
111
+ <%% end %>
112
+ </div>
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterPage
4
+ module Ui
5
+ class TableComponent < BetterPage::ApplicationViewComponent
6
+ def initialize(items:, columns:, row_actions: nil, empty_state: nil,
7
+ selectable: false, row_link: nil, actions_display: :inline)
8
+ @items = items
9
+ @columns = columns
10
+ @row_actions = row_actions
11
+ @empty_state = empty_state
12
+ @selectable = selectable
13
+ @row_link = row_link
14
+ @actions_display = actions_display&.to_sym || :inline
15
+ end
16
+
17
+ attr_reader :items, :columns, :row_actions, :empty_state, :selectable,
18
+ :row_link, :actions_display
19
+
20
+ def items? = items.any?
21
+ def row_actions? = row_actions.present?
22
+ def empty_state? = empty_state.present?
23
+ def selectable? = selectable
24
+ def row_link? = row_link.present?
25
+ def dropdown_actions? = actions_display == :dropdown
26
+ def inline_actions? = actions_display == :inline
27
+
28
+ def link_for(item)
29
+ return nil unless row_link
30
+ row_link.respond_to?(:call) ? row_link.call(item) : row_link
31
+ end
32
+
33
+ def format_value(item, column)
34
+ value = item.respond_to?(column[:key]) ? item.send(column[:key]) : item[column[:key]]
35
+
36
+ case column[:format]&.to_sym
37
+ when :currency
38
+ number_to_currency(value)
39
+ when :date
40
+ value&.strftime("%B %d, %Y")
41
+ when :datetime
42
+ value&.strftime("%B %d, %Y %H:%M")
43
+ when :boolean
44
+ value ? "Yes" : "No"
45
+ when :percentage
46
+ "#{value}%"
47
+ else
48
+ value
49
+ end
50
+ end
51
+
52
+ def actions_for(item)
53
+ return [] unless row_actions
54
+
55
+ if row_actions.respond_to?(:call)
56
+ row_actions.call(item)
57
+ else
58
+ row_actions
59
+ end
60
+ end
61
+
62
+ def action_link_class(style)
63
+ case style&.to_sym
64
+ when :danger
65
+ "text-red-600 hover:text-red-900"
66
+ when :primary
67
+ "text-blue-600 hover:text-blue-900"
68
+ else
69
+ "text-gray-600 hover:text-gray-900"
70
+ end
71
+ end
72
+
73
+ def action_dropdown_class(style)
74
+ base = "block w-full text-left px-4 py-2 text-sm hover:bg-gray-100"
75
+ color = case style&.to_sym
76
+ when :danger then "text-red-600"
77
+ when :primary then "text-blue-600"
78
+ else "text-gray-700"
79
+ end
80
+ "#{base} #{color}"
81
+ end
82
+
83
+ def item_id(item)
84
+ item.respond_to?(:id) ? item.id : item[:id]
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,52 @@
1
+ <div id="<%%= id %>"
2
+ class="bg-white shadow rounded-xl p-4"
3
+ data-controller="tabs"
4
+ data-tabs-default-value="<%%= default_tab_id %>"
5
+ data-tabs-active-class="<%%= active_classes %>"
6
+ data-tabs-inactive-class="<%%= inactive_classes %>">
7
+
8
+ <%%# Tab navigation %>
9
+ <nav class="<%%= nav_classes %>" role="tablist" aria-label="Tabs">
10
+ <%% tabs.each_with_index do |tab, index| %>
11
+ <%% if tab.link? %>
12
+ <%%= link_to tab.href,
13
+ class: "#{tab_base_classes} #{tab.active? ? active_classes : inactive_classes}" do %>
14
+ <%% if tab.icon? %><%%= tab.icon %><%% end %>
15
+ <%%= tab.label %>
16
+ <%% end %>
17
+ <%% else %>
18
+ <%% content_index = content_tabs.find_index { |t| t.id == tab.id } || 0 %>
19
+ <button type="button"
20
+ role="tab"
21
+ id="<%%= id %>-tab-<%%= tab.id %>"
22
+ data-tabs-target="tab"
23
+ data-action="click->tabs#select keydown->tabs#keydown"
24
+ data-tab-id="<%%= tab.id %>"
25
+ aria-controls="<%%= id %>-panel-<%%= tab.id %>"
26
+ aria-selected="<%%= content_index == default_index %>"
27
+ tabindex="<%%= content_index == default_index ? '0' : '-1' %>"
28
+ class="<%%= tab_base_classes %> <%%= content_index == default_index ? active_classes : inactive_classes %>">
29
+ <%% if tab.icon? %><%%= tab.icon %><%% end %>
30
+ <%%= tab.label %>
31
+ </button>
32
+ <%% end %>
33
+ <%% end %>
34
+ </nav>
35
+
36
+ <%%# Tab panels (only for content tabs) %>
37
+ <%% if content_tabs.any? %>
38
+ <div class="mt-4">
39
+ <%% content_tabs.each_with_index do |tab, index| %>
40
+ <div id="<%%= id %>-panel-<%%= tab.id %>"
41
+ data-tabs-target="panel"
42
+ data-tab-id="<%%= tab.id %>"
43
+ role="tabpanel"
44
+ aria-labelledby="<%%= id %>-tab-<%%= tab.id %>"
45
+ tabindex="0"
46
+ class="<%%= index == default_index ? '' : 'hidden' %>">
47
+ <%%= tab %>
48
+ </div>
49
+ <%% end %>
50
+ </div>
51
+ <%% end %>
52
+ </div>
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterPage
4
+ module Ui
5
+ class TabsComponent < BetterPage::ApplicationViewComponent
6
+ renders_many :tabs, "TabItem"
7
+
8
+ STYLE_CONFIG = {
9
+ nav: "border-b border-gray-200",
10
+ tab_base: "-mb-px inline-flex items-center gap-2 px-4 py-2 text-sm font-medium transition-colors",
11
+ active: "border-b-2 border-blue-600 text-blue-600",
12
+ inactive: "border-b-2 border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700"
13
+ }.freeze
14
+
15
+ def initialize(id:, default_tab: nil)
16
+ @id = id
17
+ @default_tab = default_tab
18
+ end
19
+
20
+ attr_reader :id, :default_tab
21
+
22
+ def default_tab_id
23
+ content_tabs = tabs.reject(&:link?)
24
+ @default_tab || content_tabs.first&.id
25
+ end
26
+
27
+ def default_index
28
+ content_tabs = tabs.reject(&:link?)
29
+ return 0 if @default_tab.nil? || content_tabs.empty?
30
+
31
+ content_tabs.find_index { |tab| tab.id == @default_tab } || 0
32
+ end
33
+
34
+ def content_tabs
35
+ tabs.reject(&:link?)
36
+ end
37
+
38
+ def nav_classes
39
+ "flex #{STYLE_CONFIG[:nav]}"
40
+ end
41
+
42
+ def tab_base_classes
43
+ STYLE_CONFIG[:tab_base]
44
+ end
45
+
46
+ def active_classes
47
+ STYLE_CONFIG[:active]
48
+ end
49
+
50
+ def inactive_classes
51
+ STYLE_CONFIG[:inactive]
52
+ end
53
+
54
+ # Nested TabItem component
55
+ class TabItem < BetterPage::ApplicationViewComponent
56
+ def initialize(id:, label:, icon: nil, href: nil, active: false)
57
+ @id = id
58
+ @label = label
59
+ @icon = icon
60
+ @href = href
61
+ @active = active
62
+ end
63
+
64
+ attr_reader :id, :label, :icon, :href, :active
65
+
66
+ def icon? = icon.present?
67
+ def link? = href.present?
68
+ def active? = active
69
+
70
+ def call
71
+ content
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,72 @@
1
+ <div class="<%%= size_classes %> bg-white shadow rounded-lg overflow-hidden">
2
+ <%% if title? %>
3
+ <div class="px-4 py-5 sm:px-6 border-b border-gray-200">
4
+ <h3 class="text-lg font-medium leading-6 text-gray-900"><%%= title %></h3>
5
+ </div>
6
+ <%% end %>
7
+
8
+ <div class="px-4 py-5 sm:p-6">
9
+ <%% if stats? && options[:stats] %>
10
+ <dl class="grid grid-cols-1 gap-5 sm:grid-cols-2">
11
+ <%% options[:stats].each do |stat| %>
12
+ <div class="overflow-hidden">
13
+ <dt class="truncate text-sm font-medium text-gray-500"><%%= stat[:label] %></dt>
14
+ <dd class="mt-1 text-3xl font-semibold tracking-tight text-gray-900"><%%= stat[:value] %></dd>
15
+ </div>
16
+ <%% end %>
17
+ </dl>
18
+
19
+ <%% elsif list? && options[:items] %>
20
+ <ul role="list" class="divide-y divide-gray-200">
21
+ <%% options[:items].each do |item| %>
22
+ <li class="py-4">
23
+ <div class="flex items-center space-x-4">
24
+ <%% if item[:icon] %>
25
+ <div class="flex-shrink-0">
26
+ <span class="h-8 w-8 rounded-full bg-gray-100 flex items-center justify-center">
27
+ <%%= item[:icon] %>
28
+ </span>
29
+ </div>
30
+ <%% end %>
31
+ <div class="min-w-0 flex-1">
32
+ <p class="truncate text-sm font-medium text-gray-900"><%%= item[:title] %></p>
33
+ <%% if item[:subtitle] %>
34
+ <p class="truncate text-sm text-gray-500"><%%= item[:subtitle] %></p>
35
+ <%% end %>
36
+ </div>
37
+ <%% if item[:action] %>
38
+ <%%= link_to item[:action][:label], item[:action][:path], class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
39
+ <%% end %>
40
+ </div>
41
+ </li>
42
+ <%% end %>
43
+ </ul>
44
+
45
+ <%% elsif table? && options[:table] %>
46
+ <%%= render BetterPage::Ui::TableComponent.new(**options[:table]) %>
47
+
48
+ <%% elsif text? && options[:content] %>
49
+ <div class="prose max-w-none">
50
+ <%%= options[:content].html_safe %>
51
+ </div>
52
+
53
+ <%% elsif chart? && options[:chart] %>
54
+ <div class="h-64" data-controller="chart" data-chart-type-value="<%%= options[:chart][:type] %>" data-chart-data-value="<%%= options[:chart][:data].to_json %>">
55
+ <canvas data-chart-target="canvas"></canvas>
56
+ </div>
57
+
58
+ <%% elsif custom? && options[:partial] %>
59
+ <%%= render partial: options[:partial], locals: options[:locals] || {} %>
60
+ <%% end %>
61
+ </div>
62
+
63
+ <%% if options[:footer] %>
64
+ <div class="bg-gray-50 px-4 py-4 sm:px-6">
65
+ <%% if options[:footer][:link] %>
66
+ <%%= link_to options[:footer][:link][:label], options[:footer][:link][:path], class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
67
+ <%% elsif options[:footer][:text] %>
68
+ <p class="text-sm text-gray-500"><%%= options[:footer][:text] %></p>
69
+ <%% end %>
70
+ </div>
71
+ <%% end %>
72
+ </div>
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterPage
4
+ module Ui
5
+ class WidgetComponent < BetterPage::ApplicationViewComponent
6
+ def initialize(type:, title: nil, **options)
7
+ @type = type.to_sym
8
+ @title = title
9
+ @options = options
10
+ end
11
+
12
+ attr_reader :type, :title, :options
13
+
14
+ def title? = title.present?
15
+
16
+ def chart? = type == :chart
17
+ def list? = type == :list
18
+ def stats? = type == :stats
19
+ def table? = type == :table
20
+ def text? = type == :text
21
+ def custom? = type == :custom
22
+
23
+ def size_classes
24
+ case options[:size]&.to_sym
25
+ when :small then "col-span-1"
26
+ when :medium then "col-span-2"
27
+ when :large then "col-span-3"
28
+ when :full then "col-span-full"
29
+ else "col-span-1"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :better_page do
4
+ desc "Analyze all pages for compliance with architecture conventions"
5
+ task analyze: :environment do
6
+ require "better_page/compliance/analyzer"
7
+
8
+ puts "PAGE COMPLIANCE ANALYSIS"
9
+ puts "========================"
10
+ puts
11
+
12
+ analyzer = BetterPage::Compliance::Analyzer.new
13
+ analyzer.analyze_all
14
+
15
+ puts
16
+ puts "Analysis completed!"
17
+ puts "Use VERBOSE=true for detailed output: bin/rails better_page:analyze VERBOSE=true"
18
+ end
19
+
20
+ desc "Analyze a specific page file for compliance"
21
+ task :analyze_page, [ :file_path ] => :environment do |_t, args|
22
+ require "better_page/compliance/analyzer"
23
+
24
+ if args[:file_path].blank?
25
+ puts "Please provide a page file path:"
26
+ puts " bin/rails better_page:analyze_page[app/pages/admin/users/index_page.rb]"
27
+ exit 1
28
+ end
29
+
30
+ unless File.exist?(args[:file_path])
31
+ puts "File not found: #{args[:file_path]}"
32
+ exit 1
33
+ end
34
+
35
+ puts "Analyzing: #{args[:file_path]}"
36
+ puts "========================="
37
+ puts
38
+
39
+ analyzer = BetterPage::Compliance::Analyzer.new
40
+ result = analyzer.analyze_page(args[:file_path])
41
+
42
+ puts analyzer.format_single_page_report(result)
43
+ puts
44
+
45
+ # Additional details for single page analysis
46
+ case result[:status]
47
+ when :compliant
48
+ puts "This page follows all architectural patterns!"
49
+ puts
50
+ puts "COMPLIANCE CHECKLIST:"
51
+ puts " [OK] UI configuration only (no business logic)"
52
+ puts " [OK] No database access"
53
+ puts " [OK] Template system integration"
54
+ puts " [OK] Required build_* methods implemented"
55
+ puts " [OK] Plain Hash objects (no OpenStruct)"
56
+ when :warning
57
+ puts "This page has some areas for improvement:"
58
+ puts "SUGGESTED IMPROVEMENTS:"
59
+ result[:warnings].each { |warning| puts " - #{warning}" }
60
+ when :error
61
+ puts "This page has critical compliance issues:"
62
+ puts "REQUIRED FIXES:"
63
+ result[:issues].each { |issue| puts " - #{issue}" }
64
+ if result[:warnings].any?
65
+ puts "ADDITIONAL IMPROVEMENTS:"
66
+ result[:warnings].each { |warning| puts " - #{warning}" }
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :better_page do
3
+ # # Task goes here
4
+ # end