Files
Walid ManiandClaude Sonnet 5.5 d919d02bee Show the open message beside the list, as a per-user option (#49)
A per-user Reading pane setting, Off or Right. Right shows the message
list and the open message side by side when the mail card is at least
52rem wide, with a divider that is dragged, moved with the arrow keys or
reset with a double-click. The message's share of the card is stored as
a percentage, 25 to 75, so it keeps its meaning when the calendar pane
opens or is resized. Off is the default and changes nothing.

The mode and the share live in the user's settings bag, not in the
Appearance embeddable: where a message is drawn is not part of a theme
somebody might export. A config backup carries both. No migration.

A message opened by its own address has no list behind it, so beside
the list it is shown next to the Inbox.

Review added, in the same change:

  Divider      one pixel of layout with the grip laid over it, so no
               strip of bare card sits between the list and the line.
  Toolbars     the list's and the open message's are the same height.
  Selection    Archive, Delete and Move to stay above a selection; the
               other six actions are under More.
  Shortcuts    a key acts on ticked rows wherever the list is on
               screen, and ticking a row with the mouse no longer
               leaves every key after it dead.
  Settings     a Features group with a page each for the reading pane
               and the keyboard shortcuts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:13:08 +02:00

87 lines
5.0 KiB
Twig

{#
_label_menu.html.twig
─────────────────────────────────────────────────────────────────────────────
"Label as" dropdown for the list toolbar and the thread view toolbar.
Variables:
labels Label[] flat list of the user's custom labels (role null),
pre-ordered; indentation derives from label.depth
targetType string "thread" | "message"
targetId int|null when null the Stimulus controller resolves selected
rows from the list instead (bulk mode)
activeLabels int[] ids of labels currently attached (single-target mode)
trigger bool false where something else opens the panel — the list
toolbar's "More" menu has a row for it. The button is
still rendered, and still has a box: it is what that
row and the `l` key press, so there is one way in, and
it is what the panel is placed against, so it cannot
be `display: none` — a button with no box measures as
a point in the window's corner, and the panel opened
there. Invisible, unfocusable, and filling whatever
positioned box the include sits in. Default true.
align string "right" (default) | "left": which edge of the trigger
the panel hangs from. Left where the trigger sits near
the start of a toolbar, so the panel opens into the
pane and not off its edge.
#}
{% set trigger = trigger is defined ? trigger : true %}
<div data-controller="mail--label-menu"
data-mail--label-menu-target-type-value="{{ targetType }}"
{% if targetId is defined and targetId is not null %}data-mail--label-menu-target-id-value="{{ targetId }}"{% endif %}
class="{{ trigger ? 'relative' : 'absolute inset-0' }}">
<button type="button"
data-action="click->mail--label-menu#toggle"
{% if false == trigger %}data-toolbar-flyout="label" tabindex="-1" aria-hidden="true"{% endif %}
aria-label="{{ 'toolbar.actions.label'|trans }}"
class="{{ trigger
? 'group relative flex items-center justify-center w-8 h-8 rounded-full text-ink-muted hover:text-ink hover:bg-hover transition-colors duration-100 cursor-pointer'
: 'block w-full h-full invisible' }}">
<i class="fa-solid fa-tag text-sm" aria-hidden="true"></i>
</button>
<div data-mail--label-menu-target="panel"
data-align="{{ align|default('right') }}"
class="hidden absolute {{ 'left' == align|default('right') ? 'left-0' : 'right-0' }} top-full mt-1 z-30 w-56 max-h-72 pointer-events-auto overflow-y-auto rounded-pane
bg-pane-soft backdrop-blur-sm
border border-line
shadow-pop py-1">
{% if labels|length == 0 %}
<p class="px-3 py-2 text-xs text-ink-faint">{{ 'label.menu.empty'|trans }}</p>
{% else %}
{% for label in labels %}
<button type="button"
data-action="click->mail--label-menu#toggleLabel"
data-mail--label-menu-label-id-param="{{ label.id }}"
data-label-id="{{ label.id }}"
data-attached="{{ activeLabels is defined and label.id in activeLabels ? 'true' : 'false' }}"
class="w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left
text-ink-soft
hover:bg-hover transition-colors cursor-pointer"
style="padding-left: {{ 12 + label.depth * 14 }}px">
<i data-mail--label-menu-target="check"
class="fa-solid fa-check w-3.5 text-accent
{{ activeLabels is defined and label.id in activeLabels ? '' : 'invisible' }}"
aria-hidden="true"></i>
<span class="truncate">{{ label|label_name }}</span>
</button>
{% endfor %}
{% endif %}
<div class="border-t border-line mt-1 pt-1">
<button type="button"
data-controller="ui--modal"
data-action="click->ui--modal#open click->mail--label-menu#close"
data-ui--modal-title-value="{{ 'sidebar.labels.add'|trans }}"
data-ui--modal-src-value="{{ path('app_label_new') }}"
class="w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left
text-ink-soft
hover:bg-hover transition-colors cursor-pointer">
<i class="fa-solid fa-plus w-3.5" aria-hidden="true"></i>
{{ 'label.menu.create'|trans }}
</button>
</div>
</div>
</div>