Compact icon uploader shared between light and dark icon fields
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / scan_container (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled

Extracts the icon dropzone into a reusable partial so the dark mode
icon gets the same upload / drag-and-drop / paste affordances as the
light icon. Slims the dropzone to a single-row layout (small cloud
icon plus Upload / drag-and-drop / paste hint) and a tiny format hint
below, instead of the previous tall vertically-centred block.
This commit is contained in:
Dan Milne
2026-06-07 17:13:52 +10:00
parent bfad9c4e9d
commit c5ab7dc2a5
3 changed files with 84 additions and 88 deletions

View File

@@ -36,9 +36,9 @@
<%= form.text_area :description, rows: 3, class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "Optional description of this application" %>
</div>
<div>
<div class="flex items-center justify-between">
<%= form.label :icon, "Application Icon", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<div class="space-y-4">
<div class="flex items-center justify-between -mb-2">
<span class="block text-sm font-medium text-gray-700 dark:text-gray-300">Application Icons</span>
<a href="https://dashboardicons.com" target="_blank" rel="noopener noreferrer" class="text-xs text-blue-600 hover:text-blue-800 flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
@@ -46,92 +46,22 @@
Browse icons at dashboardicons.com
</a>
</div>
<% if application.icon.attached? && application.persisted? %>
<% begin %>
<%# Only show icon if we can successfully get its URL (blob is persisted) %>
<% if application.icon.blob&.persisted? && application.icon.blob.key.present? %>
<div class="mt-2 mb-3 flex items-center gap-4">
<%= image_tag application.icon, class: "h-16 w-16 rounded-lg object-cover border border-gray-200 dark:border-gray-700", alt: "Current icon" %>
<div class="text-sm text-gray-600 dark:text-gray-400">
<p class="font-medium">Current icon</p>
<p class="text-xs"><%= number_to_human_size(application.icon.blob.byte_size) %></p>
</div>
</div>
<% end %>
<% rescue ArgumentError => e %>
<%# Handle case where icon attachment exists but can't generate signed_id %>
<% if e.message.include?("Cannot get a signed_id for a new record") %>
<div class="mt-2 mb-3 text-sm text-gray-600 dark:text-gray-400">
<p class="font-medium">Icon uploaded</p>
<p class="text-xs">File will be processed shortly</p>
</div>
<% else %>
<%# Re-raise if it's a different error %>
<% raise e %>
<% end %>
<% end %>
<% end %>
<div class="mt-2" data-controller="file-drop image-paste">
<div class="flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 dark:border-gray-600 border-dashed rounded-md hover:border-blue-400 transition-colors"
data-file-drop-target="dropzone"
data-image-paste-target="dropzone"
data-action="dragover->file-drop#dragover dragleave->file-drop#dragleave drop->file-drop#drop paste->image-paste#handlePaste"
tabindex="0">
<div class="space-y-1 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" stroke="currentColor" fill="none" viewBox="0 0 48 48">
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<div class="flex text-sm text-gray-600 dark:text-gray-400">
<label for="<%= form.field_id(:icon) %>" class="relative cursor-pointer bg-white dark:bg-gray-800 rounded-md font-medium text-blue-600 hover:text-blue-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 dark:focus-within:ring-offset-gray-900 focus-within:ring-blue-500">
<span>Upload a file</span>
<%= form.file_field :icon,
accept: "image/png,image/jpg,image/jpeg,image/gif,image/svg+xml",
class: "sr-only",
data: {
file_drop_target: "input",
image_paste_target: "input",
action: "change->file-drop#handleFiles"
} %>
</label>
<p class="pl-1">or drag and drop</p>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400">PNG, JPG, GIF, or SVG up to 2MB</p>
<p class="text-xs text-blue-600 font-medium mt-2">💡 Tip: Click here and press Ctrl+V (or Cmd+V) to paste an image from your clipboard</p>
</div>
</div>
<div data-file-drop-target="preview" class="mt-3 hidden">
<div class="flex items-center gap-3 p-3 bg-blue-50 dark:bg-blue-900/30 rounded-md border border-blue-200 dark:border-blue-700">
<img data-file-drop-target="previewImage" class="h-12 w-12 rounded object-cover" alt="Preview">
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 dark:text-gray-100" data-file-drop-target="filename"></p>
<p class="text-xs text-gray-500 dark:text-gray-400" data-file-drop-target="filesize"></p>
</div>
<button type="button" data-action="click->file-drop#clear" class="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
<%= render "icon_uploader",
form: form,
field: :icon,
label: "Icon",
current_attached: (application.persisted? ? application.icon : nil),
current_label: "Current icon" %>
<div class="mt-4">
<%= form.label :icon_dark, "Dark mode icon (optional)", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Used in place of the main icon when the user's theme is dark. If omitted, the main icon is used in both modes.</p>
<% if application.icon_dark.attached? && application.persisted? && application.icon_dark.blob&.persisted? && application.icon_dark.blob.key.present? %>
<div class="mt-2 mb-3 flex items-center gap-4">
<%= image_tag application.icon_dark, class: "h-16 w-16 rounded-lg object-cover border border-gray-200 dark:border-gray-700 bg-gray-900", alt: "Current dark-mode icon" %>
<div class="text-sm text-gray-600 dark:text-gray-400">
<p class="font-medium">Current dark-mode icon</p>
<p class="text-xs"><%= number_to_human_size(application.icon_dark.blob.byte_size) %></p>
</div>
</div>
<% end %>
<%= form.file_field :icon_dark,
accept: "image/png,image/jpg,image/jpeg,image/gif,image/svg+xml",
class: "mt-2 block w-full text-sm text-gray-700 dark:text-gray-300 file:mr-3 file:py-2 file:px-3 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-blue-50 file:text-blue-700 dark:file:bg-blue-900/30 dark:file:text-blue-300 hover:file:bg-blue-100 dark:hover:file:bg-blue-900/50" %>
</div>
<%= render "icon_uploader",
form: form,
field: :icon_dark,
label: "Dark mode icon (optional)",
help: "Used in place of the main icon when the user's theme is dark. If omitted, the main icon is used in both modes.",
current_attached: (application.persisted? ? application.icon_dark : nil),
current_label: "Current dark-mode icon",
preview_extra_class: "bg-gray-900" %>
</div>
<div>

View File

@@ -0,0 +1,66 @@
<%# Compact icon uploader. Locals:
form - the form builder
field - symbol for the file field (:icon or :icon_dark)
label - heading text
help - small helper paragraph (optional)
current_attached - the attachment to show as "current" preview
current_label - text for the preview row (e.g. "Current icon")
preview_extra_class - extra css for the preview img (e.g. "bg-gray-900")
%>
<div>
<%= form.label field, label, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<% if local_assigns[:help].present? %>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400"><%= help %></p>
<% end %>
<% if current_attached&.attached? && current_attached.blob&.persisted? && current_attached.blob.key.present? %>
<div class="mt-2 mb-3 flex items-center gap-3">
<%= image_tag current_attached, class: "h-12 w-12 rounded-md object-cover border border-gray-200 dark:border-gray-700 #{local_assigns[:preview_extra_class]}", alt: current_label %>
<div class="text-sm text-gray-600 dark:text-gray-400">
<p class="font-medium"><%= current_label %></p>
<p class="text-xs"><%= number_to_human_size(current_attached.blob.byte_size) %></p>
</div>
</div>
<% end %>
<div class="mt-2" data-controller="file-drop image-paste">
<div class="flex items-center gap-3 px-3 py-2 border border-dashed border-gray-300 dark:border-gray-600 rounded-md hover:border-blue-400 focus-within:border-blue-500 focus-within:ring-1 focus-within:ring-blue-500 transition-colors"
data-file-drop-target="dropzone"
data-image-paste-target="dropzone"
data-action="dragover->file-drop#dragover dragleave->file-drop#dragleave drop->file-drop#drop paste->image-paste#handlePaste"
tabindex="0">
<svg class="h-5 w-5 text-gray-400 dark:text-gray-500 shrink-0" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 4v12m0-12l-4 4m4-4l4 4M4 17v2a2 2 0 002 2h12a2 2 0 002-2v-2"/>
</svg>
<div class="flex-1 text-sm">
<label for="<%= form.field_id(field) %>" class="cursor-pointer font-medium text-blue-600 hover:text-blue-500 focus-within:outline-none">
<span>Upload</span>
<%= form.file_field field,
accept: "image/png,image/jpg,image/jpeg,image/gif,image/svg+xml",
class: "sr-only",
data: {
file_drop_target: "input",
image_paste_target: "input",
action: "change->file-drop#handleFiles"
} %>
</label>
<span class="text-gray-600 dark:text-gray-400"> · drag and drop · or click and paste (⌘V)</span>
<p class="text-xs text-gray-500 dark:text-gray-400">PNG, JPG, GIF or SVG · max 2MB</p>
</div>
</div>
<div data-file-drop-target="preview" class="mt-2 hidden">
<div class="flex items-center gap-3 p-2 bg-blue-50 dark:bg-blue-900/30 rounded-md border border-blue-200 dark:border-blue-700">
<img data-file-drop-target="previewImage" class="h-10 w-10 rounded object-cover" alt="Preview">
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 dark:text-gray-100" data-file-drop-target="filename"></p>
<p class="text-xs text-gray-500 dark:text-gray-400" data-file-drop-target="filesize"></p>
</div>
<button type="button" data-action="click->file-drop#clear" class="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300">
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
# frozen_string_literal: true
module Clinch
VERSION = "0.14.0"
VERSION = "0.14.1"
end