ruby_cms 0.1.6 → 0.1.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83d1d3f4aadd9f1f8ad953f3babc9fbb104c2238ba411d0d24bf97bba3574684
4
- data.tar.gz: 8bef2b9e212a3da28f8af8f387c648c57a5e76a1a50a29b757e49a54b2fe0e62
3
+ metadata.gz: cf4ff917e02707c3a9d4ed7cec086d357b3c7de708ebc58475a4b5c0276a64bd
4
+ data.tar.gz: be5e98bda847b6b1129210ba575fb29951f7e2ac96d53e9fcdb7cf58af7e97b6
5
5
  SHA512:
6
- metadata.gz: 957a2871489bcdc0680f029b43dc7696ec3109bfff994227771c9ff29cf069f529e9d9c99fed37ca1e31eb13acc049dfe2d9c72fb2617f2ac9d755e2928accf0
7
- data.tar.gz: 800a6f5e760a8d5a5a99e86d5e9d6f9ad2015664c79c2068178f10d5b6c1b59d7d88a5d40230c33577d9d44d950e9a86cfd9f124ebbd166d13a2e06eada03754
6
+ metadata.gz: 97f3797085020d26908c8189324fc3f157241be0aa1eecc4a7575d4a7839ea3920662ee7e9e2f6ad509c1fd7810cc291fc9c09f23630f98c4b2d208ef50dd1af
7
+ data.tar.gz: 7ce9c3b67c447847f407b3fa96ee3dacee1780bd14352f3da9cc310d71868cccbed42e5de0c13cd1f9a6e8edc0bfbf6c96acc0a3cad37e5c2dcbaf7db448715b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.0.7] - 2026-03-25
4
+
5
+ - Improve some styling and fix rich text
6
+
3
7
  ## [0.1.0.6] - 2026-03-23
4
8
 
5
9
  - Fix visual editor content block bug
@@ -8,27 +8,49 @@ export default class extends Controller {
8
8
  const panelId = tab.dataset.panelId;
9
9
  if (!panelId) return;
10
10
 
11
- const hideClass = "is-hidden";
11
+ const hideClasses = ["hidden", "is-hidden"];
12
+ const panels = this.element.querySelectorAll("[data-locale-panel]");
12
13
 
13
- // Hide all panels
14
+ // Hide all panels in this tabs component
15
+ panels.forEach((panel) => panel.classList.add(...hideClasses));
16
+
17
+ // Reset all tab states
14
18
  this.tabTargets.forEach((t) => {
15
- const pid = t.dataset.panelId;
16
- if (pid) {
17
- const panel = document.getElementById(pid);
18
- if (panel) panel.classList.add(hideClass);
19
- }
20
- t.classList.remove("is-active");
19
+ this.setTabState(t, false);
21
20
  });
22
21
 
23
22
  // Show selected panel
24
- const panel = document.getElementById(panelId);
25
- if (panel) panel.classList.remove(hideClass);
23
+ const panel = this.findPanel(panelId);
24
+ if (panel) panel.classList.remove(...hideClasses);
26
25
 
27
26
  // Highlight selected tab
28
- tab.classList.add("is-active");
29
- tab.setAttribute("aria-selected", "true");
30
- this.tabTargets
31
- .filter((t) => t !== tab)
32
- .forEach((t) => t.setAttribute("aria-selected", "false"));
27
+ this.setTabState(tab, true);
28
+ }
29
+
30
+ findPanel(panelId) {
31
+ // Scope lookup to this component first so multiple tab groups never conflict.
32
+ if (window.CSS && typeof window.CSS.escape === "function") {
33
+ const scoped = this.element.querySelector(`#${window.CSS.escape(panelId)}`);
34
+ if (scoped) return scoped;
35
+ }
36
+ return document.getElementById(panelId);
37
+ }
38
+
39
+ setTabState(tab, isActive) {
40
+ const activeClasses = this.splitClasses(tab.dataset.activeClasses);
41
+ const inactiveClasses = this.splitClasses(tab.dataset.inactiveClasses);
42
+
43
+ // Remove both sets first, then apply the desired one.
44
+ tab.classList.remove(...activeClasses, ...inactiveClasses, "is-active");
45
+ tab.classList.add(...(isActive ? activeClasses : inactiveClasses));
46
+ if (isActive) tab.classList.add("is-active");
47
+ tab.setAttribute("aria-selected", isActive ? "true" : "false");
48
+ }
49
+
50
+ splitClasses(classListString) {
51
+ return (classListString || "")
52
+ .split(" ")
53
+ .map((klass) => klass.trim())
54
+ .filter(Boolean);
33
55
  }
34
56
  }
@@ -9,26 +9,28 @@ class ContentBlock < ApplicationRecord
9
9
 
10
10
  def self.action_text_available?
11
11
  return false unless defined?(::ActionText::RichText)
12
- return false unless ActiveRecord::Base.connected?
13
12
 
14
13
  ActiveRecord::Base.connection.data_source_exists?("action_text_rich_texts")
15
- rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError
14
+ rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError,
15
+ ActiveRecord::StatementInvalid
16
16
  false
17
17
  end
18
18
 
19
19
  def self.active_storage_available?
20
20
  return false unless defined?(::ActiveStorage::Blob)
21
- return false unless ActiveRecord::Base.connected?
22
21
 
23
22
  c = ActiveRecord::Base.connection
24
23
  c.data_source_exists?("active_storage_blobs") &&
25
24
  c.data_source_exists?("active_storage_attachments")
26
- rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError
25
+ rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError,
26
+ ActiveRecord::StatementInvalid
27
27
  false
28
28
  end
29
29
 
30
- has_rich_text :rich_content if action_text_available?
31
- has_one_attached :image if active_storage_available?
30
+ # Define associations without requiring an active DB connection at boot time.
31
+ # In production, eager loading may happen before AR has connected.
32
+ has_rich_text :rich_content if defined?(::ActionText::RichText)
33
+ has_one_attached :image if defined?(::ActiveStorage::Blob)
32
34
 
33
35
  belongs_to :updated_by, class_name: "User", optional: true
34
36
 
@@ -1,5 +1,5 @@
1
1
  <%# Admin Sidebar Navigation %>
2
- <aside class="w-56 flex-shrink-0 bg-[#FAF9F5] flex flex-col overflow-hidden" aria-label="Admin" data-ruby-cms--mobile-menu-target="sidebar">
2
+ <aside class="w-46 flex-shrink-0 bg-[#FAF9F5] flex flex-col overflow-hidden" aria-label="Admin" data-ruby-cms--mobile-menu-target="sidebar">
3
3
  <div class="px-5 py-5 flex-shrink-0 bg-[#FAF9F5]">
4
4
  <div class="flex items-center gap-3" data-ruby-cms--mobile-menu-target="sidebarContent">
5
5
  <% if respond_to?(:image_tag, true) %>
@@ -94,7 +94,7 @@
94
94
  {},
95
95
  {
96
96
  class: "absolute inset-0 w-full h-full opacity-0 cursor-pointer",
97
- aria: { label: "Locale" },
97
+ aria: { label: t("ruby_cms.nav.locale", default: "Locale") },
98
98
  onchange: "this.form.submit()"
99
99
  } %>
100
100
  </div>
@@ -106,16 +106,16 @@
106
106
  <svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
107
107
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
108
108
  </svg>
109
- <span class="flex-1 text-left">View site</span>
109
+ <span class="flex-1 text-left"><%= t("ruby_cms.nav.view_site", default: "View site") %></span>
110
110
  <% end %>
111
111
  <% end %>
112
112
 
113
113
  <% if respond_to?(:main_app, true) && main_app.respond_to?(:session_path) %>
114
- <%= button_to main_app.session_path, method: :delete, class: "flex items-center gap-3 w-full px-3 py-2.5 text-sm font-medium text-gray-600 rounded-md bg-transparent border-0 cursor-pointer transition-colors hover:bg-blue-500 hover:text-white", data: { turbo_confirm: "Are you sure you want to logout?" } do %>
114
+ <%= button_to main_app.session_path, method: :delete, class: "flex items-center gap-3 w-full px-3 py-2.5 text-sm font-medium text-gray-600 rounded-md bg-transparent border-0 cursor-pointer transition-colors hover:bg-blue-500 hover:text-white", data: { turbo_confirm: t("ruby_cms.nav.logout_confirm", default: "Are you sure you want to logout?") } do %>
115
115
  <svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
116
116
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
117
117
  </svg>
118
- <span class="flex-1 text-left">Logout</span>
118
+ <span class="flex-1 text-left"><%= t("ruby_cms.nav.logout", default: "Logout") %></span>
119
119
  <% end %>
120
120
  <% end %>
121
121
  </div>
@@ -1,161 +1,92 @@
1
1
  <%= form_with model: @content_block,
2
- url: @content_block.persisted? ? ruby_cms_admin_content_block_path(@content_block) : ruby_cms_admin_content_blocks_path,
3
- method: (@content_block.persisted? ? :patch : :post),
2
+ url: ruby_cms_admin_content_blocks_path,
3
+ method: :post,
4
4
  id: "content-block-form",
5
5
  data: { turbo: false } do |f| %>
6
-
7
- <div class="flex items-start justify-between gap-8 px-6 py-4 border-b border-gray-100">
8
- <div class="grid grid-cols-2 gap-4 flex-1">
9
- <div class="flex flex-col gap-1">
10
- <%= f.label :key, class: "text-sm font-medium text-gray-700" %>
11
- <% if @content_block.persisted? %>
12
- <%= f.hidden_field :key, name: nil, value: @content_block.key %>
13
- <span class="text-sm font-mono text-gray-900 py-2"><%= @content_block.key %></span>
14
- <% else %>
15
- <%= f.text_field :key, name: "content_block[key]", class: "h-9 rounded-md border border-gray-200 bg-white px-3 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-teal-200" %>
16
- <% end %>
17
- </div>
18
-
19
- <% if @content_block.persisted? %>
20
- <div class="flex flex-col gap-1">
21
- <label class="text-sm font-medium text-gray-700"><%= t("ruby_cms.admin.content_blocks.content_type") %></label>
22
- <%= select_tag "content_block[content_type]",
23
- options_for_select(::ContentBlock::CONTENT_TYPES, @content_block.content_type),
24
- class: "h-9 rounded-md border border-gray-200 bg-white px-3 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-teal-200" %>
25
- </div>
26
- <% end %>
27
- </div>
28
-
29
- <div class="flex items-center gap-2 flex-shrink-0">
30
- <%= link_to "Cancel",
31
- @content_block.persisted? ? ruby_cms_admin_content_block_path(@content_block) : ruby_cms_admin_content_blocks_path,
32
- class: "inline-flex h-9 items-center justify-center rounded-md border border-gray-200 bg-white px-4 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 transition-colors no-underline" %>
33
- <%= f.submit "Save", class: "inline-flex h-9 items-center justify-center rounded-md bg-gray-900 px-4 text-sm font-medium text-white shadow-sm hover:bg-gray-800 transition-colors cursor-pointer" %>
34
- </div>
35
- </div>
36
-
37
- <% if @content_block.errors.any? %>
38
- <div class="mx-6 mt-4 rounded-md border border-rose-200 bg-rose-50 p-4">
39
- <ul class="list-disc pl-4 space-y-1">
40
- <% @content_block.errors.full_messages.each do |msg| %>
41
- <li class="text-sm text-rose-700"><%= msg %></li>
42
- <% end %>
43
- </ul>
44
- </div>
45
- <% end %>
46
-
47
- <% if @content_block.persisted? && @blocks_by_locale.present? %>
48
- <%# Multi-locale layout %>
49
-
50
- <div class="flex items-center gap-2 px-6 py-4 border-b border-gray-100">
51
- <%= hidden_field_tag "content_block[published]", "0" %>
52
- <%= check_box_tag "content_block[published]", "1",
53
- @blocks_by_locale.values.any?(&:published?),
54
- class: "h-4 w-4 rounded border-gray-300 text-teal-600 focus:ring-teal-200" %>
55
- <label class="text-sm font-medium text-gray-700"><%= t("ruby_cms.admin.content_blocks.published") %> (all translations)</label>
56
- </div>
57
-
58
- <div class="rounded-md border border-gray-200 overflow-hidden mx-6 my-4" data-controller="ruby-cms--locale-tabs">
59
- <div class="flex gap-1 p-2 bg-gray-50 border-b border-gray-200" role="tablist">
60
- <% @blocks_by_locale.each_with_index do |(locale_s, _), idx| %>
61
- <button type="button"
62
- role="tab"
63
- aria-selected="<%= idx == 0 %>"
64
- aria-controls="locale-panel-<%= locale_s %>"
65
- id="locale-tab-<%= locale_s %>"
66
- data-ruby-cms--locale-tabs-target="tab"
67
- data-panel-id="locale-panel-<%= locale_s %>"
68
- data-action="click->ruby-cms--locale-tabs#switchTab"
69
- class="px-3 py-1.5 rounded text-xs font-medium border-none cursor-pointer transition-colors <%= idx == 0 ? 'bg-white text-blue-600 shadow-sm ring-1 ring-gray-200' : 'bg-transparent text-gray-500 hover:bg-gray-100 hover:text-gray-900' %>">
70
- <%= ruby_cms_locale_display_name(locale_s) %>
71
- </button>
72
- <% end %>
73
- </div>
74
-
75
- <% @blocks_by_locale.each_with_index do |(locale_s, block), idx| %>
76
- <div id="locale-panel-<%= locale_s %>"
77
- role="tabpanel"
78
- aria-labelledby="locale-tab-<%= locale_s %>"
79
- class="p-5 <%= idx > 0 ? 'hidden' : '' %>"
80
- data-locale-panel>
81
-
82
- <%= hidden_field_tag "content_block[locales][#{locale_s}][locale]", locale_s %>
83
-
84
- <div class="grid grid-cols-2 gap-4">
85
- <div class="flex flex-col gap-1">
86
- <label class="text-sm font-medium text-gray-700"><%= t("ruby_cms.admin.content_blocks.title") %></label>
87
- <%= text_field_tag "content_block[locales][#{locale_s}][title]",
88
- block.title,
89
- class: "h-9 rounded-md border border-gray-200 bg-white px-3 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-teal-200" %>
6
+ <div class="<%= RubyCms::Admin::AdminResourceCard::CARD_CLASS %>">
7
+ <div class="p-6 space-y-6">
8
+ <% if @content_block.errors.any? %>
9
+ <div class="rounded-lg border border-destructive/30 bg-destructive/5 p-4">
10
+ <div class="flex gap-3">
11
+ <svg class="size-5 text-destructive shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
12
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
13
+ </svg>
14
+ <div>
15
+ <p class="text-sm font-medium text-destructive"><%= t("ruby_cms.admin.content_blocks.errors_title", default: "Please fix the following errors:") %></p>
16
+ <ul class="mt-1.5 text-sm text-destructive/80 list-disc list-inside">
17
+ <% @content_block.errors.full_messages.each do |message| %>
18
+ <li><%= message %></li>
19
+ <% end %>
20
+ </ul>
90
21
  </div>
91
-
92
- <div class="flex flex-col gap-1">
93
- <label class="text-sm font-medium text-gray-700"><%= t("ruby_cms.admin.content_blocks.content") %></label>
94
- <%= text_area_tag "content_block[locales][#{locale_s}][content]",
95
- block.content,
96
- rows: 5,
97
- class: "rounded-md border border-gray-200 bg-white px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-teal-200 resize-y" %>
98
- </div>
99
-
100
- <% if ::ContentBlock.respond_to?(:action_text_available?) && ::ContentBlock.action_text_available? && block.persisted? && block.respond_to?(:rich_content) %>
101
- <div class="col-span-2 flex flex-col gap-1">
102
- <label class="text-sm font-medium text-gray-700"><%= t("ruby_cms.admin.content_blocks.rich_content") %></label>
103
- <%= rich_text_area_tag "content_block[locales][#{locale_s}][rich_content]",
104
- block.rich_content.present? ? block.rich_content.to_s : "",
105
- class: "rounded-md border border-gray-200 bg-white text-sm shadow-sm focus:outline-none" %>
106
- </div>
107
- <% end %>
108
22
  </div>
109
23
  </div>
110
24
  <% end %>
111
- </div>
112
-
113
- <% else %>
114
- <%# Single-locale (new block) %>
115
-
116
- <div class="grid grid-cols-2 gap-4 p-6">
117
- <div class="flex flex-col gap-1">
118
- <%= f.label :locale, class: "text-sm font-medium text-gray-700" %>
119
- <%= f.select :locale,
120
- I18n.available_locales.map { |l| [ruby_cms_locale_display_name(l), l.to_s] },
121
- { include_blank: t("ruby_cms.admin.content_blocks.select_locale") },
122
- class: "h-9 rounded-md border border-gray-200 bg-white px-3 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-teal-200",
123
- name: "content_block[locale]" %>
124
- </div>
125
25
 
126
- <div class="flex flex-col gap-1">
127
- <%= f.label :title, class: "text-sm font-medium text-gray-700" %>
128
- <%= f.text_field :title, class: "h-9 rounded-md border border-gray-200 bg-white px-3 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-teal-200" %>
129
- </div>
26
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
27
+ <div>
28
+ <%= f.label :key, class: RubyCms::Admin::AdminResourceCard::LABEL_CLASS %>
29
+ <%= f.text_field :key,
30
+ class: RubyCms::Admin::AdminResourceCard::INPUT_CLASS,
31
+ placeholder: "homepage_hero_title",
32
+ name: "content_block[key]" %>
33
+ </div>
130
34
 
131
- <div class="flex flex-col gap-1">
132
- <%= f.label :content_type, class: "text-sm font-medium text-gray-700" %>
133
- <%= f.select :content_type, ::ContentBlock::CONTENT_TYPES, {}, class: "h-9 rounded-md border border-gray-200 bg-white px-3 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-teal-200" %>
134
- </div>
35
+ <div>
36
+ <%= f.label :locale, class: RubyCms::Admin::AdminResourceCard::LABEL_CLASS %>
37
+ <%= f.select :locale,
38
+ I18n.available_locales.map { |l| [ruby_cms_locale_display_name(l), l.to_s] },
39
+ { include_blank: t("ruby_cms.admin.content_blocks.select_locale", default: "Select locale") },
40
+ class: RubyCms::Admin::AdminResourceCard::INPUT_CLASS,
41
+ name: "content_block[locale]" %>
42
+ </div>
135
43
 
136
- <div class="col-span-2 flex flex-col gap-1">
137
- <%= f.label :content, class: "text-sm font-medium text-gray-700" %>
138
- <%= f.text_area :content, rows: 5, class: "rounded-md border border-gray-200 bg-white px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-teal-200 resize-y" %>
139
- </div>
44
+ <div>
45
+ <%= f.label :title, class: RubyCms::Admin::AdminResourceCard::LABEL_CLASS %>
46
+ <%= f.text_field :title, class: RubyCms::Admin::AdminResourceCard::INPUT_CLASS %>
47
+ </div>
140
48
 
141
- <% if ::ContentBlock.respond_to?(:action_text_available?) && ::ContentBlock.action_text_available? %>
142
- <div class="col-span-2 flex flex-col gap-1">
143
- <%= f.label :rich_content, class: "text-sm font-medium text-gray-700" %>
144
- <%= f.rich_text_area :rich_content, class: "rounded-md border border-gray-200 bg-white text-sm shadow-sm focus:outline-none" %>
49
+ <div>
50
+ <%= f.label :content_type, class: RubyCms::Admin::AdminResourceCard::LABEL_CLASS %>
51
+ <%= f.select :content_type, ::ContentBlock::CONTENT_TYPES, {}, class: RubyCms::Admin::AdminResourceCard::INPUT_CLASS %>
145
52
  </div>
146
- <% else %>
147
- <div class="col-span-2 rounded-md border border-gray-200 bg-gray-50 p-4">
148
- <p class="text-sm text-gray-500">
149
- Rich text requires Action Text. Run:
150
- <code class="font-mono text-xs bg-gray-100 px-1 rounded">bin/rails action_text:install</code> and <code class="font-mono text-xs bg-gray-100 px-1 rounded">bin/rails db:migrate</code>
151
- </p>
53
+
54
+ <div class="sm:col-span-2">
55
+ <%= f.label :content, class: RubyCms::Admin::AdminResourceCard::LABEL_CLASS %>
56
+ <%= f.text_area :content,
57
+ rows: 5,
58
+ class: "#{RubyCms::Admin::AdminResourceCard::INPUT_CLASS} resize-y" %>
152
59
  </div>
153
- <% end %>
154
60
 
155
- <div class="col-span-2 flex items-center gap-2">
156
- <%= f.check_box :published, class: "h-4 w-4 rounded border-gray-300 text-teal-600 focus:ring-teal-200" %>
157
- <%= f.label :published, class: "text-sm font-medium text-gray-700" %>
61
+ <% if ::ContentBlock.respond_to?(:action_text_available?) && ::ContentBlock.action_text_available? %>
62
+ <div class="sm:col-span-2">
63
+ <%= f.label :rich_content, class: RubyCms::Admin::AdminResourceCard::LABEL_CLASS %>
64
+ <%= f.rich_text_area :rich_content, class: "rounded-lg border border-border bg-background text-sm shadow-sm focus:outline-none" %>
65
+ </div>
66
+ <% else %>
67
+ <div class="sm:col-span-2 rounded-lg border border-border/60 bg-muted/30 p-4">
68
+ <p class="text-sm text-muted-foreground">
69
+ Rich text requires Action Text. Run:
70
+ <code class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">bin/rails action_text:install</code>
71
+ and
72
+ <code class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">bin/rails db:migrate</code>.
73
+ </p>
74
+ </div>
75
+ <% end %>
158
76
  </div>
77
+
78
+ <label class="inline-flex items-center gap-3 cursor-pointer">
79
+ <%= f.check_box :published, class: "size-4 rounded border-border text-primary focus:ring-primary/30 cursor-pointer" %>
80
+ <span class="text-sm font-medium text-foreground"><%= t("ruby_cms.admin.content_blocks.published", default: "Published") %></span>
81
+ </label>
159
82
  </div>
160
- <% end %>
83
+
84
+ <div class="<%= RubyCms::Admin::AdminResourceCard::ACTIONS_CLASS %>">
85
+ <%= link_to t("ruby_cms.admin.common.cancel", default: "Cancel"),
86
+ ruby_cms_admin_content_blocks_path,
87
+ class: RubyCms::Admin::AdminResourceCard::CANCEL_CLASS %>
88
+ <%= f.submit t("ruby_cms.admin.content_blocks.create", default: "Save"),
89
+ class: RubyCms::Admin::AdminResourceCard::SUBMIT_CLASS %>
90
+ </div>
91
+ </div>
161
92
  <% end %>
@@ -1,5 +1,10 @@
1
- <h1 class="text-2xl font-semibold text-gray-900 mb-4">New content block</h1>
1
+ <%= render RubyCms::Admin::AdminPageHeader.new(
2
+ title: t("ruby_cms.admin.content_blocks.new_title", default: "New content block"),
3
+ breadcrumbs: [
4
+ { label: t("ruby_cms.admin.nav.admin", default: "Admin"), url: ruby_cms_admin_root_path },
5
+ { label: t("ruby_cms.admin.nav.content_blocks", default: "Content Blocks"), url: ruby_cms_admin_content_blocks_path },
6
+ { label: t("ruby_cms.admin.content_blocks.new_title", default: "New content block") }
7
+ ]
8
+ ) %>
2
9
 
3
10
  <%= render "form" %>
4
-
5
- <p class="mt-4"><%= link_to "Back", ruby_cms_admin_content_blocks_path, class: "text-blue-600 hover:text-blue-800" %></p>
@@ -78,28 +78,33 @@
78
78
 
79
79
  <%# ── Locale Tabs ── %>
80
80
  <% if @blocks_by_locale.present? %>
81
+ <% active_locale = @blocks_by_locale.key?(@content_block.locale.to_s) ? @content_block.locale.to_s : @blocks_by_locale.keys.first %>
81
82
  <div class="rounded-xl border border-border/60 overflow-hidden" data-controller="ruby-cms--locale-tabs">
82
83
  <div class="flex gap-1 p-2 bg-muted/30 border-b border-border/60" role="tablist">
83
- <% @blocks_by_locale.each_with_index do |(locale_s, _), idx| %>
84
+ <% @blocks_by_locale.each do |locale_s, _| %>
85
+ <% selected = locale_s == active_locale %>
84
86
  <button type="button"
85
87
  role="tab"
86
- aria-selected="<%= idx == 0 %>"
88
+ aria-selected="<%= selected %>"
87
89
  aria-controls="locale-panel-<%= locale_s %>"
88
90
  id="locale-tab-<%= locale_s %>"
89
91
  data-ruby-cms--locale-tabs-target="tab"
90
92
  data-panel-id="locale-panel-<%= locale_s %>"
93
+ data-active-classes="bg-background text-primary shadow-sm ring-1 ring-border/60"
94
+ data-inactive-classes="bg-transparent text-muted-foreground hover:bg-muted hover:text-foreground"
91
95
  data-action="click->ruby-cms--locale-tabs#switchTab"
92
- class="px-3 py-1.5 rounded-md text-xs font-medium border-none cursor-pointer transition-colors <%= idx == 0 ? 'bg-background text-primary shadow-sm ring-1 ring-border/60' : 'bg-transparent text-muted-foreground hover:bg-muted hover:text-foreground' %>">
96
+ class="px-3 py-1.5 rounded-md text-xs font-medium border-none cursor-pointer transition-colors <%= selected ? 'bg-background text-primary shadow-sm ring-1 ring-border/60' : 'bg-transparent text-muted-foreground hover:bg-muted hover:text-foreground' %>">
93
97
  <%= ruby_cms_locale_display_name(locale_s) %>
94
98
  </button>
95
99
  <% end %>
96
100
  </div>
97
101
 
98
- <% @blocks_by_locale.each_with_index do |(locale_s, block), idx| %>
102
+ <% @blocks_by_locale.each do |locale_s, block| %>
103
+ <% selected = locale_s == active_locale %>
99
104
  <div id="locale-panel-<%= locale_s %>"
100
105
  role="tabpanel"
101
106
  aria-labelledby="locale-tab-<%= locale_s %>"
102
- class="p-5 <%= idx > 0 ? 'hidden' : '' %>"
107
+ class="p-5 <%= selected ? '' : 'hidden' %>"
103
108
  data-locale-panel>
104
109
 
105
110
  <%= hidden_field_tag "content_block[locales][#{locale_s}][locale]", locale_s %>
@@ -204,8 +209,8 @@
204
209
  <div class="space-y-3">
205
210
  <% @blocks_by_locale.each do |locale_s, block| %>
206
211
  <% next if block.title.blank? && block.content_body.blank? %>
207
- <div class="rounded-lg border border-border/60 bg-muted/30 p-3">
208
- <span class="inline-flex items-center rounded-md bg-background px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground ring-1 ring-border/60 mb-2"><%= ruby_cms_locale_display_name(locale_s) %></span>
212
+ <div class="py-1">
213
+ <span class="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground"><%= ruby_cms_locale_display_name(locale_s) %></span>
209
214
  <% if block.title.present? %>
210
215
  <p class="text-xs font-medium text-foreground"><%= block.title %></p>
211
216
  <% end %>
@@ -14,7 +14,20 @@ en:
14
14
 
15
15
  ruby_cms:
16
16
  nav:
17
+ locale: "Locale"
17
18
  settings: "Settings"
19
+ view_site: "View site"
20
+ logout: "Logout"
21
+ logout_confirm: "Are you sure you want to logout?"
22
+ items:
23
+ dashboard: "Dashboard"
24
+ visual_editor: "Visual editor"
25
+ content_blocks: "Content blocks"
26
+ analytics: "Analytics"
27
+ permissions: "Permissions"
28
+ visitor_errors: "Visitor errors"
29
+ users: "Users"
30
+ settings: "Settings"
18
31
  admin:
19
32
  breadcrumb:
20
33
  admin: "Admin"
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ nl:
4
+ ruby_cms:
5
+ nav:
6
+ locale: "Taal"
7
+ settings: "Instellingen"
8
+ view_site: "Bekijk site"
9
+ logout: "Uitloggen"
10
+ logout_confirm: "Weet je zeker dat je wilt uitloggen?"
11
+ items:
12
+ dashboard: "Dashboard"
13
+ visual_editor: "Visuele editor"
14
+ content_blocks: "Contentblokken"
15
+ analytics: "Analytics"
16
+ permissions: "Rechten"
17
+ visitor_errors: "Bezoekersfouten"
18
+ users: "Gebruikers"
19
+ settings: "Instellingen"
20
+ admin:
21
+ breadcrumb:
22
+ admin: "Beheer"
23
+ base:
24
+ authentication_required: "Authenticatie vereist."
25
+ not_authorized: "Niet geautoriseerd."
26
+ locales:
27
+ en: "Engels"
28
+ nl: "Nederlands"
29
+ de: "Duits"
30
+ fr: "Frans"
31
+ es: "Spaans"
32
+ content_blocks:
33
+ created: "Contentblok aangemaakt."
34
+ updated: "Contentblok bijgewerkt."
35
+ deleted: "Contentblok verwijderd."
36
+ select_locale: "Selecteer taal"
37
+ content_type: "Inhoudstype"
38
+ title: "Titel"
39
+ content: "Inhoud"
40
+ rich_content: "Rich content"
41
+ published: "Gepubliceerd"
@@ -260,11 +260,17 @@ module RubyCms
260
260
  def install_action_text
261
261
  migrate_dir = Rails.root.join("db/migrate")
262
262
  return unless migrate_dir.directory?
263
- return if action_text_already_installed?(migrate_dir)
264
263
 
265
- say "ℹ Task action_text: Installing Action Text for rich text/image content blocks.", :cyan
266
- run "bin/rails action_text:install"
267
- say "✓ Task action_text: Installed Action Text", :green
264
+ if action_text_already_installed?(migrate_dir)
265
+ say " Task action_text: Existing Action Text setup detected. Skipping action_text:install.",
266
+ :cyan
267
+ else
268
+ say "ℹ Task action_text: Installing Action Text for rich text/image content blocks.", :cyan
269
+ run "bin/rails action_text:install"
270
+ say "✓ Task action_text: Installed Action Text", :green
271
+ end
272
+
273
+ configure_action_text_assets
268
274
  rescue StandardError => e
269
275
  say "⚠ Task action_text: Could not install: #{e.message}. Rich text will be disabled.",
270
276
  :yellow
@@ -380,6 +386,63 @@ module RubyCms
380
386
  rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError
381
387
  false
382
388
  end
389
+
390
+ def configure_action_text_assets
391
+ add_action_text_importmap_pins
392
+ add_action_text_imports_to_admin_entrypoint
393
+ end
394
+
395
+ def add_action_text_importmap_pins
396
+ importmap_path = Rails.root.join("config/importmap.rb")
397
+ return unless File.exist?(importmap_path)
398
+
399
+ content = File.read(importmap_path)
400
+ lines_to_add = []
401
+ lines_to_add << %(pin "trix") unless content.include?('pin "trix"')
402
+ unless content.include?('pin "@rails/actiontext"')
403
+ lines_to_add << %(pin "@rails/actiontext", to: "actiontext.esm.js")
404
+ end
405
+ return if lines_to_add.empty?
406
+
407
+ inject_into_file importmap_path.to_s, before: /^end/ do
408
+ "\n # RubyCMS rich text (Action Text + Trix)\n #{lines_to_add.join("\n ")}\n"
409
+ end
410
+ say "✓ Task action_text/importmap: Added trix/actiontext pins to importmap.rb.", :green
411
+ rescue StandardError => e
412
+ say "⚠ Task action_text/importmap: Could not add pins: #{e.message}. " \
413
+ "Add pins for trix and @rails/actiontext manually.",
414
+ :yellow
415
+ end
416
+
417
+ def add_action_text_imports_to_admin_entrypoint
418
+ admin_js_path = Rails.root.join("app/javascript/admin.js")
419
+ content = File.exist?(admin_js_path) ? File.read(admin_js_path) : ""
420
+
421
+ lines_to_add = []
422
+ lines_to_add << %(import "trix") unless content.include?('import "trix"') || content.include?("import 'trix'")
423
+ unless content.include?('import "@rails/actiontext"') ||
424
+ content.include?("import '@rails/actiontext'")
425
+ lines_to_add << %(import "@rails/actiontext")
426
+ end
427
+ return if lines_to_add.empty?
428
+
429
+ if File.exist?(admin_js_path)
430
+ append_to_file admin_js_path.to_s, "\n#{lines_to_add.join("\n")}\n"
431
+ say "✓ Task action_text/js: Added Action Text imports to app/javascript/admin.js.", :green
432
+ else
433
+ content = <<~JS
434
+ // RubyCMS admin entrypoint
435
+ import "trix"
436
+ import "@rails/actiontext"
437
+ JS
438
+ File.write(admin_js_path, content)
439
+ say "✓ Task action_text/js: Created app/javascript/admin.js with Action Text imports.", :green
440
+ end
441
+ rescue StandardError => e
442
+ say "⚠ Task action_text/js: Could not update admin.js: #{e.message}. " \
443
+ "Import trix and @rails/actiontext manually.",
444
+ :yellow
445
+ end
383
446
  end
384
447
 
385
448
  def install_tailwind
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyCms
4
- VERSION = "0.1.6"
4
+ VERSION = "0.1.7"
5
5
  end
data/lib/ruby_cms.rb CHANGED
@@ -100,7 +100,7 @@ module RubyCms
100
100
  list = nav_registry
101
101
  .select {|item| nav_entry_visible?(item, view_context:, user:) }
102
102
  .sort_by {|item| nav_sort_tuple(item) }
103
- apply_nav_order(list)
103
+ localize_nav_labels(apply_nav_order(list))
104
104
  rescue StandardError => e
105
105
  Rails.logger.error("[RubyCMS] Error filtering navigation: #{e.message}") if defined?(Rails.logger)
106
106
  nav_registry
@@ -131,6 +131,13 @@ module RubyCms
131
131
  [priority, item[:order] || 1000, item[:label].to_s]
132
132
  end
133
133
 
134
+ def localize_nav_labels(list)
135
+ list.map do |item|
136
+ translated = I18n.t("ruby_cms.nav.items.#{item[:key]}", default: item[:label].to_s)
137
+ item.merge(label: translated)
138
+ end
139
+ end
140
+
134
141
  def nav_section_priority(section)
135
142
  section.to_s == NAV_SECTION_MAIN ? 0 : 2
136
143
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_cms
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Codebyjob
@@ -174,6 +174,7 @@ files:
174
174
  - config/database.yml
175
175
  - config/importmap.rb
176
176
  - config/locales/en.yml
177
+ - config/locales/nl.yml
177
178
  - config/routes.rb
178
179
  - db/migrate/20260125000001_create_ruby_cms_permissions.rb
179
180
  - db/migrate/20260125000002_create_ruby_cms_user_permissions.rb