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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +68 -164
- data/app/components/ruby_cms/admin/admin_page.rb +19 -19
- data/app/components/ruby_cms/admin/admin_page_header.rb +81 -0
- data/app/components/ruby_cms/admin/admin_resource_card.rb +55 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table.rb +4 -4
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_actions.rb +5 -5
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_body.rb +1 -1
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_cell.rb +15 -13
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_head.rb +13 -11
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_delete_modal.rb +9 -9
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header.rb +2 -2
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header_bar.rb +8 -8
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_pagination.rb +9 -9
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_row.rb +3 -4
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +25 -24
- data/app/controllers/ruby_cms/admin/base_controller.rb +10 -4
- data/app/controllers/ruby_cms/admin/content_blocks_controller.rb +4 -3
- data/app/controllers/ruby_cms/admin/locale_controller.rb +2 -1
- data/app/controllers/ruby_cms/admin/user_permissions_controller.rb +25 -7
- data/app/helpers/ruby_cms/settings_helper.rb +19 -9
- data/app/javascript/controllers/ruby_cms/bulk_action_table_controller.js +53 -12
- data/app/models/ruby_cms/permission.rb +38 -9
- data/app/models/ruby_cms/permittable.rb +0 -2
- data/app/views/layouts/ruby_cms/_admin_sidebar.html.erb +2 -2
- data/app/views/layouts/ruby_cms/admin.html.erb +13 -17
- data/app/views/ruby_cms/admin/content_blocks/index.html.erb +0 -11
- data/app/views/ruby_cms/admin/content_blocks/show.html.erb +204 -85
- data/app/views/ruby_cms/admin/settings/index.html.erb +214 -175
- data/app/views/ruby_cms/admin/user_permissions/index.html.erb +32 -2
- data/app/views/ruby_cms/admin/users/_row.html.erb +4 -1
- data/config/locales/en.yml +4 -0
- data/lib/ruby_cms/engine.rb +20 -12
- data/lib/ruby_cms/version.rb +1 -1
- data/lib/ruby_cms.rb +24 -0
- metadata +4 -3
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e1a4b15129e38c223a849cc78aff0bd173af8a104a07acc0f7195298475f4420
|
|
4
|
+
data.tar.gz: a9ea71bfc0ddcb2a89ba1ec85acd3fd7f9cc03799aa91e0e1eb805a081b1b6aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a2b7113dbd5bae7fd3233398459efca6c3f497f54d22f63d9a4b7db0577d27cb4db73abf4ebb93b3f981ca5f33d34f458af08bc75bf74bb0dfb1f5cf02783625
|
|
7
|
+
data.tar.gz: 33442c384ce70c04a1a7420632225e3a282c9929b29d28a76b92dcb76dd19d5ab15957e0c4b70bfca34dcb59b1f5ab72d7c2e388b0d2b50913dc9f67d3a30350
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -1,53 +1,33 @@
|
|
|
1
1
|
# RubyCMS
|
|
2
2
|
|
|
3
|
-
Reusable Rails engine
|
|
3
|
+
Reusable Rails engine for a CMS-style admin: permissions, admin UI shell, content blocks, and a visual editor.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
If your host app already has `/admin` routes, adjust/remove them so RubyCMS can use `/admin`.
|
|
49
29
|
|
|
50
|
-
|
|
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
|
-
|
|
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,
|
|
63
|
-
|
|
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
|
-
|
|
53
|
+
</details>
|
|
67
54
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
```
|
|
55
|
+
<details>
|
|
56
|
+
<summary>List items (badges, tags, arrays)</summary>
|
|
71
57
|
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
+
Create/edit blocks in **Admin -> Content blocks**.
|
|
117
71
|
|
|
118
|
-
|
|
72
|
+
## Visual Editor
|
|
119
73
|
|
|
120
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
81
|
+
Then open **Admin -> Visual editor**, pick a page key, and click content blocks in the preview.
|
|
145
82
|
|
|
146
|
-
|
|
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
|
-
|
|
85
|
+
RubyCMS exposes an `admin_page` helper:
|
|
154
86
|
|
|
155
|
-
|
|
87
|
+
```erb
|
|
88
|
+
<%= admin_page(title: "My Page", subtitle: "Optional") do %>
|
|
89
|
+
<p>Hello from RubyCMS admin.</p>
|
|
90
|
+
<% end %>
|
|
91
|
+
```
|
|
156
92
|
|
|
157
|
-
|
|
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
|
-
|
|
95
|
+
Public (non-admin) exceptions are captured and shown in **`/admin/visitor_errors`**.
|
|
163
96
|
|
|
164
|
-
|
|
97
|
+
In development, logging is typically disabled and exceptions are re-raised normally.
|
|
165
98
|
|
|
166
|
-
|
|
99
|
+
## Page View Tracking (Ahoy)
|
|
167
100
|
|
|
168
|
-
Include `RubyCms::PageTracking` in
|
|
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
|
|
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
|
-
|
|
113
|
+
## Seeding Content Blocks from YAML
|
|
182
114
|
|
|
183
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
219
|
-
Ahoy::Visit.count
|
|
121
|
+
Example `config/locales/en.yml`:
|
|
220
122
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
123
|
+
```yaml
|
|
124
|
+
en:
|
|
125
|
+
content_blocks:
|
|
126
|
+
hero_title: "Welcome to my site"
|
|
225
127
|
```
|
|
226
128
|
|
|
227
|
-
|
|
129
|
+
Import:
|
|
228
130
|
|
|
229
|
-
|
|
131
|
+
```bash
|
|
132
|
+
rails ruby_cms:content_blocks:seed
|
|
133
|
+
```
|
|
230
134
|
|
|
231
|
-
|
|
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-
|
|
72
|
-
ol(class: "flex items-center flex-wrap gap-
|
|
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-
|
|
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-
|
|
93
|
+
a(href: crumb[:url] || crumb[:path], class: "hover:text-foreground transition-colors") do
|
|
94
94
|
span { crumb[:label] || crumb[:text] }
|
|
95
|
-
span(class: "
|
|
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-
|
|
122
|
-
p(class: "text-sm text-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
287
|
-
"
|
|
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-
|
|
329
|
-
"red" => "text-
|
|
330
|
-
"purple" => "text-
|
|
331
|
-
"gray" => "text-
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
49
|
-
"hover:bg-
|
|
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
|
|
75
|
-
"hover:bg-
|
|
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,
|