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.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +20 -0
  3. data/README.md +195 -25
  4. data/app/assets/javascripts/railspress/admin.js +39 -0
  5. data/app/assets/javascripts/railspress/markdown_mode.js +343 -0
  6. data/app/assets/stylesheets/application.css +0 -0
  7. data/app/assets/stylesheets/railspress/admin/badges.css +70 -0
  8. data/app/assets/stylesheets/railspress/admin/base.css +25 -0
  9. data/app/assets/stylesheets/railspress/admin/buttons.css +140 -0
  10. data/app/assets/stylesheets/railspress/admin/cards.css +52 -0
  11. data/app/assets/stylesheets/railspress/admin/components/exports.css +55 -0
  12. data/app/assets/stylesheets/railspress/admin/components/focal_point.css +801 -0
  13. data/app/assets/stylesheets/railspress/admin/components/imports.css +144 -0
  14. data/app/assets/stylesheets/railspress/admin/components/lexxy.css +156 -0
  15. data/app/assets/stylesheets/railspress/admin/filters.css +73 -0
  16. data/app/assets/stylesheets/railspress/admin/flash.css +26 -0
  17. data/app/assets/stylesheets/railspress/admin/forms.css +459 -0
  18. data/app/assets/stylesheets/railspress/admin/layout.css +256 -0
  19. data/app/assets/stylesheets/railspress/admin/lists.css +24 -0
  20. data/app/assets/stylesheets/railspress/admin/page.css +111 -0
  21. data/app/assets/stylesheets/railspress/admin/responsive.css +174 -0
  22. data/app/assets/stylesheets/railspress/admin/stats.css +43 -0
  23. data/app/assets/stylesheets/railspress/admin/tables.css +163 -0
  24. data/app/assets/stylesheets/railspress/admin/utilities.css +202 -0
  25. data/app/assets/stylesheets/railspress/admin/variables.css +58 -0
  26. data/app/assets/stylesheets/railspress/application.css +44 -13
  27. data/app/controllers/railspress/admin/base_controller.rb +6 -3
  28. data/app/controllers/railspress/admin/categories_controller.rb +1 -1
  29. data/app/controllers/railspress/admin/cms_transfers_controller.rb +49 -0
  30. data/app/controllers/railspress/admin/content_element_versions_controller.rb +12 -0
  31. data/app/controllers/railspress/admin/content_elements_controller.rb +143 -0
  32. data/app/controllers/railspress/admin/content_groups_controller.rb +69 -0
  33. data/app/controllers/railspress/admin/dashboard_controller.rb +6 -0
  34. data/app/controllers/railspress/admin/entities_controller.rb +157 -0
  35. data/app/controllers/railspress/admin/exports_controller.rb +55 -0
  36. data/app/controllers/railspress/admin/focal_points_controller.rb +100 -0
  37. data/app/controllers/railspress/admin/imports_controller.rb +63 -0
  38. data/app/controllers/railspress/admin/posts_controller.rb +58 -4
  39. data/app/controllers/railspress/admin/prototypes_controller.rb +30 -0
  40. data/app/controllers/railspress/admin/tags_controller.rb +1 -1
  41. data/app/controllers/railspress/application_controller.rb +1 -0
  42. data/app/helpers/railspress/admin_helper.rb +733 -0
  43. data/app/helpers/railspress/application_helper.rb +23 -0
  44. data/app/helpers/railspress/cms_helper.rb +319 -0
  45. data/app/javascript/railspress/controllers/cms_inline_editor_controller.js +147 -0
  46. data/app/javascript/railspress/controllers/content_element_form_controller.js +15 -0
  47. data/app/javascript/railspress/controllers/crop_controller.js +224 -0
  48. data/app/javascript/railspress/controllers/dropzone_controller.js +261 -0
  49. data/app/javascript/railspress/controllers/focal_point_controller.js +124 -0
  50. data/app/javascript/railspress/controllers/image_section_controller.js +94 -0
  51. data/app/javascript/railspress/controllers/index.js +37 -0
  52. data/app/javascript/railspress/index.js +62 -0
  53. data/app/jobs/railspress/export_posts_job.rb +16 -0
  54. data/app/jobs/railspress/import_posts_job.rb +44 -0
  55. data/app/models/concerns/railspress/has_focal_point.rb +242 -0
  56. data/app/models/concerns/railspress/soft_deletable.rb +23 -0
  57. data/app/models/concerns/railspress/taggable.rb +23 -0
  58. data/app/models/railspress/content_element.rb +103 -0
  59. data/app/models/railspress/content_element_version.rb +32 -0
  60. data/app/models/railspress/content_group.rb +39 -0
  61. data/app/models/railspress/export.rb +67 -0
  62. data/app/models/railspress/focal_point.rb +70 -0
  63. data/app/models/railspress/import.rb +65 -0
  64. data/app/models/railspress/post.rb +102 -15
  65. data/app/models/railspress/post_export_processor.rb +162 -0
  66. data/app/models/railspress/post_import_processor.rb +382 -0
  67. data/app/models/railspress/tag.rb +10 -3
  68. data/app/models/railspress/tagging.rb +11 -0
  69. data/app/services/railspress/content_export_service.rb +122 -0
  70. data/app/services/railspress/content_import_service.rb +228 -0
  71. data/app/views/action_text/attachables/_remote_image.html.erb +8 -0
  72. data/app/views/active_storage/blobs/_blob.html.erb +1 -1
  73. data/app/views/layouts/railspress/admin.html.erb +3 -1
  74. data/app/views/railspress/admin/categories/index.html.erb +11 -15
  75. data/app/views/railspress/admin/cms_transfers/show.html.erb +167 -0
  76. data/app/views/railspress/admin/content_element_versions/show.html.erb +42 -0
  77. data/app/views/railspress/admin/content_elements/_form.html.erb +71 -0
  78. data/app/views/railspress/admin/content_elements/_inline_form.html.erb +32 -0
  79. data/app/views/railspress/admin/content_elements/_inline_form_frame.html.erb +6 -0
  80. data/app/views/railspress/admin/content_elements/edit.html.erb +6 -0
  81. data/app/views/railspress/admin/content_elements/index.html.erb +74 -0
  82. data/app/views/railspress/admin/content_elements/new.html.erb +6 -0
  83. data/app/views/railspress/admin/content_elements/show.html.erb +124 -0
  84. data/app/views/railspress/admin/content_groups/_form.html.erb +9 -0
  85. data/app/views/railspress/admin/content_groups/edit.html.erb +6 -0
  86. data/app/views/railspress/admin/content_groups/index.html.erb +42 -0
  87. data/app/views/railspress/admin/content_groups/new.html.erb +6 -0
  88. data/app/views/railspress/admin/content_groups/show.html.erb +92 -0
  89. data/app/views/railspress/admin/dashboard/index.html.erb +36 -1
  90. data/app/views/railspress/admin/entities/_form.html.erb +53 -0
  91. data/app/views/railspress/admin/entities/edit.html.erb +4 -0
  92. data/app/views/railspress/admin/entities/index.html.erb +74 -0
  93. data/app/views/railspress/admin/entities/new.html.erb +4 -0
  94. data/app/views/railspress/admin/entities/show.html.erb +117 -0
  95. data/app/views/railspress/admin/exports/show.html.erb +62 -0
  96. data/app/views/railspress/admin/imports/_instructions.html.erb +56 -0
  97. data/app/views/railspress/admin/imports/show.html.erb +137 -0
  98. data/app/views/railspress/admin/posts/_form.html.erb +102 -28
  99. data/app/views/railspress/admin/posts/_post_row.html.erb +40 -0
  100. data/app/views/railspress/admin/posts/index.html.erb +47 -36
  101. data/app/views/railspress/admin/posts/show.html.erb +55 -19
  102. data/app/views/railspress/admin/prototypes/image_section.html.erb +42 -0
  103. data/app/views/railspress/admin/shared/_dropzone.html.erb +84 -0
  104. data/app/views/railspress/admin/shared/_focal_point_editor.html.erb +102 -0
  105. data/app/views/railspress/admin/shared/_image_section.html.erb +159 -0
  106. data/app/views/railspress/admin/shared/_image_section_compact.html.erb +90 -0
  107. data/app/views/railspress/admin/shared/_image_section_editor.html.erb +171 -0
  108. data/app/views/railspress/admin/shared/_image_section_v2.html.erb +205 -0
  109. data/app/views/railspress/admin/shared/_sidebar.html.erb +73 -5
  110. data/app/views/railspress/admin/tags/index.html.erb +12 -16
  111. data/config/brakeman.ignore +18 -0
  112. data/config/importmap.rb +23 -0
  113. data/config/routes.rb +62 -1
  114. data/db/migrate/20241218000004_create_railspress_post_tags.rb +1 -1
  115. data/db/migrate/20241218000005_create_railspress_imports.rb +21 -0
  116. data/db/migrate/20241218000006_create_railspress_exports.rb +20 -0
  117. data/db/migrate/20241218000007_create_railspress_taggings.rb +20 -0
  118. data/db/migrate/20241218000008_drop_railspress_post_tags.rb +14 -0
  119. data/db/migrate/20241218000010_add_reading_time_to_railspress_posts.rb +5 -0
  120. data/db/migrate/20250105000002_create_railspress_focal_points.rb +20 -0
  121. data/db/migrate/20260206000001_create_railspress_content_groups.rb +18 -0
  122. data/db/migrate/20260206000002_create_railspress_content_elements.rb +21 -0
  123. data/db/migrate/20260206000003_create_railspress_content_element_versions.rb +20 -0
  124. data/db/migrate/20260207000001_add_unique_index_to_content_elements.rb +11 -0
  125. data/db/migrate/20260211112812_add_image_hint_to_railspress_content_elements.rb +7 -0
  126. data/db/migrate/20260211154040_add_required_to_railspress_content_elements.rb +5 -0
  127. data/lib/generators/railspress/entity/entity_generator.rb +89 -0
  128. data/lib/generators/railspress/entity/templates/migration.rb.tt +13 -0
  129. data/lib/generators/railspress/entity/templates/model.rb.tt +21 -0
  130. data/lib/generators/railspress/install/install_generator.rb +51 -40
  131. data/lib/generators/railspress/install/templates/initializer.rb +29 -0
  132. data/lib/railspress/engine.rb +38 -0
  133. data/lib/railspress/entity.rb +239 -0
  134. data/lib/railspress/version.rb +1 -1
  135. data/lib/railspress.rb +198 -8
  136. data/lib/tasks/railspress_tasks.rake +49 -4
  137. metadata +215 -21
  138. data/MIT-LICENSE +0 -20
  139. data/app/assets/stylesheets/railspress/admin.css +0 -1207
  140. data/app/models/railspress/post_tag.rb +0 -8
@@ -0,0 +1,171 @@
1
+ <%#
2
+ Image Section Editor
3
+ ====================
4
+ Expanded focal point editor with its own form.
5
+ Loads via Turbo Frame, saves independently of parent form.
6
+
7
+ All actions (Change, Remove, Save) submit to the focal_points controller
8
+ to avoid nested form issues.
9
+
10
+ Required locals:
11
+ record - The record with HasFocalPoint concern
12
+ attachment_name - Symbol for the attachment (e.g., :header_image)
13
+
14
+ Optional locals:
15
+ label - Section label (default: "Main Image")
16
+ contexts - Hash of context configs for previews
17
+ error_message - Error message to display
18
+ %>
19
+
20
+ <% label ||= "Main Image" %>
21
+ <% contexts ||= Railspress.image_contexts %>
22
+ <% error_message ||= nil %>
23
+ <% attachment = record.send(attachment_name) %>
24
+ <% focal_point = record.focal_point(attachment_name) %>
25
+ <% focal_point_record = record.send("#{attachment_name}_focal_point") %>
26
+ <% frame_id = dom_id(record, "image_section_#{attachment_name}") %>
27
+
28
+ <%# Build cancel URL based on record type %>
29
+ <%
30
+ if record.is_a?(Railspress::Post)
31
+ cancel_url = image_editor_admin_post_path(record, attachment: attachment_name, compact: true)
32
+ elsif record.is_a?(Railspress::ContentElement)
33
+ cancel_url = image_editor_admin_content_element_path(record, compact: true)
34
+ else
35
+ entity_type = record.class.railspress_config.route_key
36
+ cancel_url = admin_entity_image_editor_path(entity_type: entity_type, id: record.id, attachment: attachment_name, compact: true)
37
+ end
38
+ %>
39
+
40
+ <%= content_tag "turbo-frame", id: frame_id do %>
41
+ <div class="rp-image-section">
42
+ <div class="rp-image-section__label">
43
+ <label class="rp-label"><%= label %></label>
44
+ </div>
45
+
46
+ <% if error_message %>
47
+ <div class="rp-flash rp-flash--error" style="margin-bottom: var(--rp-space-sm);">
48
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
49
+ <circle cx="12" cy="12" r="10"/>
50
+ <line x1="15" y1="9" x2="9" y2="15"/>
51
+ <line x1="9" y1="9" x2="15" y2="15"/>
52
+ </svg>
53
+ <%= error_message %>
54
+ </div>
55
+ <% end %>
56
+
57
+ <div class="rp-image-section__details">
58
+ <%# Single form for all actions - submits to focal_points controller %>
59
+ <%= form_with model: focal_point_record,
60
+ url: admin_focal_point_path(focal_point_record),
61
+ method: :patch,
62
+ multipart: true,
63
+ data: {
64
+ controller: "railspress--focal-point",
65
+ railspress__focal_point_x_value: focal_point[:x],
66
+ railspress__focal_point_y_value: focal_point[:y]
67
+ },
68
+ class: "rp-image-section__editor" do |form| %>
69
+
70
+ <div class="rp-image-section__editor-header">
71
+ <span class="rp-image-section__editor-title"><%= label %> Settings</span>
72
+ <div class="rp-image-section__editor-actions">
73
+ <%# Change image - file input that auto-submits %>
74
+ <label class="rp-btn rp-btn--outline rp-btn--sm">
75
+ Change
76
+ <%= file_field_tag :image, accept: "image/*", class: "rp-sr-only", onchange: "this.form.submit()" %>
77
+ </label>
78
+
79
+ <%# Remove image - button that sets hidden field and submits %>
80
+ <button type="button"
81
+ class="rp-btn rp-btn--outline rp-btn--sm rp-btn--danger"
82
+ onclick="if(confirm('Remove this image?')) { this.form.querySelector('[name=remove_image]').value = '1'; this.form.submit(); }">
83
+ Remove
84
+ </button>
85
+ <%= hidden_field_tag :remove_image, "0" %>
86
+
87
+ <%= link_to "Cancel", cancel_url,
88
+ class: "rp-btn rp-btn--outline rp-btn--sm",
89
+ data: { turbo_frame: frame_id } %>
90
+ <%= form.submit "Save",
91
+ class: "rp-btn rp-btn--primary rp-btn--sm" %>
92
+ </div>
93
+ </div>
94
+
95
+ <div class="rp-image-section__editor-body">
96
+ <div class="rp-focal-editor__layout">
97
+ <%# Source image with crosshair %>
98
+ <div class="rp-focal-editor__source">
99
+ <div class="rp-focal-editor__image-wrap"
100
+ data-action="click->railspress--focal-point#pick
101
+ touchstart->railspress--focal-point#touch
102
+ keydown->railspress--focal-point#keydown"
103
+ tabindex="0">
104
+ <% if attachment.attached? %>
105
+ <%= image_tag main_app.url_for(attachment.variant(resize_to_limit: [1200, 800])),
106
+ data: { railspress__focal_point_target: "image" },
107
+ class: "rp-focal-editor__image",
108
+ alt: "Source image for focal point selection" %>
109
+ <% end %>
110
+ <div data-railspress--focal-point-target="crosshair"
111
+ class="rp-focal-editor__crosshair"
112
+ style="left: <%= focal_point[:x] * 100 %>%; top: <%= focal_point[:y] * 100 %>%;"></div>
113
+ </div>
114
+
115
+ <div class="rp-focal-editor__hint">
116
+ <span>Click to set focal point</span>
117
+ <span class="rp-focal-editor__coords" data-railspress--focal-point-target="coordsDisplay">
118
+ <%= "#{(focal_point[:x] * 100).round}%, #{(focal_point[:y] * 100).round}%" %>
119
+ </span>
120
+ </div>
121
+
122
+ <div class="rp-focal-editor__actions">
123
+ <button type="button"
124
+ class="rp-btn rp-btn--outline rp-btn--sm"
125
+ data-action="railspress--focal-point#reset">
126
+ Reset to Center
127
+ </button>
128
+ </div>
129
+ </div>
130
+
131
+ <%# Live previews %>
132
+ <% if contexts.any? %>
133
+ <div class="rp-focal-editor__previews">
134
+ <span class="rp-focal-editor__previews-title">Live Preview</span>
135
+
136
+ <% contexts.each do |name, config| %>
137
+ <% aspect = config[:aspect] %>
138
+ <% aspect_ratio = aspect.is_a?(Array) ? "#{aspect[0]} / #{aspect[1]}" : aspect %>
139
+ <% ratio_label = aspect.is_a?(Array) ? "#{aspect[0]}:#{aspect[1]}" : aspect.to_s %>
140
+
141
+ <div class="rp-focal-editor__preview">
142
+ <div class="rp-focal-editor__preview-image" style="aspect-ratio: <%= aspect_ratio %>;">
143
+ <% if attachment.attached? %>
144
+ <%= image_tag main_app.url_for(attachment.variant(resize_to_limit: [400, 300])),
145
+ data: { railspress__focal_point_target: "preview" },
146
+ style: record.image_css_for(name, attachment_name),
147
+ alt: "#{name.to_s.titleize} preview" %>
148
+ <% end %>
149
+ </div>
150
+ <div class="rp-focal-editor__preview-label">
151
+ <span class="rp-focal-editor__preview-name"><%= name.to_s.titleize %></span>
152
+ <span class="rp-focal-editor__preview-ratio"><%= ratio_label %></span>
153
+ </div>
154
+ </div>
155
+ <% end %>
156
+ </div>
157
+ <% end %>
158
+ </div>
159
+
160
+ <%# Hidden inputs for focal point values %>
161
+ <%= form.hidden_field :focal_x,
162
+ value: focal_point[:x],
163
+ data: { railspress__focal_point_target: "xInput" } %>
164
+ <%= form.hidden_field :focal_y,
165
+ value: focal_point[:y],
166
+ data: { railspress__focal_point_target: "yInput" } %>
167
+ </div>
168
+ <% end %>
169
+ </div>
170
+ </div>
171
+ <% end %>
@@ -0,0 +1,205 @@
1
+ <%#
2
+ Image Section V2 Partial
3
+ ========================
4
+ Improved image section with Stimulus-powered expand/collapse.
5
+ Located in main column (not sidebar) with better UX.
6
+
7
+ Required locals:
8
+ has_image - Boolean, whether an image is attached
9
+
10
+ Optional locals (when has_image is true):
11
+ image_url - URL of the image to display
12
+ filename - Original filename
13
+ filesize - Human-readable file size
14
+ focal_point - Hash with :x and :y (0.0-1.0)
15
+ contexts - Hash of context configs for previews
16
+
17
+ Optional locals (for form integration):
18
+ form - Rails form builder (enables form fields)
19
+ record - The record with HasFocalPoint concern
20
+ attachment_name - Symbol for the attachment (e.g., :header_image)
21
+
22
+ Optional locals (always):
23
+ label - Section label (default: "Main Image")
24
+ %>
25
+
26
+ <% label ||= "Main Image" %>
27
+ <% contexts ||= {} %>
28
+ <% focal_point ||= { x: 0.5, y: 0.5 } %>
29
+ <% has_form = local_assigns[:form].present? %>
30
+ <% attachment_name ||= :header_image %>
31
+ <% focal_point_record = has_form && local_assigns[:record] ? record.send("#{attachment_name}_focal_point") : nil %>
32
+
33
+ <div class="rp-image-section-v2"
34
+ data-controller="railspress--image-section"
35
+ data-railspress--image-section-expanded-value="false">
36
+
37
+ <%# Header row with label and action %>
38
+ <div class="rp-image-section-v2__header">
39
+ <label class="rp-label"><%= label %></label>
40
+ <% if local_assigns[:has_image] %>
41
+ <button type="button"
42
+ class="rp-btn rp-btn--outline rp-btn--sm"
43
+ data-railspress--image-section-target="editButton"
44
+ data-action="railspress--image-section#toggle">
45
+ Edit
46
+ </button>
47
+ <% end %>
48
+ </div>
49
+
50
+ <% if local_assigns[:has_image] %>
51
+ <%# Compact view (shown when collapsed) %>
52
+ <div class="rp-image-section-v2__compact"
53
+ data-railspress--image-section-target="compact"
54
+ data-action="click->railspress--image-section#expand">
55
+ <div class="rp-image-section-v2__thumb">
56
+ <img src="<%= image_url %>"
57
+ style="object-position: <%= focal_point[:x] * 100 %>% <%= focal_point[:y] * 100 %>%;"
58
+ alt="">
59
+ </div>
60
+ <div class="rp-image-section-v2__info">
61
+ <span class="rp-image-section-v2__filename"><%= filename %></span>
62
+ <span class="rp-image-section-v2__meta"><%= filesize %></span>
63
+ <% if focal_point[:x] != 0.5 || focal_point[:y] != 0.5 %>
64
+ <span class="rp-image-section-v2__focal-indicator">
65
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
66
+ <circle cx="12" cy="12" r="3"/>
67
+ <path d="M12 2v4m0 12v4m10-10h-4M6 12H2"/>
68
+ </svg>
69
+ Focal point set
70
+ </span>
71
+ <% end %>
72
+ </div>
73
+ </div>
74
+
75
+ <%# Expanded editor (hidden by default) %>
76
+ <div class="rp-image-section-v2__editor"
77
+ data-railspress--image-section-target="editor"
78
+ hidden>
79
+ <div class="rp-image-section-v2__editor-header">
80
+ <span class="rp-image-section-v2__editor-title"><%= label %> Settings</span>
81
+ <div class="rp-image-section-v2__editor-actions">
82
+ <% if has_form %>
83
+ <label class="rp-btn rp-btn--outline rp-btn--sm">
84
+ Change
85
+ <%= form.file_field attachment_name,
86
+ accept: "image/*",
87
+ class: "rp-sr-only",
88
+ direct_upload: true %>
89
+ </label>
90
+ <label class="rp-btn rp-btn--outline rp-btn--sm">
91
+ <%= form.check_box :"remove_#{attachment_name}", { class: "rp-sr-only" }, "1", "0" %>
92
+ Remove
93
+ </label>
94
+ <% else %>
95
+ <button type="button" class="rp-btn rp-btn--outline rp-btn--sm">Change</button>
96
+ <button type="button" class="rp-btn rp-btn--outline rp-btn--sm">Remove</button>
97
+ <% end %>
98
+ <button type="button"
99
+ class="rp-btn rp-btn--primary rp-btn--sm"
100
+ data-action="railspress--image-section#collapse">
101
+ Done
102
+ </button>
103
+ </div>
104
+ </div>
105
+
106
+ <div class="rp-image-section-v2__editor-body">
107
+ <%# Focal point editor %>
108
+ <div class="rp-focal-editor"
109
+ data-controller="railspress--focal-point"
110
+ data-railspress--focal-point-x-value="<%= focal_point[:x] %>"
111
+ data-railspress--focal-point-y-value="<%= focal_point[:y] %>">
112
+
113
+ <div class="rp-focal-editor__layout">
114
+ <%# Source image with crosshair %>
115
+ <div class="rp-focal-editor__source">
116
+ <div class="rp-focal-editor__image-wrap"
117
+ data-action="click->railspress--focal-point#pick
118
+ touchstart->railspress--focal-point#touch"
119
+ tabindex="0"
120
+ data-action="keydown->railspress--focal-point#keydown">
121
+ <img src="<%= image_url %>"
122
+ data-railspress--focal-point-target="image"
123
+ class="rp-focal-editor__image"
124
+ alt="Source image for focal point selection">
125
+ <div data-railspress--focal-point-target="crosshair"
126
+ class="rp-focal-editor__crosshair"
127
+ style="left: <%= focal_point[:x] * 100 %>%; top: <%= focal_point[:y] * 100 %>%;"></div>
128
+ </div>
129
+
130
+ <div class="rp-focal-editor__hint">
131
+ <span>Click to set focal point</span>
132
+ <span class="rp-focal-editor__coords" data-railspress--focal-point-target="coordsDisplay">
133
+ <%= "#{(focal_point[:x] * 100).round}%, #{(focal_point[:y] * 100).round}%" %>
134
+ </span>
135
+ </div>
136
+
137
+ <div class="rp-focal-editor__actions">
138
+ <button type="button"
139
+ class="rp-btn rp-btn--outline rp-btn--sm"
140
+ data-action="railspress--focal-point#reset">
141
+ Reset to Center
142
+ </button>
143
+ </div>
144
+ </div>
145
+
146
+ <%# Live previews %>
147
+ <% if contexts.any? %>
148
+ <div class="rp-focal-editor__previews">
149
+ <span class="rp-focal-editor__previews-title">Live Preview</span>
150
+
151
+ <% contexts.each do |name, config| %>
152
+ <% aspect = config[:aspect] %>
153
+ <% aspect_ratio = aspect.is_a?(Array) ? "#{aspect[0]} / #{aspect[1]}" : aspect %>
154
+ <% ratio_label = aspect.is_a?(Array) ? "#{aspect[0]}:#{aspect[1]}" : aspect.to_s %>
155
+
156
+ <div class="rp-focal-editor__preview">
157
+ <div class="rp-focal-editor__preview-image" style="aspect-ratio: <%= aspect_ratio %>;">
158
+ <img src="<%= image_url %>"
159
+ data-railspress--focal-point-target="preview"
160
+ style="object-position: <%= focal_point[:x] * 100 %>% <%= focal_point[:y] * 100 %>%;"
161
+ alt="<%= name.to_s.titleize %> preview">
162
+ </div>
163
+ <div class="rp-focal-editor__preview-label">
164
+ <span class="rp-focal-editor__preview-name"><%= name.to_s.titleize %></span>
165
+ <span class="rp-focal-editor__preview-ratio"><%= ratio_label %></span>
166
+ </div>
167
+ </div>
168
+ <% end %>
169
+ </div>
170
+ <% end %>
171
+ </div>
172
+
173
+ <%# Hidden inputs for form submission %>
174
+ <% if has_form && focal_point_record %>
175
+ <%= form.fields_for "#{attachment_name}_focal_point_attributes", focal_point_record do |fp_form| %>
176
+ <%= fp_form.hidden_field :focal_x, data: { railspress__focal_point_target: "inputX" } %>
177
+ <%= fp_form.hidden_field :focal_y, data: { railspress__focal_point_target: "inputY" } %>
178
+ <% end %>
179
+ <% end %>
180
+ </div>
181
+ </div>
182
+ </div>
183
+
184
+ <% else %>
185
+ <%# No image - show dropzone %>
186
+ <% if has_form %>
187
+ <%= render "railspress/admin/shared/dropzone",
188
+ form: form,
189
+ field_name: attachment_name,
190
+ prompt: "Click to upload #{label.downcase}" %>
191
+ <% else %>
192
+ <div class="rp-image-section-v2__dropzone">
193
+ <svg class="rp-image-section-v2__dropzone-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
194
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
195
+ <circle cx="8.5" cy="8.5" r="1.5"/>
196
+ <polyline points="21 15 16 10 5 21"/>
197
+ </svg>
198
+ <p class="rp-image-section-v2__dropzone-text">
199
+ <strong>Click to upload</strong> or drag and drop<br>
200
+ <span class="rp-hint">PNG, JPG, WebP up to 10MB</span>
201
+ </p>
202
+ </div>
203
+ <% end %>
204
+ <% end %>
205
+ </div>
@@ -1,5 +1,17 @@
1
1
  <div class="rp-sidebar-content">
2
- <h1 class="rp-logo">RailsPress</h1>
2
+ <div class="rp-sidebar-header">
3
+ <h1 class="rp-logo">RailsPress</h1>
4
+ <button class="rp-sidebar-toggle" aria-label="Toggle sidebar">
5
+ <svg class="rp-sidebar-toggle-icon rp-sidebar-toggle-icon--collapse" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
6
+ <polyline points="11 17 6 12 11 7"/>
7
+ <polyline points="18 17 13 12 18 7"/>
8
+ </svg>
9
+ <svg class="rp-sidebar-toggle-icon rp-sidebar-toggle-icon--expand" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
10
+ <polyline points="13 17 18 12 13 7"/>
11
+ <polyline points="6 17 11 12 6 7"/>
12
+ </svg>
13
+ </button>
14
+ </div>
3
15
 
4
16
  <nav class="rp-nav">
5
17
  <%= link_to railspress.admin_root_path, class: "rp-nav-link #{current_page?(railspress.admin_root_path) ? 'rp-nav-link--active' : ''}" do %>
@@ -7,7 +19,7 @@
7
19
  <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
8
20
  <polyline points="9 22 9 12 15 12 15 22"/>
9
21
  </svg>
10
- Dashboard
22
+ <span class="rp-nav-text">Dashboard</span>
11
23
  <% end %>
12
24
 
13
25
  <%= link_to railspress.admin_posts_path, class: "rp-nav-link #{controller_name == 'posts' ? 'rp-nav-link--active' : ''}" do %>
@@ -17,14 +29,14 @@
17
29
  <line x1="16" y1="13" x2="8" y2="13"/>
18
30
  <line x1="16" y1="17" x2="8" y2="17"/>
19
31
  </svg>
20
- Posts
32
+ <span class="rp-nav-text">Posts</span>
21
33
  <% end %>
22
34
 
23
35
  <%= link_to railspress.admin_categories_path, class: "rp-nav-link #{controller_name == 'categories' ? 'rp-nav-link--active' : ''}" do %>
24
36
  <svg class="rp-nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
25
37
  <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
26
38
  </svg>
27
- Categories
39
+ <span class="rp-nav-text">Categories</span>
28
40
  <% end %>
29
41
 
30
42
  <%= link_to railspress.admin_tags_path, class: "rp-nav-link #{controller_name == 'tags' ? 'rp-nav-link--active' : ''}" do %>
@@ -32,7 +44,63 @@
32
44
  <path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/>
33
45
  <line x1="7" y1="7" x2="7.01" y2="7"/>
34
46
  </svg>
35
- Tags
47
+ <span class="rp-nav-text">Tags</span>
48
+ <% end %>
49
+
50
+ <% if Railspress.cms_enabled? %>
51
+ <div class="rp-nav-divider"></div>
52
+
53
+ <%= link_to railspress.admin_content_groups_path, class: "rp-nav-link #{controller_name == 'content_groups' ? 'rp-nav-link--active' : ''}" do %>
54
+ <svg class="rp-nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
55
+ <rect x="2" y="2" width="20" height="8" rx="2" ry="2"/>
56
+ <rect x="2" y="14" width="20" height="8" rx="2" ry="2"/>
57
+ </svg>
58
+ <span class="rp-nav-text">Content Groups</span>
59
+ <% end %>
60
+
61
+ <%= link_to railspress.admin_content_elements_path, class: "rp-nav-link #{controller_name == 'content_elements' ? 'rp-nav-link--active' : ''}" do %>
62
+ <svg class="rp-nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
63
+ <polyline points="4 7 4 4 20 4 20 7"/>
64
+ <line x1="9" y1="20" x2="15" y2="20"/>
65
+ <line x1="12" y1="4" x2="12" y2="20"/>
66
+ </svg>
67
+ <span class="rp-nav-text">Content Elements</span>
68
+ <% end %>
69
+
70
+ <%= link_to railspress.admin_cms_transfer_path, class: "rp-nav-link #{controller_name == 'cms_transfers' ? 'rp-nav-link--active' : ''}" do %>
71
+ <svg class="rp-nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
72
+ <path d="M12 3v6m0 0l3-3m-3 3L9 6"/>
73
+ <path d="M12 21v-6m0 0l3 3m-3-3l-3 3"/>
74
+ <rect x="3" y="9" width="18" height="6" rx="2"/>
75
+ </svg>
76
+ <span class="rp-nav-text">CMS Transfer</span>
77
+ <% end %>
78
+ <% end %>
79
+
80
+ <% if Railspress.registered_entities.any? %>
81
+ <div class="rp-nav-divider"></div>
82
+
83
+ <% Railspress.registered_entities.each do |route_key, config| %>
84
+ <%= link_to railspress.admin_entity_index_path(entity_type: route_key),
85
+ class: "rp-nav-link #{controller_name == 'entities' && params[:entity_type] == route_key ? 'rp-nav-link--active' : ''}" do %>
86
+ <svg class="rp-nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
87
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
88
+ <line x1="3" y1="9" x2="21" y2="9"/>
89
+ <line x1="9" y1="21" x2="9" y2="9"/>
90
+ </svg>
91
+ <span class="rp-nav-text"><%= config.label %></span>
92
+ <% end %>
93
+ <% end %>
94
+ <% end %>
95
+
96
+ <div class="rp-nav-divider"></div>
97
+ <%= link_to "/", class: "rp-nav-link" do %>
98
+ <svg class="rp-nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
99
+ <circle cx="12" cy="12" r="10"/>
100
+ <line x1="2" y1="12" x2="22" y2="12"/>
101
+ <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
102
+ </svg>
103
+ <span class="rp-nav-text">Main Site</span>
36
104
  <% end %>
37
105
  </nav>
38
106
  </div>
@@ -1,11 +1,6 @@
1
1
  <% content_for :title, "Tags" %>
2
2
 
3
- <div class="rp-page-header">
4
- <h1 class="rp-page-title">Tags</h1>
5
- <div class="rp-page-actions">
6
- <%= link_to "New Tag", new_admin_tag_path, class: "rp-btn rp-btn--primary" %>
7
- </div>
8
- </div>
3
+ <%= rp_page_header "Tags", "New Tag" => new_admin_tag_path %>
9
4
 
10
5
  <div class="rp-card">
11
6
  <% if @tags.any? %>
@@ -13,10 +8,10 @@
13
8
  <table class="rp-table">
14
9
  <thead>
15
10
  <tr>
16
- <th>Name</th>
17
- <th>Slug</th>
18
- <th>Posts</th>
19
- <th class="rp-table-actions">Actions</th>
11
+ <th><%= rp_table_header("Name") %></th>
12
+ <th><%= rp_table_header("Slug") %></th>
13
+ <th><%= rp_table_header("Usage") %></th>
14
+ <th class="rp-table-actions"><%= rp_table_header("Actions") %></th>
20
15
  </tr>
21
16
  </thead>
22
17
  <tbody>
@@ -24,12 +19,13 @@
24
19
  <tr>
25
20
  <td class="rp-table-primary"><%= tag.name %></td>
26
21
  <td class="rp-table-secondary"><%= tag.slug %></td>
27
- <td><%= tag.posts.count %></td>
22
+ <td><%= tag.taggings.count %></td>
28
23
  <td class="rp-table-actions">
29
- <%= link_to "Edit", edit_admin_tag_path(tag), class: "rp-link" %>
30
- <%= button_to "Delete", admin_tag_path(tag), method: :delete,
31
- data: { turbo_confirm: "Delete this tag?" },
32
- class: "rp-link rp-link--danger" %>
24
+ <%= rp_table_action_icons(
25
+ edit_path: edit_admin_tag_path(tag),
26
+ delete_path: admin_tag_path(tag),
27
+ confirm: "Delete this tag?"
28
+ ) %>
33
29
  </td>
34
30
  </tr>
35
31
  <% end %>
@@ -37,6 +33,6 @@
37
33
  </table>
38
34
  </div>
39
35
  <% else %>
40
- <p class="rp-empty-state">No tags yet. <%= link_to "Create your first tag", new_admin_tag_path, class: "rp-link" %>.</p>
36
+ <%= rp_empty_state "No tags yet.", link_text: "Create your first tag", link_path: new_admin_tag_path %>
41
37
  <% end %>
42
38
  </div>
@@ -0,0 +1,18 @@
1
+ {
2
+ "ignored_warnings": [
3
+ {
4
+ "fingerprint": "63b49bb226efb52aff46bf077f8feded65c3d627b29de6ee403b37d2b08cfe23",
5
+ "note": "Attachment name is validated against entity_config.fields before use"
6
+ },
7
+ {
8
+ "fingerprint": "bb7ad80d40c0237746aced9b06bdfc144d6e3052b53647f09b1738005326de38",
9
+ "note": "Attachment name is validated in entities_controller#image_editor before rendering this partial"
10
+ },
11
+ {
12
+ "fingerprint": "1d94979921177aefb3064de87b6032a5f2760ef30946a52e3015b54af6150e35",
13
+ "note": "Attachment name is validated in entities_controller#image_editor before rendering this partial"
14
+ }
15
+ ],
16
+ "updated": "2026-02-14",
17
+ "brakeman_version": "8.0.2"
18
+ }
@@ -0,0 +1,23 @@
1
+ # RailsPress Engine Importmap Configuration
2
+ #
3
+ # This file is automatically loaded by host applications using importmap-rails.
4
+ #
5
+ # Usage in host app (add to app/javascript/application.js):
6
+ #
7
+ # import "railspress"
8
+ #
9
+ # This auto-registers all RailsPress Stimulus controllers.
10
+
11
+ # Lexxy rich text editor (hard dependency — auto-pinned so host apps don't need to)
12
+ pin "lexxy", to: "lexxy.js"
13
+
14
+ # Turbo for Turbo Frames support
15
+ pin "@hotwired/turbo-rails", to: "turbo.min.js"
16
+
17
+ # Main entry point - auto-registers all controllers
18
+ pin "railspress", to: "railspress/index.js"
19
+
20
+ # Individual controllers (for advanced use cases)
21
+ pin_all_from Railspress::Engine.root.join("app/javascript/railspress/controllers"),
22
+ under: "railspress/controllers",
23
+ to: "railspress/controllers"
data/config/routes.rb CHANGED
@@ -4,6 +4,67 @@ Railspress::Engine.routes.draw do
4
4
  root "dashboard#index"
5
5
  resources :categories, except: [ :show ]
6
6
  resources :tags, except: [ :show ]
7
- resources :posts
7
+
8
+ # Content Element CMS (opt-in via config.enable_cms)
9
+ if Railspress.cms_enabled?
10
+ resources :content_groups
11
+ resources :content_elements do
12
+ member do
13
+ get :inline
14
+ get :image_editor
15
+ end
16
+ end
17
+ resources :content_element_versions, only: [ :show ]
18
+
19
+ # CMS Content Transfer (export/import)
20
+ resource :cms_transfer, only: [ :show ] do
21
+ post :export, on: :member
22
+ post :import, on: :member
23
+ end
24
+ end
25
+
26
+ resources :posts do
27
+ member do
28
+ get "image_editor/:attachment", action: :image_editor, as: :image_editor
29
+ end
30
+ end
31
+
32
+ # Standalone focal point updates (works outside parent form)
33
+ resources :focal_points, only: [ :update ]
34
+ resources :imports, only: [ :create ] do
35
+ collection do
36
+ get ":type", action: :show, as: :typed
37
+ end
38
+ end
39
+ resources :exports, only: [ :create ] do
40
+ collection do
41
+ get ":type", action: :show, as: :typed
42
+ end
43
+ member do
44
+ get :download
45
+ end
46
+ end
47
+
48
+ # UI/UX Prototypes (development only)
49
+ resources :prototypes, only: [] do
50
+ collection do
51
+ get :image_section
52
+ end
53
+ end
54
+
55
+ # Dynamic entity routes for host-defined CMS models
56
+ scope "entities" do
57
+ scope ":entity_type", constraints: ->(req) { Railspress.entity_registered?(req.params[:entity_type]) } do
58
+ get "/", to: "entities#index", as: :entity_index
59
+ get "/new", to: "entities#new", as: :new_entity
60
+ post "/", to: "entities#create"
61
+ get "/:id", to: "entities#show", as: :entity
62
+ get "/:id/edit", to: "entities#edit", as: :edit_entity
63
+ get "/:id/image_editor/:attachment", to: "entities#image_editor", as: :entity_image_editor
64
+ patch "/:id", to: "entities#update"
65
+ put "/:id", to: "entities#update"
66
+ delete "/:id", to: "entities#destroy"
67
+ end
68
+ end
8
69
  end
9
70
  end
@@ -7,6 +7,6 @@ class CreateRailspressPostTags < ActiveRecord::Migration[8.0]
7
7
  t.timestamps
8
8
  end
9
9
 
10
- add_index :railspress_post_tags, [:post_id, :tag_id], unique: true
10
+ add_index :railspress_post_tags, [ :post_id, :tag_id ], unique: true
11
11
  end
12
12
  end