feat(handlers): add file icons with standard and retro variants

Introduce file icon support to the file browser. Icons are loaded on
startup and mapped based on file name and content type.

- Load file icon mappings in the App handler initialization.
- Add `HasThumbnail`, `IconURL`, and `IconRetroURL` to the file view.
- Update CSS to support displaying file icons alongside thumbnails.
- Add retro theme support to swap standard icons with pixelated retro
  variants when the retro theme is active.
This commit is contained in:
2026-06-02 13:02:51 +03:00
parent 6c87187c6d
commit 8e3f783780
33 changed files with 594 additions and 53 deletions

View File

@@ -65,7 +65,7 @@
.file-card {
position: relative;
padding-bottom: 2.6rem;
padding-block: 0.65rem 2.6rem;
}
.file-reaction-dock {
@@ -303,24 +303,60 @@ html.reaction-picker-open body {
object-fit: contain;
}
.thumb-link {
display: block;
/* A file row behaves like an entry in a desktop file explorer: a small
thumbnail/icon followed by the name and metadata. The whole row is the click
target (raw view of the file). */
.file-open {
min-width: 0;
flex: 1;
display: flex;
align-items: center;
gap: 0.8rem;
color: var(--foreground);
text-decoration: none;
}
.file-media {
flex: 0 0 3rem;
width: 3rem;
height: 3rem;
display: grid;
place-items: center;
overflow: hidden;
flex: 0 0 4.75rem;
width: 4.75rem;
aspect-ratio: 16 / 10;
border: 1px solid var(--border);
border-radius: calc(var(--radius) - 0.125rem);
background: var(--muted);
}
.thumb-link img {
.file-thumb {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
.file-icon {
width: 2.1rem;
height: 2.1rem;
display: block;
object-fit: contain;
}
/* Retro (Win98) icons are tiny pixel art — keep them crisp and swap them in
only when the retro theme is active. */
.file-icon-retro {
display: none;
image-rendering: pixelated;
}
[data-theme="retro"] .file-icon-standard {
display: none;
}
[data-theme="retro"] .file-icon-retro {
display: block;
}
.file-main {
min-width: 0;
max-width: 100%;
@@ -329,46 +365,47 @@ html.reaction-picker-open body {
text-decoration: none;
}
.file-actions {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.preview-action [hidden] {
display: none;
.file-main small {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-browser.is-thumbs {
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
}
.file-browser.is-thumbs .file-card {
display: grid;
min-width: 0;
align-content: start;
gap: 0.7rem;
gap: 0.5rem;
}
.file-browser.is-thumbs .file-open {
display: grid;
gap: 0.55rem;
text-align: center;
justify-items: center;
}
.file-browser.is-thumbs .file-media {
width: 100%;
height: auto;
flex-basis: auto;
aspect-ratio: 16 / 10;
}
.file-browser.is-thumbs .file-icon {
width: 45%;
height: auto;
}
.file-browser.is-thumbs .file-main {
width: 100%;
}
.file-browser.is-thumbs .thumb-link {
width: 100%;
flex-basis: auto;
}
.file-browser.is-thumbs .button {
width: 100%;
}
.file-browser.is-thumbs .file-actions {
width: 100%;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.file-browser.images-only .file-card:not([data-kind="image"]) {
display: none;
}