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
@@ -19,20 +19,22 @@ module RubyCms
19
19
  end
20
20
 
21
21
  def view_template
22
- td(class: "w-12 px-6 py-3",
22
+ td(class: "w-10 px-4 py-3",
23
23
  data: { action: "click->#{@controller_name}#stopPropagation" }) do
24
- input(
25
- type: "checkbox",
26
- role: "checkbox",
27
- value: @item_id,
28
- class: "h-4 w-4 rounded border-gray-300 text-teal-600 focus:ring-teal-200",
29
- data: {
30
- "#{@controller_name}-target": "itemCheckbox",
31
- item_id: @item_id,
32
- action: "change->#{@controller_name}#updateSelection"
33
- },
34
- aria_label: "Select row"
35
- )
24
+ div(class: "inline-flex items-center justify-center") do
25
+ input(
26
+ type: "checkbox",
27
+ role: "checkbox",
28
+ value: @item_id,
29
+ class: "size-4 rounded border-border/80 text-primary focus:ring-primary/30 focus:ring-offset-0 cursor-pointer transition-colors",
30
+ data: {
31
+ "#{@controller_name}-target": "itemCheckbox",
32
+ item_id: @item_id,
33
+ action: "change->#{@controller_name}#updateSelection"
34
+ },
35
+ aria_label: "Select row"
36
+ )
37
+ end
36
38
  end
37
39
  end
38
40
  end
@@ -14,17 +14,19 @@ module RubyCms
14
14
  end
15
15
 
16
16
  def view_template
17
- th(class: "w-12 px-6 py-3") do
18
- input(
19
- type: "checkbox",
20
- role: "checkbox",
21
- class: "h-4 w-4 rounded border-gray-300 text-teal-600 focus:ring-teal-200",
22
- data: {
23
- "#{@controller_name}-target": "selectAllCheckbox",
24
- action: "change->#{@controller_name}#toggleSelectAll"
25
- },
26
- aria_label: "Select all"
27
- )
17
+ th(class: "w-10 px-4 py-3") do
18
+ div(class: "inline-flex items-center justify-center") do
19
+ input(
20
+ type: "checkbox",
21
+ role: "checkbox",
22
+ class: "size-4 rounded border-border/80 text-primary focus:ring-primary/30 focus:ring-offset-0 cursor-pointer transition-colors",
23
+ data: {
24
+ "#{@controller_name}-target": "selectAllCheckbox",
25
+ action: "change->#{@controller_name}#toggleSelectAll"
26
+ },
27
+ aria_label: "Select all"
28
+ )
29
+ end
28
30
  end
29
31
  end
30
32
  end
@@ -63,7 +63,7 @@ module RubyCms
63
63
 
64
64
  def dialog_content_attributes
65
65
  {
66
- class: "w-full max-w-md rounded-lg border border-gray-200 bg-white p-6 shadow-lg",
66
+ class: "w-full max-w-md rounded-xl border border-border/60 bg-white p-6 shadow-lg ring-1 ring-black/[0.03]",
67
67
  data: {
68
68
  "#{@controller_name}-target": "dialogContent"
69
69
  },
@@ -74,8 +74,8 @@ module RubyCms
74
74
  def render_close_button
75
75
  button(
76
76
  type: "button",
77
- class: "inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 " \
78
- "hover:bg-gray-100 hover:text-gray-900 transition-colors",
77
+ class: "inline-flex size-8 items-center justify-center rounded-md text-muted-foreground " \
78
+ "hover:bg-muted hover:text-foreground transition-colors",
79
79
  data: {
80
80
  action: "click->#{@controller_name}#closeDialog"
81
81
  },
@@ -119,7 +119,7 @@ module RubyCms
119
119
  def render_header
120
120
  h3(
121
121
  id: "dialog-title",
122
- class: "text-base font-semibold text-gray-900",
122
+ class: "text-base font-semibold text-foreground",
123
123
  data: {
124
124
  "#{@controller_name}-target": "dialogTitle"
125
125
  }
@@ -128,7 +128,7 @@ module RubyCms
128
128
 
129
129
  def render_message
130
130
  div(
131
- class: "mt-3 text-sm text-gray-600 space-y-1",
131
+ class: "mt-3 text-sm text-muted-foreground space-y-1",
132
132
  data: {
133
133
  "#{@controller_name}-target": "dialogMessage"
134
134
  }
@@ -149,8 +149,8 @@ module RubyCms
149
149
  button(
150
150
  type: "button",
151
151
  class: "inline-flex h-9 items-center justify-center rounded-md border " \
152
- "border-gray-200 bg-white px-4 text-sm font-medium text-gray-900 " \
153
- "shadow-sm hover:bg-gray-50 transition-colors",
152
+ "border-border bg-white px-4 text-sm font-medium text-foreground " \
153
+ "shadow-sm hover:bg-muted transition-colors",
154
154
  data: {
155
155
  action: "click->#{@controller_name}#closeDialog"
156
156
  }
@@ -160,8 +160,8 @@ module RubyCms
160
160
  def render_confirm_button
161
161
  button(
162
162
  type: "button",
163
- class: "inline-flex h-9 items-center justify-center rounded-md bg-rose-600 px-4 " \
164
- "text-sm font-medium text-white shadow-sm hover:bg-rose-700 transition-colors",
163
+ class: "inline-flex h-9 items-center justify-center rounded-md bg-destructive px-4 " \
164
+ "text-sm font-medium text-destructive-foreground shadow-sm hover:bg-destructive/90 transition-colors",
165
165
  data: {
166
166
  "#{@controller_name}-target": "dialogConfirmButton",
167
167
  action: "click->#{@controller_name}#confirmAction"
@@ -22,7 +22,7 @@ module RubyCms
22
22
  end
23
23
 
24
24
  def view_template
25
- thead(class: "bg-gray-50") do
25
+ thead(class: "sticky top-0 z-20 bg-muted/40 [&_tr]:border-b [&_tr]:border-border/60") do
26
26
  tr do
27
27
  render_bulk_checkbox_header
28
28
  render_table_headers
@@ -51,7 +51,7 @@ module RubyCms
51
51
  end
52
52
 
53
53
  def th_base_classes
54
- "px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500"
54
+ "px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-muted-foreground"
55
55
  end
56
56
  end
57
57
  end
@@ -31,7 +31,7 @@ module RubyCms
31
31
  end
32
32
 
33
33
  def view_template
34
- div(class: "px-6 py-4 border-b border-gray-200/80 bg-white") do
34
+ div(class: "px-5 py-3 border-b border-border/60 bg-white") do
35
35
  div(class: "flex flex-wrap items-center justify-between gap-4") do
36
36
  render_title_group if @title.present?
37
37
  div(class: "flex items-center gap-2 flex-wrap") do
@@ -47,7 +47,7 @@ module RubyCms
47
47
 
48
48
  def render_title_group
49
49
  div(class: "min-w-0") do
50
- h2(class: "text-sm font-semibold text-gray-900") { @title }
50
+ h2(class: "text-sm font-semibold text-foreground") { @title }
51
51
  end
52
52
  end
53
53
 
@@ -88,7 +88,7 @@ module RubyCms
88
88
  end
89
89
 
90
90
  def render_search_icon
91
- span(class: "absolute left-3 text-gray-400 pointer-events-none") do
91
+ span(class: "absolute left-3 text-muted-foreground pointer-events-none") do
92
92
  svg(class: "h-4 w-4", fill: "none", stroke: "currentColor",
93
93
  viewBox: "0 0 24 24") do |s|
94
94
  s.path(
@@ -106,8 +106,8 @@ module RubyCms
106
106
  type: "search",
107
107
  name: @search_param,
108
108
  placeholder: "Search",
109
- class: "h-9 w-full sm:w-72 rounded-md border border-gray-200 bg-white pl-9 " \
110
- "pr-3 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-teal-200",
109
+ class: "h-9 w-full sm:w-72 rounded-md border border-border bg-white pl-9 " \
110
+ "pr-3 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/20",
111
111
  value: search_value,
112
112
  data: { action: "input->turbo-frame#submit" }
113
113
  )
@@ -125,15 +125,15 @@ module RubyCms
125
125
  {
126
126
  "blue" => "text-blue-600 hover:bg-blue-50",
127
127
  "green" => "text-emerald-600 hover:bg-emerald-50",
128
- "red" => "text-rose-600 hover:bg-rose-50",
128
+ "red" => "text-destructive hover:bg-destructive/10",
129
129
  "purple" => "text-violet-600 hover:bg-violet-50",
130
- "gray" => "text-gray-700 hover:bg-gray-50",
130
+ "gray" => "text-muted-foreground hover:bg-muted",
131
131
  "teal" => "text-teal-600 hover:bg-teal-50"
132
132
  }
133
133
  end
134
134
 
135
135
  def icon_button_base_classes
136
- "inline-flex items-center justify-center h-9 w-9 rounded-md border border-gray-200 " \
136
+ "inline-flex items-center justify-center size-9 rounded-md border border-border " \
137
137
  "bg-white shadow-sm transition-colors"
138
138
  end
139
139
 
@@ -24,7 +24,7 @@ module RubyCms
24
24
  def view_template
25
25
  return unless @pagination[:total_pages] && @pagination[:total_pages] > 1
26
26
 
27
- div(class: "px-6 py-3 flex items-center justify-between gap-4") do
27
+ div(class: "px-5 py-3 flex items-center justify-between gap-4 bg-muted/30") do
28
28
  render_pagination_info
29
29
  render_pagination_controls
30
30
  end
@@ -35,7 +35,7 @@ module RubyCms
35
35
  def render_pagination_info
36
36
  return unless @pagination[:start_item] && @pagination[:end_item] && @pagination[:total_count]
37
37
 
38
- div(class: "text-sm text-gray-500") { pagination_info_text }
38
+ div(class: "text-xs text-muted-foreground tabular-nums") { pagination_info_text }
39
39
  end
40
40
 
41
41
  def pagination_info_text
@@ -75,14 +75,14 @@ module RubyCms
75
75
  end
76
76
 
77
77
  def pagination_button_classes
78
- "inline-flex h-9 items-center justify-center rounded-md border border-gray-200 " \
79
- "bg-white px-3 text-sm font-medium text-gray-900 shadow-sm hover:bg-gray-50 " \
78
+ "inline-flex h-8 items-center justify-center rounded-md border border-border " \
79
+ "bg-white px-3 text-xs font-medium text-foreground shadow-sm hover:bg-muted " \
80
80
  "transition-colors"
81
81
  end
82
82
 
83
83
  def pagination_button_disabled_classes
84
- "inline-flex h-9 items-center justify-center rounded-md border border-gray-200 " \
85
- "bg-white px-3 text-sm font-medium text-gray-400 opacity-60 cursor-not-allowed"
84
+ "inline-flex h-8 items-center justify-center rounded-md border border-border " \
85
+ "bg-white px-3 text-xs font-medium text-muted-foreground opacity-50 cursor-not-allowed"
86
86
  end
87
87
 
88
88
  def render_next_button
@@ -127,7 +127,7 @@ module RubyCms
127
127
  end
128
128
 
129
129
  def render_ellipsis
130
- span(class: "px-2 text-sm text-gray-500") { "…" }
130
+ span(class: "px-2 py-1.5 text-xs font-medium text-muted-foreground select-none") { "…" }
131
131
  end
132
132
 
133
133
  def render_current_page(page_num)
@@ -144,8 +144,8 @@ module RubyCms
144
144
  end
145
145
 
146
146
  def current_page_classes
147
- "inline-flex h-9 items-center justify-center rounded-md border border-gray-200 " \
148
- "bg-gray-900 px-3 text-sm font-medium text-white shadow-sm"
147
+ "inline-flex h-8 items-center justify-center rounded-md border border-primary " \
148
+ "bg-primary px-3 text-xs font-medium text-primary-foreground shadow-sm pointer-events-none"
149
149
  end
150
150
 
151
151
  def calculate_pages_to_show(current_page, total_pages)
@@ -73,7 +73,7 @@ module RubyCms
73
73
  end
74
74
 
75
75
  def build_row_classes
76
- classes = ["hover:bg-gray-50 transition-colors"]
76
+ classes = ["border-b border-border/40 hover:bg-muted/50 transition-colors"]
77
77
  classes << @row_class if @row_class
78
78
  classes << "cursor-pointer" if @click_url
79
79
  build_classes(classes)
@@ -84,9 +84,8 @@ module RubyCms
84
84
  attrs[:item_id] = @data[:item_id] if @data[:item_id]
85
85
 
86
86
  if @click_url
87
- attrs[:controller] = "clickable-row"
88
- attrs[:clickable_row_click_url_value] = @click_url
89
- attrs[:action] = "click->clickable-row#navigate"
87
+ attrs[:click_url] = @click_url
88
+ attrs[:action] = "click->#{@controller_name}#rowClick"
90
89
  end
91
90
 
92
91
  attrs
@@ -26,15 +26,12 @@ module RubyCms
26
26
 
27
27
  def view_template
28
28
  div(
29
- class: "hidden px-6 py-3",
29
+ class: "hidden bg-primary/5 border-t border-border/60 px-5 py-3 backdrop-blur-sm",
30
30
  data: {
31
31
  "#{@controller_name}-target": "bulkBar"
32
32
  }
33
33
  ) do
34
- div(
35
- class: "flex items-center justify-between gap-3 rounded-lg border " \
36
- "border-gray-200/80 bg-white px-4 py-3 shadow-sm"
37
- ) do
34
+ div(class: "flex items-center justify-between gap-4 max-w-full") do
38
35
  render_selection_info
39
36
  render_action_buttons
40
37
  end
@@ -44,17 +41,22 @@ module RubyCms
44
41
  private
45
42
 
46
43
  def render_selection_info
47
- div(class: "flex items-center gap-3 flex-wrap") do
48
- span(
49
- class: "text-sm font-medium text-gray-700",
50
- data: {
51
- "#{@controller_name}-target": "selectedCount"
52
- }
53
- ) { "0 #{@item_name}s selected:" }
44
+ div(class: "flex items-center gap-3") do
45
+ div(class: "flex items-center gap-2") do
46
+ div(class: "size-6 rounded-full bg-primary/10 flex items-center justify-center") do
47
+ span(
48
+ class: "text-xs font-bold text-primary tabular-nums",
49
+ data: {
50
+ "#{@controller_name}-target": "selectedCount"
51
+ }
52
+ ) { "0" }
53
+ end
54
+ span(class: "text-sm font-medium text-foreground") { "selected" }
55
+ end
56
+ div(class: "h-4 w-px bg-border/60")
54
57
  button(
55
58
  type: "button",
56
- class: "text-sm font-medium text-gray-600 hover:text-gray-900 hover:underline " \
57
- "transition-colors",
59
+ class: "text-xs font-medium text-muted-foreground hover:text-foreground transition-colors",
58
60
  data: {
59
61
  "#{@controller_name}-target": "selectAllButton",
60
62
  action: "click->#{@controller_name}#selectAll"
@@ -62,17 +64,16 @@ module RubyCms
62
64
  ) { "Select all" }
63
65
  button(
64
66
  type: "button",
65
- class: "text-sm font-medium text-gray-600 hover:text-gray-900 hover:underline " \
66
- "transition-colors",
67
+ class: "text-xs font-medium text-muted-foreground hover:text-foreground transition-colors",
67
68
  data: {
68
69
  action: "click->#{@controller_name}#clearSelection"
69
70
  }
70
- ) { "Clear selection" }
71
+ ) { "Clear" }
71
72
  end
72
73
  end
73
74
 
74
75
  def render_action_buttons
75
- div(class: "flex items-center gap-2 flex-wrap") do
76
+ div(class: "flex items-center gap-2") do
76
77
  @bulk_action_buttons.each do |button_config|
77
78
  render_custom_action_button(button_config)
78
79
  end
@@ -93,9 +94,9 @@ module RubyCms
93
94
  end
94
95
 
95
96
  def build_button_class(config)
96
- base = "inline-flex items-center justify-center rounded-md border border-gray-200 " \
97
- "bg-white px-3 py-2 text-sm font-medium text-gray-900 shadow-sm " \
98
- "hover:bg-gray-50 transition-colors"
97
+ base = "inline-flex items-center justify-center rounded-md border border-border " \
98
+ "bg-white px-3 py-1.5 text-sm font-medium text-foreground shadow-sm " \
99
+ "hover:bg-muted transition-colors"
99
100
  config[:class].present? ? "#{base} #{config[:class]}" : base
100
101
  end
101
102
 
@@ -118,9 +119,9 @@ module RubyCms
118
119
  def render_delete_button
119
120
  button(
120
121
  type: "button",
121
- class: "inline-flex items-center justify-center rounded-md border border-rose-200 " \
122
- "bg-white px-3 py-2 text-sm font-medium text-rose-700 shadow-sm " \
123
- "hover:bg-rose-50 transition-colors",
122
+ class: "inline-flex items-center justify-center rounded-md border border-destructive/30 " \
123
+ "bg-white px-3 py-1.5 text-sm font-medium text-destructive shadow-sm " \
124
+ "hover:bg-destructive/10 transition-colors",
124
125
  data: {
125
126
  action: "click->#{@controller_name}#showActionDialog",
126
127
  action_name: "delete",
@@ -86,10 +86,16 @@ module RubyCms
86
86
  end
87
87
 
88
88
  def set_cms_locale
89
- if session[:ruby_cms_locale].present? &&
90
- I18n.available_locales.include?(session[:ruby_cms_locale].to_sym)
91
- I18n.locale = session[:ruby_cms_locale].to_sym
92
- end
89
+ locale = session[:ruby_cms_locale].presence || session[:admin_locale].presence
90
+ return if locale.blank?
91
+
92
+ locale = locale.to_sym
93
+ return unless I18n.available_locales.include?(locale)
94
+
95
+ # Keep both session keys in sync for host app + engine controllers.
96
+ session[:ruby_cms_locale] = locale
97
+ session[:admin_locale] = locale
98
+ I18n.locale = locale
93
99
  end
94
100
 
95
101
  # Resolve parameter key for model params
@@ -23,7 +23,7 @@ module RubyCms
23
23
  end
24
24
 
25
25
  def show
26
- @blocks_by_locale = load_blocks_by_locale_for_show
26
+ @blocks_by_locale = load_blocks_by_locale_for_edit
27
27
  respond_with_block(@content_block)
28
28
  end
29
29
 
@@ -37,6 +37,7 @@ module RubyCms
37
37
 
38
38
  def edit
39
39
  @blocks_by_locale = load_blocks_by_locale_for_edit
40
+ render :show
40
41
  end
41
42
 
42
43
  def create
@@ -135,12 +136,12 @@ module RubyCms
135
136
  def handle_locale_update_errors(errors)
136
137
  @content_block.errors.add(:base, errors.join("; "))
137
138
  @blocks_by_locale = load_blocks_by_locale_for_edit
138
- render :edit, status: :unprocessable_content
139
+ render :show, status: :unprocessable_content
139
140
  end
140
141
 
141
142
  def update_single_block
142
143
  @content_block.record_update_by(current_user_cms)
143
- save_and_respond(@content_block, :edit)
144
+ save_and_respond(@content_block, :show)
144
145
  end
145
146
 
146
147
  def save_and_respond(block, failure_view)
@@ -7,9 +7,10 @@ module RubyCms
7
7
  before_action :ensure_authenticated, only: [:update]
8
8
 
9
9
  def update
10
- locale = params[:locale].to_sym
10
+ locale = params[:locale].to_s.presence&.to_sym
11
11
  if I18n.available_locales.include?(locale)
12
12
  session[:ruby_cms_locale] = locale
13
+ session[:admin_locale] = locale
13
14
  I18n.locale = locale
14
15
  end
15
16
 
@@ -17,14 +17,11 @@ module RubyCms
17
17
  end
18
18
 
19
19
  def create
20
- permission = RubyCms::Permission.find(params[:permission_id])
21
- if RubyCms::UserPermission.find_or_create_by!(user: @user, permission: permission)
22
- redirect_to ruby_cms_admin_user_permissions_path(@user),
23
- notice: t("ruby_cms.admin.user_permissions.granted")
20
+ if params[:template].present?
21
+ apply_template
22
+ else
23
+ grant_individual_permission
24
24
  end
25
- rescue ActiveRecord::RecordInvalid
26
- redirect_to ruby_cms_admin_user_permissions_path(@user),
27
- alert: t("ruby_cms.admin.user_permissions.could_not_grant")
28
25
  end
29
26
 
30
27
  def destroy
@@ -54,6 +51,27 @@ module RubyCms
54
51
  def user_class
55
52
  Object.const_get(Rails.application.config.ruby_cms.user_class_name.presence || "User")
56
53
  end
54
+
55
+ def apply_template
56
+ template_key = params[:template].to_sym
57
+ RubyCms::UserPermission.where(user: @user).destroy_all
58
+ RubyCms::Permission.apply_template!(@user, template_key)
59
+ @user.make_admin! if @user.respond_to?(:make_admin!) && !@user.admin?
60
+ redirect_to ruby_cms_admin_user_permissions_path(@user),
61
+ notice: "Applied #{RubyCms::Permission.templates.dig(template_key, :label)} template."
62
+ rescue ArgumentError => e
63
+ redirect_to ruby_cms_admin_user_permissions_path(@user), alert: e.message
64
+ end
65
+
66
+ def grant_individual_permission
67
+ permission = RubyCms::Permission.find(params[:permission_id])
68
+ RubyCms::UserPermission.find_or_create_by!(user: @user, permission: permission)
69
+ redirect_to ruby_cms_admin_user_permissions_path(@user),
70
+ notice: t("ruby_cms.admin.user_permissions.granted")
71
+ rescue ActiveRecord::RecordInvalid
72
+ redirect_to ruby_cms_admin_user_permissions_path(@user),
73
+ alert: t("ruby_cms.admin.user_permissions.could_not_grant")
74
+ end
57
75
  end
58
76
  end
59
77
  end
@@ -34,6 +34,16 @@ module RubyCms
34
34
  key.tr("_", " ").humanize
35
35
  end
36
36
 
37
+ # SVG path fragment for a nav item (matches sidebar), or nil.
38
+ def settings_nav_visibility_icon(entry)
39
+ key_str = entry.key.to_s
40
+ return nil unless key_str.start_with?("nav_show_")
41
+
42
+ nav_key = key_str.delete_prefix("nav_show_")
43
+ item = RubyCms.nav_registry.find { |e| e[:key].to_s == nav_key }
44
+ item&.dig(:icon)
45
+ end
46
+
37
47
  def render_setting_field(entry:, value:, tab:)
38
48
  case entry.type.to_sym
39
49
  when :integer
@@ -50,13 +60,13 @@ module RubyCms
50
60
  private
51
61
 
52
62
  def input_base_classes
53
- "w-full h-9 rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-900 " \
54
- "shadow-sm focus:outline-none focus:ring-2 focus:ring-teal-200"
63
+ "w-full h-9 rounded-lg border border-border bg-background px-3 text-sm text-foreground " \
64
+ "shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/20"
55
65
  end
56
66
 
57
67
  def textarea_base_classes
58
- "w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm text-gray-900 " \
59
- "shadow-sm focus:outline-none focus:ring-2 focus:ring-teal-200"
68
+ "w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground " \
69
+ "shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/20"
60
70
  end
61
71
 
62
72
  def render_integer_setting_field(entry:, value:, tab:)
@@ -88,18 +98,18 @@ module RubyCms
88
98
  :label,
89
99
  "",
90
100
  for: setting_input_id(entry),
91
- class: "relative inline-flex h-6 w-11 cursor-pointer items-center rounded-full " \
92
- "bg-gray-200 transition-colors peer-checked:bg-teal-600"
101
+ class: "relative inline-flex h-7 w-12 cursor-pointer items-center rounded-full " \
102
+ "border border-border bg-muted transition-colors peer-checked:bg-primary peer-checked:border-primary"
93
103
  ) do
94
104
  content_tag(
95
105
  :span,
96
106
  "",
97
- class: "inline-block h-5 w-5 transform rounded-full bg-white shadow-sm transition " \
98
- "translate-x-0.5 peer-checked:translate-x-5"
107
+ class: "inline-block h-5 w-5 transform rounded-full bg-background shadow-sm ring-1 ring-border transition " \
108
+ "translate-x-1 peer-checked:translate-x-6"
99
109
  )
100
110
  end
101
111
 
102
- content_tag(:div, class: "inline-flex items-center") { hidden + checkbox + label }
112
+ content_tag(:div, class: "inline-flex items-center shrink-0") { hidden + checkbox + label }
103
113
  end
104
114
 
105
115
  def render_json_setting_field(entry:, value:, tab:)
@@ -112,8 +112,7 @@ export default class extends Controller {
112
112
 
113
113
  if (count > 0) {
114
114
  bulkBar.classList.remove("hidden");
115
- const itemName = this.itemNameValue || "item";
116
- selectedCount.textContent = `${count} ${itemName}${count === 1 ? "" : "s"} selected:`;
115
+ selectedCount.textContent = `${count}`;
117
116
  } else {
118
117
  bulkBar.classList.add("hidden");
119
118
  }
@@ -143,10 +142,14 @@ export default class extends Controller {
143
142
  const itemId = String(row.getAttribute("data-item-id") || "");
144
143
  if (selectedIdsStr.includes(itemId)) {
145
144
  row.setAttribute("data-state", "selected");
146
- row.classList.add("bg-gray-50");
145
+ row.classList.add("bg-primary/5");
146
+ row.classList.remove("hover:bg-muted/50");
147
147
  } else {
148
148
  row.removeAttribute("data-state");
149
- row.classList.remove("bg-gray-50");
149
+ row.classList.remove("bg-primary/5");
150
+ if (!row.classList.contains("hover:bg-muted/50")) {
151
+ row.classList.add("hover:bg-muted/50");
152
+ }
150
153
  }
151
154
  });
152
155
  }
@@ -302,6 +305,41 @@ export default class extends Controller {
302
305
  this.isProcessing = false;
303
306
  }
304
307
 
308
+ rowClick(event) {
309
+ const row = event.currentTarget;
310
+ const target = event.target;
311
+
312
+ if (
313
+ target.closest('input[type="checkbox"]') ||
314
+ target.closest("button") ||
315
+ target.closest("a") ||
316
+ target.closest("[data-action*='stopPropagation']")
317
+ ) {
318
+ return;
319
+ }
320
+
321
+ if (event.ctrlKey || event.metaKey) {
322
+ event.preventDefault();
323
+ const checkbox = row.querySelector(
324
+ 'input[type="checkbox"][data-item-id]',
325
+ );
326
+ if (checkbox) {
327
+ checkbox.checked = !checkbox.checked;
328
+ this.updateBulkBar();
329
+ }
330
+ return;
331
+ }
332
+
333
+ const clickUrl = row.dataset.clickUrl;
334
+ if (clickUrl) {
335
+ if (window.Turbo) {
336
+ window.Turbo.visit(clickUrl);
337
+ } else {
338
+ window.location.href = clickUrl;
339
+ }
340
+ }
341
+ }
342
+
305
343
  stopPropagation(event) {
306
344
  event.stopPropagation();
307
345
  }
@@ -517,20 +555,23 @@ export default class extends Controller {
517
555
 
518
556
  showNotification(message, type = "info") {
519
557
  const toast = document.createElement("div");
520
- toast.className = `fixed top-4 right-4 z-50 px-4 py-2 rounded-md shadow-lg text-white max-w-sm ${
521
- type === "success"
522
- ? "bg-green-600"
523
- : type === "error"
524
- ? "bg-red-600"
525
- : "bg-blue-600"
558
+ const colorMap = {
559
+ success: "bg-emerald-600",
560
+ error: "bg-destructive",
561
+ info: "bg-primary",
562
+ };
563
+ toast.className = `fixed top-4 right-4 z-50 px-4 py-3 rounded-lg shadow-lg text-white text-sm max-w-sm animate-in slide-in-from-top-2 ${
564
+ colorMap[type] || colorMap.info
526
565
  }`;
527
566
  toast.textContent = message;
528
567
 
529
568
  document.body.appendChild(toast);
530
569
 
531
570
  setTimeout(() => {
532
- toast.remove();
533
- }, 5000);
571
+ toast.style.opacity = "0";
572
+ toast.style.transition = "opacity 200ms ease-out";
573
+ setTimeout(() => toast.remove(), 200);
574
+ }, 4000);
534
575
  }
535
576
 
536
577
  clearItemIdsFromUrl() {