ruby_cms 0.1.1 → 0.1.2

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/README.md +68 -164
  4. data/app/components/ruby_cms/admin/admin_page.rb +19 -19
  5. data/app/components/ruby_cms/admin/admin_page_header.rb +81 -0
  6. data/app/components/ruby_cms/admin/admin_resource_card.rb +55 -0
  7. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table.rb +4 -4
  8. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_actions.rb +5 -5
  9. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_body.rb +1 -1
  10. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_cell.rb +15 -13
  11. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_head.rb +13 -11
  12. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_delete_modal.rb +9 -9
  13. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header.rb +2 -2
  14. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header_bar.rb +8 -8
  15. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_pagination.rb +9 -9
  16. data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_row.rb +3 -4
  17. data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +25 -24
  18. data/app/controllers/ruby_cms/admin/base_controller.rb +10 -4
  19. data/app/controllers/ruby_cms/admin/content_blocks_controller.rb +4 -3
  20. data/app/controllers/ruby_cms/admin/locale_controller.rb +2 -1
  21. data/app/controllers/ruby_cms/admin/user_permissions_controller.rb +25 -7
  22. data/app/helpers/ruby_cms/settings_helper.rb +19 -9
  23. data/app/javascript/controllers/ruby_cms/bulk_action_table_controller.js +53 -12
  24. data/app/models/ruby_cms/permission.rb +38 -9
  25. data/app/models/ruby_cms/permittable.rb +0 -2
  26. data/app/views/layouts/ruby_cms/_admin_sidebar.html.erb +2 -2
  27. data/app/views/layouts/ruby_cms/admin.html.erb +13 -17
  28. data/app/views/ruby_cms/admin/content_blocks/index.html.erb +0 -11
  29. data/app/views/ruby_cms/admin/content_blocks/show.html.erb +204 -85
  30. data/app/views/ruby_cms/admin/settings/index.html.erb +214 -175
  31. data/app/views/ruby_cms/admin/user_permissions/index.html.erb +32 -2
  32. data/app/views/ruby_cms/admin/users/_row.html.erb +4 -1
  33. data/config/locales/en.yml +4 -0
  34. data/lib/ruby_cms/engine.rb +20 -12
  35. data/lib/ruby_cms/version.rb +1 -1
  36. data/lib/ruby_cms.rb +24 -0
  37. metadata +4 -3
  38. data/app/views/ruby_cms/admin/content_blocks/edit.html.erb +0 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ebf960fc18b9e0b70b1745056eaadd2e352b95c940d4e3cb5cbda3c37c6d816b
4
- data.tar.gz: 2828ff26dbd77bfcbac8ee44e4d94241a6498c6d476187910d8e272afdef240c
3
+ metadata.gz: e1a4b15129e38c223a849cc78aff0bd173af8a104a07acc0f7195298475f4420
4
+ data.tar.gz: a9ea71bfc0ddcb2a89ba1ec85acd3fd7f9cc03799aa91e0e1eb805a081b1b6aa
5
5
  SHA512:
6
- metadata.gz: f4f531ef56ebe15593ced939e0a459b812e183563e78aa729b39dddbaea82f0d8f786ece925e9c591d567608fe59cf60fa14faa2a93a26f1a52d3d8c71b3eb15
7
- data.tar.gz: 0d4de8e1de20b7d4fe9c6746ddcab2ec89b59de49cbc6016412c58467b16855f7e3112a1981b78904d9f9454da720d560869b2446500dcc8c457b107b7853431
6
+ metadata.gz: a2b7113dbd5bae7fd3233398459efca6c3f497f54d22f63d9a4b7db0577d27cb4db73abf4ebb93b3f981ca5f33d34f458af08bc75bf74bb0dfb1f5cf02783625
7
+ data.tar.gz: 33442c384ce70c04a1a7420632225e3a282c9929b29d28a76b92dcb76dd19d5ab15957e0c4b70bfca34dcb59b1f5ab72d7c2e388b0d2b50913dc9f67d3a30350
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.2] - 2026-03-23
4
+
5
+ - Update settings page and the bulk action table
6
+ - Combined the permissions
7
+
3
8
  ## [0.1.1] - 2026-03-18
4
9
 
5
10
  - Fix admin templates calling `AdminPage(...)` instead of the `admin_page` helper
data/README.md CHANGED
@@ -1,53 +1,33 @@
1
1
  # RubyCMS
2
2
 
3
- Reusable Rails engine: admin-only auth, permissions, admin shell, content blocks, and visual editor.
3
+ Reusable Rails engine for a CMS-style admin: permissions, admin UI shell, content blocks, and a visual editor.
4
4
 
5
- **Vision:** The CMS manages content (content blocks, visual editor); the programmer builds the SaaS product (auth, billing, dashboards, etc.). You define pages and templates in your app; you edit content using the visual editor.
5
+ Vision: your app owns product features (pages, models, business logic); RubyCMS manages content workflows and admin screens.
6
6
 
7
7
  ## Features
8
8
 
9
- - **Visual Editor** - Inline editing of content blocks
10
- - **Content Blocks** - Reusable content snippets with rich text support
11
- - **Permissions** - Fine-grained permission system
12
- - **Users** - User management with permission assignments
13
- - **Visitor Error Tracking** - Automatic exception logging with admin interface
14
- - **Page View Tracking** - Ahoy-based analytics for page views and events
9
+ * Visual editor (inline editing for `content_block` regions)
10
+ * Content blocks (rich text + placeholders + list items)
11
+ * Permissions and users (admin access control)
12
+ * Visitor error tracking (`/admin/visitor_errors`)
13
+ * Analytics via Ahoy (page views + events)
15
14
 
16
- ## Documentation
17
-
18
- - **[Installation](#installation)** - Get started with RubyCMS
19
- - **[Usage](#usage)** - Basic usage examples
20
-
21
- ## Installation {#installation}
22
-
23
- ### 1. Create a Rails app (or use an existing one)
24
-
25
- ```bash
26
- rails new my_cms_app -d sqlite3
27
- cd my_cms_app
28
- ```
29
-
30
- ### 2. Install RubyCMS
15
+ ## Quick Start
31
16
 
32
17
  ```bash
33
18
  rails g ruby_cms:install
34
19
  ```
35
20
 
36
- This generator:
37
-
38
- - configures RubyCMS (`config/initializers/ruby_cms.rb`) and mounts the engine,
39
- - ensures authentication is present (generates `User`, `Session`, and `Authentication` on Rails 8+ apps that need it),
40
- - installs Action Text / Active Storage when missing and runs the required `db:migrate`,
41
- - creates the RubyCMS tables and seeds permissions,
42
- - guides you through picking or creating the first admin user and granting CMS permissions.
43
-
44
- ### 3. Resolve route conflicts
21
+ The generator sets up:
45
22
 
46
- If the app already has `/admin` routes, remove or change them so RubyCMS can use `/admin`. The install adds `mount RubyCms::Engine => "/"`; keep it after your main routes (e.g. `root`, `resources`) so it doesn’t override them.
23
+ * `config/initializers/ruby_cms.rb`
24
+ * mounts the engine (`/admin/...` on the host app)
25
+ * migrations + RubyCMS tables
26
+ * seed permissions + initial admin setup
47
27
 
48
- ## Usage
28
+ If your host app already has `/admin` routes, adjust/remove them so RubyCMS can use `/admin`.
49
29
 
50
- ### Content blocks
30
+ ## Using Content Blocks
51
31
 
52
32
  In any view:
53
33
 
@@ -56,20 +36,26 @@ In any view:
56
36
  <%= content_block("footer", cache: true) %>
57
37
  ```
58
38
 
59
- **Important:** For **placeholders** (input `placeholder`, `alt`, meta tags), use `wrap: false` or `content_block_text`. The `content_block` helper normally wraps content in a `<span>` for the visual editor; that HTML must not go into placeholder attributes:
39
+ <details>
40
+ <summary>Placeholders (attributes like `placeholder`, `alt`, meta tags)</summary>
41
+
42
+ `content_block` wraps output for the visual editor, so do not put it inside HTML attributes.
43
+ Use `wrap: false` (or `content_block_text`):
60
44
 
61
45
  ```erb
62
- <%= text_field_tag :name, nil, placeholder: content_block("contact.name_placeholder", wrap: false, fallback: "Your name") %>
63
- <%= text_area_tag :message, nil, placeholder: content_block("contact.message_placeholder", wrap: false, fallback: "Your message...") %>
46
+ <%= text_field_tag :name, nil,
47
+ placeholder: content_block("contact.name_placeholder", wrap: false, fallback: "Your name") %>
48
+
49
+ <%= text_area_tag :message, nil,
50
+ placeholder: content_block_text("contact.message_placeholder", fallback: "Your message...") %>
64
51
  ```
65
52
 
66
- Or use `content_block_text` (equivalent to `content_block(..., wrap: false)`):
53
+ </details>
67
54
 
68
- ```erb
69
- <%= text_field_tag :name, nil, placeholder: content_block_text("contact.name_placeholder", fallback: "Your name") %>
70
- ```
55
+ <details>
56
+ <summary>List items (badges, tags, arrays)</summary>
71
57
 
72
- For **lists** (badges, tags) that you need to iterate over, use `content_block_list_items`—it returns an array instead of HTML:
58
+ Use `content_block_list_items` to get an Array:
73
59
 
74
60
  ```erb
75
61
  <% content_block_list_items("education.item.badges", fallback: item[:badges]).each do |badge| %>
@@ -79,157 +65,75 @@ For **lists** (badges, tags) that you need to iterate over, use `content_block_l
79
65
 
80
66
  Store list content as JSON (`["Ruby", "Rails"]`) or newline-separated text in the CMS.
81
67
 
82
- Create and edit blocks under **Admin → Content blocks**.
83
-
84
- ### Seeding content blocks from YAML
85
-
86
- 1. In `config/initializers/ruby_cms.rb`, set the translation namespace (the install generator sets this by default):
87
-
88
- ```ruby
89
- c.content_blocks_translation_namespace = "content_blocks"
90
- ```
91
-
92
- 2. Add content under that key in your locale files (e.g. `config/locales/en.yml`):
93
-
94
- ```yaml
95
- en:
96
- content_blocks:
97
- hero_title: "Welcome to my site"
98
- about_intro: "We build things."
99
- footer_copyright: "© 2025"
100
- ```
101
-
102
- 3. Run the seed task to import into the database (creates/updates blocks, marks them published):
103
-
104
- ```bash
105
- rails ruby_cms:content_blocks:seed
106
- ```
107
-
108
- Or call it from `db/seeds.rb`:
109
-
110
- ```ruby
111
- Rake::Task["ruby_cms:content_blocks:seed"].invoke
112
- ```
113
-
114
- ENV overrides: `published=false` to import as unpublished; `create_missing=false` or `update_existing=false` to limit what is changed.
68
+ </details>
115
69
 
116
- ### Visual editor
70
+ Create/edit blocks in **Admin -> Content blocks**.
117
71
 
118
- 1. **Preview templates** come from `config.ruby_cms.preview_templates` in `config/initializers/ruby_cms.rb`.
72
+ ## Visual Editor
119
73
 
120
- ```ruby
121
- c.preview_templates = { "home" => "pages/home", "about" => "pages/about" }
122
- c.preview_data = ->(page_key, view) { { products: Product.limit(5) } }
123
- ```
74
+ Configure preview templates + (optional) preview data in `config/initializers/ruby_cms.rb`:
124
75
 
125
- 2. Create the view templates (e.g. `app/views/pages/home.html.erb`) and use the `content_block("key")` helper for editable regions. Wrap editable elements in `<div class="ruby_cms-content-block" data-content-key="...">`.
126
-
127
- 3. Open **Admin → Visual editor**, pick a page, and click any content block in the preview to edit in the modal.
128
-
129
- 4. **postMessage**: The preview iframe and parent communicate via postMessage for content block editing and updates.
130
-
131
- ### Visitor Error Tracking
132
-
133
- RubyCMS automatically captures unhandled exceptions from public pages (non-admin) and logs them to the `ruby_cms_visitor_errors` table.
134
-
135
- **Note**: Error logging is disabled in development environment (errors are skipped and only re-raised for normal Rails error pages).
136
-
137
- #### How it works
138
-
139
- 1. The install generator adds `RubyCms::VisitorErrorCapture` to your `ApplicationController` with `rescue_from StandardError`
140
- 2. When an exception occurs in production/staging, it's logged with full context (backtrace, request params, IP, user agent, etc.)
141
- 3. The exception is re-raised so users still see the standard error page
142
- 4. Admin users can view and manage errors at `/admin/visitor_errors`
76
+ ```ruby
77
+ c.preview_templates = { "home" => "pages/home", "about" => "pages/about" }
78
+ c.preview_data = ->(page_key, view) { { products: Product.limit(5) } }
79
+ ```
143
80
 
144
- #### What gets logged
81
+ Then open **Admin -> Visual editor**, pick a page key, and click content blocks in the preview.
145
82
 
146
- - Error class and message
147
- - Request path and method
148
- - IP address and user agent
149
- - Session ID
150
- - First 10 lines of backtrace
151
- - Sanitized request params (passwords/tokens excluded)
83
+ ## Admin UI Pages (custom admin templates)
152
84
 
153
- #### Admin interface
85
+ RubyCMS exposes an `admin_page` helper:
154
86
 
155
- Visit `/admin/visitor_errors` to:
87
+ ```erb
88
+ <%= admin_page(title: "My Page", subtitle: "Optional") do %>
89
+ <p>Hello from RubyCMS admin.</p>
90
+ <% end %>
91
+ ```
156
92
 
157
- - View all errors with filtering by path, error type, and resolved status
158
- - See full error details including backtrace and request context
159
- - Mark errors as resolved (single or bulk)
160
- - Delete errors (bulk action)
93
+ ## Visitor Error Tracking
161
94
 
162
- ### Page View Tracking (Ahoy)
95
+ Public (non-admin) exceptions are captured and shown in **`/admin/visitor_errors`**.
163
96
 
164
- RubyCMS includes Ahoy for visit and event tracking. The install generator sets up Ahoy with server-side tracking (no JavaScript required).
97
+ In development, logging is typically disabled and exceptions are re-raised normally.
165
98
 
166
- #### Tracking page views
99
+ ## Page View Tracking (Ahoy)
167
100
 
168
- Include `RubyCms::PageTracking` in your public controllers to automatically track page views:
101
+ Include `RubyCms::PageTracking` in public controllers:
169
102
 
170
103
  ```ruby
171
104
  class PagesController < ApplicationController
172
105
  include RubyCms::PageTracking
173
106
 
174
107
  def home
175
- # @page_name is set to controller_name by default
176
- # Override if needed: @page_name = "home"
108
+ # @page_name can be set; defaults vary by controller
177
109
  end
178
110
  end
179
111
  ```
180
112
 
181
- Page views are stored in `ahoy_events` with:
113
+ ## Seeding Content Blocks from YAML
182
114
 
183
- - `name: "page_view"`
184
- - `page_name`: Controller-specific identifier
185
- - `request_path`: Full request path
186
- - `visit`: Associated Ahoy visit (includes IP, user agent, browser, etc.)
187
-
188
- #### What Ahoy tracks
189
-
190
- **Visits** (`ahoy_visits` table):
191
-
192
- - Visit token (unique per session)
193
- - IP address and user agent
194
- - Browser, OS, device type
195
- - Landing page and referrer
196
- - UTM parameters
197
- - User ID (when authenticated)
198
-
199
- **Events** (`ahoy_events` table):
200
-
201
- - Event name (e.g., "page_view")
202
- - Timestamp
203
- - Associated visit
204
- - Custom properties (page_name, request_path, etc.)
205
- - User ID (when authenticated)
206
-
207
- #### Accessing analytics data
208
-
209
- Query the Ahoy tables directly or use the Ahoy gem's built-in methods:
115
+ If you want to import blocks from locales, set:
210
116
 
211
117
  ```ruby
212
- # Get all page views
213
- Ahoy::Event.where(name: "page_view")
214
-
215
- # Page views for a specific page
216
- Ahoy::Event.where(name: "page_view", page_name: "home")
118
+ c.content_blocks_translation_namespace = "content_blocks"
119
+ ```
217
120
 
218
- # Unique visitors (visits)
219
- Ahoy::Visit.count
121
+ Example `config/locales/en.yml`:
220
122
 
221
- # Page views by page
222
- Ahoy::Event.where(name: "page_view")
223
- .group(:page_name)
224
- .count
123
+ ```yaml
124
+ en:
125
+ content_blocks:
126
+ hero_title: "Welcome to my site"
225
127
  ```
226
128
 
227
- #### Architecture notes
129
+ Import:
228
130
 
229
- **Visitor Errors** and **Ahoy** are independent systems:
131
+ ```bash
132
+ rails ruby_cms:content_blocks:seed
133
+ ```
230
134
 
231
- - Visitor Errors log exceptions via `ApplicationController#rescue_from`
232
- - Ahoy tracks visits/events via Rack middleware and controller callbacks
233
- - No direct relationship between the two systems
135
+ ## Common Rake Tasks
234
136
 
235
- ---
137
+ * `rails ruby_cms:seed_permissions`
138
+ * `rails ruby_cms:setup_admin`
139
+ * `rails ruby_cms:content_blocks:seed`
@@ -68,8 +68,8 @@ module RubyCms
68
68
  end
69
69
 
70
70
  def render_breadcrumbs
71
- nav(class: "text-sm text-gray-500", aria_label: "Breadcrumb") do
72
- ol(class: "flex items-center flex-wrap gap-x-2 gap-y-1") do
71
+ nav(class: "text-sm text-muted-foreground", aria_label: "Breadcrumb") do
72
+ ol(class: "flex items-center flex-wrap gap-y-1") do
73
73
  @breadcrumbs.each_with_index do |crumb, index|
74
74
  render_breadcrumb_item(crumb, index == @breadcrumbs.size - 1)
75
75
  end
@@ -84,15 +84,15 @@ module RubyCms
84
84
  end
85
85
 
86
86
  def render_breadcrumb_current(crumb)
87
- span(class: "font-medium text-gray-900", aria_current: "page") do
87
+ span(class: "font-medium text-foreground", aria_current: "page") do
88
88
  crumb[:label] || crumb[:text]
89
89
  end
90
90
  end
91
91
 
92
92
  def render_breadcrumb_link(crumb)
93
- a(href: crumb[:url] || crumb[:path], class: "hover:text-gray-700") do
93
+ a(href: crumb[:url] || crumb[:path], class: "hover:text-foreground transition-colors") do
94
94
  span { crumb[:label] || crumb[:text] }
95
- span(class: "px-2 text-gray-300") { "/" }
95
+ span(class: "mx-1.5 text-muted-foreground/40 select-none") { "/" }
96
96
  end
97
97
  end
98
98
 
@@ -118,8 +118,8 @@ module RubyCms
118
118
  return unless @title || @subtitle
119
119
 
120
120
  div(class: "min-w-0") do
121
- h1(class: "text-lg font-semibold text-gray-900 truncate") { @title } if @title
122
- p(class: "text-sm text-gray-500 mt-0.5") { @subtitle } if @subtitle
121
+ h1(class: "text-lg font-semibold tracking-tight text-foreground truncate") { @title } if @title
122
+ p(class: "text-sm text-muted-foreground mt-0.5") { @subtitle } if @subtitle
123
123
  end
124
124
  end
125
125
 
@@ -217,8 +217,8 @@ module RubyCms
217
217
 
218
218
  def build_action_attributes(action)
219
219
  base = "inline-flex items-center justify-center rounded-lg px-3 py-2 " \
220
- "text-sm font-medium transition"
221
- secondary = "bg-white text-gray-700 ring-1 ring-gray-200 hover:bg-gray-50"
220
+ "text-sm font-medium transition-colors"
221
+ secondary = "bg-white text-foreground border border-border shadow-sm hover:bg-muted"
222
222
  variant = action_primary?(action) ? primary_action_classes : secondary
223
223
  attrs = { class: build_classes(base, variant, action[:class]) }
224
224
  attrs[:data] = action[:data] if action[:data]
@@ -251,7 +251,7 @@ module RubyCms
251
251
  div(class: "flex-1 flex flex-col min-h-0") do
252
252
  if @content_card
253
253
  div(
254
- class: "bg-white rounded-lg border border-gray-200/80 shadow-sm " \
254
+ class: "bg-white rounded-xl border border-border/60 shadow-sm ring-1 ring-black/[0.03] " \
255
255
  "p-5 sm:p-6 flex-1 flex flex-col min-h-0"
256
256
  ) { yield if block_given? }
257
257
  elsif block_given?
@@ -276,15 +276,15 @@ module RubyCms
276
276
  form_with(url: opts[:url] || "#", method: :get, class: "w-full sm:w-auto",
277
277
  data: { turbo_frame: opts[:turbo_frame] || "admin_table_content" }) do
278
278
  div(class: "relative flex items-center") do
279
- span(class: "absolute left-3 text-gray-400 pointer-events-none") do
279
+ span(class: "absolute left-3 text-muted-foreground pointer-events-none") do
280
280
  svg_icon_path("M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z")
281
281
  end
282
282
  input(
283
283
  type: "search",
284
284
  name: opts[:name] || "q",
285
285
  placeholder: opts[:placeholder] || "Search",
286
- class: "w-full sm:w-72 pl-10 pr-3 py-2 text-sm rounded-lg bg-white ring-1 " \
287
- "ring-gray-200 focus:outline-none focus:ring-2 focus:ring-teal-200",
286
+ class: "h-9 w-full sm:w-72 pl-10 pr-3 text-sm rounded-lg bg-white border border-border " \
287
+ "shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/20",
288
288
  value: opts[:value],
289
289
  data: { action: "input->turbo-frame#submit" }
290
290
  )
@@ -325,20 +325,20 @@ module RubyCms
325
325
  def icon_color_class_map
326
326
  {
327
327
  "blue" => "text-blue-600 hover:bg-blue-50",
328
- "green" => "text-green-600 hover:bg-green-50",
329
- "red" => "text-red-600 hover:bg-red-50",
330
- "purple" => "text-purple-600 hover:bg-purple-50",
331
- "gray" => "text-gray-700 hover:bg-gray-50",
328
+ "green" => "text-emerald-600 hover:bg-emerald-50",
329
+ "red" => "text-destructive hover:bg-destructive/10",
330
+ "purple" => "text-violet-600 hover:bg-violet-50",
331
+ "gray" => "text-muted-foreground hover:bg-muted",
332
332
  "teal" => "text-teal-600 hover:bg-teal-50"
333
333
  }
334
334
  end
335
335
 
336
336
  def icon_base_classes
337
- "inline-flex items-center justify-center w-9 h-9 rounded-lg"
337
+ "inline-flex items-center justify-center size-9 rounded-md transition-colors"
338
338
  end
339
339
 
340
340
  def primary_action_classes
341
- "bg-teal-600 text-white hover:bg-teal-700"
341
+ "bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm"
342
342
  end
343
343
  end
344
344
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Admin
5
+ # Reusable page header for admin pages.
6
+ # Renders breadcrumbs, title, optional subtitle, and action slot.
7
+ #
8
+ # Usage from ERB:
9
+ #
10
+ # <%= render RubyCms::Admin::AdminPageHeader.new(
11
+ # title: "Sports",
12
+ # breadcrumbs: [
13
+ # { label: "Admin", url: admin_root_path },
14
+ # { label: "Sports" }
15
+ # ]
16
+ # ) do %>
17
+ # <%= link_to new_admin_sport_path, class: "..." do %>
18
+ # + Add
19
+ # <% end %>
20
+ # <% end %>
21
+ #
22
+ class AdminPageHeader < BaseComponent
23
+ def initialize(title:, breadcrumbs: [], subtitle: nil, **options)
24
+ super()
25
+ @title = title
26
+ @breadcrumbs = Array(breadcrumbs)
27
+ @subtitle = subtitle
28
+ @header_class = options[:class]
29
+ end
30
+
31
+ def view_template(&block)
32
+ header(class: build_classes("flex-shrink-0 mb-4", @header_class)) do
33
+ render_breadcrumbs if @breadcrumbs.any?
34
+
35
+ div(class: "flex flex-wrap items-center justify-between gap-4") do
36
+ render_title_section
37
+ render_actions(&block) if block
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def render_title_section
45
+ div(class: "min-w-0") do
46
+ h1(class: "text-lg font-semibold tracking-tight text-foreground") { @title }
47
+ p(class: "text-sm text-muted-foreground mt-0.5") { @subtitle } if @subtitle
48
+ end
49
+ end
50
+
51
+ def render_breadcrumbs
52
+ nav(class: "mb-1 text-sm text-muted-foreground", aria_label: "Breadcrumb") do
53
+ ol(class: "flex items-center flex-wrap gap-y-1") do
54
+ @breadcrumbs.each_with_index do |crumb, index|
55
+ render_breadcrumb_item(crumb, last: index == @breadcrumbs.size - 1)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ def render_breadcrumb_item(crumb, last:)
62
+ li(class: "flex items-center") do
63
+ label = crumb[:label] || crumb[:text]
64
+
65
+ if last
66
+ span(class: "font-medium text-foreground") { label }
67
+ else
68
+ a(href: crumb[:url] || crumb[:path] || "#", class: "hover:text-foreground transition-colors") { label }
69
+ span(class: "mx-1.5 text-muted-foreground/40 select-none") { "/" }
70
+ end
71
+ end
72
+ end
73
+
74
+ def render_actions(&block)
75
+ div(class: "flex items-center gap-3 flex-shrink-0") do
76
+ yield
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCms
4
+ module Admin
5
+ # Reusable card wrapper for admin resource pages (combined show/edit).
6
+ # Provides a two-column layout with form fields on the left and a
7
+ # details sidebar on the right, plus an actions footer.
8
+ #
9
+ # Intended to be wrapped in a form_with tag by the view so the
10
+ # submit button in the actions footer works naturally.
11
+ #
12
+ # Usage:
13
+ #
14
+ # <%= form_with model: [:admin, @sport], local: true do |form| %>
15
+ # <div class="<%= RubyCms::Admin::AdminResourceCard::CARD_CLASS %>">
16
+ # <div class="<%= RubyCms::Admin::AdminResourceCard::GRID_CLASS %>">
17
+ # <div class="<%= RubyCms::Admin::AdminResourceCard::MAIN_CLASS %>">
18
+ # form fields...
19
+ # </div>
20
+ # <div class="<%= RubyCms::Admin::AdminResourceCard::SIDEBAR_CLASS %>">
21
+ # details...
22
+ # </div>
23
+ # </div>
24
+ # <div class="<%= RubyCms::Admin::AdminResourceCard::ACTIONS_CLASS %>">
25
+ # cancel / save
26
+ # </div>
27
+ # </div>
28
+ # <% end %>
29
+ #
30
+ class AdminResourceCard < BaseComponent
31
+ CARD_CLASS = "bg-card shadow-sm rounded-xl border border-border/60 ring-1 ring-black/[0.03] overflow-hidden"
32
+ GRID_CLASS = "grid grid-cols-1 lg:grid-cols-3"
33
+ MAIN_CLASS = "lg:col-span-2 p-6 space-y-6"
34
+ SIDEBAR_CLASS = "border-t lg:border-t-0 lg:border-l border-border/60 bg-muted/20 p-6 space-y-6"
35
+ ACTIONS_CLASS = "flex items-center justify-end gap-3 border-t border-border/60 px-6 py-4 bg-muted/20"
36
+
37
+ INPUT_CLASS = "block w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground shadow-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-colors"
38
+ LABEL_CLASS = "block text-sm font-medium text-foreground mb-1.5"
39
+ HINT_CLASS = "mt-1 text-xs text-muted-foreground"
40
+ FILE_INPUT_CLASS = "block w-full text-sm text-muted-foreground file:mr-3 file:py-1.5 file:px-3 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-primary/10 file:text-primary hover:file:bg-primary/20 transition-colors"
41
+
42
+ SECTION_CLASS = "rounded-xl border border-border/60 bg-muted/30 p-5"
43
+ SECTION_TITLE_CLASS = "text-sm font-semibold text-foreground tracking-tight mb-4"
44
+ DETAIL_LABEL_CLASS = "text-xs font-medium text-muted-foreground uppercase tracking-wider"
45
+ DETAIL_VALUE_CLASS = "mt-1 text-sm text-foreground"
46
+
47
+ CANCEL_CLASS = "inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg border border-border bg-background text-foreground hover:bg-muted transition-colors"
48
+ SUBMIT_CLASS = "inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm transition-colors"
49
+
50
+ def view_template(&block)
51
+ div(class: CARD_CLASS, &block)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -58,7 +58,7 @@ module RubyCms
58
58
  def render_table_content(&block)
59
59
  div(
60
60
  class: build_classes(
61
- "rounded-lg border border-gray-200/80 bg-white shadow-sm overflow-hidden " \
61
+ "rounded-xl border border-border/60 bg-white shadow-sm ring-1 ring-black/[0.03] overflow-hidden " \
62
62
  "flex flex-col",
63
63
  @user_attrs[:class]
64
64
  ),
@@ -66,7 +66,7 @@ module RubyCms
66
66
  ) do
67
67
  render_header
68
68
  render_table_wrapper(&block)
69
- div(class: "border-t border-gray-200/80 bg-white") do
69
+ div(class: "border-t border-border/60 bg-white") do
70
70
  render_bulk_actions if @has_bulk_actions
71
71
  render_pagination if @pagination && @pagination_path
72
72
  end
@@ -84,7 +84,7 @@ module RubyCms
84
84
  turbo_frame: @turbo_frame
85
85
  )
86
86
  elsif @header
87
- div(class: "px-6 py-4 border-b border-gray-200/80 bg-white") do
87
+ div(class: "px-6 py-4 border-b border-border/60 bg-white") do
88
88
  if @header.respond_to?(:call)
89
89
  raw(@header.call) # rubocop:disable Rails/OutputSafety -- legacy capture support
90
90
  elsif @header.kind_of?(String)
@@ -96,7 +96,7 @@ module RubyCms
96
96
 
97
97
  def render_table_wrapper(&)
98
98
  div(class: "w-full overflow-x-auto") do
99
- table(class: "min-w-full text-sm") do
99
+ table(class: "min-w-full text-sm caption-bottom") do
100
100
  yield if block_given?
101
101
  end
102
102
  end
@@ -33,7 +33,7 @@ module RubyCms
33
33
  end
34
34
 
35
35
  def view_template
36
- div(class: "flex items-center justify-end gap-1") do
36
+ div(class: "flex items-center justify-end gap-1 pr-2") do
37
37
  render_edit_button if @edit_path
38
38
 
39
39
  render_delete_button if @delete_path
@@ -45,8 +45,8 @@ module RubyCms
45
45
  def render_edit_button
46
46
  link_options = {
47
47
  href: @edit_path,
48
- class: "inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 " \
49
- "hover:bg-gray-100 hover:text-gray-900 transition-colors"
48
+ class: "inline-flex size-8 items-center justify-center rounded-md text-muted-foreground " \
49
+ "hover:bg-muted hover:text-foreground transition-colors"
50
50
  }
51
51
  link_options[:data] = { turbo_frame: @turbo_frame } if @turbo_frame
52
52
 
@@ -71,8 +71,8 @@ module RubyCms
71
71
  item_id = @item_id || extract_item_id_from_path
72
72
  button(
73
73
  type: "button",
74
- class: "inline-flex h-8 w-8 items-center justify-center rounded-md text-rose-600 " \
75
- "hover:bg-rose-50 hover:text-rose-700 transition-colors",
74
+ class: "inline-flex size-8 items-center justify-center rounded-md text-muted-foreground " \
75
+ "hover:bg-destructive/10 hover:text-destructive transition-colors",
76
76
  data: {
77
77
  action: "click->#{@controller_name}#showIndividualDeleteDialog",
78
78
  "#{@controller_name}-item-id-param": item_id,
@@ -7,7 +7,7 @@ module RubyCms
7
7
  # Simple wrapper for <tbody> content
8
8
  class BulkActionTableBody < BaseComponent
9
9
  def view_template(&)
10
- tbody(class: "divide-y divide-gray-100", &)
10
+ tbody(class: "[&_tr:last-child]:border-0 [&_td]:text-sm", &)
11
11
  end
12
12
  end
13
13
  end