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,309 @@
1
+ # Building Form Pages
2
+
3
+ A complete guide to building new and edit form pages.
4
+
5
+ ### Basic Form Page Structure
6
+
7
+ ```ruby
8
+ class Products::NewPage < FormBasePage
9
+ def initialize(product, metadata = {})
10
+ @product = product
11
+ @user = metadata[:user]
12
+ super(product, metadata)
13
+ end
14
+
15
+ private
16
+
17
+ def header
18
+ { title: "New Product" }
19
+ end
20
+
21
+ def panels
22
+ [
23
+ panel_format(title: "Basic Info", fields: basic_fields)
24
+ ]
25
+ end
26
+ end
27
+ ```
28
+
29
+ --------------------------------
30
+
31
+ ### Adding Header with Description
32
+
33
+ ```ruby
34
+ def header
35
+ {
36
+ title: "New Product",
37
+ description: "Create a new product in your catalog",
38
+ breadcrumbs: [
39
+ { label: "Products", path: products_path },
40
+ { label: "New" }
41
+ ]
42
+ }
43
+ end
44
+ ```
45
+
46
+ --------------------------------
47
+
48
+ ### Using field_format Helper
49
+
50
+ Build form fields with the helper method.
51
+
52
+ ```ruby
53
+ field_format(
54
+ name: :email,
55
+ type: :email, # :text, :email, :password, :number, :textarea, :select, :checkbox, :radio
56
+ label: "Email Address",
57
+ required: true,
58
+ placeholder: "Enter email",
59
+ hint: "We'll never share your email"
60
+ )
61
+ ```
62
+
63
+ --------------------------------
64
+
65
+ ### Field Types
66
+
67
+ ```ruby
68
+ # Text input
69
+ field_format(name: :name, type: :text, label: "Name", required: true)
70
+
71
+ # Email input
72
+ field_format(name: :email, type: :email, label: "Email")
73
+
74
+ # Password input
75
+ field_format(name: :password, type: :password, label: "Password")
76
+
77
+ # Number input
78
+ field_format(name: :price, type: :number, label: "Price", min: 0, step: 0.01)
79
+
80
+ # Textarea
81
+ field_format(name: :description, type: :textarea, label: "Description", rows: 5)
82
+
83
+ # Select dropdown
84
+ field_format(name: :category, type: :select, label: "Category", options: category_options)
85
+
86
+ # Checkbox
87
+ field_format(name: :active, type: :checkbox, label: "Active")
88
+
89
+ # Radio buttons
90
+ field_format(name: :status, type: :radio, label: "Status", options: status_options)
91
+ ```
92
+
93
+ --------------------------------
94
+
95
+ ### Using panel_format Helper
96
+
97
+ Build form panels with the helper method.
98
+
99
+ ```ruby
100
+ panel_format(
101
+ title: "Basic Information",
102
+ description: "Enter the product details",
103
+ fields: [
104
+ field_format(name: :name, type: :text, label: "Name", required: true),
105
+ field_format(name: :price, type: :number, label: "Price", required: true)
106
+ ]
107
+ )
108
+ ```
109
+
110
+ --------------------------------
111
+
112
+ ### Important: Separate Checkbox Panels
113
+
114
+ Checkbox and radio fields MUST be in separate panels from text inputs.
115
+
116
+ ```ruby
117
+ def panels
118
+ [
119
+ # Text inputs panel
120
+ panel_format(
121
+ title: "Product Details",
122
+ fields: [
123
+ field_format(name: :name, type: :text, label: "Name", required: true),
124
+ field_format(name: :price, type: :number, label: "Price"),
125
+ field_format(name: :category, type: :select, label: "Category", options: categories)
126
+ ]
127
+ ),
128
+ # Checkbox panel - MUST be separate
129
+ panel_format(
130
+ title: "Settings",
131
+ fields: [
132
+ field_format(name: :active, type: :checkbox, label: "Active"),
133
+ field_format(name: :featured, type: :checkbox, label: "Featured on Homepage")
134
+ ]
135
+ )
136
+ ]
137
+ end
138
+ ```
139
+
140
+ --------------------------------
141
+
142
+ ### Select Field with Options
143
+
144
+ ```ruby
145
+ def panels
146
+ [
147
+ panel_format(
148
+ title: "Details",
149
+ fields: [
150
+ field_format(
151
+ name: :category_id,
152
+ type: :select,
153
+ label: "Category",
154
+ options: category_options,
155
+ include_blank: "Select a category"
156
+ )
157
+ ]
158
+ )
159
+ ]
160
+ end
161
+
162
+ def category_options
163
+ [
164
+ { value: 1, label: "Electronics" },
165
+ { value: 2, label: "Clothing" },
166
+ { value: 3, label: "Books" }
167
+ ]
168
+ end
169
+ ```
170
+
171
+ --------------------------------
172
+
173
+ ### Configuring Footer
174
+
175
+ ```ruby
176
+ def footer
177
+ {
178
+ primary_action: {
179
+ label: "Save Product",
180
+ style: "primary"
181
+ },
182
+ secondary_actions: [
183
+ { label: "Cancel", path: products_path, style: "secondary" },
184
+ { label: "Save as Draft", action: :draft, style: "outline" }
185
+ ]
186
+ }
187
+ end
188
+ ```
189
+
190
+ --------------------------------
191
+
192
+ ### Edit Page Example
193
+
194
+ ```ruby
195
+ class Products::EditPage < FormBasePage
196
+ def initialize(product, metadata = {})
197
+ @product = product
198
+ @user = metadata[:user]
199
+ super(product, metadata)
200
+ end
201
+
202
+ private
203
+
204
+ def header
205
+ {
206
+ title: "Edit #{@product.name}",
207
+ breadcrumbs: [
208
+ { label: "Products", path: products_path },
209
+ { label: @product.name, path: product_path(@product) },
210
+ { label: "Edit" }
211
+ ]
212
+ }
213
+ end
214
+
215
+ def panels
216
+ [
217
+ panel_format(title: "Product Details", fields: detail_fields),
218
+ panel_format(title: "Pricing", fields: pricing_fields),
219
+ panel_format(title: "Settings", fields: settings_fields)
220
+ ]
221
+ end
222
+
223
+ def detail_fields
224
+ [
225
+ field_format(name: :name, type: :text, label: "Name", required: true, value: @product.name),
226
+ field_format(name: :description, type: :textarea, label: "Description", value: @product.description)
227
+ ]
228
+ end
229
+
230
+ def pricing_fields
231
+ [
232
+ field_format(name: :price, type: :number, label: "Price", value: @product.price, min: 0),
233
+ field_format(name: :compare_price, type: :number, label: "Compare at Price", value: @product.compare_price)
234
+ ]
235
+ end
236
+
237
+ def settings_fields
238
+ [
239
+ field_format(name: :active, type: :checkbox, label: "Active", checked: @product.active?),
240
+ field_format(name: :featured, type: :checkbox, label: "Featured", checked: @product.featured?)
241
+ ]
242
+ end
243
+ end
244
+ ```
245
+
246
+ --------------------------------
247
+
248
+ ### Complete New Page Example
249
+
250
+ ```ruby
251
+ class Products::NewPage < FormBasePage
252
+ def initialize(product, metadata = {})
253
+ @product = product
254
+ @user = metadata[:user]
255
+ @categories = metadata[:categories] || []
256
+ super(product, metadata)
257
+ end
258
+
259
+ private
260
+
261
+ def header
262
+ {
263
+ title: "New Product",
264
+ description: "Add a new product to your catalog",
265
+ breadcrumbs: [{ label: "Products", path: products_path }, { label: "New" }]
266
+ }
267
+ end
268
+
269
+ def panels
270
+ [
271
+ panel_format(
272
+ title: "Basic Information",
273
+ description: "Enter the product details",
274
+ fields: [
275
+ field_format(name: :name, type: :text, label: "Product Name", required: true),
276
+ field_format(name: :sku, type: :text, label: "SKU"),
277
+ field_format(name: :category_id, type: :select, label: "Category", options: category_options),
278
+ field_format(name: :description, type: :textarea, label: "Description", rows: 4)
279
+ ]
280
+ ),
281
+ panel_format(
282
+ title: "Pricing & Inventory",
283
+ fields: [
284
+ field_format(name: :price, type: :number, label: "Price", required: true, min: 0, step: 0.01),
285
+ field_format(name: :stock, type: :number, label: "Stock Quantity", min: 0)
286
+ ]
287
+ ),
288
+ panel_format(
289
+ title: "Settings",
290
+ fields: [
291
+ field_format(name: :active, type: :checkbox, label: "Active"),
292
+ field_format(name: :featured, type: :checkbox, label: "Featured on Homepage")
293
+ ]
294
+ )
295
+ ]
296
+ end
297
+
298
+ def footer
299
+ {
300
+ primary_action: { label: "Create Product", style: "primary" },
301
+ secondary_actions: [{ label: "Cancel", path: products_path, style: "secondary" }]
302
+ }
303
+ end
304
+
305
+ def category_options
306
+ @categories.map { |c| { value: c.id, label: c.name } }
307
+ end
308
+ end
309
+ ```
@@ -0,0 +1,325 @@
1
+ # Building Custom Pages
2
+
3
+ A complete guide to building dashboards, reports, and other custom pages.
4
+
5
+ ### Basic Custom Page Structure
6
+
7
+ ```ruby
8
+ class Admin::DashboardPage < CustomBasePage
9
+ def initialize(data, metadata = {})
10
+ @data = data
11
+ @user = metadata[:user]
12
+ super(data, metadata)
13
+ end
14
+
15
+ private
16
+
17
+ def header
18
+ { title: "Dashboard" }
19
+ end
20
+
21
+ def content
22
+ { widgets: [] }
23
+ end
24
+ end
25
+ ```
26
+
27
+ --------------------------------
28
+
29
+ ### Adding Widgets
30
+
31
+ ```ruby
32
+ def content
33
+ {
34
+ widgets: [
35
+ widget_format(title: "Total Users", type: :counter, data: { value: @data[:users_count] }),
36
+ widget_format(title: "Total Orders", type: :counter, data: { value: @data[:orders_count] }),
37
+ widget_format(title: "Revenue", type: :counter, data: { value: format_currency(@data[:revenue]) })
38
+ ]
39
+ }
40
+ end
41
+ ```
42
+
43
+ --------------------------------
44
+
45
+ ### Using widget_format Helper
46
+
47
+ Build widgets with the helper method.
48
+
49
+ ```ruby
50
+ widget_format(
51
+ title: "Active Users",
52
+ type: :counter, # :counter, :chart, :list, :table
53
+ data: { value: 1234 },
54
+ color: "blue",
55
+ icon: "users"
56
+ )
57
+ ```
58
+
59
+ --------------------------------
60
+
61
+ ### Adding Charts
62
+
63
+ ```ruby
64
+ def content
65
+ {
66
+ widgets: [
67
+ chart_format(
68
+ title: "Revenue Over Time",
69
+ type: :line,
70
+ data: {
71
+ labels: @data[:months],
72
+ datasets: [
73
+ { label: "Revenue", data: @data[:revenue_by_month], color: "blue" }
74
+ ]
75
+ }
76
+ )
77
+ ]
78
+ }
79
+ end
80
+ ```
81
+
82
+ --------------------------------
83
+
84
+ ### Using chart_format Helper
85
+
86
+ Build charts with the helper method.
87
+
88
+ ```ruby
89
+ chart_format(
90
+ title: "Sales Chart",
91
+ type: :line, # :line, :bar, :pie, :doughnut, :area
92
+ data: {
93
+ labels: ["Jan", "Feb", "Mar", "Apr"],
94
+ datasets: [
95
+ { label: "Sales", data: [100, 200, 150, 300], color: "blue" },
96
+ { label: "Returns", data: [10, 20, 15, 30], color: "red" }
97
+ ]
98
+ }
99
+ )
100
+ ```
101
+
102
+ --------------------------------
103
+
104
+ ### Chart Types
105
+
106
+ ```ruby
107
+ # Line Chart
108
+ chart_format(title: "Trend", type: :line, data: chart_data)
109
+
110
+ # Bar Chart
111
+ chart_format(title: "Comparison", type: :bar, data: chart_data)
112
+
113
+ # Pie Chart
114
+ chart_format(title: "Distribution", type: :pie, data: pie_data)
115
+
116
+ # Doughnut Chart
117
+ chart_format(title: "Breakdown", type: :doughnut, data: pie_data)
118
+
119
+ # Area Chart
120
+ chart_format(title: "Growth", type: :area, data: chart_data)
121
+ ```
122
+
123
+ --------------------------------
124
+
125
+ ### Adding List Widget
126
+
127
+ ```ruby
128
+ def content
129
+ {
130
+ widgets: [
131
+ widget_format(
132
+ title: "Recent Orders",
133
+ type: :list,
134
+ data: {
135
+ items: @data[:recent_orders].map do |order|
136
+ {
137
+ title: "Order ##{order.id}",
138
+ subtitle: order.customer_name,
139
+ value: format_currency(order.total),
140
+ path: order_path(order)
141
+ }
142
+ end
143
+ }
144
+ )
145
+ ]
146
+ }
147
+ end
148
+ ```
149
+
150
+ --------------------------------
151
+
152
+ ### Adding Table Widget
153
+
154
+ ```ruby
155
+ def content
156
+ {
157
+ widgets: [
158
+ widget_format(
159
+ title: "Top Products",
160
+ type: :table,
161
+ data: {
162
+ columns: [
163
+ { key: :name, label: "Product" },
164
+ { key: :sales, label: "Sales" },
165
+ { key: :revenue, label: "Revenue" }
166
+ ],
167
+ rows: @data[:top_products]
168
+ }
169
+ )
170
+ ]
171
+ }
172
+ end
173
+ ```
174
+
175
+ --------------------------------
176
+
177
+ ### Dashboard with Grid Layout
178
+
179
+ ```ruby
180
+ def content
181
+ {
182
+ layout: :grid,
183
+ columns: 3,
184
+ widgets: [
185
+ # Row 1: Statistics
186
+ widget_format(title: "Users", type: :counter, data: { value: @data[:users] }),
187
+ widget_format(title: "Orders", type: :counter, data: { value: @data[:orders] }),
188
+ widget_format(title: "Revenue", type: :counter, data: { value: @data[:revenue] }),
189
+
190
+ # Row 2: Charts (span 2 columns)
191
+ chart_format(title: "Sales Trend", type: :line, data: sales_chart_data, span: 2),
192
+ chart_format(title: "Categories", type: :pie, data: category_chart_data, span: 1),
193
+
194
+ # Row 3: Lists
195
+ widget_format(title: "Recent Orders", type: :list, data: orders_data, span: 2),
196
+ widget_format(title: "Top Products", type: :list, data: products_data, span: 1)
197
+ ]
198
+ }
199
+ end
200
+ ```
201
+
202
+ --------------------------------
203
+
204
+ ### Reports Page Example
205
+
206
+ ```ruby
207
+ class Reports::SalesPage < CustomBasePage
208
+ def initialize(report_data, metadata = {})
209
+ @report_data = report_data
210
+ @user = metadata[:user]
211
+ @period = metadata[:period]
212
+ super(report_data, metadata)
213
+ end
214
+
215
+ private
216
+
217
+ def header
218
+ {
219
+ title: "Sales Report",
220
+ description: "Sales performance for #{@period}",
221
+ actions: [
222
+ { label: "Export PDF", path: export_report_path(format: :pdf), icon: "download" },
223
+ { label: "Export CSV", path: export_report_path(format: :csv), icon: "file" }
224
+ ]
225
+ }
226
+ end
227
+
228
+ def content
229
+ {
230
+ widgets: [
231
+ # Summary statistics
232
+ widget_format(title: "Total Sales", type: :counter, data: { value: @report_data[:total_sales] }),
233
+ widget_format(title: "Orders", type: :counter, data: { value: @report_data[:order_count] }),
234
+ widget_format(title: "Avg Order Value", type: :counter, data: { value: @report_data[:avg_order] }),
235
+
236
+ # Charts
237
+ chart_format(title: "Daily Sales", type: :line, data: daily_sales_data),
238
+ chart_format(title: "Sales by Category", type: :pie, data: category_data),
239
+
240
+ # Tables
241
+ widget_format(title: "Top Selling Products", type: :table, data: top_products_data)
242
+ ]
243
+ }
244
+ end
245
+
246
+ def daily_sales_data
247
+ {
248
+ labels: @report_data[:dates],
249
+ datasets: [{ label: "Sales", data: @report_data[:daily_totals], color: "blue" }]
250
+ }
251
+ end
252
+
253
+ def category_data
254
+ {
255
+ labels: @report_data[:categories].keys,
256
+ datasets: [{ data: @report_data[:categories].values }]
257
+ }
258
+ end
259
+
260
+ def top_products_data
261
+ {
262
+ columns: [
263
+ { key: :name, label: "Product" },
264
+ { key: :quantity, label: "Qty Sold" },
265
+ { key: :revenue, label: "Revenue" }
266
+ ],
267
+ rows: @report_data[:top_products]
268
+ }
269
+ end
270
+ end
271
+ ```
272
+
273
+ --------------------------------
274
+
275
+ ### Complete Dashboard Example
276
+
277
+ ```ruby
278
+ class Admin::DashboardPage < CustomBasePage
279
+ def initialize(stats, metadata = {})
280
+ @stats = stats
281
+ @user = metadata[:user]
282
+ super(stats, metadata)
283
+ end
284
+
285
+ private
286
+
287
+ def header
288
+ {
289
+ title: "Dashboard",
290
+ description: "Welcome back, #{@user.name}"
291
+ }
292
+ end
293
+
294
+ def content
295
+ {
296
+ layout: :grid,
297
+ columns: 4,
298
+ widgets: statistics_widgets + chart_widgets + list_widgets
299
+ }
300
+ end
301
+
302
+ def statistics_widgets
303
+ [
304
+ widget_format(title: "Users", type: :counter, data: { value: @stats[:users], change: "+12%" }, icon: "users", color: "blue"),
305
+ widget_format(title: "Orders", type: :counter, data: { value: @stats[:orders], change: "+8%" }, icon: "cart", color: "green"),
306
+ widget_format(title: "Revenue", type: :counter, data: { value: format_currency(@stats[:revenue]), change: "+15%" }, icon: "dollar", color: "purple"),
307
+ widget_format(title: "Conversion", type: :counter, data: { value: "#{@stats[:conversion]}%", change: "-2%" }, icon: "chart", color: "yellow")
308
+ ]
309
+ end
310
+
311
+ def chart_widgets
312
+ [
313
+ chart_format(title: "Revenue Trend", type: :area, data: revenue_chart_data, span: 2),
314
+ chart_format(title: "Orders by Status", type: :doughnut, data: orders_chart_data, span: 2)
315
+ ]
316
+ end
317
+
318
+ def list_widgets
319
+ [
320
+ widget_format(title: "Recent Orders", type: :list, data: { items: recent_orders }, span: 2),
321
+ widget_format(title: "Low Stock", type: :list, data: { items: low_stock_items }, span: 2)
322
+ ]
323
+ end
324
+ end
325
+ ```