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,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterPage
|
|
4
|
+
# Base class for custom pages that don't fit index/show/form patterns.
|
|
5
|
+
# Uses page_type to inherit components from global configuration.
|
|
6
|
+
#
|
|
7
|
+
# Available components (from configuration):
|
|
8
|
+
# - content (required): Main custom content section
|
|
9
|
+
# - header, footer, alerts
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# class Admin::Dashboard::CustomPage < CustomBasePage
|
|
13
|
+
# def header
|
|
14
|
+
# { title: "Dashboard", breadcrumbs: [] }
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# def content
|
|
18
|
+
# { widgets: [...], charts: [...] }
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
class CustomBasePage < BasePage
|
|
23
|
+
page_type :custom
|
|
24
|
+
|
|
25
|
+
# Main method that builds the complete custom page configuration
|
|
26
|
+
# @return [Hash] complete custom page configuration with :klass for rendering
|
|
27
|
+
def custom
|
|
28
|
+
build_page
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Note: frame_custom and stream_custom are dynamically generated via method_missing in ComponentRegistry
|
|
32
|
+
# For custom pages with different action names (e.g. #daily), frame_daily/stream_daily work automatically
|
|
33
|
+
# Usage:
|
|
34
|
+
# page.frame_custom(:content) # Single component for Turbo Frame
|
|
35
|
+
# page.stream_custom # All stream components for Turbo Streams
|
|
36
|
+
# page.frame_daily(:chart) # For a page with #daily method
|
|
37
|
+
# page.stream_daily(:chart, :summary) # Multiple components for Turbo Streams
|
|
38
|
+
|
|
39
|
+
# The ViewComponent class used to render this custom page
|
|
40
|
+
# @return [Class] BetterPage::CustomViewComponent
|
|
41
|
+
def view_component_class
|
|
42
|
+
return BetterPage::CustomViewComponent if defined?(BetterPage::CustomViewComponent)
|
|
43
|
+
|
|
44
|
+
raise NotImplementedError, "BetterPage::CustomViewComponent not found. Run: rails g better_page:install"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Components to include in stream updates by default
|
|
48
|
+
# @return [Array<Symbol>]
|
|
49
|
+
def stream_components
|
|
50
|
+
%i[alerts content]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
protected
|
|
54
|
+
|
|
55
|
+
# Helper to build a widget section
|
|
56
|
+
# @param title [String] widget title
|
|
57
|
+
# @param type [Symbol] widget type
|
|
58
|
+
# @param data [Hash, Array] widget data
|
|
59
|
+
# @param options [Hash] additional options
|
|
60
|
+
# @return [Hash] formatted widget
|
|
61
|
+
def widget_format(title:, type:, data:, **options)
|
|
62
|
+
{
|
|
63
|
+
title: title,
|
|
64
|
+
type: type,
|
|
65
|
+
data: data,
|
|
66
|
+
**options
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Helper to build a chart configuration
|
|
71
|
+
# @param title [String] chart title
|
|
72
|
+
# @param type [Symbol] chart type (:line, :bar, :pie, etc.)
|
|
73
|
+
# @param data [Hash] chart data with labels and datasets
|
|
74
|
+
# @param options [Hash] additional chart options
|
|
75
|
+
# @return [Hash] formatted chart
|
|
76
|
+
def chart_format(title:, type:, data:, **options)
|
|
77
|
+
{
|
|
78
|
+
title: title,
|
|
79
|
+
type: type,
|
|
80
|
+
data: data,
|
|
81
|
+
**options
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterPage
|
|
4
|
+
# Registers all default components provided by the gem.
|
|
5
|
+
# Called automatically during Rails initialization before user initializers.
|
|
6
|
+
#
|
|
7
|
+
# Components are organized by page type:
|
|
8
|
+
# - Index: header, table, alerts, statistics, metrics, tabs, search, pagination, overview, calendar, footer, modals, split_view
|
|
9
|
+
# - Show: header, alerts, statistics, overview, content_sections, footer
|
|
10
|
+
# - Form: header, alerts, errors, panels, footer
|
|
11
|
+
# - Custom: header, content, footer, alerts
|
|
12
|
+
#
|
|
13
|
+
module DefaultComponents
|
|
14
|
+
class << self
|
|
15
|
+
# Register all default components
|
|
16
|
+
# @return [void]
|
|
17
|
+
def register!
|
|
18
|
+
BetterPage.configure do |config|
|
|
19
|
+
register_shared_components(config)
|
|
20
|
+
register_index_components(config)
|
|
21
|
+
register_show_components(config)
|
|
22
|
+
register_form_components(config)
|
|
23
|
+
register_custom_components(config)
|
|
24
|
+
|
|
25
|
+
map_components_to_page_types(config)
|
|
26
|
+
set_required_components(config)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get all default component names
|
|
31
|
+
# @return [Array<Symbol>]
|
|
32
|
+
def component_names
|
|
33
|
+
%i[
|
|
34
|
+
header table alerts statistics metrics tabs search pagination
|
|
35
|
+
overview calendar footer modals split_view content_sections
|
|
36
|
+
errors panels content
|
|
37
|
+
]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def register_shared_components(config)
|
|
43
|
+
# Header - used by all page types
|
|
44
|
+
config.register_component :header do
|
|
45
|
+
required(:title).filled(:string)
|
|
46
|
+
optional(:breadcrumbs).array(:hash)
|
|
47
|
+
optional(:metadata).array(:hash)
|
|
48
|
+
optional(:actions).array(:hash)
|
|
49
|
+
optional(:description).filled(:string)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Alerts - used by all page types
|
|
53
|
+
config.register_component :alerts, default: []
|
|
54
|
+
|
|
55
|
+
# Footer - used by all page types
|
|
56
|
+
config.register_component :footer, default: { enabled: false }
|
|
57
|
+
|
|
58
|
+
# Statistics - used by index and show
|
|
59
|
+
config.register_component :statistics, default: []
|
|
60
|
+
|
|
61
|
+
# Overview - used by index and show
|
|
62
|
+
config.register_component :overview, default: { enabled: false }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def register_index_components(config)
|
|
66
|
+
# Table - primary component for index pages
|
|
67
|
+
config.register_component :table do
|
|
68
|
+
required(:items).value(:array)
|
|
69
|
+
optional(:columns).array(:hash)
|
|
70
|
+
optional(:actions)
|
|
71
|
+
optional(:empty_state).hash do
|
|
72
|
+
optional(:icon).filled(:string)
|
|
73
|
+
optional(:title).filled(:string)
|
|
74
|
+
optional(:message).filled(:string)
|
|
75
|
+
optional(:action).hash
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Metrics section
|
|
80
|
+
config.register_component :metrics, default: []
|
|
81
|
+
|
|
82
|
+
# Tabs navigation
|
|
83
|
+
config.register_component :tabs, default: { enabled: false, current_tab: "all", tabs: [] } do
|
|
84
|
+
optional(:enabled).filled(:bool)
|
|
85
|
+
optional(:current_tab).filled(:string)
|
|
86
|
+
optional(:tabs).array(:hash)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Search section
|
|
90
|
+
config.register_component :search, default: {
|
|
91
|
+
enabled: false,
|
|
92
|
+
placeholder: "Search...",
|
|
93
|
+
current_search: "",
|
|
94
|
+
results_count: 0
|
|
95
|
+
} do
|
|
96
|
+
optional(:enabled).filled(:bool)
|
|
97
|
+
optional(:placeholder).filled(:string)
|
|
98
|
+
optional(:current_search).maybe(:string)
|
|
99
|
+
optional(:results_count).filled(:integer)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Pagination
|
|
103
|
+
config.register_component :pagination, default: { enabled: false } do
|
|
104
|
+
optional(:enabled).filled(:bool)
|
|
105
|
+
optional(:page).filled(:integer)
|
|
106
|
+
optional(:total_pages).filled(:integer)
|
|
107
|
+
optional(:total_count).filled(:integer)
|
|
108
|
+
optional(:per_page).filled(:integer)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Calendar view
|
|
112
|
+
config.register_component :calendar, default: {
|
|
113
|
+
enabled: false,
|
|
114
|
+
current_date: nil,
|
|
115
|
+
view_type: "month",
|
|
116
|
+
events: [],
|
|
117
|
+
navigation: {}
|
|
118
|
+
} do
|
|
119
|
+
optional(:enabled).filled(:bool)
|
|
120
|
+
optional(:current_date)
|
|
121
|
+
optional(:view_type).filled(:string)
|
|
122
|
+
optional(:events).array(:hash)
|
|
123
|
+
optional(:navigation).hash
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Modals
|
|
127
|
+
config.register_component :modals, default: []
|
|
128
|
+
|
|
129
|
+
# Split view
|
|
130
|
+
config.register_component :split_view, default: {
|
|
131
|
+
enabled: false,
|
|
132
|
+
selected_id: nil,
|
|
133
|
+
items: [],
|
|
134
|
+
list_title: "Items",
|
|
135
|
+
detail_title: "Details",
|
|
136
|
+
list_item_config: nil,
|
|
137
|
+
detail_path: nil,
|
|
138
|
+
empty_state: {
|
|
139
|
+
icon: "inbox",
|
|
140
|
+
title: "Select an item",
|
|
141
|
+
message: "Click on an item from the list to see its details"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def register_show_components(config)
|
|
147
|
+
# Content sections
|
|
148
|
+
config.register_component :content_sections, default: []
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def register_form_components(config)
|
|
152
|
+
# Errors component
|
|
153
|
+
config.register_component :errors, default: nil
|
|
154
|
+
|
|
155
|
+
# Panels component
|
|
156
|
+
config.register_component :panels
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def register_custom_components(config)
|
|
160
|
+
# Content component
|
|
161
|
+
config.register_component :content
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def map_components_to_page_types(config)
|
|
165
|
+
# Index page components
|
|
166
|
+
config.allow_components :index,
|
|
167
|
+
:header, :table, :alerts, :statistics, :metrics,
|
|
168
|
+
:tabs, :search, :pagination, :overview, :calendar,
|
|
169
|
+
:footer, :modals, :split_view
|
|
170
|
+
|
|
171
|
+
# Show page components
|
|
172
|
+
config.allow_components :show,
|
|
173
|
+
:header, :alerts, :statistics, :overview,
|
|
174
|
+
:content_sections, :footer
|
|
175
|
+
|
|
176
|
+
# Form page components
|
|
177
|
+
config.allow_components :form,
|
|
178
|
+
:header, :alerts, :errors, :panels, :footer
|
|
179
|
+
|
|
180
|
+
# Custom page components
|
|
181
|
+
config.allow_components :custom,
|
|
182
|
+
:header, :content, :footer, :alerts
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def set_required_components(config)
|
|
186
|
+
# Index pages require header and table
|
|
187
|
+
config.require_components :index, :header, :table
|
|
188
|
+
|
|
189
|
+
# Show pages require header
|
|
190
|
+
config.require_components :show, :header
|
|
191
|
+
|
|
192
|
+
# Form pages require header and panels
|
|
193
|
+
config.require_components :form, :header, :panels
|
|
194
|
+
|
|
195
|
+
# Custom pages require content
|
|
196
|
+
config.require_components :custom, :content
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterPage
|
|
4
|
+
# Base class for form pages (new/edit).
|
|
5
|
+
# Uses page_type to inherit components from global configuration.
|
|
6
|
+
#
|
|
7
|
+
# FORM ORGANIZATION RULES (MANDATORY):
|
|
8
|
+
# =====================================
|
|
9
|
+
#
|
|
10
|
+
# RULE 1: INPUT TYPE SEPARATION
|
|
11
|
+
# - CHECKBOX fields must be grouped in dedicated panels, never mixed with other inputs
|
|
12
|
+
# - RADIO BUTTON fields must be grouped in dedicated panels, never mixed with other inputs
|
|
13
|
+
# - Text, email, select, textarea, number, date can be mixed together
|
|
14
|
+
#
|
|
15
|
+
# CORRECT EXAMPLE:
|
|
16
|
+
# [
|
|
17
|
+
# {
|
|
18
|
+
# title: 'Basic Information',
|
|
19
|
+
# fields: [
|
|
20
|
+
# { name: :name, type: :text, ... }, # OK
|
|
21
|
+
# { name: :email, type: :email, ... } # OK
|
|
22
|
+
# ]
|
|
23
|
+
# },
|
|
24
|
+
# {
|
|
25
|
+
# title: 'Settings', # Separate panel
|
|
26
|
+
# fields: [
|
|
27
|
+
# { name: :is_primary, type: :checkbox, ... }, # OK
|
|
28
|
+
# { name: :is_active, type: :checkbox, ... } # OK
|
|
29
|
+
# ]
|
|
30
|
+
# }
|
|
31
|
+
# ]
|
|
32
|
+
#
|
|
33
|
+
# WRONG EXAMPLE:
|
|
34
|
+
# [
|
|
35
|
+
# {
|
|
36
|
+
# title: 'Information',
|
|
37
|
+
# fields: [
|
|
38
|
+
# { name: :name, type: :text, ... }, # OK
|
|
39
|
+
# { name: :is_primary, type: :checkbox, ... } # VIOLATION - checkbox with text
|
|
40
|
+
# ]
|
|
41
|
+
# }
|
|
42
|
+
# ]
|
|
43
|
+
#
|
|
44
|
+
# Available components (from configuration):
|
|
45
|
+
# - header (required): Form header with title, description, breadcrumbs
|
|
46
|
+
# - panels (required): Form panels with fields
|
|
47
|
+
# - alerts, errors, footer
|
|
48
|
+
#
|
|
49
|
+
# @example
|
|
50
|
+
# class Admin::Users::FormPage < FormBasePage
|
|
51
|
+
# def header
|
|
52
|
+
# { title: @item.new_record? ? "New User" : "Edit User" }
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# def panels
|
|
56
|
+
# [{ title: "Basic Info", fields: [...] }]
|
|
57
|
+
# end
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
class FormBasePage < BasePage
|
|
61
|
+
page_type :form
|
|
62
|
+
|
|
63
|
+
# Override the global footer default with form-specific default
|
|
64
|
+
register_component :footer, default: {
|
|
65
|
+
primary_action: { label: "Save", style: :primary },
|
|
66
|
+
secondary_actions: []
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Main method that builds the complete form page configuration
|
|
70
|
+
# @return [Hash] complete form page configuration with :klass for rendering
|
|
71
|
+
def form
|
|
72
|
+
result = build_page
|
|
73
|
+
validate_form_panels_rules(result[:panels]) if result[:panels]
|
|
74
|
+
result
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Note: frame_form and stream_form are dynamically generated via method_missing in ComponentRegistry
|
|
78
|
+
# Usage:
|
|
79
|
+
# page.frame_form(:panels) # Single component for Turbo Frame
|
|
80
|
+
# page.stream_form # All stream components for Turbo Streams
|
|
81
|
+
# page.stream_form(:panels, :errors) # Specific components for Turbo Streams
|
|
82
|
+
|
|
83
|
+
# The ViewComponent class used to render this form page
|
|
84
|
+
# @return [Class] BetterPage::FormViewComponent
|
|
85
|
+
def view_component_class
|
|
86
|
+
return BetterPage::FormViewComponent if defined?(BetterPage::FormViewComponent)
|
|
87
|
+
|
|
88
|
+
raise NotImplementedError, "BetterPage::FormViewComponent not found. Run: rails g better_page:install"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Components to include in stream updates by default
|
|
92
|
+
# @return [Array<Symbol>]
|
|
93
|
+
def stream_components
|
|
94
|
+
%i[alerts errors panels]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
protected
|
|
98
|
+
|
|
99
|
+
# Validates that all panels follow the input separation rules
|
|
100
|
+
# Logs warnings in development mode when rules are violated
|
|
101
|
+
# @param panels [Array<Hash>] panels to validate
|
|
102
|
+
# @return [void]
|
|
103
|
+
def validate_form_panels_rules(panels)
|
|
104
|
+
return unless defined?(Rails) && Rails.env.development?
|
|
105
|
+
|
|
106
|
+
panels.each_with_index do |panel, index|
|
|
107
|
+
next unless panel[:fields].is_a?(Array)
|
|
108
|
+
|
|
109
|
+
checkbox_count = panel[:fields].count { |field| field[:type] == :checkbox }
|
|
110
|
+
radio_count = panel[:fields].count { |field| field[:type] == :radio }
|
|
111
|
+
other_count = panel[:fields].count { |field| %i[checkbox radio].exclude?(field[:type]) }
|
|
112
|
+
|
|
113
|
+
# RULE 1: Checkboxes must be in separate panels
|
|
114
|
+
if checkbox_count.positive? && other_count.positive?
|
|
115
|
+
Rails.logger.warn "[BetterPage::FormBasePage] RULE VIOLATION in panel '#{panel[:title]}' (#{index}): " \
|
|
116
|
+
"checkboxes mixed with other inputs (checkboxes: #{checkbox_count}, others: #{other_count})"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# RULE 1: Radio buttons must be in separate panels
|
|
120
|
+
if radio_count.positive? && other_count.positive?
|
|
121
|
+
Rails.logger.warn "[BetterPage::FormBasePage] RULE VIOLATION in panel '#{panel[:title]}' (#{index}): " \
|
|
122
|
+
"radio buttons mixed with other inputs (radio: #{radio_count}, others: #{other_count})"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Helper to build a form field
|
|
128
|
+
# @param name [Symbol] field name
|
|
129
|
+
# @param type [Symbol] field type (:text, :email, :select, :checkbox, etc.)
|
|
130
|
+
# @param label [String] field label
|
|
131
|
+
# @param options [Hash] additional options (required, placeholder, collection, etc.)
|
|
132
|
+
# @return [Hash] formatted field
|
|
133
|
+
def field_format(name:, type:, label:, **options)
|
|
134
|
+
{
|
|
135
|
+
name: name,
|
|
136
|
+
type: type,
|
|
137
|
+
label: label,
|
|
138
|
+
**options
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Helper to build a form panel
|
|
143
|
+
# @param title [String] panel title
|
|
144
|
+
# @param fields [Array<Hash>] panel fields
|
|
145
|
+
# @param description [String, nil] panel description
|
|
146
|
+
# @param icon [String, nil] panel icon
|
|
147
|
+
# @return [Hash] formatted panel
|
|
148
|
+
def panel_format(title:, fields:, description: nil, icon: nil)
|
|
149
|
+
panel = {
|
|
150
|
+
title: title,
|
|
151
|
+
fields: fields
|
|
152
|
+
}
|
|
153
|
+
panel[:description] = description if description
|
|
154
|
+
panel[:icon] = icon if icon
|
|
155
|
+
panel
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Default breadcrumbs for forms
|
|
159
|
+
# @return [Array<Hash>] empty breadcrumbs
|
|
160
|
+
def default_breadcrumbs
|
|
161
|
+
[]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Extract resource name from class name
|
|
165
|
+
# @return [String] downcased resource name
|
|
166
|
+
def resource_name
|
|
167
|
+
self.class.name.split("::").last.gsub(/Page$/, "").downcase
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterPage
|
|
4
|
+
# Base class for index/list pages.
|
|
5
|
+
# Uses page_type to inherit components from global configuration.
|
|
6
|
+
#
|
|
7
|
+
# Available components (from configuration):
|
|
8
|
+
# - header (required): Page header with title, breadcrumbs, metadata, actions
|
|
9
|
+
# - table (required): Table with items, columns, actions, empty_state
|
|
10
|
+
# - alerts, statistics, metrics, tabs, search, pagination
|
|
11
|
+
# - overview, calendar, footer, modals, split_view
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# class Admin::Users::IndexPage < IndexBasePage
|
|
15
|
+
# def header
|
|
16
|
+
# { title: "Users", breadcrumbs: [], actions: [] }
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# def table
|
|
20
|
+
# { items: @users, columns: [...], empty_state: {...} }
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
class IndexBasePage < BasePage
|
|
25
|
+
page_type :index
|
|
26
|
+
|
|
27
|
+
# Main method that builds the complete index page configuration
|
|
28
|
+
# @return [Hash] complete index page configuration with :klass for rendering
|
|
29
|
+
def index
|
|
30
|
+
build_page
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Note: frame_index and stream_index are dynamically generated via method_missing in ComponentRegistry
|
|
34
|
+
# Usage:
|
|
35
|
+
# page.frame_index(:table) # Single component for Turbo Frame
|
|
36
|
+
# page.stream_index # All stream components for Turbo Streams
|
|
37
|
+
# page.stream_index(:table, :pagination) # Specific components for Turbo Streams
|
|
38
|
+
|
|
39
|
+
# The ViewComponent class used to render this index page
|
|
40
|
+
# @return [Class] BetterPage::IndexViewComponent
|
|
41
|
+
def view_component_class
|
|
42
|
+
return BetterPage::IndexViewComponent if defined?(BetterPage::IndexViewComponent)
|
|
43
|
+
|
|
44
|
+
raise NotImplementedError, "BetterPage::IndexViewComponent not found. Run: rails g better_page:install"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Components to include in stream updates by default
|
|
48
|
+
# @return [Array<Symbol>]
|
|
49
|
+
def stream_components
|
|
50
|
+
%i[alerts statistics table pagination]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
protected
|
|
54
|
+
|
|
55
|
+
# Helper for split view empty state
|
|
56
|
+
# @param icon [String] icon name
|
|
57
|
+
# @param title [String] title text
|
|
58
|
+
# @param message [String] message text
|
|
59
|
+
# @return [Hash] empty state configuration
|
|
60
|
+
def split_view_empty_state_format(icon: "hand-pointer", title: "Select an item",
|
|
61
|
+
message: "Click on an item from the list to see its details")
|
|
62
|
+
{
|
|
63
|
+
icon: icon,
|
|
64
|
+
title: title,
|
|
65
|
+
message: message
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterPage
|
|
4
|
+
class Railtie < ::Rails::Railtie
|
|
5
|
+
# Add app/pages to the autoload paths
|
|
6
|
+
# Must run before :set_autoload_paths to avoid FrozenError in Rails 8+
|
|
7
|
+
initializer "better_page.autoload_paths", before: :set_autoload_paths do |app|
|
|
8
|
+
pages_path = Rails.root.join("app", "pages")
|
|
9
|
+
if pages_path.exist?
|
|
10
|
+
app.config.autoload_paths << pages_path.to_s
|
|
11
|
+
app.config.eager_load_paths << pages_path.to_s
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Register default components before user initializers
|
|
16
|
+
# This allows users to override or extend defaults in their initializers
|
|
17
|
+
initializer "better_page.register_defaults", before: :load_config_initializers do
|
|
18
|
+
BetterPage::DefaultComponents.register!
|
|
19
|
+
BetterPage.defaults_registered!
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Load rake tasks
|
|
23
|
+
rake_tasks do
|
|
24
|
+
load "tasks/better_page.rake"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Add generators path
|
|
28
|
+
generators do
|
|
29
|
+
require "generators/better_page/install_generator"
|
|
30
|
+
require "generators/better_page/page_generator"
|
|
31
|
+
require "generators/better_page/sync_generator"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterPage
|
|
4
|
+
# Base class for show/detail pages.
|
|
5
|
+
# Uses page_type to inherit components from global configuration.
|
|
6
|
+
#
|
|
7
|
+
# Available components (from configuration):
|
|
8
|
+
# - header (required): Page header with title, breadcrumbs, metadata, actions
|
|
9
|
+
# - alerts, statistics, overview, content_sections, footer
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# class Admin::Users::ShowPage < ShowBasePage
|
|
13
|
+
# def header
|
|
14
|
+
# { title: @user.name, breadcrumbs: [], actions: [] }
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# def content_sections
|
|
18
|
+
# [{ title: "Details", type: :info_grid, items: [...] }]
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
class ShowBasePage < BasePage
|
|
23
|
+
page_type :show
|
|
24
|
+
|
|
25
|
+
# Main method that builds the complete show page configuration
|
|
26
|
+
# @return [Hash] complete show page configuration with :klass for rendering
|
|
27
|
+
def show
|
|
28
|
+
build_page
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Note: frame_show and stream_show are dynamically generated via method_missing in ComponentRegistry
|
|
32
|
+
# Usage:
|
|
33
|
+
# page.frame_show(:overview) # Single component for Turbo Frame
|
|
34
|
+
# page.stream_show # All stream components for Turbo Streams
|
|
35
|
+
# page.stream_show(:overview, :content_sections) # Specific components for Turbo Streams
|
|
36
|
+
|
|
37
|
+
# The ViewComponent class used to render this show page
|
|
38
|
+
# @return [Class] BetterPage::ShowViewComponent
|
|
39
|
+
def view_component_class
|
|
40
|
+
return BetterPage::ShowViewComponent if defined?(BetterPage::ShowViewComponent)
|
|
41
|
+
|
|
42
|
+
raise NotImplementedError, "BetterPage::ShowViewComponent not found. Run: rails g better_page:install"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Components to include in stream updates by default
|
|
46
|
+
# @return [Array<Symbol>]
|
|
47
|
+
def stream_components
|
|
48
|
+
%i[alerts statistics overview content_sections]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
protected
|
|
52
|
+
|
|
53
|
+
# Helper to convert hash to info grid format
|
|
54
|
+
# @param hash [Hash] key-value pairs to convert
|
|
55
|
+
# @return [Array<Hash>] formatted info grid items
|
|
56
|
+
def info_grid_content_format(hash)
|
|
57
|
+
hash.map { |name, value| { name: name, value: value } }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Helper to build content section
|
|
61
|
+
# @param title [String] section title
|
|
62
|
+
# @param icon [String] section icon
|
|
63
|
+
# @param color [String] section color
|
|
64
|
+
# @param type [Symbol] section type (:info_grid, :text_content, :custom)
|
|
65
|
+
# @param content [Hash, Array, nil] section content
|
|
66
|
+
# @return [Hash] formatted content section
|
|
67
|
+
def content_section_format(title:, icon:, color:, type:, content: nil)
|
|
68
|
+
section = {
|
|
69
|
+
title: title,
|
|
70
|
+
icon: icon,
|
|
71
|
+
color: color,
|
|
72
|
+
type: type
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if type == :info_grid && content.is_a?(Array) && content.first.is_a?(Hash) && content.first.key?(:name)
|
|
76
|
+
section[:items] = content
|
|
77
|
+
elsif type == :info_grid && content.is_a?(Hash)
|
|
78
|
+
section[:items] = info_grid_content_format(content)
|
|
79
|
+
elsif type == :info_grid
|
|
80
|
+
section[:items] = content
|
|
81
|
+
else
|
|
82
|
+
section[:content] = content
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
section
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Helper to build statistic card
|
|
89
|
+
# @param label [String] statistic label
|
|
90
|
+
# @param value [String, Numeric] statistic value
|
|
91
|
+
# @param icon [String] statistic icon
|
|
92
|
+
# @param color [String] statistic color
|
|
93
|
+
# @return [Hash] formatted statistic
|
|
94
|
+
def statistic_format(label:, value:, icon:, color:)
|
|
95
|
+
{
|
|
96
|
+
label: label,
|
|
97
|
+
value: value,
|
|
98
|
+
icon: icon,
|
|
99
|
+
color: color
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Helper to build header action
|
|
104
|
+
# @param path [String] action path
|
|
105
|
+
# @param label [String] action label
|
|
106
|
+
# @param icon [String] action icon
|
|
107
|
+
# @param style [String] action style
|
|
108
|
+
# @param method [Symbol] HTTP method
|
|
109
|
+
# @return [Hash] formatted action
|
|
110
|
+
def action_format(path:, label:, icon:, style:, method: :get)
|
|
111
|
+
{
|
|
112
|
+
path: path,
|
|
113
|
+
label: label,
|
|
114
|
+
icon: icon,
|
|
115
|
+
style: style,
|
|
116
|
+
method: method
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|