Generated monogram fallback + optional dark-mode icon per application
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

When an application has no icon attached, render a deterministic
monogram SVG instead of the generic picture-frame placeholder. Initials
are picked from capital letters in the name (ShelfLife -> SL); fall
back to the first two letters when fewer than two capitals exist
(Audiobookshelf -> AU). Background colour is hashed from the name for
stable per-app identity across visits.

Adds an optional second icon attachment, icon_dark, alongside the main
icon. When present, render a <picture> with a prefers-color-scheme:
dark source so the browser swaps automatically; when absent, the main
icon is used in both modes. The SVG sanitization, content-type fix,
and size/format validation now run over both attachments uniformly.

Bumps to 0.14.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dan Milne
2026-06-07 17:02:53 +10:00
parent 5b41db2c6a
commit bfad9c4e9d
12 changed files with 201 additions and 65 deletions

View File

@@ -115,6 +115,23 @@
</div>
</div>
</div>
<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>
</div>
<div>