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
@@ -22,10 +22,41 @@ module Macron
22
22
  end
23
23
 
24
24
  render widget_class.new(
25
+ article_content: article_content,
25
26
  payload: article_content.payload,
26
27
  fields: article_content.content.normalized_fields
27
28
  )
28
29
  end
30
+
31
+ def cms_article_background_style(article)
32
+ return "" if article.nil?
33
+
34
+ styles = []
35
+ if article.background_color.present?
36
+ styles << "background-color: #{article.background_color};"
37
+ end
38
+
39
+ if article.respond_to?(:text_color) && article.text_color.present?
40
+ styles << "color: #{article.text_color};"
41
+ end
42
+
43
+ if article.respond_to?(:background_image) && article.background_image.attached?
44
+ image_path = if respond_to?(:rails_blob_path)
45
+ rails_blob_path(article.background_image, only_path: true)
46
+ elsif respond_to?(:url_for)
47
+ url_for(article.background_image)
48
+ else
49
+ Rails.application.routes.url_helpers.rails_blob_path(article.background_image, only_path: true)
50
+ end
51
+ styles << "background-image: url('#{image_path}');"
52
+ styles << "background-repeat: #{article.background_repeat};"
53
+ styles << "background-size: #{article.background_size};"
54
+ styles << "background-position: #{article.background_position};"
55
+ styles << "background-attachment: #{article.background_attachment};"
56
+ end
57
+
58
+ styles.join(" ")
59
+ end
29
60
  end
30
61
  end
31
62
  end
@@ -1,7 +1,17 @@
1
+ require "kramdown"
2
+ require "kramdown-parser-gfm"
3
+
1
4
  module Macron
2
5
  module Cms
3
6
  module FieldsHelper
4
- SUPPORTED_KINDS = %w[string text].freeze
7
+ SUPPORTED_KINDS = %w[string text dropdown file color markdown json].freeze
8
+
9
+ def cms_render_markdown(text)
10
+ return "".html_safe if text.blank?
11
+
12
+ raw_html = Kramdown::Document.new(text.to_s, input: "GFM").to_html
13
+ sanitize(raw_html)
14
+ end
5
15
 
6
16
  def cms_render_field(field_def, value: nil, input_name: nil, input_id: nil)
7
17
  field_hash = field_def.stringify_keys
@@ -1,6 +1,16 @@
1
1
  module Macron
2
2
  module Cms
3
3
  class Article < ApplicationRecord
4
+ VALID_BACKGROUND_REPEATS = %w[no-repeat repeat repeat-x repeat-y].freeze
5
+ VALID_BACKGROUND_SIZES = %w[cover contain auto].freeze
6
+ VALID_BACKGROUND_POSITIONS = %w[center top bottom left right].freeze
7
+ VALID_BACKGROUND_ATTACHMENTS = %w[scroll fixed].freeze
8
+
9
+ DEFAULT_BACKGROUND_REPEAT = "no-repeat"
10
+ DEFAULT_BACKGROUND_SIZE = "cover"
11
+ DEFAULT_BACKGROUND_POSITION = "center"
12
+ DEFAULT_BACKGROUND_ATTACHMENT = "scroll"
13
+
4
14
  before_validation :derive_name_from_title
5
15
 
6
16
  extend FriendlyId
@@ -12,6 +22,8 @@ module Macron
12
22
  has_many :article_contents, -> { order(:sort_order) }, class_name: "Macron::Cms::ArticleContent", dependent: :destroy
13
23
  has_many :contents, through: :article_contents
14
24
 
25
+ has_one_attached :background_image
26
+
15
27
  def seo_title
16
28
  meta_title.presence || title
17
29
  end
@@ -20,6 +32,30 @@ module Macron
20
32
  meta_description.presence
21
33
  end
22
34
 
35
+ def normalized_background_options
36
+ (background_options || {}).to_h.stringify_keys
37
+ end
38
+
39
+ def background_repeat
40
+ val = normalized_background_options["repeat"].to_s.strip.presence
41
+ VALID_BACKGROUND_REPEATS.include?(val) ? val : DEFAULT_BACKGROUND_REPEAT
42
+ end
43
+
44
+ def background_size
45
+ val = normalized_background_options["size"].to_s.strip.presence
46
+ VALID_BACKGROUND_SIZES.include?(val) ? val : DEFAULT_BACKGROUND_SIZE
47
+ end
48
+
49
+ def background_position
50
+ val = normalized_background_options["position"].to_s.strip.presence
51
+ VALID_BACKGROUND_POSITIONS.include?(val) ? val : DEFAULT_BACKGROUND_POSITION
52
+ end
53
+
54
+ def background_attachment
55
+ val = normalized_background_options["attachment"].to_s.strip.presence
56
+ VALID_BACKGROUND_ATTACHMENTS.include?(val) ? val : DEFAULT_BACKGROUND_ATTACHMENT
57
+ end
58
+
23
59
  private
24
60
 
25
61
  def derive_name_from_title
@@ -4,17 +4,49 @@ module Macron
4
4
  belongs_to :article, class_name: "Macron::Cms::Article"
5
5
  belongs_to :content, class_name: "Macron::Cms::Content"
6
6
 
7
+ has_many_attached :files
8
+
7
9
  before_validation :assign_default_sort_order, on: :create
8
10
 
9
11
  validates :sort_order, numericality: { only_integer: true }
10
12
 
11
13
  default_scope -> { order(:sort_order, :id) }
12
14
 
15
+ def upload
16
+ files.first
17
+ end
18
+
19
+ def file
20
+ files.first
21
+ end
22
+
23
+ def uploads
24
+ files
25
+ end
26
+
27
+ def attach_upload(uploaded, replace: false)
28
+ return unless uploaded.present?
29
+
30
+ if replace && files.attached?
31
+ files.purge
32
+ end
33
+
34
+ files.attach(uploaded)
35
+ end
36
+
13
37
  def filter_payload(raw_payload)
14
38
  return {} unless content && raw_payload.respond_to?(:to_h)
15
39
 
16
40
  allowed = content.field_names
17
- raw_payload.to_h.stringify_keys.slice(*allowed)
41
+ raw_payload.to_h.stringify_keys.slice(*allowed).transform_values do |val|
42
+ if val.respond_to?(:original_filename)
43
+ val.original_filename
44
+ elsif val.is_a?(Array) && val.any? { |v| v.respond_to?(:original_filename) }
45
+ val.filter_map { |v| v.respond_to?(:original_filename) ? v.original_filename : v }
46
+ else
47
+ val
48
+ end
49
+ end
18
50
  end
19
51
 
20
52
  private
@@ -7,6 +7,10 @@
7
7
  <%= csp_meta_tag %>
8
8
 
9
9
  <%= stylesheet_link_tag "macron/cms/application", media: "all" %>
10
+ <%= stylesheet_link_tag "macron/cms/codemirror", media: "all" %>
11
+ <%= turbo_include_tags %>
12
+ <%= javascript_include_tag "macron/cms/codemirror", defer: true %>
13
+ <%= javascript_include_tag "macron/cms/editor", defer: true %>
10
14
  <%= yield :head %>
11
15
  </head>
12
16
  <body class="min-h-screen text-zinc-900 antialiased flex bg-zinc-50">
@@ -100,24 +104,8 @@
100
104
  </header>
101
105
 
102
106
  <!-- Flash Notifications -->
103
- <div class="max-w-7xl w-full mx-auto px-6 sm:px-8 pt-6">
104
- <% if notice.present? %>
105
- <div class="p-4 mb-4 rounded-xl bg-emerald-50 border border-emerald-200 text-emerald-800 text-sm flex items-center gap-3">
106
- <svg class="w-5 h-5 text-emerald-600 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
107
- <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
108
- </svg>
109
- <span class="font-medium"><%= notice %></span>
110
- </div>
111
- <% end %>
112
-
113
- <% if alert.present? %>
114
- <div class="p-4 mb-4 rounded-xl bg-rose-50 border border-rose-200 text-rose-800 text-sm flex items-center gap-3">
115
- <svg class="w-5 h-5 text-rose-600 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
116
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
117
- </svg>
118
- <span class="font-medium"><%= alert %></span>
119
- </div>
120
- <% end %>
107
+ <div id="flash-messages" class="max-w-7xl w-full mx-auto px-6 sm:px-8 pt-6">
108
+ <%= render "macron/cms/shared/flash" %>
121
109
  </div>
122
110
 
123
111
  <!-- Main Content Canvas -->