The first picture now shows what a desktop opens on, and what the
README's first line promises. The fixture user stays on the mail alone
for the regression suite, so the capture asks for split and restores
the mail afterwards.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two icon captures now show the horn in the top bar without its
tile, and the rest show the pl mark at its new size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two new captures — an icon chosen in its own colours, and the same icon open
on the colourway grid — with the topbar in both frames, since where the icon
ends up is the point of choosing it. The rest are retaken against the demo
mailbox as usual before a tag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seven columns used to divide whatever width there was, so a phone — or a narrow
docked pane — gave each of them about fifty pixels, and a block showed its
colour and little else. A column has a floor now: below the width where seven
of them fit the grid pans sideways under an hour gutter that stays pinned, and
four days are legible where none were. The time-grid shell's own header costed
that fix as disproportionate once — "three scroll containers kept in step by
JavaScript" — and it is not, because the three grids already share one
scroller; all that was left was making each one's first cell sticky.
A BLOCK IS TWO ROWS NOW. The time on one line with the calendar's colour as a
bar down the left, the title under it, which is a whole title in a 96px column
where one line was four characters of each. Day view keeps the single line and
is told so rather than measured for it: a day column is the whole grid and is
never the width that makes stacking worth doing.
The agenda's date rail is gone. A day is a heading with its events under it,
and the times sit in a lane that is a grid track rather than a fixed guess —
sized against every time in the view, so it is 43px where an "all day" needs it
and 33px where nothing does. The month gives up its words entirely below 5.5rem
of cell and draws one bar per entry, which is what a phone can honestly say in
forty pixels, and "N more" is gone along with a count that a variable cell
height could never make true; the date is the link into the day instead.
The grid opens on now rather than on a fixed 07:00, and a chip too small for
its own title opens to it while the pointer rests on it. Width only: growing
the height made the chip inside stop following its parent, and a click aimed at
the middle of a Day-view block landed under the button and opened nothing.
Two bugs fell out of the work. An all-day entry sorted below the morning east
of UTC, because its start is a floating midnight in UTC while a timed one's is
an instant — the time grid never showed it, since it puts all-day entries in a
band of their own and never asks. And a grid block had never had a background
at all: an inline colour was overriding the surface class written beside it, so
the only paint it carried was a tenth of a calendar colour over whatever
happened to be behind it.
Opening on now has one consequence worth naming: a fixture at 10:00 is on
screen all morning and scrolled above the viewport by the afternoon, and
`boundingBox()` answers for an off-screen element with coordinates that land
under the pinned day headings. calendar-timegrid's drag helpers scroll the
block into view before measuring it now; without that the suite passed for
hours and then did not, and the failure read as "dragging is broken".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A theme is chosen by rewriting `data-theme` on <html>, which unsets the old
theme's block in one step. Anything that block declared and the new one does
not therefore falls back — to :root, to .dark, or to whatever an inline style
left there — and the user is looking at a colour nobody picked for the theme
they are in.
Three of the six themes were four-line diffs against a light-tuned base, so
most of what they painted was inherited rather than chosen, and two of the
inherited values were visibly wrong. Nord and Dusk took --rgb-info from :root,
which is the light theme's link blue, and rendered it on a near-black surface
beside a --rgb-danger, --rgb-warning and --rgb-success that had all been
lightened for that surface — info was simply forgotten when the others were
tuned. Both also took --rgb-ink-soft from .dark, a neutral grey that in Nord's
case is DARKER than its own --rgb-ink-muted, so the ink ramp ran backwards.
Solarized had a #f3f4f6 inset panel on a #fdf6e3 page.
So every palette block now declares the whole inventory: 38 variables plus
color-scheme, spelled out in :root, .dark and each of the five [data-theme]
blocks. Reading one block tells you what the theme looks like without tracing
three levels of cascade, and there is nothing left for a switch to leave over.
`light` and `system` still have no block of their own on purpose — :root is
the light palette and .dark is the dark one, and a copy under a selector would
be a second place to forget.
── The accent was a genuine remnant, and a persistent one ─────────────────
Theme::defaults() seeds settings when a theme is picked, and it named an
accent for Paper and Dark only. The accent is written inline on <html> from
the user's setting, so an inline style is exactly what survives a switch:
picking Paper and then Nord left Nord wearing Paper's clay. Not just in the
preview — save() reads the value back out of the form, so it was written to
the account and came back after the reload. The theme picker's own swatch
showed a frost blue the app never painted.
defaults() is now one expression for every case, reading swatch()[2], so there
is no case that declines to overwrite. pickTheme() also clears any seeded key
the incoming theme does not carry before applying the ones it does; that
clears nothing today, and it is what keeps this fixed if a theme ever grows a
key the others lack.
── system on a dark desktop ───────────────────────────────────────────────
The picker toggled `.dark` from data-dark, which is Theme::isDark(), and
System is not dark by that measure — it is whatever the OS says, which is the
question the no-flash script in app.html.twig asks on every load. So picking
System on a dark desktop dropped the class and painted light until the next
navigation put it back. It now asks the same question the script does.
── One mail-sheet, in the theme's own light ───────────────────────────────
app.css had two @utility mail-sheet blocks: the warm-paper redesign from
aa0edab and the white one from the first reading-view pass. @utility blocks of
the same name do not merge — the later one wins outright — and the white one
was further down the file, so the redesign had been dead code that read as
live for a fortnight. --rgb-sheet and --rgb-sheet-ink were added by that same
commit and wired to nothing.
Resolved to the redesign, keeping the one thing the white block had that it
did not: the isolate/stacking-context fix, without which Chrome re-samples the
app background through the sheet at some viewport sizes and flashes the pane
dark.
The sheet's colours now come from the theme rather than from a fixed palette.
Light is a constraint it has to meet for the mail's sake — an email's HTML was
authored against a light background, and rendering it on a near-black surface
misrepresents what was sent — but WHICH light is the theme's business: Nord
reads its mail on snow storm, Solarized on base3, Paper on the cream it is
already made of. Everything else inside the sheet derives from those two ends
of the ramp, so the hairlines, insets and fields need no channels of their own.
── Keeping it that way ────────────────────────────────────────────────────
ThemeVariableCompletenessTest parses app.css and fails when a block drifts
from the inventory, when a knob appears outside :root, when a variable in the
inventory is read by nothing, when mail-sheet is defined twice, or when a
theme seeds a different set of settings than its siblings.
theme-switch-parity.spec.ts is the acceptance criterion: it switches through
every theme IN SEQUENCE without reloading — the order matters, because it
takes a previous theme to leave a remnant — and asserts the painted palette,
the class list and color-scheme match what a cold load of that theme produces.
Under both layouts, and once more for system on a dark desktop. Both new tests
were confirmed to fail against the code they replace.
README screenshots re-recorded: the reading pane is the warm sheet the
redesign intended rather than white, and Paper's shadows and raised surfaces
are its own warm ink rather than neutral black.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A quarter of an hour is 1/96th of the day column — about twelve pixels at
the height the grid usually gets — and the chip inside needs twenty-four
for one line of "9:00 Standup". So a fifteen-minute meeting drew a sliver
with its own label hanging out of it. It reached the README screenshot,
which is where it was noticed.
The floor is 1.75rem, which is the all-day lane's floor directly above it:
one number for "the smallest a chip may be drawn" rather than a second
one that agrees with it today.
A floored block is then taller than its duration and reaches over the one
after it. That is the trade, taken deliberately: the block's TOP is where
the meeting starts and stays honest, only the padded bottom is a fiction,
and the alternative — reflowing so short events push their neighbours
down — moves a 10:00 meeting off the 10:00 line, which is the one thing a
time grid exists to get right. Blocks are emitted in start order, so the
later one paints over the fiction rather than under it.
Presentational only, and worth saying because it looks like it should not
be: the drag reads data-starts-at and data-ends-at, never the rendered
box, so a floored block still resizes from its real duration. The resize
preview writes a pixel height computed from minutes, which the floor
clamps — the value posted is unaffected.
Also corrects this file's own header. It claimed the screenshot suite
cannot run against the E2E stack because nothing seeds the demo mailbox.
app:test:seed-demo does, so it runs — and leaves a you@example.com account
behind, which makes account-scope.spec.ts fail on the FIRST account in the
sidebar and read as a regression in account scoping. Found by doing it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The admin screenshot in the README was a red Symfony exception page —
"Access Denied. The user doesn't have ROLE_ADMIN." — and had been for as
long as it has existed. The capture ran as the shared fixture user, which
is not an administrator, and nothing failed: a screenshot of a 403 is a
perfectly good screenshot.
Two fixes, and the second is the one that matters. The admin capture gets
its own session and its own administrator, for the reason
admin-panels.spec.ts already gives — granting ROLE_ADMIN to the shared
user mid-run deauthenticates every other spec. And every capture in the
file now goes through capture(), which refuses to write a PNG of a
Symfony error page. Each of the other seven happens to assert something
visible first, which is why only the admin one rotted; a habit that holds
seven times out of eight is a rule waiting to be written down.
The demo mailbox is seeded for the administrator too. The picture is of
the admin screen, but the sidebar is in shot, and one reading "No
accounts yet" beside "No labels yet" says the app is empty.
All eight are regenerated. calendar.png now shows the time grid rather
than the column list it was taken from.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The compose shot picks a recipient out of the autocomplete, which reads
the contact book — filled by harvesting synced mail, not by seeding it,
so it was the one screenshot the demo mailbox could not produce. The seed
writes its senders as contacts, and clears fixture labels for the same
reason it clears the fixture account: "E2E Label" in the sidebar gives
the game away exactly like "E2E Trash Me" in the list.
All eight are regenerated. Six of them predate this week's sidebar work
and showed a UI that no longer exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>