mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 10:04:18 +00:00
Every sidebar row went out with no colour class, and ui--sidebar gave the open one bg-accent-soft and text-accent (and every other row text-ink-muted) on connect. By then the browser had already styled the page, and the rows carry transition-colors for their hover, so on every full load and every Turbo visit the open row faded in over 150ms from the inherited black and a transparent pill, and the rest faded from black to their muted ink. Measured in headless Chromium against the test stack: app.css loaded at ~155ms, first paint at ~200ms with the rows black, the controller's pass at ~250ms, transitionrun on every visible row in the next frame. The stylesheet order was never the cause, and neither was the per-frame sampler: plain transitionrun listeners see the same thing. The server now renders the state. SidebarRowStateExtension restates ui--sidebar#_matches against the main request, and every link row in the sidebar, the account list and an expanded account's folders takes its classes from sidebar_row_state(). The controller's pass is then a no-op on everything the server drew, the first style the browser resolves is the final one, and the hover transitions are untouched (still ~130ms in and out). The pass stays for frame navigations and for rows that arrive later. After: the open row's first sample is already its final colour, and no sidebar row runs a transition on load, on a Turbo visit or in the mobile drawer. Two transitions remain at load and neither is a sidebar row: the Mercure dot, which is neutral until the stream reports, by design, and the topbar calendar toggle, which is still rendered as "mail" and corrected on connect. SidebarRowStateTest pins the rendered state, the matching rule, and that the class lists agree with the controller's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
124 lines
7.3 KiB
Twig
124 lines
7.3 KiB
Twig
{% set _drawerClose = _drawerClose is defined ? _drawerClose : '' %}
|
|
{# '' for the desktop rail, '-drawer' for the mobile one. Both render this
|
|
partial into the same document, so every id below has to carry it. #}
|
|
{% set _idSuffix = _idSuffix is defined ? _idSuffix : '' %}
|
|
|
|
{% for account in accounts %}
|
|
{% set expanded = app.user.expandedAccountId == account.id %}
|
|
{% set frameId = 'account-folders-' ~ account.id ~ _idSuffix %}
|
|
|
|
{# The frame id travels with the request too. Turbo matches the response
|
|
against the id of the frame that asked, so a drawer frame fetching a
|
|
reply that says `account-folders-20` finds nothing to swap and the
|
|
folders never appear. #}
|
|
<div data-controller="settings--account-expand"
|
|
data-settings--account-expand-folders-url-value="{{ path('app_mail_account_folders', { account: account.id, frame: frameId }) }}"
|
|
data-settings--account-expand-persist-url-value="{{ path('app_mail_sidebar_account_expanded') }}"
|
|
data-settings--account-expand-account-value="{{ account.id }}"
|
|
data-settings--account-expand-active-value="{{ expanded ? 'true' : 'false' }}">
|
|
|
|
{# Two targets, not one. The name opens the account — everything in it,
|
|
whatever it is labelled — and the chevron opens its label list. The
|
|
whole row used to toggle, which left no way to ask for the account
|
|
itself, and expanding was the only thing clicking an account could
|
|
do. #}
|
|
{# The row is the .nav-item, so an account gets the same pill as every
|
|
other sidebar row — a rounded-lg box on this wrapper was the last
|
|
square highlight left in the sidebar. It has to be the wrapper and
|
|
not the anchor: on the anchor the pill stops before the chevron and
|
|
cuts the row in half.
|
|
|
|
Open or closed is rendered, not applied on connect — see
|
|
App\Twig\SidebarRowStateExtension. #}
|
|
{% set _href = path('app_mail_account', { account: account.id }) %}
|
|
<div class="nav-item flex items-center transition-colors {{ sidebar_row_state(_href) }}">
|
|
{# Named explicitly rather than by the address inside it: that span
|
|
is `truncate`, and a name a screen reader reads as "kontakt@…"
|
|
because CSS ran out of room is not a name. #}
|
|
<a
|
|
href="{{ _href }}"
|
|
data-ui--sidebar-target="link"
|
|
data-action="{{ _drawerClose }}"
|
|
aria-label="{{ 'sidebar.accounts.open'|trans({ '%account%': account.displayAddress }) }}"
|
|
title="{{ account.displayAddress }}"
|
|
data-nav-tier="row"
|
|
class="flex items-center gap-2 flex-1 min-w-0"
|
|
>
|
|
{# The account's IDENTITY colour, the same tone its rows wear in
|
|
the unified lists — so a coloured dot on a message and this
|
|
dot in the sidebar are the same account. It used to be a
|
|
green/grey status light, which made every connected account
|
|
identical and told the list dots apart from nothing. Inactive
|
|
accounts dim rather than change hue: still recognisably the
|
|
same account, visibly asleep.
|
|
|
|
Two things were wrong with it. It read `sortOrder`, so
|
|
reordering the accounts swapped their colours and the mark
|
|
stopped identifying anything; that is `colorIndex` now, which
|
|
is assigned once and never moves. And it had no name of any
|
|
kind, so an orange circle beside one address and a green one
|
|
beside another could not be interpreted at all — the obvious
|
|
reading being a status light, which it is not and never was.
|
|
Account health is where status is reported; this says which
|
|
account, and now says so out loud. The one piece of state it
|
|
does carry — switched off — is named too, and agrees with the
|
|
toggle in Settings that put it there. #}
|
|
<span class="w-2 h-2 rounded-full flex-shrink-0 {{ account.colorIndex|account_color }}{{ account.isActive ? '' : ' opacity-40' }}"
|
|
role="img"
|
|
title="{{ account.isActive
|
|
? 'sidebar.accounts.colour'|trans({ '%account%': account.displayAddress })
|
|
: 'sidebar.accounts.colour_disabled'|trans({ '%account%': account.displayAddress }) }}"
|
|
aria-label="{{ account.isActive
|
|
? 'sidebar.accounts.colour'|trans({ '%account%': account.displayAddress })
|
|
: 'sidebar.accounts.colour_disabled'|trans({ '%account%': account.displayAddress }) }}"></span>
|
|
<span class="truncate text-sm">{{ account.displayAddress }}</span>
|
|
</a>
|
|
{# aria-expanded, because this is a disclosure button and which way
|
|
it will go is not visible to a screen reader — nor, since the
|
|
open account moved into the user's settings, to anything else
|
|
that has to know the state before pressing it. The controller
|
|
keeps it in step.
|
|
|
|
The account's address is IN the label. Every account renders one
|
|
of these, so "Toggle account folders" three times over is three
|
|
controls a screen-reader user cannot tell apart — the list of
|
|
buttons on the page reads the same word three times and none of
|
|
them says which mailbox it opens. #}
|
|
<button
|
|
type="button"
|
|
aria-label="{{ 'sidebar.accounts.toggle'|trans({ '%account%': account.displayAddress }) }}"
|
|
title="{{ 'sidebar.accounts.toggle'|trans({ '%account%': account.displayAddress }) }}"
|
|
aria-expanded="{{ expanded ? 'true' : 'false' }}"
|
|
aria-controls="{{ frameId }}"
|
|
data-settings--account-expand-target="toggle"
|
|
data-nav-tier="row"
|
|
class="px-2 rounded-lg text-gray-400 cursor-pointer hover:text-ink"
|
|
data-action="click->settings--account-expand#toggle"
|
|
>
|
|
<i data-settings--account-expand-target="chevron"
|
|
class="fa-solid fa-chevron-right text-xs shrink-0 transition-transform duration-150
|
|
{{ expanded ? 'rotate-90' : '' }}"></i>
|
|
</button>
|
|
</div>
|
|
|
|
{# The expanded account's folders are rendered here, not fetched after
|
|
the fact: the sidebar re-renders on every visit, and a list that
|
|
arrives one request later is a list that visibly blinks on each
|
|
navigation. Any other account still loads its frame on demand. #}
|
|
<turbo-frame
|
|
id="{{ frameId }}"
|
|
data-settings--account-expand-target="frame"
|
|
class="{{ expanded ? '' : 'hidden' }}"
|
|
>
|
|
{% if expanded %}
|
|
{% include 'mail/_account_folder_rows.html.twig' with {
|
|
account: account,
|
|
labels: sidebarCounts.labelsForAccount(account),
|
|
} only %}
|
|
{% endif %}
|
|
</turbo-frame>
|
|
</div>
|
|
{% else %}
|
|
<p class="px-3 text-sm text-ink-faint italic" data-nav-tier="row">{{ 'sidebar.accounts.none'|trans }}</p>
|
|
{% endfor %}
|