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
|
@@ -1,34 +1,70 @@
|
|
|
1
1
|
<div class="rp-page-header">
|
|
2
2
|
<div>
|
|
3
3
|
<h1 class="rp-page-title"><%= @post.title %></h1>
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
<span><%= @post.category.name %></span>
|
|
8
|
-
<% end %>
|
|
9
|
-
<% if authors_enabled? && @post.author %>
|
|
10
|
-
<span>by <%= @post.author.public_send(Railspress.author_display_method) %></span>
|
|
11
|
-
<% end %>
|
|
12
|
-
<span><%= @post.created_at.strftime("%b %d, %Y") %></span>
|
|
13
|
-
</div>
|
|
4
|
+
<% if authors_enabled? && @post.author %>
|
|
5
|
+
<p class="rp-byline">by <%= @post.author.public_send(Railspress.author_display_method) %></p>
|
|
6
|
+
<% end %>
|
|
14
7
|
</div>
|
|
15
8
|
<div class="rp-page-actions">
|
|
9
|
+
<% if @post.live? %>
|
|
10
|
+
<%= link_to "View", "#{Railspress.blog_path}/#{@post.slug}", class: "rp-btn rp-btn--secondary", target: "_blank" %>
|
|
11
|
+
<% end %>
|
|
16
12
|
<%= link_to "Edit", edit_admin_post_path(@post), class: "rp-btn rp-btn--primary" %>
|
|
17
13
|
<%= link_to "Back", admin_posts_path, class: "rp-btn rp-btn--secondary" %>
|
|
18
14
|
</div>
|
|
19
15
|
</div>
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
<div class="rp-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<% end %>
|
|
17
|
+
<div class="rp-post-meta-card">
|
|
18
|
+
<div class="rp-meta-item">
|
|
19
|
+
<span class="rp-meta-label">Status</span>
|
|
20
|
+
<span class="rp-badge rp-badge--<%= @post.display_status %>"><%= @post.display_status.titleize %></span>
|
|
26
21
|
</div>
|
|
27
|
-
<%
|
|
22
|
+
<% if @post.published_at %>
|
|
23
|
+
<div class="rp-meta-item">
|
|
24
|
+
<span class="rp-meta-label">Published</span>
|
|
25
|
+
<span class="rp-meta-value"><%= @post.published_at.strftime("%b %d, %Y at %l:%M %p") %></span>
|
|
26
|
+
</div>
|
|
27
|
+
<% end %>
|
|
28
|
+
<div class="rp-meta-item">
|
|
29
|
+
<span class="rp-meta-label">Created</span>
|
|
30
|
+
<span class="rp-meta-value"><%= @post.created_at.strftime("%b %d, %Y") %></span>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="rp-meta-item">
|
|
33
|
+
<span class="rp-meta-label">Updated</span>
|
|
34
|
+
<span class="rp-meta-value"><%= @post.updated_at.strftime("%b %d, %Y") %></span>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="rp-meta-item">
|
|
37
|
+
<span class="rp-meta-label">Reading Time</span>
|
|
38
|
+
<span class="rp-meta-value"><%= @post.reading_time_display %> min</span>
|
|
39
|
+
</div>
|
|
40
|
+
<% if @post.category || @post.tags.any? %>
|
|
41
|
+
<div class="rp-meta-row">
|
|
42
|
+
<% if @post.category %>
|
|
43
|
+
<div class="rp-meta-item">
|
|
44
|
+
<span class="rp-meta-label">Category</span>
|
|
45
|
+
<span class="rp-meta-value"><%= @post.category.name %></span>
|
|
46
|
+
</div>
|
|
47
|
+
<% end %>
|
|
48
|
+
<% if @post.tags.any? %>
|
|
49
|
+
<div class="rp-meta-item rp-meta-item--tags">
|
|
50
|
+
<span class="rp-meta-label">Tags</span>
|
|
51
|
+
<div class="rp-tag-list">
|
|
52
|
+
<% @post.tags.each do |tag| %>
|
|
53
|
+
<span class="rp-tag"><%= tag.name %></span>
|
|
54
|
+
<% end %>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<% end %>
|
|
58
|
+
</div>
|
|
59
|
+
<% end %>
|
|
60
|
+
</div>
|
|
28
61
|
|
|
29
|
-
<% if
|
|
30
|
-
<div class="rp-
|
|
31
|
-
<%=
|
|
62
|
+
<% if post_images_enabled? && @post.header_image.attached? %>
|
|
63
|
+
<div class="rp-image-section-card">
|
|
64
|
+
<%= render "railspress/admin/shared/image_section_compact",
|
|
65
|
+
record: @post,
|
|
66
|
+
attachment_name: :header_image,
|
|
67
|
+
label: "Main Image" %>
|
|
32
68
|
</div>
|
|
33
69
|
<% end %>
|
|
34
70
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<h1 class="rp-page-title rp-page-title--standalone">Image Section Prototype</h1>
|
|
2
|
+
|
|
3
|
+
<p class="rp-hint" style="margin-bottom: var(--rp-space-xl); max-width: 700px;">
|
|
4
|
+
<strong>Purpose:</strong> Iterate on the main image section UI/UX.
|
|
5
|
+
This uses real CSS and Stimulus controllers with mock data.
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<div class="rp-card rp-card--padded" style="max-width: 800px;">
|
|
9
|
+
<h2 style="font-size: 1rem; margin-bottom: var(--rp-space-lg); color: var(--rp-text-muted);">
|
|
10
|
+
State 1: Image Exists (Compact View)
|
|
11
|
+
</h2>
|
|
12
|
+
|
|
13
|
+
<%= render "railspress/admin/shared/image_section_v2",
|
|
14
|
+
has_image: true,
|
|
15
|
+
image_url: @mock_image_url,
|
|
16
|
+
filename: @mock_filename,
|
|
17
|
+
filesize: @mock_filesize,
|
|
18
|
+
focal_point: @mock_focal_point,
|
|
19
|
+
contexts: @contexts,
|
|
20
|
+
label: "Main Image" %>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="rp-card rp-card--padded" style="max-width: 800px; margin-top: var(--rp-space-xl);">
|
|
24
|
+
<h2 style="font-size: 1rem; margin-bottom: var(--rp-space-lg); color: var(--rp-text-muted);">
|
|
25
|
+
State 2: No Image (Upload Prompt)
|
|
26
|
+
</h2>
|
|
27
|
+
|
|
28
|
+
<%= render "railspress/admin/shared/image_section_v2",
|
|
29
|
+
has_image: false,
|
|
30
|
+
label: "Main Image" %>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div style="margin-top: var(--rp-space-2xl); padding: var(--rp-space-lg); background: var(--rp-bg); border: 1px solid var(--rp-border); border-radius: var(--rp-radius);">
|
|
34
|
+
<h3 style="font-size: 0.875rem; margin-bottom: var(--rp-space-md);">Implementation Notes</h3>
|
|
35
|
+
<ul style="font-size: 0.875rem; color: var(--rp-text-muted); padding-left: var(--rp-space-lg); margin: 0;">
|
|
36
|
+
<li>Stimulus controller handles expand/collapse (no server round-trip)</li>
|
|
37
|
+
<li>Clicking compact view or Edit button expands the editor</li>
|
|
38
|
+
<li>Done button collapses back to compact view</li>
|
|
39
|
+
<li>Focal point picker reuses existing <code>railspress--focal-point</code> controller</li>
|
|
40
|
+
<li>CSS animations for smooth transitions</li>
|
|
41
|
+
</ul>
|
|
42
|
+
</div>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Dropzone Partial
|
|
3
|
+
================
|
|
4
|
+
A drag-and-drop file upload component with progress indicator.
|
|
5
|
+
|
|
6
|
+
Required locals:
|
|
7
|
+
form - The Rails form builder
|
|
8
|
+
field_name - Symbol for the attachment field (e.g., :header_image)
|
|
9
|
+
|
|
10
|
+
Optional locals:
|
|
11
|
+
accept - MIME types to accept (default: "image/*")
|
|
12
|
+
max_size - Max file size in bytes (default: 10MB)
|
|
13
|
+
prompt - Text prompt to display (default: "Drag image here or click to browse")
|
|
14
|
+
hint - Hint text below prompt (default: "PNG, JPG, WebP up to 10MB")
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
render "railspress/admin/shared/dropzone",
|
|
18
|
+
form: form, field_name: :header_image, prompt: "Drop featured image here"
|
|
19
|
+
%>
|
|
20
|
+
|
|
21
|
+
<% accept ||= "image/*" %>
|
|
22
|
+
<% max_size ||= 10 * 1024 * 1024 %>
|
|
23
|
+
<% prompt ||= "Drag image here or click to browse" %>
|
|
24
|
+
<% hint ||= "PNG, JPG, WebP up to 10MB" %>
|
|
25
|
+
|
|
26
|
+
<div data-controller="railspress--dropzone"
|
|
27
|
+
data-railspress--dropzone-url-value="<%= main_app.rails_direct_uploads_path %>"
|
|
28
|
+
data-railspress--dropzone-accept-value="<%= accept %>"
|
|
29
|
+
data-railspress--dropzone-max-size-value="<%= max_size %>"
|
|
30
|
+
data-action="drop->railspress--dropzone#drop
|
|
31
|
+
dragover->railspress--dropzone#dragover
|
|
32
|
+
dragleave->railspress--dropzone#dragleave"
|
|
33
|
+
class="rp-dropzone">
|
|
34
|
+
|
|
35
|
+
<%# Hidden file input %>
|
|
36
|
+
<%= form.file_field field_name,
|
|
37
|
+
accept: accept,
|
|
38
|
+
class: "rp-sr-only",
|
|
39
|
+
data: {
|
|
40
|
+
railspress__dropzone_target: "input",
|
|
41
|
+
action: "change->railspress--dropzone#change"
|
|
42
|
+
} %>
|
|
43
|
+
|
|
44
|
+
<%# Hidden field for signed_id (populated after upload) %>
|
|
45
|
+
<input type="hidden"
|
|
46
|
+
name="<%= form.object_name %>[<%= field_name %>]"
|
|
47
|
+
data-railspress--dropzone-target="signedId">
|
|
48
|
+
|
|
49
|
+
<%# Drop area %>
|
|
50
|
+
<div data-railspress--dropzone-target="dropArea" class="rp-dropzone__area">
|
|
51
|
+
<svg class="rp-dropzone__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
52
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
|
53
|
+
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
54
|
+
<polyline points="21 15 16 10 5 21"/>
|
|
55
|
+
</svg>
|
|
56
|
+
<p class="rp-dropzone__prompt" data-railspress--dropzone-target="prompt">
|
|
57
|
+
<strong><%= prompt.split.first %></strong> <%= prompt.split[1..].join(" ") %>
|
|
58
|
+
</p>
|
|
59
|
+
<p class="rp-dropzone__hint"><%= hint %></p>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<%# Preview (shown after file selected) %>
|
|
63
|
+
<div data-railspress--dropzone-target="preview" class="rp-dropzone__preview" hidden>
|
|
64
|
+
<img data-railspress--dropzone-target="previewImage" class="rp-dropzone__preview-image" alt="">
|
|
65
|
+
<button type="button"
|
|
66
|
+
class="rp-dropzone__remove rp-btn rp-btn--ghost rp-btn--sm"
|
|
67
|
+
data-action="railspress--dropzone#remove">
|
|
68
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
69
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
70
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
71
|
+
</svg>
|
|
72
|
+
Remove
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<%# Progress indicator %>
|
|
77
|
+
<div data-railspress--dropzone-target="progress" class="rp-dropzone__progress" hidden>
|
|
78
|
+
<div class="rp-dropzone__progress-bar" data-railspress--dropzone-target="progressBar"></div>
|
|
79
|
+
<span class="rp-dropzone__progress-text" data-railspress--dropzone-target="progressText">0%</span>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<%# Error message %>
|
|
83
|
+
<div data-railspress--dropzone-target="error" class="rp-dropzone__error" hidden></div>
|
|
84
|
+
</div>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Focal Point Editor Partial
|
|
3
|
+
==========================
|
|
4
|
+
An interactive focal point picker with live previews.
|
|
5
|
+
|
|
6
|
+
Required locals:
|
|
7
|
+
form - The Rails form builder
|
|
8
|
+
record - The record with HasFocalPoint concern
|
|
9
|
+
attachment_name - Symbol for the attachment (e.g., :header_image)
|
|
10
|
+
|
|
11
|
+
Optional locals:
|
|
12
|
+
contexts - Array of context configs for previews (default: Railspress.image_contexts)
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
render "railspress/admin/shared/focal_point_editor",
|
|
16
|
+
form: form, record: @post, attachment_name: :header_image
|
|
17
|
+
%>
|
|
18
|
+
|
|
19
|
+
<% contexts ||= Railspress.image_contexts %>
|
|
20
|
+
<% focal_point = record.focal_point(attachment_name) %>
|
|
21
|
+
<% attachment = record.send(attachment_name) %>
|
|
22
|
+
|
|
23
|
+
<div data-controller="railspress--focal-point"
|
|
24
|
+
data-railspress--focal-point-x-value="<%= focal_point[:x] %>"
|
|
25
|
+
data-railspress--focal-point-y-value="<%= focal_point[:y] %>"
|
|
26
|
+
class="rp-focal-editor">
|
|
27
|
+
|
|
28
|
+
<div class="rp-focal-editor__layout">
|
|
29
|
+
<%# Source image with crosshair %>
|
|
30
|
+
<div class="rp-focal-editor__source">
|
|
31
|
+
<div class="rp-focal-editor__image-wrap"
|
|
32
|
+
data-action="click->railspress--focal-point#pick
|
|
33
|
+
touchstart->railspress--focal-point#touch"
|
|
34
|
+
tabindex="0"
|
|
35
|
+
data-action="keydown->railspress--focal-point#keydown">
|
|
36
|
+
<% if attachment.attached? %>
|
|
37
|
+
<%= image_tag main_app.url_for(attachment.variant(resize_to_limit: [1200, 800])),
|
|
38
|
+
data: { railspress__focal_point_target: "image" },
|
|
39
|
+
class: "rp-focal-editor__image",
|
|
40
|
+
alt: "Source image for focal point selection" %>
|
|
41
|
+
<% end %>
|
|
42
|
+
<div data-railspress--focal-point-target="crosshair"
|
|
43
|
+
class="rp-focal-editor__crosshair"
|
|
44
|
+
style="left: <%= focal_point[:x] * 100 %>%; top: <%= focal_point[:y] * 100 %>%;"></div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="rp-focal-editor__hint">
|
|
48
|
+
<span>Click to set focal point</span>
|
|
49
|
+
<span class="rp-focal-editor__coords" data-railspress--focal-point-target="coordsDisplay">
|
|
50
|
+
<%= "#{(focal_point[:x] * 100).round}%, #{(focal_point[:y] * 100).round}%" %>
|
|
51
|
+
</span>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="rp-focal-editor__actions">
|
|
55
|
+
<button type="button"
|
|
56
|
+
class="rp-btn rp-btn--outline rp-btn--sm"
|
|
57
|
+
data-action="railspress--focal-point#reset">
|
|
58
|
+
Reset to Center
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<%# Live previews %>
|
|
64
|
+
<% if contexts.any? %>
|
|
65
|
+
<div class="rp-focal-editor__previews">
|
|
66
|
+
<span class="rp-focal-editor__previews-title">Live Preview</span>
|
|
67
|
+
|
|
68
|
+
<% contexts.each do |name, config| %>
|
|
69
|
+
<% aspect = config[:aspect] %>
|
|
70
|
+
<% aspect_ratio = aspect.is_a?(Array) ? "#{aspect[0]} / #{aspect[1]}" : aspect %>
|
|
71
|
+
<% ratio_label = aspect.is_a?(Array) ? "#{aspect[0]}:#{aspect[1]}" : aspect.to_s %>
|
|
72
|
+
|
|
73
|
+
<div class="rp-focal-editor__preview">
|
|
74
|
+
<div class="rp-focal-editor__preview-image" style="aspect-ratio: <%= aspect_ratio %>;">
|
|
75
|
+
<% if attachment.attached? %>
|
|
76
|
+
<%= image_tag main_app.url_for(attachment.variant(resize_to_limit: [400, 300])),
|
|
77
|
+
data: { railspress__focal_point_target: "preview" },
|
|
78
|
+
style: record.image_css_for(name, attachment_name),
|
|
79
|
+
alt: "#{name.to_s.titleize} preview" %>
|
|
80
|
+
<% end %>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="rp-focal-editor__preview-label">
|
|
83
|
+
<span class="rp-focal-editor__preview-name"><%= name.to_s.titleize %></span>
|
|
84
|
+
<span class="rp-focal-editor__preview-ratio"><%= ratio_label %></span>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
<% end %>
|
|
88
|
+
</div>
|
|
89
|
+
<% end %>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<%# Hidden inputs for form submission via nested attributes %>
|
|
93
|
+
<% association_name = :"#{attachment_name}_focal_point" %>
|
|
94
|
+
<%= form.fields_for association_name, record.send(association_name) do |fp| %>
|
|
95
|
+
<%= fp.hidden_field :focal_x,
|
|
96
|
+
value: focal_point[:x],
|
|
97
|
+
data: { railspress__focal_point_target: "xInput" } %>
|
|
98
|
+
<%= fp.hidden_field :focal_y,
|
|
99
|
+
value: focal_point[:y],
|
|
100
|
+
data: { railspress__focal_point_target: "yInput" } %>
|
|
101
|
+
<% end %>
|
|
102
|
+
</div>
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Image Section Partial
|
|
3
|
+
=====================
|
|
4
|
+
Complete image upload and focal point editing section.
|
|
5
|
+
Shows compact view with Edit button; expands to full editor.
|
|
6
|
+
|
|
7
|
+
Required locals:
|
|
8
|
+
form - The Rails form builder
|
|
9
|
+
record - The record with HasFocalPoint concern
|
|
10
|
+
attachment_name - Symbol for the attachment (e.g., :header_image)
|
|
11
|
+
|
|
12
|
+
Optional locals:
|
|
13
|
+
label - Section label (default: "Main Image")
|
|
14
|
+
contexts - Array of context configs for previews
|
|
15
|
+
show_advanced - Show per-context overrides (default: false)
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
render "railspress/admin/shared/image_section",
|
|
19
|
+
form: form, record: @post, attachment_name: :header_image
|
|
20
|
+
%>
|
|
21
|
+
|
|
22
|
+
<% label ||= "Main Image" %>
|
|
23
|
+
<% contexts ||= Railspress.image_contexts %>
|
|
24
|
+
<% show_advanced ||= false %>
|
|
25
|
+
<% attachment = record.send(attachment_name) %>
|
|
26
|
+
<% has_image = attachment.attached? && attachment.blob&.persisted? %>
|
|
27
|
+
|
|
28
|
+
<div class="rp-image-section">
|
|
29
|
+
<div class="rp-image-section__label">
|
|
30
|
+
<label class="rp-label"><%= label %></label>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<% if has_image %>
|
|
34
|
+
<%# Compact view (default, collapsed state) %>
|
|
35
|
+
<details class="rp-image-section__details">
|
|
36
|
+
<summary class="rp-image-section__compact">
|
|
37
|
+
<div class="rp-image-section__thumb">
|
|
38
|
+
<%= image_tag main_app.url_for(attachment.variant(resize_to_limit: [120, 80])),
|
|
39
|
+
style: record.focal_point_css(attachment_name),
|
|
40
|
+
alt: "" %>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="rp-image-section__info">
|
|
43
|
+
<span class="rp-image-section__filename"><%= attachment.filename %></span>
|
|
44
|
+
<span class="rp-image-section__meta">
|
|
45
|
+
<%= number_to_human_size(attachment.byte_size) %>
|
|
46
|
+
</span>
|
|
47
|
+
<% if record.has_focal_point?(attachment_name) %>
|
|
48
|
+
<span class="rp-image-section__focal-indicator">
|
|
49
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
|
50
|
+
<circle cx="12" cy="12" r="3"/>
|
|
51
|
+
<path d="M12 2v4m0 12v4m10-10h-4M6 12H2"/>
|
|
52
|
+
</svg>
|
|
53
|
+
Focal point set
|
|
54
|
+
</span>
|
|
55
|
+
<% end %>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="rp-image-section__toggle">
|
|
58
|
+
<span class="rp-btn rp-btn--outline rp-btn--sm">Edit</span>
|
|
59
|
+
</div>
|
|
60
|
+
</summary>
|
|
61
|
+
|
|
62
|
+
<%# Expanded editor %>
|
|
63
|
+
<div class="rp-image-section__editor">
|
|
64
|
+
<div class="rp-image-section__editor-header">
|
|
65
|
+
<span class="rp-image-section__editor-title"><%= label %> Settings</span>
|
|
66
|
+
<div class="rp-image-section__editor-actions">
|
|
67
|
+
<label class="rp-btn rp-btn--outline rp-btn--sm">
|
|
68
|
+
Change
|
|
69
|
+
<%= form.file_field attachment_name,
|
|
70
|
+
accept: "image/*",
|
|
71
|
+
class: "rp-sr-only",
|
|
72
|
+
direct_upload: true %>
|
|
73
|
+
</label>
|
|
74
|
+
<label class="rp-checkbox-inline rp-btn rp-btn--outline rp-btn--sm">
|
|
75
|
+
<%= form.check_box :"remove_#{attachment_name}", {}, "1", "0" %>
|
|
76
|
+
Remove
|
|
77
|
+
</label>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="rp-image-section__editor-body">
|
|
82
|
+
<%= render "railspress/admin/shared/focal_point_editor",
|
|
83
|
+
form: form,
|
|
84
|
+
record: record,
|
|
85
|
+
attachment_name: attachment_name,
|
|
86
|
+
contexts: contexts %>
|
|
87
|
+
|
|
88
|
+
<% if show_advanced && contexts.any? %>
|
|
89
|
+
<details class="rp-advanced-section">
|
|
90
|
+
<summary class="rp-advanced-toggle">
|
|
91
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
|
92
|
+
<polyline points="6 9 12 15 18 9"/>
|
|
93
|
+
</svg>
|
|
94
|
+
Advanced: Per-context image overrides
|
|
95
|
+
</summary>
|
|
96
|
+
|
|
97
|
+
<div class="rp-advanced-content">
|
|
98
|
+
<p class="rp-hint">Override specific contexts with custom crops or separate images.</p>
|
|
99
|
+
|
|
100
|
+
<div class="rp-context-overrides">
|
|
101
|
+
<% contexts.each do |name, config| %>
|
|
102
|
+
<% override = record.image_override(name, attachment_name) %>
|
|
103
|
+
<% has_override = record.has_image_override?(name, attachment_name) %>
|
|
104
|
+
|
|
105
|
+
<div class="rp-context-override">
|
|
106
|
+
<div class="rp-context-override__header">
|
|
107
|
+
<span class="rp-context-override__name"><%= name.to_s.titleize %></span>
|
|
108
|
+
<span class="rp-context-override__badge <%= 'rp-context-override__badge--custom' if has_override %>">
|
|
109
|
+
<%= has_override ? override[:type].titleize : "Using focal" %>
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div class="rp-context-override__preview" style="aspect-ratio: <%= config[:aspect].is_a?(Array) ? "#{config[:aspect][0]} / #{config[:aspect][1]}" : config[:aspect] %>;">
|
|
114
|
+
<% context_image = record.image_for(name, attachment_name) %>
|
|
115
|
+
<% if context_image.respond_to?(:variant) %>
|
|
116
|
+
<%= image_tag main_app.url_for(context_image.variant(resize_to_limit: [400, 300])),
|
|
117
|
+
style: record.image_css_for(name, attachment_name),
|
|
118
|
+
alt: "" %>
|
|
119
|
+
<% elsif context_image.respond_to?(:url) %>
|
|
120
|
+
<%= image_tag main_app.url_for(context_image),
|
|
121
|
+
style: record.image_css_for(name, attachment_name),
|
|
122
|
+
alt: "" %>
|
|
123
|
+
<% end %>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="rp-context-override__actions">
|
|
127
|
+
<% if has_override %>
|
|
128
|
+
<button type="button" class="rp-btn rp-btn--outline rp-btn--sm" style="flex:1;">
|
|
129
|
+
Change
|
|
130
|
+
</button>
|
|
131
|
+
<button type="button" class="rp-btn rp-btn--outline rp-btn--sm" style="flex:1;">
|
|
132
|
+
Use Focal
|
|
133
|
+
</button>
|
|
134
|
+
<% else %>
|
|
135
|
+
<button type="button" class="rp-btn rp-btn--outline rp-btn--sm" style="flex:1;">
|
|
136
|
+
Custom Crop
|
|
137
|
+
</button>
|
|
138
|
+
<button type="button" class="rp-btn rp-btn--outline rp-btn--sm" style="flex:1;">
|
|
139
|
+
Upload
|
|
140
|
+
</button>
|
|
141
|
+
<% end %>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
<% end %>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</details>
|
|
148
|
+
<% end %>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</details>
|
|
152
|
+
<% else %>
|
|
153
|
+
<%# No image - show dropzone %>
|
|
154
|
+
<%= render "railspress/admin/shared/dropzone",
|
|
155
|
+
form: form,
|
|
156
|
+
field_name: attachment_name,
|
|
157
|
+
prompt: "Click to upload #{label.downcase}" %>
|
|
158
|
+
<% end %>
|
|
159
|
+
</div>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Image Section Compact View
|
|
3
|
+
==========================
|
|
4
|
+
Shows thumbnail, filename, and Edit button.
|
|
5
|
+
Clicking Edit loads the editor via Turbo Frame.
|
|
6
|
+
|
|
7
|
+
Required locals:
|
|
8
|
+
record - The record with HasFocalPoint concern
|
|
9
|
+
attachment_name - Symbol for the attachment (e.g., :header_image)
|
|
10
|
+
|
|
11
|
+
Optional locals:
|
|
12
|
+
label - Section label (default: "Main Image")
|
|
13
|
+
flash_message - Success message to display
|
|
14
|
+
contexts - Hash of context configs (for editor URL)
|
|
15
|
+
%>
|
|
16
|
+
|
|
17
|
+
<% label ||= "Main Image" %>
|
|
18
|
+
<% flash_message ||= nil %>
|
|
19
|
+
<% contexts ||= Railspress.image_contexts %>
|
|
20
|
+
<% attachment = record.send(attachment_name) %>
|
|
21
|
+
<% has_image = attachment.attached? && attachment.blob&.persisted? %>
|
|
22
|
+
<% frame_id = dom_id(record, "image_section_#{attachment_name}") %>
|
|
23
|
+
|
|
24
|
+
<%# Build editor URL based on record type (can be overridden via editor_url local) %>
|
|
25
|
+
<%
|
|
26
|
+
editor_url ||= nil
|
|
27
|
+
unless editor_url
|
|
28
|
+
if record.is_a?(Railspress::Post)
|
|
29
|
+
editor_url = image_editor_admin_post_path(record, attachment: attachment_name)
|
|
30
|
+
elsif record.is_a?(Railspress::ContentElement)
|
|
31
|
+
editor_url = image_editor_admin_content_element_path(record)
|
|
32
|
+
else
|
|
33
|
+
entity_type = record.class.railspress_config.route_key
|
|
34
|
+
editor_url = admin_entity_image_editor_path(entity_type: entity_type, id: record.id, attachment: attachment_name)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
%>
|
|
38
|
+
|
|
39
|
+
<%= content_tag "turbo-frame", id: frame_id do %>
|
|
40
|
+
<div class="rp-image-section">
|
|
41
|
+
<div class="rp-image-section__label">
|
|
42
|
+
<label class="rp-label"><%= label %></label>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<% if flash_message %>
|
|
46
|
+
<div class="rp-flash rp-flash--success" style="margin-bottom: var(--rp-space-sm);">
|
|
47
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
|
48
|
+
<path d="M20 6L9 17l-5-5"/>
|
|
49
|
+
</svg>
|
|
50
|
+
<%= flash_message %>
|
|
51
|
+
</div>
|
|
52
|
+
<% end %>
|
|
53
|
+
|
|
54
|
+
<% if has_image %>
|
|
55
|
+
<div class="rp-image-section__details">
|
|
56
|
+
<div class="rp-image-section__compact">
|
|
57
|
+
<div class="rp-image-section__preview">
|
|
58
|
+
<%= image_tag main_app.url_for(attachment.variant(resize_to_limit: [800, 450])),
|
|
59
|
+
style: record.focal_point_css(attachment_name),
|
|
60
|
+
alt: "" %>
|
|
61
|
+
<div class="rp-image-section__overlay">
|
|
62
|
+
<%= link_to editor_url, class: "rp-btn", data: { turbo_frame: frame_id } do %>
|
|
63
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right: 6px;">
|
|
64
|
+
<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/>
|
|
65
|
+
<path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
|
66
|
+
</svg>
|
|
67
|
+
Edit
|
|
68
|
+
<% end %>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
<% if record.has_focal_point?(attachment_name) %>
|
|
73
|
+
<div class="rp-image-section__footer">
|
|
74
|
+
<span class="rp-image-section__focal-indicator">
|
|
75
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
|
76
|
+
<circle cx="12" cy="12" r="3"/>
|
|
77
|
+
<path d="M12 2v4m0 12v4m10-10h-4M6 12H2"/>
|
|
78
|
+
</svg>
|
|
79
|
+
Focal point set
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
<% end %>
|
|
83
|
+
</div>
|
|
84
|
+
<% else %>
|
|
85
|
+
<div class="rp-image-section__empty">
|
|
86
|
+
<p class="rp-hint">No image attached.</p>
|
|
87
|
+
</div>
|
|
88
|
+
<% end %>
|
|
89
|
+
</div>
|
|
90
|
+
<% end %>
|