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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +62 -0
- data/MIT-LICENSE +20 -0
- data/README.md +357 -0
- data/Rakefile +3 -0
- data/docs/00-README.md +17 -0
- data/docs/01-getting-started.md +137 -0
- data/docs/02-component-registry.md +192 -0
- data/docs/03-base-pages.md +238 -0
- data/docs/04-schema-validation.md +180 -0
- data/docs/05-turbo-support.md +220 -0
- data/docs/06-compliance-analyzer.md +147 -0
- data/docs/07-configuration.md +157 -0
- data/guide/00-README.md +32 -0
- data/guide/01-quick-start.md +148 -0
- data/guide/02-building-index-page.md +258 -0
- data/guide/03-building-show-page.md +266 -0
- data/guide/04-building-form-page.md +309 -0
- data/guide/05-custom-pages.md +325 -0
- data/guide/06-best-practices.md +311 -0
- data/lib/better_page/base_page.rb +161 -0
- data/lib/better_page/compliance/analyzer.rb +409 -0
- data/lib/better_page/component_registry.rb +393 -0
- data/lib/better_page/config.rb +165 -0
- data/lib/better_page/configuration.rb +153 -0
- data/lib/better_page/custom_base_page.rb +85 -0
- data/lib/better_page/default_components.rb +200 -0
- data/lib/better_page/form_base_page.rb +170 -0
- data/lib/better_page/index_base_page.rb +69 -0
- data/lib/better_page/railtie.rb +34 -0
- data/lib/better_page/show_base_page.rb +120 -0
- data/lib/better_page/validation_error.rb +7 -0
- data/lib/better_page/version.rb +3 -0
- data/lib/better_page.rb +80 -0
- data/lib/generators/better_page/component_generator.rb +131 -0
- data/lib/generators/better_page/install_generator.rb +160 -0
- data/lib/generators/better_page/page_generator.rb +101 -0
- data/lib/generators/better_page/sync_generator.rb +109 -0
- data/lib/generators/better_page/templates/application_page.rb.tt +12 -0
- data/lib/generators/better_page/templates/better_page_initializer.rb.tt +53 -0
- data/lib/generators/better_page/templates/custom_base_page.rb.tt +83 -0
- data/lib/generators/better_page/templates/custom_page.rb.tt +31 -0
- data/lib/generators/better_page/templates/edit_page.rb.tt +46 -0
- data/lib/generators/better_page/templates/form_base_page.rb.tt +126 -0
- data/lib/generators/better_page/templates/index_base_page.rb.tt +65 -0
- data/lib/generators/better_page/templates/index_page.rb.tt +56 -0
- data/lib/generators/better_page/templates/javascript/controllers/app_nav_controller.js +57 -0
- data/lib/generators/better_page/templates/javascript/controllers/drawer_controller.js +99 -0
- data/lib/generators/better_page/templates/javascript/controllers/dropdown_controller.js +60 -0
- data/lib/generators/better_page/templates/javascript/controllers/index.js +36 -0
- data/lib/generators/better_page/templates/javascript/controllers/modal_controller.js +70 -0
- data/lib/generators/better_page/templates/javascript/controllers/sidebar_controller.js +152 -0
- data/lib/generators/better_page/templates/javascript/controllers/table_controller.js +60 -0
- data/lib/generators/better_page/templates/javascript/controllers/tabs_controller.js +89 -0
- data/lib/generators/better_page/templates/new_page.rb.tt +46 -0
- data/lib/generators/better_page/templates/show_base_page.rb.tt +117 -0
- data/lib/generators/better_page/templates/show_page.rb.tt +45 -0
- data/lib/generators/better_page/templates/view_components/application_view_component.rb.tt +7 -0
- data/lib/generators/better_page/templates/view_components/custom_view_component.html.erb.tt +21 -0
- data/lib/generators/better_page/templates/view_components/custom_view_component.rb.tt +21 -0
- data/lib/generators/better_page/templates/view_components/form_view_component.html.erb.tt +25 -0
- data/lib/generators/better_page/templates/view_components/form_view_component.rb.tt +23 -0
- data/lib/generators/better_page/templates/view_components/index_view_component.html.erb.tt +33 -0
- data/lib/generators/better_page/templates/view_components/index_view_component.rb.tt +29 -0
- data/lib/generators/better_page/templates/view_components/show_view_component.html.erb.tt +29 -0
- data/lib/generators/better_page/templates/view_components/show_view_component.rb.tt +25 -0
- data/lib/generators/better_page/templates/view_components/ui/alerts_component.html.erb.tt +47 -0
- data/lib/generators/better_page/templates/view_components/ui/alerts_component.rb.tt +47 -0
- data/lib/generators/better_page/templates/view_components/ui/content_section_component.html.erb.tt +42 -0
- data/lib/generators/better_page/templates/view_components/ui/content_section_component.rb.tt +34 -0
- data/lib/generators/better_page/templates/view_components/ui/drawer_component.html.erb.tt +73 -0
- data/lib/generators/better_page/templates/view_components/ui/drawer_component.rb.tt +78 -0
- data/lib/generators/better_page/templates/view_components/ui/errors_component.html.erb.tt +23 -0
- data/lib/generators/better_page/templates/view_components/ui/errors_component.rb.tt +18 -0
- data/lib/generators/better_page/templates/view_components/ui/field_component.html.erb.tt +65 -0
- data/lib/generators/better_page/templates/view_components/ui/field_component.rb.tt +91 -0
- data/lib/generators/better_page/templates/view_components/ui/footer_component.html.erb.tt +33 -0
- data/lib/generators/better_page/templates/view_components/ui/footer_component.rb.tt +32 -0
- data/lib/generators/better_page/templates/view_components/ui/header_component.html.erb.tt +55 -0
- data/lib/generators/better_page/templates/view_components/ui/header_component.rb.tt +39 -0
- data/lib/generators/better_page/templates/view_components/ui/modal_component.html.erb.tt +70 -0
- data/lib/generators/better_page/templates/view_components/ui/modal_component.rb.tt +54 -0
- data/lib/generators/better_page/templates/view_components/ui/overview_component.html.erb.tt +22 -0
- data/lib/generators/better_page/templates/view_components/ui/overview_component.rb.tt +71 -0
- data/lib/generators/better_page/templates/view_components/ui/pagination_component.html.erb.tt +63 -0
- data/lib/generators/better_page/templates/view_components/ui/pagination_component.rb.tt +69 -0
- data/lib/generators/better_page/templates/view_components/ui/panel_component.html.erb.tt +31 -0
- data/lib/generators/better_page/templates/view_components/ui/panel_component.rb.tt +23 -0
- data/lib/generators/better_page/templates/view_components/ui/statistics_component.html.erb.tt +33 -0
- data/lib/generators/better_page/templates/view_components/ui/statistics_component.rb.tt +51 -0
- data/lib/generators/better_page/templates/view_components/ui/table_component.html.erb.tt +112 -0
- data/lib/generators/better_page/templates/view_components/ui/table_component.rb.tt +88 -0
- data/lib/generators/better_page/templates/view_components/ui/tabs_component.html.erb.tt +52 -0
- data/lib/generators/better_page/templates/view_components/ui/tabs_component.rb.tt +76 -0
- data/lib/generators/better_page/templates/view_components/ui/widget_component.html.erb.tt +72 -0
- data/lib/generators/better_page/templates/view_components/ui/widget_component.rb.tt +34 -0
- data/lib/tasks/better_page.rake +70 -0
- data/lib/tasks/better_page_tasks.rake +4 -0
- 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
|