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,311 @@
1
+ # Best Practices
2
+
3
+ Guidelines for building maintainable and compliant pages.
4
+
5
+ ### Keep Pages Thin
6
+
7
+ Pages should only configure UI. Move complex logic to the controller or service layer.
8
+
9
+ ```ruby
10
+ # WRONG - Logic in page
11
+ class Products::IndexPage < IndexBasePage
12
+ def statistics
13
+ total = @products.sum(&:price) * 1.1 # Tax calculation
14
+ [{ label: "Total", value: total }]
15
+ end
16
+ end
17
+
18
+ # CORRECT - Logic in controller
19
+ class ProductsController < ApplicationController
20
+ def index
21
+ products = Product.all
22
+ stats = { total_with_tax: ProductCalculator.total_with_tax(products) }
23
+ @page = Products::IndexPage.new(products, user: current_user, stats: stats).index
24
+ end
25
+ end
26
+
27
+ class Products::IndexPage < IndexBasePage
28
+ def initialize(products, metadata = {})
29
+ @products = products
30
+ @user = metadata[:user]
31
+ @stats = metadata[:stats]
32
+ super(products, metadata)
33
+ end
34
+
35
+ def statistics
36
+ [{ label: "Total", value: @stats[:total_with_tax] }]
37
+ end
38
+ end
39
+ ```
40
+
41
+ --------------------------------
42
+
43
+ ### Pass All Data via Constructor
44
+
45
+ Never query the database in a page. All data should be passed through the constructor.
46
+
47
+ ```ruby
48
+ # WRONG - Database query in page
49
+ def header
50
+ { title: "#{User.count} Users" }
51
+ end
52
+
53
+ # CORRECT - Data passed via constructor
54
+ def initialize(users, metadata = {})
55
+ @users = users
56
+ @user = metadata[:user]
57
+ @stats = metadata[:stats]
58
+ super(users, metadata)
59
+ end
60
+
61
+ def header
62
+ { title: "#{@stats[:count]} Users" }
63
+ end
64
+ ```
65
+
66
+ --------------------------------
67
+
68
+ ### Use Helper Methods for Reusable Configurations
69
+
70
+ Extract repeated configurations into private helper methods.
71
+
72
+ ```ruby
73
+ class Products::IndexPage < IndexBasePage
74
+ private
75
+
76
+ def header
77
+ { title: "Products", breadcrumbs: breadcrumb_items, actions: header_actions }
78
+ end
79
+
80
+ def table
81
+ { items: @products, columns: table_columns, empty_state: empty_config }
82
+ end
83
+
84
+ # Extracted helper methods
85
+ def breadcrumb_items
86
+ [{ label: "Home", path: root_path }, { label: "Products" }]
87
+ end
88
+
89
+ def header_actions
90
+ [{ label: "New", path: new_product_path, icon: "plus", style: :primary }]
91
+ end
92
+
93
+ def table_columns
94
+ [
95
+ { key: :name, label: "Name", type: :link },
96
+ { key: :price, label: "Price", format: :currency }
97
+ ]
98
+ end
99
+
100
+ def empty_config
101
+ { icon: "box", title: "No products", message: "Create your first product" }
102
+ end
103
+ end
104
+ ```
105
+
106
+ --------------------------------
107
+
108
+ ### Use Built-in Format Helpers
109
+
110
+ Use the provided helper methods for consistent formatting.
111
+
112
+ ```ruby
113
+ # ShowBasePage helpers
114
+ action_format(path: edit_path, label: "Edit", icon: "edit", style: "primary")
115
+ statistic_format(label: "Total", value: 100, icon: "chart", color: "blue")
116
+ content_section_format(title: "Details", type: :info_grid, content: {...})
117
+
118
+ # FormBasePage helpers
119
+ field_format(name: :email, type: :email, label: "Email", required: true)
120
+ panel_format(title: "Basic Info", fields: [...])
121
+
122
+ # CustomBasePage helpers
123
+ widget_format(title: "Users", type: :counter, data: { value: 100 })
124
+ chart_format(title: "Revenue", type: :line, data: {...})
125
+ ```
126
+
127
+ --------------------------------
128
+
129
+ ### Separate Checkbox Panels
130
+
131
+ Always put checkbox and radio fields in separate panels from text inputs.
132
+
133
+ ```ruby
134
+ # CORRECT
135
+ def panels
136
+ [
137
+ panel_format(
138
+ title: "Details",
139
+ fields: [
140
+ field_format(name: :name, type: :text, label: "Name"),
141
+ field_format(name: :email, type: :email, label: "Email")
142
+ ]
143
+ ),
144
+ panel_format(
145
+ title: "Settings",
146
+ fields: [
147
+ field_format(name: :active, type: :checkbox, label: "Active"),
148
+ field_format(name: :newsletter, type: :checkbox, label: "Subscribe")
149
+ ]
150
+ )
151
+ ]
152
+ end
153
+ ```
154
+
155
+ --------------------------------
156
+
157
+ ### Use Meaningful Component Names
158
+
159
+ Name components clearly to describe their purpose.
160
+
161
+ ```ruby
162
+ # Clear component naming
163
+ def header_actions
164
+ [{ label: "New", path: new_path }]
165
+ end
166
+
167
+ def table_columns
168
+ [{ key: :name, label: "Name" }]
169
+ end
170
+
171
+ def filter_tabs
172
+ [{ label: "All", path: index_path }]
173
+ end
174
+ ```
175
+
176
+ --------------------------------
177
+
178
+ ### Handle Empty States Gracefully
179
+
180
+ Always provide helpful empty states for lists and tables.
181
+
182
+ ```ruby
183
+ def table
184
+ {
185
+ items: @products,
186
+ columns: table_columns,
187
+ empty_state: {
188
+ icon: "inbox",
189
+ title: "No products yet",
190
+ message: "Get started by creating your first product",
191
+ action: {
192
+ label: "Create Product",
193
+ path: new_product_path,
194
+ style: :primary
195
+ }
196
+ }
197
+ }
198
+ end
199
+ ```
200
+
201
+ --------------------------------
202
+
203
+ ### Use Consistent Styling
204
+
205
+ Define consistent style patterns across your application.
206
+
207
+ ```ruby
208
+ # Define constants for reuse
209
+ module PageStyles
210
+ BUTTON_STYLES = {
211
+ primary: "primary",
212
+ secondary: "secondary",
213
+ danger: "danger"
214
+ }.freeze
215
+
216
+ COLORS = {
217
+ success: "green",
218
+ warning: "yellow",
219
+ error: "red",
220
+ info: "blue"
221
+ }.freeze
222
+ end
223
+
224
+ # Use in pages
225
+ def header_actions
226
+ [
227
+ { label: "Save", style: PageStyles::BUTTON_STYLES[:primary] },
228
+ { label: "Cancel", style: PageStyles::BUTTON_STYLES[:secondary] }
229
+ ]
230
+ end
231
+ ```
232
+
233
+ --------------------------------
234
+
235
+ ### Run Compliance Checks Regularly
236
+
237
+ Add compliance checks to your CI pipeline.
238
+
239
+ ```bash
240
+ # Run compliance analyzer
241
+ rake better_page:analyze
242
+
243
+ # In CI (exit with error if issues found)
244
+ STRICT=true rake better_page:analyze
245
+ ```
246
+
247
+ --------------------------------
248
+
249
+ ### Document Custom Components
250
+
251
+ Add comments explaining custom configurations.
252
+
253
+ ```ruby
254
+ class Products::IndexPage < IndexBasePage
255
+ private
256
+
257
+ # Table columns with custom formatter for price
258
+ # Price is displayed with currency symbol and 2 decimal places
259
+ def table_columns
260
+ [
261
+ { key: :name, label: "Name", type: :link },
262
+ { key: :price, label: "Price", format: :currency, precision: 2 }
263
+ ]
264
+ end
265
+
266
+ # Statistics shown above the table
267
+ # Only visible to admin users
268
+ def statistics
269
+ return [] unless @current_user.admin?
270
+
271
+ [
272
+ { label: "Total", value: @products.size },
273
+ { label: "Revenue", value: format_currency(@stats[:revenue]) }
274
+ ]
275
+ end
276
+ end
277
+ ```
278
+
279
+ --------------------------------
280
+
281
+ ### Test Your Pages
282
+
283
+ Write tests for page output.
284
+
285
+ ```ruby
286
+ require "test_helper"
287
+
288
+ class Products::IndexPageTest < ActiveSupport::TestCase
289
+ test "returns correct header" do
290
+ products = [Product.new(name: "Test")]
291
+ page = Products::IndexPage.new(products, user: users(:admin)).index
292
+
293
+ assert_equal "Products", page[:header][:title]
294
+ assert_equal 1, page[:header][:actions].size
295
+ end
296
+
297
+ test "returns table with products" do
298
+ products = [Product.new(name: "Test", price: 100)]
299
+ page = Products::IndexPage.new(products, user: users(:admin)).index
300
+
301
+ assert_equal 1, page[:table][:items].size
302
+ assert_equal 3, page[:table][:columns].size
303
+ end
304
+
305
+ test "returns empty state when no products" do
306
+ page = Products::IndexPage.new([], user: users(:admin)).index
307
+
308
+ assert_equal "No products", page[:table][:empty_state][:title]
309
+ end
310
+ end
311
+ ```
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterPage
4
+ # Base class for all page objects.
5
+ # Pages are presentation-layer classes that configure UI without business logic.
6
+ #
7
+ # Rules:
8
+ # - No database queries
9
+ # - No business logic
10
+ # - No service layer access
11
+ # - UI configuration only (Hash-based structures)
12
+ #
13
+ # Components are registered using the DSL:
14
+ # register_component :header, required: true do
15
+ # required(:title).filled(:string)
16
+ # end
17
+ #
18
+ class BasePage
19
+ include Rails.application.routes.url_helpers
20
+ include ComponentRegistry
21
+
22
+ attr_reader :user, :items, :stats, :item, :primary_data, :metadata
23
+
24
+ # @param primary_data [Object] The main data for the page (e.g., collection, record)
25
+ # @param metadata [Hash] Additional metadata (user, stats, item, etc.)
26
+ def initialize(primary_data, metadata = {})
27
+ @primary_data = primary_data
28
+ @metadata = metadata
29
+ @items = primary_data
30
+ @user = metadata[:user]
31
+ @stats = metadata[:stats]
32
+ @item = metadata[:item]
33
+ end
34
+
35
+ protected
36
+
37
+ # Helper for pluralized count text
38
+ # @param count [Integer] the count
39
+ # @param singular [String] singular form
40
+ # @param plural [String] plural form
41
+ # @return [String] formatted count text
42
+ def count_text(count, singular, plural)
43
+ "#{count} #{count == 1 ? singular : plural}"
44
+ end
45
+
46
+ # Helper for formatted dates
47
+ # @param date [Date, Time, nil] the date to format
48
+ # @param format [String] strftime format string
49
+ # @return [String] formatted date or default message
50
+ def format_date(date, format = "%d/%m/%Y")
51
+ return "N/A" unless date
52
+
53
+ date.strftime(format)
54
+ end
55
+
56
+ # Helper for percentage calculation
57
+ # @param part [Numeric] the part value
58
+ # @param total [Numeric] the total value
59
+ # @return [Float] percentage rounded to 1 decimal
60
+ def percentage(part, total)
61
+ return 0 if total.zero?
62
+
63
+ ((part.to_f / total) * 100).round(1)
64
+ end
65
+
66
+ # Helper for empty state with action
67
+ # @param icon [String] icon name
68
+ # @param title [String] title text
69
+ # @param message [String] message text
70
+ # @param action_label [String, nil] action button label
71
+ # @param action_path [String, nil] action button path
72
+ # @param action_icon [String] action button icon
73
+ # @return [Hash] empty state configuration
74
+ def empty_state_with_action(icon:, title:, message:, action_label: nil, action_path: nil, action_icon: "plus")
75
+ state = {
76
+ icon: icon,
77
+ title: title,
78
+ message: message
79
+ }
80
+
81
+ if action_label && action_path
82
+ state[:action] = {
83
+ label: action_label,
84
+ path: action_path,
85
+ icon: action_icon
86
+ }
87
+ end
88
+
89
+ state
90
+ end
91
+
92
+ # Default breadcrumbs configuration
93
+ # Override in subclasses for custom breadcrumbs
94
+ # @return [Array<Hash>] breadcrumbs array
95
+ def breadcrumbs_config
96
+ []
97
+ end
98
+
99
+ # Default metadata configuration
100
+ # @return [Array<Hash>] metadata array
101
+ def default_metadata
102
+ []
103
+ end
104
+
105
+ # Default actions configuration
106
+ # @return [Array<Hash>] actions array
107
+ def default_actions
108
+ []
109
+ end
110
+
111
+ # Default alerts configuration
112
+ # @return [Array<Hash>] alerts array
113
+ def default_alerts
114
+ []
115
+ end
116
+
117
+ # Default statistics configuration
118
+ # @return [Array<Hash>] statistics array
119
+ def default_statistics
120
+ []
121
+ end
122
+
123
+ # Default tabs configuration
124
+ # @return [Hash] tabs configuration
125
+ def default_tabs_config
126
+ {
127
+ enabled: false,
128
+ current_tab: "all",
129
+ tabs: []
130
+ }
131
+ end
132
+
133
+ # Default table configuration
134
+ # @return [Hash] table configuration
135
+ def default_table_config
136
+ {
137
+ items: @items || [],
138
+ empty_state: {
139
+ icon: "inbox",
140
+ title: "No items found",
141
+ message: "There are no items to display at the moment."
142
+ },
143
+ columns: [],
144
+ actions: {
145
+ type: :button_group,
146
+ buttons: []
147
+ }
148
+ }
149
+ end
150
+
151
+ # Default footer info configuration
152
+ # @return [Hash] footer configuration
153
+ def default_footer_info
154
+ {
155
+ enabled: false,
156
+ title: "Information",
157
+ sections: []
158
+ }
159
+ end
160
+ end
161
+ end