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,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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterPage
4
+ # Error raised when component validation fails in development mode
5
+ class ValidationError < StandardError
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module BetterPage
2
+ VERSION = "2.0.0"
3
+ end