feat(library): cut the wall by month and day, and stand a ruler timeline on its right edge

This commit is contained in:
2026-10-13 10:37:57 +07:00
parent f8cac67506
commit 822833c1ef
6 changed files with 1044 additions and 626 deletions
+46 -56
View File
@@ -1249,6 +1249,9 @@ 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;
@@ -1315,11 +1318,30 @@ form[data-key='immich-modal'] {
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. */
.adm-month-head {
height: 54px;
display: flex;
align-items: flex-end;
margin: 0;
padding-bottom: 8px;
font-size: 19px;
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`). */
.adm-day-head {
height: 28px;
display: flex;
align-items: center;
gap: 10px;
margin: 6px 0 2px;
margin: 0;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.04em;
@@ -1481,61 +1503,29 @@ form[data-key='immich-modal'] {
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; min-width: 0; overflow-y: auto; padding-right: 6px; overflow-anchor: none; align-content: start; }
/* The wall and the rail beside it: the wall is the shelf and takes the room, the
rail is the time down its right edge and takes what it needs. The wall's own
scrollbar is inside its box, so the rail stands outside the frames rather than
over them. */
.lib-wall { display: flex; flex: 1 1 auto; min-height: 0; min-width: 0; gap: 6px; }
.lib-rail {
position: relative;
flex: 0 0 auto;
width: 92px;
overflow-y: auto;
overscroll-behavior: contain;
/* A shelf of years is longer than the wall is tall, so the rail scrolls under
the reader's own hand — a thin bar of its own rather than the platform's. */
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
padding: 2px 0;
border-left: 1px solid var(--border);
}
.lib-rail::-webkit-scrollbar { width: 8px; }
.lib-rail::-webkit-scrollbar-track { background: transparent; }
.lib-rail::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
.lib-rail::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
/* An entry of the rail: the year standing over its months, and a month said in the
dim voice until the eye is in it — in the accent, with the rule that carries the
eye, once it is. */
.lib-rail-item {
display: block;
width: 100%;
padding: 3px 8px;
border: 0;
border-left: 2px solid transparent;
background: none;
color: var(--text-dim);
font: inherit;
font-size: 11px;
font-variant-numeric: tabular-nums;
text-align: left;
white-space: nowrap;
cursor: pointer;
}
.lib-rail-item:hover { color: var(--text); background: var(--bg-sunken); }
/* The year is a head and not a month of the shelf: it stands on its own line, above
the months it holds, and its own press takes the wall to the first of them. */
.lib-rail-year {
margin-top: 6px;
padding-top: 6px;
border-top: 1px solid var(--border);
color: var(--text);
font-weight: 700;
letter-spacing: 0.04em;
}
.lib-rail-year:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.lib-rail-month { padding-left: 14px; }
.lib-rail-item.on { color: var(--accent); border-left-color: var(--accent); font-weight: 600; }
.lib-root .adm-grid { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 6px; overflow-anchor: none; align-content: start; }
/* 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
that height with the ruler and no longer fills the stage by itself. */
.lib-root .lib-wall-row { flex: 1 1 auto; min-height: 0; display: flex; gap: 4px; }
.lib-root .lib-wall-row > .adm-grid { min-width: 0; }
/* The shelf's time down its right edge, read the way a ruler is read: the year
is the tick that is named, the months of it are the ticks under it, and the
band the eye is standing in carries the needle. */
.lib-ruler { flex: 0 0 74px; display: flex; flex-direction: column; min-height: 0; padding-left: 6px; }
.lib-ruler-head { flex: 0 0 auto; display: flex; flex-direction: column; gap: 1px; padding-bottom: 6px; }
.lib-ruler-name { font-size: 9px; font-weight: 700; letter-spacing: 0.12em; color: var(--text-dim); }
.lib-ruler-value { font-size: 12px; font-weight: 700; color: var(--accent); white-space: nowrap; }
.lib-ruler-scale { position: relative; flex: 1 1 auto; min-height: 0; cursor: ns-resize; touch-action: none; user-select: none; }
/* 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-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); }
.lib-ruler-needle { position: absolute; left: 0; right: 0; height: 2px; background: var(--accent); pointer-events: none; }
/* What the thumbnail wall is read through: the selects and the count of what is
left. One row above the wall, and it does not scroll with it. */
.lib-root .adm-stage > .lib-filters,