macron-cms 1.0.0 → 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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +298 -21
  3. data/app/assets/javascripts/macron/cms/codemirror.js +6 -0
  4. data/app/assets/javascripts/macron/cms/editor.js +101 -0
  5. data/app/assets/stylesheets/macron/cms/application.css +1 -1
  6. data/app/assets/stylesheets/macron/cms/codemirror.css +64 -0
  7. data/app/assets/tailwind/macron/cms/application.css +1 -0
  8. data/app/components/macron/cms/widget_component.rb +25 -3
  9. data/app/controllers/macron/cms/article_contents_controller.rb +117 -10
  10. data/app/controllers/macron/cms/articles_controller.rb +16 -2
  11. data/app/helpers/macron/cms/content_helper.rb +31 -0
  12. data/app/helpers/macron/cms/fields_helper.rb +11 -1
  13. data/app/models/macron/cms/article.rb +36 -0
  14. data/app/models/macron/cms/article_content.rb +33 -1
  15. data/app/views/layouts/macron/cms/application.html.erb +6 -18
  16. data/app/views/macron/cms/articles/_content_blocks_section.html.erb +531 -0
  17. data/app/views/macron/cms/articles/_form.html.erb +121 -0
  18. data/app/views/macron/cms/articles/show.html.erb +11 -298
  19. data/app/views/macron/cms/content_types/show.html.erb +33 -9
  20. data/app/views/macron/cms/fields/_color.html.erb +35 -0
  21. data/app/views/macron/cms/fields/_dropdown.html.erb +28 -0
  22. data/app/views/macron/cms/fields/_file.html.erb +27 -0
  23. data/app/views/macron/cms/fields/_json.html.erb +17 -0
  24. data/app/views/macron/cms/fields/_markdown.html.erb +17 -0
  25. data/app/views/macron/cms/shared/_flash.html.erb +34 -0
  26. data/config/routes.rb +3 -0
  27. data/db/migrate/20261028000007_add_background_fields_to_macron_cms_articles.rb +6 -0
  28. data/db/migrate/20261028000008_add_text_color_to_macron_cms_articles.rb +5 -0
  29. data/lib/macron/cms/version.rb +1 -1
  30. metadata +55 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c629d799d5fb1f6ccc3e6fe6efda7f40d5feb72dada6f572c4d81d0acc2dfa4
4
- data.tar.gz: c201d162fc7ab490a975358470847fc95d481c7ae3692fbee774fea1331f6bba
3
+ metadata.gz: 8377dd550eb5e06468380e45e6b112ea5fccdf5a791d4c382a54cbf5c8273a22
4
+ data.tar.gz: da9ca19a2ca4191b3591d59a594e436ae3db02686170f2186257e93c8d7d8e62
5
5
  SHA512:
6
- metadata.gz: c516904323f9a7d51b76c6236654d5f3435a51183fff5740b2cf26fd0116fd515e9535fde2dceeeef771e80502fe31231171cb5c679f030beebe891473d1cf47
7
- data.tar.gz: 72d5b3adcaed790f9eb56e45b6c577f8a8dc1aa49545ab5969d6c486b4d74f51bb9c035cee1936b00d7b3dd7def2eafa08e03e2bd81a2cf7959c14e40b013177
6
+ metadata.gz: '0379d659ad66a227790269275c41272f6a88076d06ecc88c59299b2db776f00fc1cbae6180a4cc1fa0d5bab115fae1dcb8c9223a044b4a3be9dadad262cb645f'
7
+ data.tar.gz: 4d1dffb4a71491a89d10050f67ba69c313ce9fc0e3d1571071540ebc15f312aa75ba17b1ad840bff823b676c3621bc268143b4457e425e47ed0decb12c0912e1
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MacronCMS
2
2
 
3
- [![Gem Version](https://img.shields.io/badge/version-1.0.0-emerald.svg)](#version)
3
+ [![Gem Version](https://img.shields.io/badge/version-2.0.0-emerald.svg)](#version)
4
4
  [![Rails 8+](https://img.shields.io/badge/rails-8%2B-red.svg)](https://rubyonrails.org)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](MIT-LICENSE)
6
6
 
@@ -10,7 +10,7 @@ A simple, flexible CMS engine for Ruby on Rails, designed for building highly in
10
10
 
11
11
  ## Overview
12
12
 
13
- **MacronCMS** brings simplicity and developer joy back to content management, without sacrificing the power needed to create dynamic digital experiences. Built natively for Ruby on Rails, it gives developers total freedom to define content structures with Rails models while effortlessly powering modern front-end presentations.
13
+ **MacronCMS** brings simplicity and developer joy back to content management, without sacrificing the power needed to create dynamic digital experiences. Built natively for Ruby on Rails 8, it gives developers total freedom to define content structures with Rails models while effortlessly powering modern front-end presentations.
14
14
 
15
15
  ---
16
16
 
@@ -22,11 +22,35 @@ A simple, flexible CMS engine for Ruby on Rails, designed for building highly in
22
22
  - **Deletion Safeguards**: Browser confirmation dialogs on delete actions with article titles to prevent accidental deletion.
23
23
  - **Host App Integration**: Quick links between the CMS admin console and public-facing host views.
24
24
 
25
- ### 🧱 Dynamic Article Content Blocks & Payload Storage
26
- - **Multiple Blocks Per Article**: Associate an arbitrary series of content blocks with an article (`Macron::Cms::ArticleContent`). Add multiple blocks of the same content type (e.g. multiple headings or paragraphs) with distinct values.
27
- - **Dynamic Field Populating**: When adding or editing a block, input controls are automatically generated based on the content type's defined fields. Values are validated against the schema and saved into the block's `payload` JSONB column.
28
- - **Extensible Field Kinds**: Out-of-the-box support for `string` (text input) and `text` (multiline textarea), with a partial-based architecture (`app/views/macron/cms/fields/_<kind>.html.erb`) making it effortless to add new field types.
29
- - **Inline Editing & Deletion**: Edit block payloads inline directly on the Article show page, or remove blocks with confirmation.
25
+ ### 🧱 Dynamic Article Content Blocks & In-Place Inserters
26
+ - **Multiple Blocks Per Article**: Associate an arbitrary series of content blocks with an article (`Macron::Cms::ArticleContent`). Add multiple blocks of the same content type (e.g. headings, markdown blocks, images) with distinct values.
27
+ - **In-Place Block Inserters**: Add new blocks directly above, between, or below existing blocks on the Article management screen with one click.
28
+ - **Inline Turbo Stream Updates**: Add, edit, and reorder content blocks with instantaneous Turbo Stream updates—no full-page reloads.
29
+ - **Dynamic Field Generation**: Field controls are generated dynamically based on the content type schema and validated before saving into the block's `payload` JSONB column.
30
+ - **7 Extensible Field Kinds**:
31
+ - `string`: Single-line text input.
32
+ - `text`: Multiline text area.
33
+ - `dropdown`: Select menu configured with custom option lists from the schema YAML.
34
+ - `color`: Interactive color picker with hex value support.
35
+ - `markdown`: CodeMirror-backed markdown editor with formatted preview on the article show page.
36
+ - `file`: Direct file attachment powered by Active Storage.
37
+ - `json`: Dedicated JSON data editor.
38
+
39
+ ### 🎨 Article Background & Typography Customization
40
+ - **Custom Background Colors & Images**: Configure an optional background color or attach a background image directly to any article via Active Storage, with live image previews and one-click purge.
41
+ - **CSS Background Display Controls**: Fine-tune background display settings per article:
42
+ - **Size**: `cover`, `contain`, `auto`
43
+ - **Repeat**: `no-repeat`, `repeat`, `repeat-x`, `repeat-y`
44
+ - **Position**: `center`, `top`, `bottom`, `left`, `right`
45
+ - **Attachment**: `scroll`, `fixed`
46
+ - **Article-Wide Text Color Propagation**: Set a custom text color at the article level that automatically propagates to the host view's `<body>` and header, as well as to child `WidgetComponent` instances (headings, paragraphs, and markdown prose).
47
+ - **Public Style Helper**: Use `cms_article_background_style(@article)` to generate clean inline CSS applied directly to the host application's `<body>` tag.
48
+
49
+ ### 📝 CodeMirror Markdown & Rich Media
50
+ - **CodeMirror Markdown Editor**: Integrated CodeMirror editor in the CMS admin for writing Markdown content with syntax highlighting.
51
+ - **Formatted Markdown Previews**: Formatted GitHub Flavored Markdown (GFM) previews rendered directly on the article management page.
52
+ - **Sandboxed Embeds**: Render third-party media and raw HTML snippets safely inside isolated `<iframe>` elements using `srcdoc` to prevent Active Storage download blocks and script collisions.
53
+ - **Responsive Media Controls**: Configure image and embed widgets with responsive width options (`25%`, `33%`, `50%`, `66%`, `75%`, `100%`) and alignment (`left`, `center`, `right`).
30
54
 
31
55
  ### ↕️ Drag-and-Drop & Button Reordering
32
56
  - **HTML5 Drag-and-Drop**: Easily re-arrange article content blocks on the Article show page with immediate position updates and asynchronous persistence.
@@ -38,7 +62,7 @@ A simple, flexible CMS engine for Ruby on Rails, designed for building highly in
38
62
  - **Rake Import Pipeline**: Idempotently sync YAML definitions into database records (`Macron::Cms::Content`) with automatic schema introspection and admin dashboard views.
39
63
 
40
64
  ### ⚡ Front-End ViewComponent Widgets
41
- - **`Macron::Cms::WidgetComponent`**: Base component class providing automatic method accessors for all defined schema fields and payload values.
65
+ - **`Macron::Cms::WidgetComponent`**: Base component class providing automatic method accessors for schema fields, payload values, attached files (`file`, `upload`), and article-level styles (`article_text_color`).
42
66
  - **Seamless Rendering Helpers**: Render all article content blocks ordered by `sort_order` with `<%= cms_render_article_contents(@article) %>`, or render individual blocks via `<%= cms_render_widget(article_content) %>`.
43
67
  - **Informative Error Handling**: Raises `Macron::Cms::MissingWidgetError` if a content type does not have a corresponding `Widgets::<Name>Component` in the host application.
44
68
 
@@ -85,7 +109,15 @@ Or install it directly:
85
109
  $ gem install macron-cms
86
110
  ```
87
111
 
88
- ### Engine Mounting & Migrations
112
+ ### 1. Active Storage Setup
113
+
114
+ MacronCMS utilizes Active Storage for block file attachments (such as images) and article background images. If your application does not already have Active Storage installed, set it up:
115
+
116
+ ```bash
117
+ $ bin/rails active_storage:install
118
+ ```
119
+
120
+ ### 2. Engine Mounting & Migrations
89
121
 
90
122
  Mount the engine inside your host application's `config/routes.rb`:
91
123
 
@@ -111,9 +143,11 @@ $ bin/rails db:migrate
111
143
  Scaffold content types using the built-in Rails generator:
112
144
 
113
145
  ```bash
114
- $ bin/rails generate macron:cms:content_type heading title:string level:string
146
+ $ bin/rails generate macron:cms:content_type heading title:string level:dropdown color:color
115
147
  $ bin/rails generate macron:cms:content_type paragraph content:text
116
- $ bin/rails generate macron:cms:content_type image url:string caption:text
148
+ $ bin/rails generate macron:cms:content_type markdown content:markdown
149
+ $ bin/rails generate macron:cms:content_type image file:file width:dropdown align:dropdown caption:string
150
+ $ bin/rails generate macron:cms:content_type embed html:text width:dropdown align:dropdown
117
151
  ```
118
152
 
119
153
  This generates:
@@ -121,7 +155,106 @@ This generates:
121
155
  2. `app/components/widgets/<name>_component.rb` (the ViewComponent class)
122
156
  3. `app/components/widgets/<name>_component.html.erb` (the ViewComponent template)
123
157
 
124
- ### 2. Importing Content Types
158
+ ---
159
+
160
+ ### 2. Supported Field Kinds
161
+
162
+ | Kind | Description | Config Options | Accessor in Widget |
163
+ |---|---|---|---|
164
+ | `string` | Single-line text input | — | `field_name` |
165
+ | `text` | Multiline textarea | — | `field_name` |
166
+ | `dropdown` | Select dropdown menu | `options: [...]` in YAML schema | `field_name` |
167
+ | `color` | Color picker input (hex color code) | — | `field_name` |
168
+ | `markdown` | Multiline markdown with CodeMirror editor | — | `field_name` |
169
+ | `file` | File upload backed by Active Storage | — | `file` or `upload` (Active Storage attachment) |
170
+ | `json` | Formatted JSON editor | — | `field_name` |
171
+
172
+ ---
173
+
174
+ ### 3. Schema Examples
175
+
176
+ #### Section Heading (`config/content_types/heading.yml`)
177
+ ```yaml
178
+ name: Heading
179
+ description: Section heading with customizable level and color
180
+ fields:
181
+ - name: title
182
+ kind: string
183
+ - name: level
184
+ kind: dropdown
185
+ options:
186
+ - h1
187
+ - h2
188
+ - h3
189
+ - h4
190
+ - h5
191
+ - h6
192
+ - name: color
193
+ kind: color
194
+ ```
195
+
196
+ #### GitHub-Flavored Markdown (`config/content_types/markdown.yml`)
197
+ ```yaml
198
+ name: Markdown
199
+ description: Rich formatted content using GitHub-flavored Markdown
200
+ fields:
201
+ - name: content
202
+ kind: markdown
203
+ ```
204
+
205
+ #### Responsive Image with Active Storage (`config/content_types/image.yml`)
206
+ ```yaml
207
+ name: Image
208
+ description: Image with configurable responsive width, alignment, and optional caption
209
+ fields:
210
+ - name: file
211
+ kind: file
212
+ - name: width
213
+ kind: dropdown
214
+ options:
215
+ - 25%
216
+ - 33%
217
+ - 50%
218
+ - 66%
219
+ - 75%
220
+ - 100%
221
+ - name: align
222
+ kind: dropdown
223
+ options:
224
+ - left
225
+ - center
226
+ - right
227
+ - name: caption
228
+ kind: string
229
+ ```
230
+
231
+ #### Sandboxed Embed / HTML (`config/content_types/embed.yml`)
232
+ ```yaml
233
+ name: Embed
234
+ description: Embed external rich media or raw HTML snippets safely
235
+ fields:
236
+ - name: html
237
+ kind: text
238
+ - name: width
239
+ kind: dropdown
240
+ options:
241
+ - 25%
242
+ - 33%
243
+ - 50%
244
+ - 66%
245
+ - 75%
246
+ - 100%
247
+ - name: align
248
+ kind: dropdown
249
+ options:
250
+ - left
251
+ - center
252
+ - right
253
+ ```
254
+
255
+ ---
256
+
257
+ ### 4. Importing Content Types
125
258
 
126
259
  Import all YAML content type definitions into the database (`Macron::Cms::Content`):
127
260
 
@@ -137,23 +270,44 @@ $ bin/rails macron:cms:content:import
137
270
 
138
271
  The import task is idempotent and creates or updates existing records. Once imported, content types can be inspected at `/cms/content_types` and added as content blocks to articles in `/cms/articles/:id`.
139
272
 
140
- ### 3. Rendering Content Blocks on the Front End
273
+ ---
141
274
 
142
- In your front-end article view (e.g. `app/views/articles/show.html.erb`), render all blocks ordered by `sort_order`:
275
+ ### 5. Front-End Integration & ViewComponent Widgets
143
276
 
144
- ```erb
145
- <% cms_seo_meta(@article) %>
277
+ #### Host Application View (`app/views/articles/show.html.erb`)
146
278
 
147
- <article class="max-w-3xl mx-auto py-12">
148
- <h1 class="text-4xl font-bold"><%= @article.title %></h1>
279
+ Use `cms_seo_meta` for SEO metadata and `cms_article_background_style` to apply custom background colors, images, and text colors directly to the layout:
149
280
 
150
- <div class="mt-8 space-y-6">
281
+ ```erb
282
+ <% cms_seo_meta(@article) %>
283
+ <% if cms_article_background_style(@article).present? %>
284
+ <% content_for :body_style, cms_article_background_style(@article) %>
285
+ <% end %>
286
+
287
+ <article class="max-w-4xl mx-auto px-4 py-12">
288
+ <header class="pb-8 mb-8 border-b border-zinc-200">
289
+ <h1 class="text-4xl font-bold" style="<%= "color: #{@article.text_color};" if @article.text_color.present? %>">
290
+ <%= @article.title %>
291
+ </h1>
292
+ </header>
293
+
294
+ <!-- Render all content blocks in order -->
295
+ <div class="space-y-6">
151
296
  <%= cms_render_article_contents(@article) %>
152
297
  </div>
153
298
  </article>
154
299
  ```
155
300
 
156
- Each widget inherits from `Macron::Cms::WidgetComponent`, allowing you to access fields directly by name or via `field_value(name)`:
301
+ In your application layout (`app/views/layouts/application.html.erb`), apply the `:body_style` slot:
302
+
303
+ ```erb
304
+ <body class="min-h-screen text-zinc-900 <%= 'bg-zinc-50' unless content_for?(:body_style) %>"
305
+ style="<%= yield :body_style if content_for?(:body_style) %>">
306
+ <%= yield %>
307
+ </body>
308
+ ```
309
+
310
+ #### Heading Widget Component Example
157
311
 
158
312
  ```ruby
159
313
  # app/components/widgets/heading_component.rb
@@ -162,13 +316,136 @@ module Widgets
162
316
  def tag_name
163
317
  level.presence || "h2"
164
318
  end
319
+
320
+ def resolved_color
321
+ color.presence || article_text_color
322
+ end
165
323
  end
166
324
  end
167
325
  ```
168
326
 
169
327
  ```erb
170
328
  <%# app/components/widgets/heading_component.html.erb %>
171
- <%= content_tag(tag_name, title, class: "font-bold text-zinc-900") %>
329
+ <%= content_tag(tag_name, title, class: "font-bold tracking-tight", style: ("color: #{resolved_color};" if resolved_color.present?)) %>
330
+ ```
331
+
332
+ #### Markdown Widget Component Example
333
+
334
+ Render Markdown with Kramdown and inherit article-level text styling:
335
+
336
+ ```ruby
337
+ # app/components/widgets/markdown_component.rb
338
+ module Widgets
339
+ class MarkdownComponent < Macron::Cms::WidgetComponent
340
+ def rendered_markdown
341
+ raw = field_value(:content).to_s
342
+ return "".html_safe if raw.blank?
343
+
344
+ Kramdown::Document.new(raw, input: "GFM").to_html.html_safe
345
+ end
346
+
347
+ def text_style
348
+ "color: #{article_text_color};" if article_text_color.present?
349
+ end
350
+ end
351
+ end
352
+ ```
353
+
354
+ ```erb
355
+ <%# app/components/widgets/markdown_component.html.erb %>
356
+ <div class="prose max-w-none text-inherit dark:prose-invert" style="<%= text_style %>">
357
+ <%= rendered_markdown %>
358
+ </div>
359
+ ```
360
+
361
+ #### Image Widget Component Example
362
+
363
+ Access Active Storage attachments using `file` or `upload`:
364
+
365
+ ```ruby
366
+ # app/components/widgets/image_component.rb
367
+ module Widgets
368
+ class ImageComponent < Macron::Cms::WidgetComponent
369
+ def attached_file
370
+ file
371
+ end
372
+
373
+ def width_class
374
+ case width
375
+ when "25%" then "max-w-xs"
376
+ when "33%" then "max-w-sm"
377
+ when "50%" then "max-w-md"
378
+ when "66%" then "max-w-lg"
379
+ when "75%" then "max-w-2xl"
380
+ else "w-full"
381
+ end
382
+ end
383
+
384
+ def align_class
385
+ case align
386
+ when "left" then "mr-auto"
387
+ when "right" then "ml-auto"
388
+ else "mx-auto"
389
+ end
390
+ end
391
+ end
392
+ end
393
+ ```
394
+
395
+ ```erb
396
+ <%# app/components/widgets/image_component.html.erb %>
397
+ <% if attached_file.present? %>
398
+ <figure class="<%= width_class %> <%= align_class %> my-6">
399
+ <%= image_tag attached_file, class: "rounded-lg shadow-sm w-full h-auto" %>
400
+ <% if caption.present? %>
401
+ <figcaption class="mt-2 text-center text-sm text-zinc-500"><%= caption %></figcaption>
402
+ <% end %>
403
+ </figure>
404
+ <% end %>
405
+ ```
406
+
407
+ #### Sandboxed Embed Widget Component Example
408
+
409
+ ```ruby
410
+ # app/components/widgets/embed_component.rb
411
+ module Widgets
412
+ class EmbedComponent < Macron::Cms::WidgetComponent
413
+ def embed_html
414
+ field_value(:html).to_s
415
+ end
416
+
417
+ def width_class
418
+ case width
419
+ when "25%" then "max-w-xs"
420
+ when "33%" then "max-w-sm"
421
+ when "50%" then "max-w-md"
422
+ when "66%" then "max-w-lg"
423
+ when "75%" then "max-w-2xl"
424
+ else "w-full"
425
+ end
426
+ end
427
+
428
+ def align_class
429
+ case align
430
+ when "left" then "mr-auto"
431
+ when "right" then "ml-auto"
432
+ else "mx-auto"
433
+ end
434
+ end
435
+ end
436
+ end
437
+ ```
438
+
439
+ ```erb
440
+ <%# app/components/widgets/embed_component.html.erb %>
441
+ <div class="<%= width_class %> <%= align_class %> my-6">
442
+ <iframe
443
+ srcdoc="<%= embed_html %>"
444
+ sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
445
+ class="w-full border-0 rounded overflow-hidden"
446
+ loading="lazy">
447
+ </iframe>
448
+ </div>
172
449
  ```
173
450
 
174
451
  ---