feat(library): lay the ALL THUMBS wall out in justified rows per day, and stop clipping the frames at the pane's edges

This commit is contained in:
2026-10-13 11:10:57 +07:00
parent 326bcb0c5e
commit 1ca8ac6031
4 changed files with 698 additions and 678 deletions
+64 -31
View File
@@ -1249,9 +1249,6 @@ form[data-key='immich-modal'] {
It is given the width they would have taken, and it must not shrink, or the
scrollbar would measure the window instead of the shelf. */
.adm-thumb-gap { flex: 0 0 auto; }
/* The wall's stand-in for the rows it is not drawing: a run of cards' worth of
height, spanning the whole grid so nothing lands beside it. */
.adm-wall-gap { grid-column: 1 / -1; }
.adm-thumb {
flex: 0 0 132px;
margin: 0;
@@ -1312,44 +1309,36 @@ form[data-key='immich-modal'] {
.adm-note { margin: 12px 0; }
.adm-sub { margin: 0 0 14px; }
.adm-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
/* A day of the wall: the band that names it and the rows under it. It spans the
whole grid — the wall is one column of days, not a day per cell — and the rows
inside are the wall's own grid again, so the cards keep the columns they had
when the wall drew them flat. */
.adm-day { grid-column: 1 / -1; }
.adm-day-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
/* The month's own band, over the first day it holds: a heavier line than a day's,
and the only line of the wall that is not a date — so a shelf is read by the
month it was shot and only then by the day. Its height is fixed (54px, the
`MONTH_BAND` the wall measures its shelf with): the wall stands in spacers for
the months it has not drawn, and a band that grew with its own text is a shelf
whose end moves. */
month it was shot and only then by the day. Its height is fixed (44px, the
`MONTH_BAND` the wall lays its shelf out with): the shelf's whole height is
built out of it before a frame is drawn, and a band that grew with its own text
is a shelf whose end moves. */
.adm-month-head {
height: 54px;
height: 44px;
display: flex;
align-items: flex-end;
margin: 0;
padding-bottom: 8px;
font-size: 19px;
padding-bottom: 6px;
font-size: 18px;
font-weight: 700;
letter-spacing: 0.01em;
color: var(--text);
}
/* A day's own line under it, at the height the wall counts it in (`DAY_BAND`). */
/* The day a run of frames was shot on, over the frames of it: the small line and
the dot it is read by, at the height the wall lays it in (`DAY_BAND`). */
.adm-day-head {
height: 28px;
height: 30px;
display: flex;
align-items: center;
gap: 10px;
gap: 7px;
margin: 0;
font-size: 12px;
font-size: 12.5px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text-dim, #6b7280);
}
/* The rule that carries the eye from the date to the edge of the wall. */
.adm-day-line { flex: 1 1 auto; height: 1px; background: var(--border); }
.adm-day-dot { flex: 0 0 auto; width: 5px; height: 5px; border-radius: 50%; background: currentColor; opacity: 0.7; }
.adm-card {
display: flex;
flex-direction: column;
@@ -1498,12 +1487,56 @@ form[data-key='immich-modal'] {
.lib-root .adm-stage > .chip-row { display: flex; justify-content: center; }
}
/* All thumbs: the wall scrolls down inside the stage, and its own scrollbar on
the right edge is what reaches the frames below. */
/* `align-content: start`, because the grid is stretched to the stage: a wall with
fewer frames than there are columns is one row, and a stretched row makes every
card's border that much taller than the picture inside it. Rows keep the height
their own frames give them however few of them there are. */
.lib-root .adm-grid { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 6px; overflow-anchor: none; align-content: start; }
the right edge is what reaches the frames below. It is a plain scroll box and
not a grid of its own: the rows inside it are laid out at the tops the shelf's
own layout gives them (see `wallLayout` in ui/dateGroups.ts), and the box's
height is that layout's, so the scrollbar spans the roll and not the screen. */
.lib-root .adm-grid { display: block; position: relative; flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 0; overflow-anchor: none; }
/* The shelf's whole height, which the rows are laid in. The rows are out of the
flow — absolute, at the top the layout gave each of them — so this element is
what the scrollbar measures, and it stands the layout's height exactly: a height
read off the rows drawn is a scrollbar that moves with the window, and the last
frames of a roll are then frames nobody can scroll to. */
.adm-wall-body { position: relative; width: 100%; }
/* A row of the wall: a run of frames of one day, meeting both edges — the widths
come off the layout, which packed them to this wall — with the gap a contact
sheet leaves (the `WALL_GAP` in Library.tsx, the same 3px). */
.adm-row { position: absolute; left: 0; right: 0; display: flex; gap: 3px; }
/* The month's band and the day's, which are rows of the shelf too, and stand one
over the other inside the band row. */
.adm-row.adm-band { display: block; }
/* A frame on the wall: the picture and nothing else. The wall is read as one shelf
of photographs, so a tile carries no frame of its own, no padding and no caption
— the name and the date are on the tile's own title — and the picture fills the
box the layout gave it, cropped to it rather than letter-boxed, since the box is
the frame's own proportions to within a pixel or two. */
.lib-tile {
position: relative;
margin: 0;
height: 100%;
overflow: hidden;
border-radius: 4px;
background: var(--bg-sunken);
cursor: pointer;
}
.lib-tile img { display: block; width: 100%; height: 100%; object-fit: cover; }
.lib-tile .lib-noimg { width: 100%; height: 100%; aspect-ratio: auto; border-radius: 0; }
.lib-tile.selected { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The score, over the picture: on the frame the pointer is on, and left standing
on a frame that has one. The click stops at the star, so the tile's own click —
the pick — is not what a score costs. */
.lib-tile .lib-stars {
position: absolute;
right: 2px;
bottom: 2px;
padding: 0 3px;
border-radius: 6px;
background: color-mix(in srgb, var(--bg) 72%, transparent);
opacity: 0;
}
.lib-tile:hover .lib-stars,
.lib-tile .lib-stars.on { opacity: 1; }
.lib-tile .lib-star { font-size: 12px; padding: 1px 2px; }
/* The wall and the ruler beside it: the wall takes what is left, the ruler is
the fixed strip down the right edge. The row is the stage's own middle track —
it carries the height the grid used to carry alone, since the grid now shares
@@ -1521,7 +1554,7 @@ form[data-key='immich-modal'] {
/* A year's own field, the months of it standing inside it: what makes the ruler
read by the year first and the month under it after. */
.lib-ruler-band { position: absolute; left: 0; right: 0; background: var(--bg-elev); border-radius: 3px; }
.lib-ruler-tick { position: absolute; left: 0; width: 10px; height: 1px; background: var(--border); }
.lib-ruler-tick { position: absolute; left: 0; width: 10px; height: 1px; background: color-mix(in srgb, var(--text-dim) 30%, transparent); }
.lib-ruler-year { width: 100%; height: 2px; background: var(--text-dim); }
.lib-ruler-year i { position: absolute; top: 2px; left: 14px; font-size: 9px; font-style: normal; font-weight: 700; letter-spacing: 0.06em; color: var(--text-dim); }
.lib-ruler-tick.on { width: 22px; background: var(--accent); }