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,192 @@
1
+ # Component Registry
2
+
3
+ BetterPage uses a hybrid component registration system with three levels.
4
+
5
+ ### Registration Levels
6
+
7
+ Components can be registered at three levels:
8
+
9
+ 1. **Global Configuration** - In `config/initializers/better_page.rb`
10
+ 2. **Base Page Classes** - In local base pages like `IndexBasePage`
11
+ 3. **Individual Pages** - In specific page classes
12
+
13
+ --------------------------------
14
+
15
+ ### Global Configuration (Initializer)
16
+
17
+ Register components globally in your initializer. These are available to all pages of the mapped type.
18
+
19
+ ```ruby
20
+ # config/initializers/better_page.rb
21
+ BetterPage.configure do |config|
22
+ # Add a custom component with schema
23
+ config.register_component :sidebar, default: { enabled: false } do
24
+ optional(:enabled).filled(:bool)
25
+ optional(:items).array(:hash)
26
+ end
27
+
28
+ # Map to page types
29
+ config.allow_components :index, :sidebar
30
+ config.allow_components :show, :sidebar
31
+
32
+ # Make required for specific page types
33
+ config.require_components :index, :sidebar
34
+ end
35
+ ```
36
+
37
+ --------------------------------
38
+
39
+ ### Base Page Classes (page_type DSL)
40
+
41
+ Local base classes use `page_type` to inherit components from global configuration.
42
+
43
+ ```ruby
44
+ # app/pages/index_base_page.rb
45
+ class IndexBasePage < ApplicationPage
46
+ page_type :index
47
+
48
+ # Add component only for index pages
49
+ register_component :quick_filters, default: []
50
+ end
51
+ ```
52
+
53
+ --------------------------------
54
+
55
+ ### Individual Pages
56
+
57
+ Register components specific to a single page.
58
+
59
+ ```ruby
60
+ class Admin::Users::IndexPage < IndexBasePage
61
+ # Component only for this page
62
+ register_component :user_stats, default: nil
63
+
64
+ def user_stats
65
+ { active_count: @users.active.count }
66
+ end
67
+ end
68
+ ```
69
+
70
+ --------------------------------
71
+
72
+ ### Register Component with Schema
73
+
74
+ Use dry-schema for validation. Required components raise errors if nil.
75
+
76
+ ```ruby
77
+ register_component :header, required: true do
78
+ required(:title).filled(:string)
79
+ optional(:description).filled(:string)
80
+ optional(:breadcrumbs).array(:hash)
81
+ optional(:actions).array(:hash)
82
+ end
83
+ ```
84
+
85
+ --------------------------------
86
+
87
+ ### Register Optional Component with Default
88
+
89
+ Optional components use default values when method is not defined.
90
+
91
+ ```ruby
92
+ register_component :alerts, default: []
93
+
94
+ register_component :pagination, default: { enabled: false } do
95
+ optional(:enabled).filled(:bool)
96
+ optional(:page).filled(:integer)
97
+ optional(:total_pages).filled(:integer)
98
+ end
99
+ ```
100
+
101
+ --------------------------------
102
+
103
+ ### Implement Component Methods
104
+
105
+ Define methods matching the component names in your page class.
106
+
107
+ ```ruby
108
+ class Admin::Users::IndexPage < IndexBasePage
109
+ def initialize(users, current_user)
110
+ @users = users
111
+ @current_user = current_user
112
+ end
113
+
114
+ private
115
+
116
+ def header
117
+ { title: "Users", breadcrumbs: breadcrumbs_config }
118
+ end
119
+
120
+ def table
121
+ { items: @users, columns: columns_config }
122
+ end
123
+
124
+ # Optional - uses default [] if not defined
125
+ def statistics
126
+ [{ label: "Total", value: @users.count, icon: "users" }]
127
+ end
128
+ end
129
+ ```
130
+
131
+ --------------------------------
132
+
133
+ ### Build Page Method
134
+
135
+ The build_page method collects all registered components.
136
+
137
+ ```ruby
138
+ def index
139
+ build_page
140
+ end
141
+
142
+ # Returns:
143
+ # {
144
+ # header: { title: "Users", breadcrumbs: [...] },
145
+ # table: { items: [...], columns: [...] },
146
+ # statistics: [...],
147
+ # pagination: { enabled: false },
148
+ # alerts: [],
149
+ # ...
150
+ # }
151
+ ```
152
+
153
+ --------------------------------
154
+
155
+ ### Component Inheritance
156
+
157
+ Components are inherited through the class hierarchy and merged with global configuration.
158
+
159
+ ```ruby
160
+ # Global configuration provides components for each page_type
161
+ # app/pages/index_base_page.rb inherits from ApplicationPage and uses page_type :index
162
+ # Individual pages inherit from their base class
163
+
164
+ class Admin::Products::IndexPage < IndexBasePage
165
+ # Gets components from:
166
+ # 1. Global config mapped to :index (header, table, statistics, pagination, etc.)
167
+ # 2. IndexBasePage local components
168
+ # 3. ApplicationPage components (alerts, footer)
169
+ end
170
+ ```
171
+
172
+ ### Check for New Components
173
+
174
+ When upgrading BetterPage, check for new default components:
175
+
176
+ ```bash
177
+ rails generate better_page:sync
178
+ ```
179
+
180
+ --------------------------------
181
+
182
+ ### Validation Error Handling
183
+
184
+ Validation errors raise in development, log warnings in production.
185
+
186
+ ```ruby
187
+ # Development - raises exception
188
+ BetterPage::ValidationError: Component :header validation failed: {:title=>["is missing"]}
189
+
190
+ # Production - logs warning
191
+ [BetterPage] Component :header validation failed: {:title=>["is missing"]}
192
+ ```
@@ -0,0 +1,238 @@
1
+ # Base Pages Reference
2
+
3
+ ### Page Types Overview
4
+
5
+ BetterPage provides four base page classes, generated locally in your `app/pages/` folder.
6
+
7
+ | Type | Base Class | Required Components | Use Case |
8
+ |------|-----------|---------------------|----------|
9
+ | Index | `IndexBasePage` | `header`, `table` | List views |
10
+ | Show | `ShowBasePage` | `header` | Detail views |
11
+ | Form | `FormBasePage` | `header`, `panels` | New/Edit forms |
12
+ | Custom | `CustomBasePage` | `content` | Dashboards, reports |
13
+
14
+ ### Inheritance Hierarchy
15
+
16
+ ```
17
+ BetterPage::BasePage (gem)
18
+
19
+
20
+ ApplicationPage (app/pages/)
21
+
22
+ ├── IndexBasePage (page_type :index)
23
+ ├── ShowBasePage (page_type :show)
24
+ ├── FormBasePage (page_type :form)
25
+ └── CustomBasePage (page_type :custom)
26
+ ```
27
+
28
+ --------------------------------
29
+
30
+ ### IndexBasePage Components
31
+
32
+ Components available for list/index pages.
33
+
34
+ | Component | Required | Default | Description |
35
+ |-----------|----------|---------|-------------|
36
+ | `header` | Yes | - | Page header with title, breadcrumbs, actions |
37
+ | `table` | Yes | - | Table with items, columns, actions |
38
+ | `alerts` | No | `[]` | Alert messages |
39
+ | `statistics` | No | `[]` | Statistic cards |
40
+ | `metrics` | No | `[]` | Metric displays |
41
+ | `tabs` | No | `{ enabled: false }` | Tab navigation |
42
+ | `search` | No | `{ enabled: false }` | Search configuration |
43
+ | `pagination` | No | `{ enabled: false }` | Pagination settings |
44
+ | `footer` | No | `{ enabled: false }` | Footer section |
45
+
46
+ --------------------------------
47
+
48
+ ### IndexBasePage Example
49
+
50
+ ```ruby
51
+ class Admin::Users::IndexPage < IndexBasePage
52
+ def initialize(users, metadata = {})
53
+ @users = users
54
+ @user = metadata[:user]
55
+ super(users, metadata)
56
+ end
57
+
58
+ private
59
+
60
+ def header
61
+ { title: "Users", actions: [{ label: "New", path: new_admin_user_path }] }
62
+ end
63
+
64
+ def table
65
+ { items: @users, columns: table_columns, empty_state: empty_config }
66
+ end
67
+
68
+ def statistics
69
+ [
70
+ { label: "Total", value: @users.size, icon: "users" },
71
+ { label: "Active", value: @users.count(&:active?), icon: "check" }
72
+ ]
73
+ end
74
+
75
+ def pagination
76
+ { enabled: true, page: 1, total_pages: 10 }
77
+ end
78
+ end
79
+ ```
80
+
81
+ --------------------------------
82
+
83
+ ### ShowBasePage Components
84
+
85
+ Components available for detail/show pages.
86
+
87
+ | Component | Required | Default | Description |
88
+ |-----------|----------|---------|-------------|
89
+ | `header` | Yes | - | Page header with title, metadata, actions |
90
+ | `alerts` | No | `[]` | Alert messages |
91
+ | `statistics` | No | `[]` | Statistic cards |
92
+ | `overview` | No | `{ enabled: false }` | Overview section |
93
+ | `content_sections` | No | `[]` | Content sections (info grids, text) |
94
+ | `footer` | No | `{ enabled: false }` | Footer section |
95
+
96
+ --------------------------------
97
+
98
+ ### ShowBasePage Helper Methods
99
+
100
+ ```ruby
101
+ # Convert hash to info grid format
102
+ info_grid_content_format({ "Name" => "John", "Email" => "john@example.com" })
103
+ # => [{ name: "Name", value: "John" }, { name: "Email", value: "john@example.com" }]
104
+
105
+ # Build content section
106
+ content_section_format(
107
+ title: "Details",
108
+ icon: "info",
109
+ color: "blue",
110
+ type: :info_grid,
111
+ content: { "Name" => "John" }
112
+ )
113
+
114
+ # Build statistic
115
+ statistic_format(label: "Total", value: 100, icon: "users", color: "blue")
116
+
117
+ # Build action button
118
+ action_format(path: edit_path, label: "Edit", icon: "edit", style: "primary")
119
+ ```
120
+
121
+ --------------------------------
122
+
123
+ ### FormBasePage Components
124
+
125
+ Components available for new/edit form pages.
126
+
127
+ | Component | Required | Default | Description |
128
+ |-----------|----------|---------|-------------|
129
+ | `header` | Yes | - | Form header with title, description |
130
+ | `panels` | Yes | - | Form panels with fields |
131
+ | `alerts` | No | `[]` | Alert messages |
132
+ | `errors` | No | `nil` | Custom error configuration |
133
+ | `footer` | No | `{ primary_action: {...} }` | Form footer with buttons |
134
+
135
+ --------------------------------
136
+
137
+ ### FormBasePage Helper Methods
138
+
139
+ ```ruby
140
+ # Build a field
141
+ field_format(name: :email, type: :email, label: "Email", required: true)
142
+
143
+ # Build a panel
144
+ panel_format(title: "Basic Info", fields: [...], description: "Enter details")
145
+ ```
146
+
147
+ **Important**: Checkbox and radio fields must be in separate panels.
148
+
149
+ --------------------------------
150
+
151
+ ### FormBasePage Example
152
+
153
+ ```ruby
154
+ class Admin::Users::NewPage < FormBasePage
155
+ def initialize(user, metadata = {})
156
+ @user = user
157
+ @current_user = metadata[:user]
158
+ super(user, metadata)
159
+ end
160
+
161
+ private
162
+
163
+ def header
164
+ { title: "New User", description: "Create a new user account" }
165
+ end
166
+
167
+ def panels
168
+ [
169
+ panel_format(
170
+ title: "Account Details",
171
+ fields: [
172
+ field_format(name: :email, type: :email, label: "Email", required: true),
173
+ field_format(name: :name, type: :text, label: "Name", required: true)
174
+ ]
175
+ ),
176
+ panel_format(
177
+ title: "Settings",
178
+ fields: [
179
+ field_format(name: :active, type: :checkbox, label: "Active")
180
+ ]
181
+ )
182
+ ]
183
+ end
184
+ end
185
+ ```
186
+
187
+ --------------------------------
188
+
189
+ ### CustomBasePage Components
190
+
191
+ Components available for custom pages.
192
+
193
+ | Component | Required | Default | Description |
194
+ |-----------|----------|---------|-------------|
195
+ | `header` | No | `nil` | Optional page header |
196
+ | `content` | Yes | - | Custom content configuration |
197
+ | `footer` | No | `nil` | Optional footer |
198
+
199
+ --------------------------------
200
+
201
+ ### CustomBasePage Helper Methods
202
+
203
+ ```ruby
204
+ # Build a widget
205
+ widget_format(title: "Stats", type: :chart, data: {...})
206
+
207
+ # Build a chart
208
+ chart_format(title: "Revenue", type: :line, data: { labels: [...], datasets: [...] })
209
+ ```
210
+
211
+ --------------------------------
212
+
213
+ ### CustomBasePage Example
214
+
215
+ ```ruby
216
+ class Admin::DashboardPage < CustomBasePage
217
+ def initialize(data, metadata = {})
218
+ @data = data
219
+ @user = metadata[:user]
220
+ super(data, metadata)
221
+ end
222
+
223
+ private
224
+
225
+ def header
226
+ { title: "Dashboard" }
227
+ end
228
+
229
+ def content
230
+ {
231
+ widgets: [
232
+ widget_format(title: "Users", type: :counter, data: { value: @data[:users_count] }),
233
+ chart_format(title: "Revenue", type: :line, data: @data[:revenue_chart])
234
+ ]
235
+ }
236
+ end
237
+ end
238
+ ```
@@ -0,0 +1,180 @@
1
+ # Schema Validation
2
+
3
+ BetterPage uses [dry-schema](https://dry-rb.org/gems/dry-schema/) for component validation.
4
+
5
+ ### String Type
6
+
7
+ ```ruby
8
+ register_component :header, required: true do
9
+ required(:title).filled(:string)
10
+ optional(:subtitle).filled(:string)
11
+ end
12
+ ```
13
+
14
+ --------------------------------
15
+
16
+ ### Integer Type
17
+
18
+ ```ruby
19
+ register_component :pagination do
20
+ optional(:page).filled(:integer)
21
+ optional(:per_page).filled(:integer)
22
+ end
23
+ ```
24
+
25
+ --------------------------------
26
+
27
+ ### Boolean Type
28
+
29
+ ```ruby
30
+ register_component :tabs do
31
+ optional(:enabled).filled(:bool)
32
+ end
33
+ ```
34
+
35
+ --------------------------------
36
+
37
+ ### Array Type
38
+
39
+ ```ruby
40
+ register_component :header do
41
+ optional(:breadcrumbs).array(:hash)
42
+ optional(:actions).array(:hash)
43
+ end
44
+ ```
45
+
46
+ --------------------------------
47
+
48
+ ### Hash Type with Nested Schema
49
+
50
+ ```ruby
51
+ register_component :table do
52
+ optional(:empty_state).hash do
53
+ optional(:icon).filled(:string)
54
+ optional(:title).filled(:string)
55
+ optional(:message).filled(:string)
56
+ end
57
+ end
58
+ ```
59
+
60
+ --------------------------------
61
+
62
+ ### Required vs Optional Fields
63
+
64
+ ```ruby
65
+ # Must be present and non-empty
66
+ required(:title).filled(:string)
67
+
68
+ # If present, must be non-empty string
69
+ optional(:subtitle).filled(:string)
70
+
71
+ # Array of hash objects
72
+ optional(:columns).array(:hash)
73
+ ```
74
+
75
+ --------------------------------
76
+
77
+ ### Complex Nested Structure
78
+
79
+ ```ruby
80
+ register_component :footer do
81
+ optional(:primary_action).hash do
82
+ required(:label).filled(:string)
83
+ optional(:style).filled(:string)
84
+ end
85
+ optional(:secondary_actions).array(:hash)
86
+ end
87
+ ```
88
+
89
+ --------------------------------
90
+
91
+ ### Header Component Schema Example
92
+
93
+ ```ruby
94
+ register_component :header, required: true do
95
+ required(:title).filled(:string)
96
+ optional(:description).filled(:string)
97
+ optional(:breadcrumbs).array(:hash)
98
+ optional(:metadata).array(:hash)
99
+ optional(:actions).array(:hash)
100
+ end
101
+ ```
102
+
103
+ Valid:
104
+
105
+ ```ruby
106
+ def header
107
+ {
108
+ title: "Users",
109
+ breadcrumbs: [
110
+ { label: "Home", path: "/" },
111
+ { label: "Users", path: "/users" }
112
+ ],
113
+ actions: [
114
+ { label: "New", path: "/users/new", icon: "plus" }
115
+ ]
116
+ }
117
+ end
118
+ ```
119
+
120
+ --------------------------------
121
+
122
+ ### Table Component Schema Example
123
+
124
+ ```ruby
125
+ register_component :table, required: true do
126
+ required(:items).value(:array)
127
+ optional(:columns).array(:hash)
128
+ optional(:actions)
129
+ optional(:empty_state).hash do
130
+ optional(:icon).filled(:string)
131
+ optional(:title).filled(:string)
132
+ optional(:message).filled(:string)
133
+ optional(:action).hash
134
+ end
135
+ end
136
+ ```
137
+
138
+ Valid:
139
+
140
+ ```ruby
141
+ def table
142
+ {
143
+ items: @users,
144
+ columns: [
145
+ { key: :name, label: "Name", type: :link },
146
+ { key: :email, label: "Email", type: :text }
147
+ ],
148
+ empty_state: {
149
+ icon: "users",
150
+ title: "No users found",
151
+ message: "Create your first user to get started"
152
+ }
153
+ }
154
+ end
155
+ ```
156
+
157
+ --------------------------------
158
+
159
+ ### Validation Error Handling
160
+
161
+ ```ruby
162
+ # Development - raises exception
163
+ BetterPage::ValidationError: Component :header validation failed: {:title=>["is missing"]}
164
+
165
+ # Production - logs warning
166
+ [BetterPage] Component :header validation failed: {:title=>["is missing"]}
167
+ ```
168
+
169
+ --------------------------------
170
+
171
+ ### Components Without Schema
172
+
173
+ Components can be registered without schema validation.
174
+
175
+ ```ruby
176
+ register_component :alerts, default: []
177
+ register_component :custom_data, default: nil
178
+ ```
179
+
180
+ These components accept any value without validation.