railspress-engine 0.1.2 → 1.2.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.
- checksums.yaml +4 -4
- data/LICENSE +20 -0
- data/README.md +195 -25
- data/app/assets/javascripts/railspress/admin.js +39 -0
- data/app/assets/javascripts/railspress/markdown_mode.js +343 -0
- data/app/assets/stylesheets/application.css +0 -0
- data/app/assets/stylesheets/railspress/admin/badges.css +70 -0
- data/app/assets/stylesheets/railspress/admin/base.css +25 -0
- data/app/assets/stylesheets/railspress/admin/buttons.css +140 -0
- data/app/assets/stylesheets/railspress/admin/cards.css +52 -0
- data/app/assets/stylesheets/railspress/admin/components/exports.css +55 -0
- data/app/assets/stylesheets/railspress/admin/components/focal_point.css +801 -0
- data/app/assets/stylesheets/railspress/admin/components/imports.css +144 -0
- data/app/assets/stylesheets/railspress/admin/components/lexxy.css +156 -0
- data/app/assets/stylesheets/railspress/admin/filters.css +73 -0
- data/app/assets/stylesheets/railspress/admin/flash.css +26 -0
- data/app/assets/stylesheets/railspress/admin/forms.css +459 -0
- data/app/assets/stylesheets/railspress/admin/layout.css +256 -0
- data/app/assets/stylesheets/railspress/admin/lists.css +24 -0
- data/app/assets/stylesheets/railspress/admin/page.css +111 -0
- data/app/assets/stylesheets/railspress/admin/responsive.css +174 -0
- data/app/assets/stylesheets/railspress/admin/stats.css +43 -0
- data/app/assets/stylesheets/railspress/admin/tables.css +163 -0
- data/app/assets/stylesheets/railspress/admin/utilities.css +202 -0
- data/app/assets/stylesheets/railspress/admin/variables.css +58 -0
- data/app/assets/stylesheets/railspress/application.css +44 -13
- data/app/controllers/railspress/admin/base_controller.rb +6 -3
- data/app/controllers/railspress/admin/categories_controller.rb +1 -1
- data/app/controllers/railspress/admin/cms_transfers_controller.rb +49 -0
- data/app/controllers/railspress/admin/content_element_versions_controller.rb +12 -0
- data/app/controllers/railspress/admin/content_elements_controller.rb +143 -0
- data/app/controllers/railspress/admin/content_groups_controller.rb +69 -0
- data/app/controllers/railspress/admin/dashboard_controller.rb +6 -0
- data/app/controllers/railspress/admin/entities_controller.rb +157 -0
- data/app/controllers/railspress/admin/exports_controller.rb +55 -0
- data/app/controllers/railspress/admin/focal_points_controller.rb +100 -0
- data/app/controllers/railspress/admin/imports_controller.rb +63 -0
- data/app/controllers/railspress/admin/posts_controller.rb +58 -4
- data/app/controllers/railspress/admin/prototypes_controller.rb +30 -0
- data/app/controllers/railspress/admin/tags_controller.rb +1 -1
- data/app/controllers/railspress/application_controller.rb +1 -0
- data/app/helpers/railspress/admin_helper.rb +733 -0
- data/app/helpers/railspress/application_helper.rb +23 -0
- data/app/helpers/railspress/cms_helper.rb +319 -0
- data/app/javascript/railspress/controllers/cms_inline_editor_controller.js +147 -0
- data/app/javascript/railspress/controllers/content_element_form_controller.js +15 -0
- data/app/javascript/railspress/controllers/crop_controller.js +224 -0
- data/app/javascript/railspress/controllers/dropzone_controller.js +261 -0
- data/app/javascript/railspress/controllers/focal_point_controller.js +124 -0
- data/app/javascript/railspress/controllers/image_section_controller.js +94 -0
- data/app/javascript/railspress/controllers/index.js +37 -0
- data/app/javascript/railspress/index.js +62 -0
- data/app/jobs/railspress/export_posts_job.rb +16 -0
- data/app/jobs/railspress/import_posts_job.rb +44 -0
- data/app/models/concerns/railspress/has_focal_point.rb +242 -0
- data/app/models/concerns/railspress/soft_deletable.rb +23 -0
- data/app/models/concerns/railspress/taggable.rb +23 -0
- data/app/models/railspress/content_element.rb +103 -0
- data/app/models/railspress/content_element_version.rb +32 -0
- data/app/models/railspress/content_group.rb +39 -0
- data/app/models/railspress/export.rb +67 -0
- data/app/models/railspress/focal_point.rb +70 -0
- data/app/models/railspress/import.rb +65 -0
- data/app/models/railspress/post.rb +102 -15
- data/app/models/railspress/post_export_processor.rb +162 -0
- data/app/models/railspress/post_import_processor.rb +382 -0
- data/app/models/railspress/tag.rb +10 -3
- data/app/models/railspress/tagging.rb +11 -0
- data/app/services/railspress/content_export_service.rb +122 -0
- data/app/services/railspress/content_import_service.rb +228 -0
- data/app/views/action_text/attachables/_remote_image.html.erb +8 -0
- data/app/views/active_storage/blobs/_blob.html.erb +1 -1
- data/app/views/layouts/railspress/admin.html.erb +3 -1
- data/app/views/railspress/admin/categories/index.html.erb +11 -15
- data/app/views/railspress/admin/cms_transfers/show.html.erb +167 -0
- data/app/views/railspress/admin/content_element_versions/show.html.erb +42 -0
- data/app/views/railspress/admin/content_elements/_form.html.erb +71 -0
- data/app/views/railspress/admin/content_elements/_inline_form.html.erb +32 -0
- data/app/views/railspress/admin/content_elements/_inline_form_frame.html.erb +6 -0
- data/app/views/railspress/admin/content_elements/edit.html.erb +6 -0
- data/app/views/railspress/admin/content_elements/index.html.erb +74 -0
- data/app/views/railspress/admin/content_elements/new.html.erb +6 -0
- data/app/views/railspress/admin/content_elements/show.html.erb +124 -0
- data/app/views/railspress/admin/content_groups/_form.html.erb +9 -0
- data/app/views/railspress/admin/content_groups/edit.html.erb +6 -0
- data/app/views/railspress/admin/content_groups/index.html.erb +42 -0
- data/app/views/railspress/admin/content_groups/new.html.erb +6 -0
- data/app/views/railspress/admin/content_groups/show.html.erb +92 -0
- data/app/views/railspress/admin/dashboard/index.html.erb +36 -1
- data/app/views/railspress/admin/entities/_form.html.erb +53 -0
- data/app/views/railspress/admin/entities/edit.html.erb +4 -0
- data/app/views/railspress/admin/entities/index.html.erb +74 -0
- data/app/views/railspress/admin/entities/new.html.erb +4 -0
- data/app/views/railspress/admin/entities/show.html.erb +117 -0
- data/app/views/railspress/admin/exports/show.html.erb +62 -0
- data/app/views/railspress/admin/imports/_instructions.html.erb +56 -0
- data/app/views/railspress/admin/imports/show.html.erb +137 -0
- data/app/views/railspress/admin/posts/_form.html.erb +102 -28
- data/app/views/railspress/admin/posts/_post_row.html.erb +40 -0
- data/app/views/railspress/admin/posts/index.html.erb +47 -36
- data/app/views/railspress/admin/posts/show.html.erb +55 -19
- data/app/views/railspress/admin/prototypes/image_section.html.erb +42 -0
- data/app/views/railspress/admin/shared/_dropzone.html.erb +84 -0
- data/app/views/railspress/admin/shared/_focal_point_editor.html.erb +102 -0
- data/app/views/railspress/admin/shared/_image_section.html.erb +159 -0
- data/app/views/railspress/admin/shared/_image_section_compact.html.erb +90 -0
- data/app/views/railspress/admin/shared/_image_section_editor.html.erb +171 -0
- data/app/views/railspress/admin/shared/_image_section_v2.html.erb +205 -0
- data/app/views/railspress/admin/shared/_sidebar.html.erb +73 -5
- data/app/views/railspress/admin/tags/index.html.erb +12 -16
- data/config/brakeman.ignore +18 -0
- data/config/importmap.rb +23 -0
- data/config/routes.rb +62 -1
- data/db/migrate/20241218000004_create_railspress_post_tags.rb +1 -1
- data/db/migrate/20241218000005_create_railspress_imports.rb +21 -0
- data/db/migrate/20241218000006_create_railspress_exports.rb +20 -0
- data/db/migrate/20241218000007_create_railspress_taggings.rb +20 -0
- data/db/migrate/20241218000008_drop_railspress_post_tags.rb +14 -0
- data/db/migrate/20241218000010_add_reading_time_to_railspress_posts.rb +5 -0
- data/db/migrate/20250105000002_create_railspress_focal_points.rb +20 -0
- data/db/migrate/20260206000001_create_railspress_content_groups.rb +18 -0
- data/db/migrate/20260206000002_create_railspress_content_elements.rb +21 -0
- data/db/migrate/20260206000003_create_railspress_content_element_versions.rb +20 -0
- data/db/migrate/20260207000001_add_unique_index_to_content_elements.rb +11 -0
- data/db/migrate/20260211112812_add_image_hint_to_railspress_content_elements.rb +7 -0
- data/db/migrate/20260211154040_add_required_to_railspress_content_elements.rb +5 -0
- data/lib/generators/railspress/entity/entity_generator.rb +89 -0
- data/lib/generators/railspress/entity/templates/migration.rb.tt +13 -0
- data/lib/generators/railspress/entity/templates/model.rb.tt +21 -0
- data/lib/generators/railspress/install/install_generator.rb +51 -40
- data/lib/generators/railspress/install/templates/initializer.rb +29 -0
- data/lib/railspress/engine.rb +38 -0
- data/lib/railspress/entity.rb +239 -0
- data/lib/railspress/version.rb +1 -1
- data/lib/railspress.rb +198 -8
- data/lib/tasks/railspress_tasks.rake +49 -4
- metadata +215 -21
- data/MIT-LICENSE +0 -20
- data/app/assets/stylesheets/railspress/admin.css +0 -1207
- data/app/models/railspress/post_tag.rb +0 -8
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
module Railspress
|
|
2
|
+
# Helper methods for building consistent admin views.
|
|
3
|
+
# Use these helpers to ensure styling consistency across all entity views.
|
|
4
|
+
module AdminHelper
|
|
5
|
+
# ============================================================
|
|
6
|
+
# FIELD RENDERING HELPERS
|
|
7
|
+
# ============================================================
|
|
8
|
+
|
|
9
|
+
# Master dispatcher that renders the appropriate input based on type.
|
|
10
|
+
# @param form [ActionView::Helpers::FormBuilder] the form builder
|
|
11
|
+
# @param name [Symbol] the field name
|
|
12
|
+
# @param type [Symbol] the field type (:string, :text, :rich_text, :boolean, :datetime, :date, :integer, :decimal, :attachment, :attachments, :select)
|
|
13
|
+
# @param options [Hash] additional options passed to the specific renderer
|
|
14
|
+
# @return [String] rendered HTML
|
|
15
|
+
#
|
|
16
|
+
# @example Basic usage
|
|
17
|
+
# rp_render_field(f, :title, type: :string)
|
|
18
|
+
# rp_render_field(f, :content, type: :rich_text)
|
|
19
|
+
# rp_render_field(f, :featured, type: :boolean, label: "Featured post?")
|
|
20
|
+
def rp_render_field(form, name, type:, **options)
|
|
21
|
+
case type
|
|
22
|
+
when :string
|
|
23
|
+
rp_string_field(form, name, **options)
|
|
24
|
+
when :text
|
|
25
|
+
rp_text_field(form, name, **options)
|
|
26
|
+
when :rich_text
|
|
27
|
+
rp_rich_text_field(form, name, **options)
|
|
28
|
+
when :boolean
|
|
29
|
+
rp_boolean_field(form, name, **options)
|
|
30
|
+
when :datetime
|
|
31
|
+
rp_datetime_field(form, name, **options)
|
|
32
|
+
when :date
|
|
33
|
+
rp_date_field(form, name, **options)
|
|
34
|
+
when :integer
|
|
35
|
+
rp_integer_field(form, name, **options)
|
|
36
|
+
when :decimal
|
|
37
|
+
rp_decimal_field(form, name, **options)
|
|
38
|
+
when :attachment
|
|
39
|
+
rp_attachment_field(form, name, multiple: false, **options)
|
|
40
|
+
when :attachments
|
|
41
|
+
rp_attachment_field(form, name, multiple: true, **options)
|
|
42
|
+
when :focal_point_image
|
|
43
|
+
rp_focal_point_image_field(form, name, **options)
|
|
44
|
+
when :select
|
|
45
|
+
rp_select_field(form, name, **options)
|
|
46
|
+
when :list
|
|
47
|
+
rp_list_field(form, name, **options)
|
|
48
|
+
when :lines
|
|
49
|
+
rp_lines_field(form, name, **options)
|
|
50
|
+
else
|
|
51
|
+
rp_string_field(form, name, **options)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Renders a string input field with label.
|
|
56
|
+
# @param form [ActionView::Helpers::FormBuilder] the form builder
|
|
57
|
+
# @param name [Symbol] the field name
|
|
58
|
+
# @param primary [Boolean] whether this is the primary/title input
|
|
59
|
+
# @param mono [Boolean] whether to use monospace font
|
|
60
|
+
# @param placeholder [String] placeholder text
|
|
61
|
+
# @param required [Boolean] whether field is required
|
|
62
|
+
# @param label [String] custom label text
|
|
63
|
+
# @param hint [String] hint text shown below input
|
|
64
|
+
# @return [String] rendered HTML
|
|
65
|
+
def rp_string_field(form, name, primary: false, mono: false, placeholder: nil, required: false, label: nil, hint: nil, **options)
|
|
66
|
+
placeholder ||= "Enter #{name.to_s.humanize.downcase}..."
|
|
67
|
+
input_class = rp_input_class(primary: primary, mono: mono)
|
|
68
|
+
|
|
69
|
+
content_tag(:div, class: "rp-form-group") do
|
|
70
|
+
output = form.label(name, label, class: rp_label_class(required: required))
|
|
71
|
+
output += form.text_field(name, class: input_class, placeholder: placeholder, required: required, **options)
|
|
72
|
+
output += rp_hint(hint) if hint
|
|
73
|
+
output
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Renders a text area field with label.
|
|
78
|
+
# @param form [ActionView::Helpers::FormBuilder] the form builder
|
|
79
|
+
# @param name [Symbol] the field name
|
|
80
|
+
# @param rows [Integer] number of rows
|
|
81
|
+
# @param placeholder [String] placeholder text
|
|
82
|
+
# @param label [String] custom label text
|
|
83
|
+
# @param hint [String] hint text shown below input
|
|
84
|
+
# @return [String] rendered HTML
|
|
85
|
+
def rp_text_field(form, name, rows: 4, placeholder: nil, label: nil, hint: nil, **options)
|
|
86
|
+
placeholder ||= "Enter #{name.to_s.humanize.downcase}..."
|
|
87
|
+
|
|
88
|
+
content_tag(:div, class: "rp-form-group") do
|
|
89
|
+
output = form.label(name, label, class: "rp-label")
|
|
90
|
+
output += form.text_area(name, rows: rows, class: "rp-input", placeholder: placeholder, **options)
|
|
91
|
+
output += rp_hint(hint) if hint
|
|
92
|
+
output
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Renders a rich text (Trix) editor field with label.
|
|
97
|
+
# @param form [ActionView::Helpers::FormBuilder] the form builder
|
|
98
|
+
# @param name [Symbol] the field name
|
|
99
|
+
# @param placeholder [String] placeholder text
|
|
100
|
+
# @param label [String] custom label text
|
|
101
|
+
# @param hint [String] hint text shown below input
|
|
102
|
+
# @return [String] rendered HTML
|
|
103
|
+
def rp_rich_text_field(form, name, placeholder: nil, label: nil, hint: nil, **options)
|
|
104
|
+
content_tag(:div, class: "rp-form-group") do
|
|
105
|
+
output = form.label(name, label, class: "rp-label")
|
|
106
|
+
output += form.rich_text_area(name, class: "rp-rich-text", **options)
|
|
107
|
+
output += rp_hint(hint) if hint
|
|
108
|
+
output
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Renders a boolean checkbox field.
|
|
113
|
+
# @param form [ActionView::Helpers::FormBuilder] the form builder
|
|
114
|
+
# @param name [Symbol] the field name
|
|
115
|
+
# @param label [String] custom label text
|
|
116
|
+
# @return [String] rendered HTML
|
|
117
|
+
def rp_boolean_field(form, name, label: nil, hint: nil, **options)
|
|
118
|
+
label_text = label || name.to_s.humanize
|
|
119
|
+
|
|
120
|
+
content_tag(:div, class: "rp-form-group") do
|
|
121
|
+
content_tag(:label, class: "rp-checkbox-label") do
|
|
122
|
+
form.check_box(name, options) + " ".html_safe + label_text
|
|
123
|
+
end +
|
|
124
|
+
(hint ? content_tag(:span, hint, class: "rp-hint") : "".html_safe)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Renders a datetime-local input field with label.
|
|
129
|
+
# @param form [ActionView::Helpers::FormBuilder] the form builder
|
|
130
|
+
# @param name [Symbol] the field name
|
|
131
|
+
# @param label [String] custom label text
|
|
132
|
+
# @param hint [String] hint text shown below input
|
|
133
|
+
# @return [String] rendered HTML
|
|
134
|
+
def rp_datetime_field(form, name, label: nil, hint: nil, **options)
|
|
135
|
+
content_tag(:div, class: "rp-form-group") do
|
|
136
|
+
output = form.label(name, label, class: "rp-label")
|
|
137
|
+
output += form.datetime_local_field(name, class: "rp-input", **options)
|
|
138
|
+
output += rp_hint(hint) if hint
|
|
139
|
+
output
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Renders a date input field with label.
|
|
144
|
+
# @param form [ActionView::Helpers::FormBuilder] the form builder
|
|
145
|
+
# @param name [Symbol] the field name
|
|
146
|
+
# @param label [String] custom label text
|
|
147
|
+
# @param hint [String] hint text shown below input
|
|
148
|
+
# @return [String] rendered HTML
|
|
149
|
+
def rp_date_field(form, name, label: nil, hint: nil, **options)
|
|
150
|
+
content_tag(:div, class: "rp-form-group") do
|
|
151
|
+
output = form.label(name, label, class: "rp-label")
|
|
152
|
+
output += form.date_field(name, class: "rp-input", **options)
|
|
153
|
+
output += rp_hint(hint) if hint
|
|
154
|
+
output
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Renders an integer number input field with label.
|
|
159
|
+
# @param form [ActionView::Helpers::FormBuilder] the form builder
|
|
160
|
+
# @param name [Symbol] the field name
|
|
161
|
+
# @param label [String] custom label text
|
|
162
|
+
# @param hint [String] hint text shown below input
|
|
163
|
+
# @return [String] rendered HTML
|
|
164
|
+
def rp_integer_field(form, name, label: nil, hint: nil, **options)
|
|
165
|
+
content_tag(:div, class: "rp-form-group") do
|
|
166
|
+
output = form.label(name, label, class: "rp-label")
|
|
167
|
+
output += form.number_field(name, class: "rp-input", step: 1, **options)
|
|
168
|
+
output += rp_hint(hint) if hint
|
|
169
|
+
output
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Renders a decimal number input field with label.
|
|
174
|
+
# @param form [ActionView::Helpers::FormBuilder] the form builder
|
|
175
|
+
# @param name [Symbol] the field name
|
|
176
|
+
# @param label [String] custom label text
|
|
177
|
+
# @param hint [String] hint text shown below input
|
|
178
|
+
# @return [String] rendered HTML
|
|
179
|
+
def rp_decimal_field(form, name, label: nil, hint: nil, **options)
|
|
180
|
+
content_tag(:div, class: "rp-form-group") do
|
|
181
|
+
output = form.label(name, label, class: "rp-label")
|
|
182
|
+
output += form.number_field(name, class: "rp-input", step: "any", **options)
|
|
183
|
+
output += rp_hint(hint) if hint
|
|
184
|
+
output
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Renders a select dropdown field with label.
|
|
189
|
+
# @param form [ActionView::Helpers::FormBuilder] the form builder
|
|
190
|
+
# @param name [Symbol] the field name
|
|
191
|
+
# @param choices [Array] options for select (array of [text, value] or just values)
|
|
192
|
+
# @param include_blank [Boolean, String] whether to include blank option
|
|
193
|
+
# @param label [String] custom label text
|
|
194
|
+
# @param hint [String] hint text shown below input
|
|
195
|
+
# @return [String] rendered HTML
|
|
196
|
+
#
|
|
197
|
+
# @example Basic usage
|
|
198
|
+
# rp_select_field(f, :status, choices: Post.statuses.keys)
|
|
199
|
+
# rp_select_field(f, :category_id, choices: Category.pluck(:name, :id), include_blank: "No category")
|
|
200
|
+
def rp_select_field(form, name, choices:, include_blank: false, label: nil, hint: nil, **options)
|
|
201
|
+
content_tag(:div, class: "rp-form-group") do
|
|
202
|
+
output = form.label(name, label, class: "rp-label")
|
|
203
|
+
output += form.select(name, choices, { include_blank: include_blank }, { class: "rp-select" }.merge(options))
|
|
204
|
+
output += rp_hint(hint) if hint
|
|
205
|
+
output
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Renders a comma-separated list input field with label.
|
|
210
|
+
# Uses the virtual attribute `#{name}_list` for form binding.
|
|
211
|
+
# @param form [ActionView::Helpers::FormBuilder] the form builder
|
|
212
|
+
# @param name [Symbol] the field name (e.g., :tech_stack)
|
|
213
|
+
# @param placeholder [String] placeholder text
|
|
214
|
+
# @param label [String] custom label text
|
|
215
|
+
# @param hint [String] hint text shown below input
|
|
216
|
+
# @return [String] rendered HTML
|
|
217
|
+
#
|
|
218
|
+
# @example Usage
|
|
219
|
+
# rp_list_field(f, :tech_stack)
|
|
220
|
+
# rp_list_field(f, :tech_stack, hint: "Add technologies separated by commas")
|
|
221
|
+
def rp_list_field(form, name, placeholder: nil, label: nil, hint: nil, **options)
|
|
222
|
+
virtual_name = "#{name}_list"
|
|
223
|
+
placeholder ||= "Item 1, Item 2, Item 3"
|
|
224
|
+
|
|
225
|
+
content_tag(:div, class: "rp-form-group") do
|
|
226
|
+
output = form.label(virtual_name, label || name.to_s.humanize, class: "rp-label")
|
|
227
|
+
output += form.text_field(virtual_name, class: "rp-input", placeholder: placeholder, **options)
|
|
228
|
+
output += rp_hint(hint || "Separate items with commas")
|
|
229
|
+
output
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Renders a line-separated list textarea field with label.
|
|
234
|
+
# Uses the virtual attribute `#{name}_list` for form binding.
|
|
235
|
+
# @param form [ActionView::Helpers::FormBuilder] the form builder
|
|
236
|
+
# @param name [Symbol] the field name (e.g., :highlights)
|
|
237
|
+
# @param rows [Integer] number of textarea rows
|
|
238
|
+
# @param placeholder [String] placeholder text
|
|
239
|
+
# @param label [String] custom label text
|
|
240
|
+
# @param hint [String] hint text shown below input
|
|
241
|
+
# @return [String] rendered HTML
|
|
242
|
+
#
|
|
243
|
+
# @example Usage
|
|
244
|
+
# rp_lines_field(f, :highlights)
|
|
245
|
+
# rp_lines_field(f, :highlights, rows: 6, hint: "Each line becomes one item")
|
|
246
|
+
def rp_lines_field(form, name, rows: 5, placeholder: nil, label: nil, hint: nil, **options)
|
|
247
|
+
virtual_name = "#{name}_list"
|
|
248
|
+
placeholder ||= "One item per line"
|
|
249
|
+
|
|
250
|
+
content_tag(:div, class: "rp-form-group") do
|
|
251
|
+
output = form.label(virtual_name, label || name.to_s.humanize, class: "rp-label")
|
|
252
|
+
output += form.text_area(virtual_name, rows: rows, class: "rp-input", placeholder: placeholder, **options)
|
|
253
|
+
output += rp_hint(hint || "Enter one item per line")
|
|
254
|
+
output
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Renders a file attachment field with preview and removal option.
|
|
259
|
+
# @param form [ActionView::Helpers::FormBuilder] the form builder
|
|
260
|
+
# @param name [Symbol] the field name
|
|
261
|
+
# @param multiple [Boolean] whether to allow multiple files
|
|
262
|
+
# @param accept [String] accepted file types (e.g., "image/*")
|
|
263
|
+
# @param record [ActiveRecord::Base] the record (defaults to form.object)
|
|
264
|
+
# @param param_key [String] the param key for removal checkbox
|
|
265
|
+
# @param label [String] custom label text
|
|
266
|
+
# @param hint [String] hint text shown below input
|
|
267
|
+
# @return [String] rendered HTML
|
|
268
|
+
def rp_attachment_field(form, name, multiple: false, accept: "image/*", record: nil, param_key: nil, label: nil, hint: nil, **options)
|
|
269
|
+
record ||= form.object
|
|
270
|
+
param_key ||= record.model_name.param_key
|
|
271
|
+
attachment = record.public_send(name)
|
|
272
|
+
|
|
273
|
+
content_tag(:div, class: "rp-form-group") do
|
|
274
|
+
output = "".html_safe
|
|
275
|
+
|
|
276
|
+
if multiple && attachment.attached?
|
|
277
|
+
# Multiple attachments preview
|
|
278
|
+
output += content_tag(:div, class: "rp-gallery-preview") do
|
|
279
|
+
attachment.map do |att|
|
|
280
|
+
content_tag(:div, class: "rp-gallery-item") do
|
|
281
|
+
item = if att.image?
|
|
282
|
+
image_tag(main_app.url_for(att), class: "rp-gallery-thumb")
|
|
283
|
+
else
|
|
284
|
+
content_tag(:div, class: "rp-gallery-file") do
|
|
285
|
+
content_tag(:span, "📄", class: "rp-gallery-file-icon") +
|
|
286
|
+
content_tag(:span, att.filename, class: "rp-gallery-file-name")
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
item += content_tag(:label, class: "rp-gallery-remove") do
|
|
290
|
+
check_box_tag("#{param_key}[remove_#{name}][]", att.id, false) + " Remove"
|
|
291
|
+
end
|
|
292
|
+
item
|
|
293
|
+
end
|
|
294
|
+
end.join.html_safe
|
|
295
|
+
end
|
|
296
|
+
output += form.label(name, label || "Add images", class: "rp-label")
|
|
297
|
+
output += form.file_field(name, multiple: true, accept: accept, class: "rp-file-input", direct_upload: true, **options)
|
|
298
|
+
output += rp_hint(hint || "Select multiple images to upload") if hint != false
|
|
299
|
+
elsif !multiple && attachment.attached?
|
|
300
|
+
# Single attachment preview
|
|
301
|
+
output += content_tag(:div, class: "rp-attachment-preview") do
|
|
302
|
+
preview = if attachment.image?
|
|
303
|
+
image_tag(main_app.url_for(attachment), class: "rp-attachment-thumb")
|
|
304
|
+
else
|
|
305
|
+
content_tag(:div, class: "rp-attachment-file") do
|
|
306
|
+
content_tag(:span, attachment.filename, class: "rp-attachment-file-name")
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
preview += content_tag(:label, class: "rp-attachment-remove") do
|
|
310
|
+
check_box_tag("#{param_key}[remove_#{name}]", "1", false) + " Remove"
|
|
311
|
+
end
|
|
312
|
+
preview
|
|
313
|
+
end
|
|
314
|
+
output += form.label(name, label, class: "rp-label")
|
|
315
|
+
output += form.file_field(name, accept: accept, class: "rp-file-input", direct_upload: true, **options)
|
|
316
|
+
output += rp_hint(hint) if hint
|
|
317
|
+
else
|
|
318
|
+
# No attachment yet
|
|
319
|
+
output += form.label(name, label, class: "rp-label")
|
|
320
|
+
if multiple
|
|
321
|
+
output += form.file_field(name, multiple: true, accept: accept, class: "rp-file-input", direct_upload: true, **options)
|
|
322
|
+
else
|
|
323
|
+
output += form.file_field(name, accept: accept, class: "rp-file-input", direct_upload: true, **options)
|
|
324
|
+
end
|
|
325
|
+
output += rp_hint(hint) if hint
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
output
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Renders a focal point image field with the compact/editor UI.
|
|
333
|
+
# For persisted records with images, shows the compact view with Edit button.
|
|
334
|
+
# For new records or no image, shows a dropzone upload.
|
|
335
|
+
# @param form [ActionView::Helpers::FormBuilder] the form builder
|
|
336
|
+
# @param name [Symbol] the attachment field name (e.g., :main_image)
|
|
337
|
+
# @param record [ActiveRecord::Base] the record (defaults to form.object)
|
|
338
|
+
# @param label [String] custom label text
|
|
339
|
+
# @return [String] rendered HTML
|
|
340
|
+
def rp_focal_point_image_field(form, name, record: nil, label: nil, **options)
|
|
341
|
+
record ||= form.object
|
|
342
|
+
label ||= name.to_s.humanize
|
|
343
|
+
attachment = record.public_send(name)
|
|
344
|
+
has_image = attachment.attached? && attachment.blob&.persisted?
|
|
345
|
+
|
|
346
|
+
if record.persisted? && has_image
|
|
347
|
+
# Persisted record with image - render focal point compact view
|
|
348
|
+
render partial: "railspress/admin/shared/image_section_compact",
|
|
349
|
+
locals: {
|
|
350
|
+
record: record,
|
|
351
|
+
attachment_name: name,
|
|
352
|
+
label: label
|
|
353
|
+
}
|
|
354
|
+
else
|
|
355
|
+
# New record or no image - render dropzone
|
|
356
|
+
content_tag(:div, class: "rp-form-group") do
|
|
357
|
+
output = content_tag(:label, label, class: "rp-label")
|
|
358
|
+
if has_image
|
|
359
|
+
# Image uploaded but record not saved yet - show preview
|
|
360
|
+
output += content_tag(:div, class: "rp-image-section__compact") do
|
|
361
|
+
preview = content_tag(:div, class: "rp-image-section__thumb") do
|
|
362
|
+
image_tag(main_app.url_for(attachment.variant(resize_to_limit: [ 120, 80 ])), alt: "")
|
|
363
|
+
end
|
|
364
|
+
preview += content_tag(:div, class: "rp-image-section__info") do
|
|
365
|
+
content_tag(:span, attachment.filename, class: "rp-image-section__filename") +
|
|
366
|
+
content_tag(:span, number_to_human_size(attachment.byte_size), class: "rp-image-section__meta")
|
|
367
|
+
end
|
|
368
|
+
preview += content_tag(:div, class: "rp-image-section__actions") do
|
|
369
|
+
content_tag(:label, class: "rp-btn rp-btn--outline rp-btn--sm") do
|
|
370
|
+
"Change".html_safe + form.file_field(name, accept: "image/*", class: "rp-sr-only", direct_upload: true)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
preview
|
|
374
|
+
end
|
|
375
|
+
output += rp_hint("Save to enable focal point editing.")
|
|
376
|
+
else
|
|
377
|
+
# No image - show dropzone
|
|
378
|
+
output += render(partial: "railspress/admin/shared/dropzone",
|
|
379
|
+
locals: { form: form, field_name: name, prompt: "Click to upload #{label.downcase}" })
|
|
380
|
+
end
|
|
381
|
+
output
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# ============================================================
|
|
387
|
+
# TABLE ACTION HELPERS
|
|
388
|
+
# ============================================================
|
|
389
|
+
|
|
390
|
+
# Renders the standard edit icon button for table rows.
|
|
391
|
+
# @param path [String] the edit path
|
|
392
|
+
# @param title [String] tooltip text
|
|
393
|
+
# @return [String] rendered HTML
|
|
394
|
+
def rp_edit_icon(path, title: "Edit")
|
|
395
|
+
link_to path, class: "rp-icon-btn", title: title do
|
|
396
|
+
rp_icon(:edit)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Renders the standard delete icon button for table rows.
|
|
401
|
+
# @param path [String] the delete path
|
|
402
|
+
# @param confirm [String] confirmation message
|
|
403
|
+
# @param title [String] tooltip text
|
|
404
|
+
# @return [String] rendered HTML
|
|
405
|
+
def rp_delete_icon(path, confirm: "Delete this item?", title: "Delete", disabled: false, disabled_title: nil)
|
|
406
|
+
if disabled
|
|
407
|
+
content_tag(:span, class: "rp-icon-btn rp-icon-btn--danger rp-icon-btn--disabled",
|
|
408
|
+
title: disabled_title || title) do
|
|
409
|
+
rp_icon(:trash)
|
|
410
|
+
end
|
|
411
|
+
else
|
|
412
|
+
button_to path, method: :delete,
|
|
413
|
+
data: { turbo_confirm: confirm },
|
|
414
|
+
class: "rp-icon-btn rp-icon-btn--danger", title: title do
|
|
415
|
+
rp_icon(:trash)
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Renders standard edit and delete action icons for table rows.
|
|
421
|
+
# @param edit_path [String] the edit path
|
|
422
|
+
# @param delete_path [String] the delete path
|
|
423
|
+
# @param confirm [String] confirmation message for delete
|
|
424
|
+
# @return [String] rendered HTML
|
|
425
|
+
#
|
|
426
|
+
# @example Usage
|
|
427
|
+
# rp_table_actions(edit_admin_category_path(category), admin_category_path(category), confirm: "Delete this category?")
|
|
428
|
+
def rp_table_action_icons(edit_path:, delete_path:, confirm: "Delete this item?",
|
|
429
|
+
delete_disabled: false, disabled_title: nil)
|
|
430
|
+
rp_edit_icon(edit_path) +
|
|
431
|
+
rp_delete_icon(delete_path, confirm: confirm,
|
|
432
|
+
disabled: delete_disabled, disabled_title: disabled_title)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Renders an SVG icon.
|
|
436
|
+
# @param name [Symbol] the icon name (:edit, :trash, :plus, :search)
|
|
437
|
+
# @return [String] rendered SVG HTML
|
|
438
|
+
def rp_icon(name)
|
|
439
|
+
icons = {
|
|
440
|
+
edit: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>',
|
|
441
|
+
trash: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>',
|
|
442
|
+
plus: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg>',
|
|
443
|
+
search: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>'
|
|
444
|
+
}
|
|
445
|
+
icons[name]&.html_safe || ""
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# ============================================================
|
|
449
|
+
# TABLE DISPLAY HELPERS
|
|
450
|
+
# ============================================================
|
|
451
|
+
|
|
452
|
+
# Truncates text with ellipsis, HTML-safe.
|
|
453
|
+
# @param text [String] the text to truncate
|
|
454
|
+
# @param length [Integer] maximum length
|
|
455
|
+
# @return [String] truncated text
|
|
456
|
+
def rp_truncated_text(text, length: 50)
|
|
457
|
+
truncate(text.to_s, length: length)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Shows a colored badge for status values.
|
|
461
|
+
# @param value [String, Symbol] the status value
|
|
462
|
+
# @param type [Symbol] badge type (:success, :warning, :danger, :default, or status name like :published, :draft)
|
|
463
|
+
# @return [String] rendered HTML
|
|
464
|
+
#
|
|
465
|
+
# @example Usage
|
|
466
|
+
# rp_status_badge(post.status)
|
|
467
|
+
# rp_status_badge("Active", type: :success)
|
|
468
|
+
def rp_status_badge(value, type: nil)
|
|
469
|
+
type ||= value.to_s.downcase.to_sym
|
|
470
|
+
rp_badge(value.to_s.titleize, status: type)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Shows "Yes" / "No" badge with appropriate styling.
|
|
474
|
+
# @param value [Boolean] the boolean value
|
|
475
|
+
# @return [String] rendered HTML
|
|
476
|
+
#
|
|
477
|
+
# @example Usage
|
|
478
|
+
# rp_boolean_badge(post.featured) # => green "Yes" or gray "No"
|
|
479
|
+
def rp_boolean_badge(value)
|
|
480
|
+
if value
|
|
481
|
+
rp_badge("Yes", status: :published)
|
|
482
|
+
else
|
|
483
|
+
rp_badge("No", status: :draft)
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Shows attachment status badge.
|
|
488
|
+
# @param attachment [ActiveStorage::Attached] the attachment or attachments
|
|
489
|
+
# @return [String] rendered HTML
|
|
490
|
+
#
|
|
491
|
+
# @example Usage
|
|
492
|
+
# rp_attachment_badge(post.header_image) # => "Attached" or "None"
|
|
493
|
+
# rp_attachment_badge(project.gallery) # => "5 images" or "None"
|
|
494
|
+
def rp_attachment_badge(attachment)
|
|
495
|
+
if attachment.respond_to?(:attached?) && attachment.attached?
|
|
496
|
+
if attachment.respond_to?(:count)
|
|
497
|
+
count = attachment.count
|
|
498
|
+
rp_badge(pluralize(count, "file"), status: :published)
|
|
499
|
+
else
|
|
500
|
+
rp_badge("Attached", status: :published)
|
|
501
|
+
end
|
|
502
|
+
else
|
|
503
|
+
rp_badge("None", status: :draft)
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# ============================================================
|
|
508
|
+
# FLASH/FEEDBACK HELPERS
|
|
509
|
+
# ============================================================
|
|
510
|
+
|
|
511
|
+
# Renders all flash message types with appropriate styling.
|
|
512
|
+
# @return [String] rendered HTML
|
|
513
|
+
#
|
|
514
|
+
# @example Usage (in layout)
|
|
515
|
+
# <%= rp_flash_messages %>
|
|
516
|
+
def rp_flash_messages
|
|
517
|
+
return unless flash.any?
|
|
518
|
+
|
|
519
|
+
flash_type_classes = {
|
|
520
|
+
notice: "rp-flash--success",
|
|
521
|
+
alert: "rp-flash--danger",
|
|
522
|
+
warning: "rp-flash--warning",
|
|
523
|
+
info: "rp-flash--info"
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
content_tag(:div, class: "rp-flash-container") do
|
|
527
|
+
flash.map do |type, message|
|
|
528
|
+
css_class = flash_type_classes[type.to_sym] || "rp-flash--info"
|
|
529
|
+
content_tag(:div, message, class: "rp-flash #{css_class}")
|
|
530
|
+
end.join.html_safe
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# ============================================================
|
|
535
|
+
# LAYOUT HELPERS (existing methods below)
|
|
536
|
+
# ============================================================
|
|
537
|
+
# Renders a page header with title and optional action buttons.
|
|
538
|
+
# @param title [String] the page title
|
|
539
|
+
# @param actions [Hash] action links to render (label => path or label => [path, options])
|
|
540
|
+
# @return [String] rendered HTML
|
|
541
|
+
#
|
|
542
|
+
# @example Basic usage
|
|
543
|
+
# <%= rp_page_header "Posts" %>
|
|
544
|
+
#
|
|
545
|
+
# @example With primary action
|
|
546
|
+
# <%= rp_page_header "Posts", "New Post" => new_admin_post_path %>
|
|
547
|
+
#
|
|
548
|
+
# @example With multiple actions
|
|
549
|
+
# <%= rp_page_header "Posts",
|
|
550
|
+
# "Export" => [admin_exports_path, class: "rp-btn rp-btn--secondary"],
|
|
551
|
+
# "New Post" => new_admin_post_path %>
|
|
552
|
+
def rp_page_header(title, actions = {})
|
|
553
|
+
content_tag(:div, class: "rp-page-header") do
|
|
554
|
+
header_content = content_tag(:h1, title, class: "rp-page-title")
|
|
555
|
+
|
|
556
|
+
if actions.any?
|
|
557
|
+
action_links = actions.map do |label, target|
|
|
558
|
+
path, options = target.is_a?(Array) ? target : [ target, {} ]
|
|
559
|
+
btn_class = options.delete(:class) || "rp-btn rp-btn--primary"
|
|
560
|
+
link_to(label, path, options.merge(class: btn_class))
|
|
561
|
+
end.join.html_safe
|
|
562
|
+
|
|
563
|
+
header_content += content_tag(:div, action_links, class: "rp-page-actions")
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
header_content
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# Renders a standalone page title (for new/edit pages without actions).
|
|
571
|
+
# @param title [String] the page title
|
|
572
|
+
# @return [String] rendered HTML
|
|
573
|
+
def rp_page_title(title)
|
|
574
|
+
content_tag(:h1, title, class: "rp-page-title rp-page-title--standalone")
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# Renders a card wrapper for content.
|
|
578
|
+
# @param padded [Boolean] whether to add internal padding
|
|
579
|
+
# @param options [Hash] additional HTML attributes
|
|
580
|
+
# @yield the card content
|
|
581
|
+
# @return [String] rendered HTML
|
|
582
|
+
#
|
|
583
|
+
# @example Basic card
|
|
584
|
+
# <%= rp_card do %>
|
|
585
|
+
# <table>...</table>
|
|
586
|
+
# <% end %>
|
|
587
|
+
#
|
|
588
|
+
# @example Padded card for forms
|
|
589
|
+
# <%= rp_card(padded: true) do %>
|
|
590
|
+
# <%= render "form" %>
|
|
591
|
+
# <% end %>
|
|
592
|
+
def rp_card(padded: false, **options, &block)
|
|
593
|
+
classes = [ "rp-card" ]
|
|
594
|
+
classes << "rp-card--padded" if padded
|
|
595
|
+
classes << options.delete(:class) if options[:class]
|
|
596
|
+
|
|
597
|
+
content_tag(:div, options.merge(class: classes.join(" ")), &block)
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
# Renders form errors in the standard style.
|
|
601
|
+
# @param record [ActiveRecord::Base] the record to check for errors
|
|
602
|
+
# @return [String, nil] rendered HTML or nil if no errors
|
|
603
|
+
def rp_form_errors(record)
|
|
604
|
+
return unless record.errors.any?
|
|
605
|
+
|
|
606
|
+
content_tag(:div, class: "rp-form-errors") do
|
|
607
|
+
content_tag(:ul) do
|
|
608
|
+
record.errors.full_messages.map do |msg|
|
|
609
|
+
content_tag(:li, msg)
|
|
610
|
+
end.join.html_safe
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
# Renders a form group (label + input wrapper) with consistent styling.
|
|
616
|
+
# @yield the form group content (label and input)
|
|
617
|
+
# @return [String] rendered HTML
|
|
618
|
+
def rp_form_group(&block)
|
|
619
|
+
content_tag(:div, class: "rp-form-group", &block)
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
# Renders form actions (submit + cancel) with consistent styling.
|
|
623
|
+
# @param form [ActionView::Helpers::FormBuilder] the form builder
|
|
624
|
+
# @param cancel_path [String] path for cancel link
|
|
625
|
+
# @param submit_text [String] text for submit button (defaults to form default)
|
|
626
|
+
# @return [String] rendered HTML
|
|
627
|
+
def rp_form_actions(form, cancel_path, submit_text: nil)
|
|
628
|
+
content_tag(:div, class: "rp-form-actions") do
|
|
629
|
+
submit_options = { class: "rp-btn rp-btn--primary" }
|
|
630
|
+
buttons = form.submit(submit_text, submit_options)
|
|
631
|
+
buttons += link_to("Cancel", cancel_path, class: "rp-btn rp-btn--secondary")
|
|
632
|
+
buttons
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# Renders a sidebar section for complex forms.
|
|
637
|
+
# @param title [String] the section title
|
|
638
|
+
# @yield the section content
|
|
639
|
+
# @return [String] rendered HTML
|
|
640
|
+
def rp_sidebar_section(title, &block)
|
|
641
|
+
content_tag(:div, class: "rp-sidebar-section") do
|
|
642
|
+
content_tag(:h3, title, class: "rp-sidebar-title") +
|
|
643
|
+
capture(&block)
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# Renders an empty state message for lists with no items.
|
|
648
|
+
# @param message [String] the message to display
|
|
649
|
+
# @param link_text [String, nil] optional link text
|
|
650
|
+
# @param link_path [String, nil] optional link path
|
|
651
|
+
# @return [String] rendered HTML
|
|
652
|
+
def rp_empty_state(message, link_text: nil, link_path: nil)
|
|
653
|
+
content = message
|
|
654
|
+
content += " " + link_to(link_text, link_path, class: "rp-link") + "." if link_text && link_path
|
|
655
|
+
|
|
656
|
+
content_tag(:p, content.html_safe, class: "rp-empty-state")
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
# Renders a badge with the appropriate status styling.
|
|
660
|
+
# @param text [String] the badge text
|
|
661
|
+
# @param status [Symbol, String] the status type (:draft, :published, :pending, etc.)
|
|
662
|
+
# @return [String] rendered HTML
|
|
663
|
+
def rp_badge(text, status:)
|
|
664
|
+
content_tag(:span, text, class: "rp-badge rp-badge--#{status}")
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
# Renders a hint/help text below a form input.
|
|
668
|
+
# @param text [String] the hint text
|
|
669
|
+
# @return [String] rendered HTML
|
|
670
|
+
def rp_hint(text)
|
|
671
|
+
content_tag(:p, text, class: "rp-hint")
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# CSS classes for a standard text input.
|
|
675
|
+
# @param primary [Boolean] whether this is the primary/title input
|
|
676
|
+
# @param mono [Boolean] whether to use monospace font (for slugs, codes)
|
|
677
|
+
# @param size [Symbol] input size (:sm, :lg, or nil for default)
|
|
678
|
+
# @return [String] CSS class string
|
|
679
|
+
def rp_input_class(primary: false, mono: false, size: nil)
|
|
680
|
+
classes = [ "rp-input" ]
|
|
681
|
+
classes << "rp-input--title" if primary
|
|
682
|
+
classes << "rp-input--mono" if mono
|
|
683
|
+
classes << "rp-input--#{size}" if size
|
|
684
|
+
classes.join(" ")
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# CSS classes for a label.
|
|
688
|
+
# @param large [Boolean] whether to use large label style
|
|
689
|
+
# @param required [Boolean] whether to show required indicator
|
|
690
|
+
# @return [String] CSS class string
|
|
691
|
+
def rp_label_class(large: false, required: false)
|
|
692
|
+
classes = [ "rp-label" ]
|
|
693
|
+
classes << "rp-label--lg" if large
|
|
694
|
+
classes << "rp-label--required" if required
|
|
695
|
+
classes.join(" ")
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
# Renders a sortable table header link.
|
|
699
|
+
# Clicking toggles between ascending and descending order.
|
|
700
|
+
# @param column [Symbol, String] the column name for sorting
|
|
701
|
+
# @param label [String] the display text for the header
|
|
702
|
+
# @param current_sort [String, nil] the currently sorted column
|
|
703
|
+
# @param current_direction [String] current sort direction ("asc" or "desc")
|
|
704
|
+
# @return [String] rendered HTML
|
|
705
|
+
#
|
|
706
|
+
# @example Basic usage
|
|
707
|
+
# <%= rp_sortable_header(:title, "Title", current_sort: @sort, current_direction: @direction) %>
|
|
708
|
+
def rp_sortable_header(column, label, current_sort:, current_direction:)
|
|
709
|
+
column = column.to_s
|
|
710
|
+
is_active = current_sort == column
|
|
711
|
+
# Toggle direction if clicking the same column, otherwise default to asc
|
|
712
|
+
new_direction = is_active && current_direction == "asc" ? "desc" : "asc"
|
|
713
|
+
|
|
714
|
+
classes = [ "rp-sortable" ]
|
|
715
|
+
classes << "rp-sortable--active" if is_active
|
|
716
|
+
classes << "rp-sortable--#{current_direction}" if is_active
|
|
717
|
+
|
|
718
|
+
link_to(
|
|
719
|
+
label,
|
|
720
|
+
url_for(request.query_parameters.merge(sort: column, direction: new_direction)),
|
|
721
|
+
class: classes.join(" ")
|
|
722
|
+
)
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
# Renders a non-sortable table header.
|
|
726
|
+
# @param label [String] the display text for the header
|
|
727
|
+
# @param options [Hash] additional HTML attributes
|
|
728
|
+
# @return [String] rendered HTML
|
|
729
|
+
def rp_table_header(label, **options)
|
|
730
|
+
content_tag(:span, label, options)
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
end
|