style(header): hover moves the icon, never the button

A global `.btn-icon-header:hover { transform: rotate(45deg) }`, meant for
the settings gear, turned every header icon button on hover, so the folder,
Tiles, Split and the rest swung their rounded hover background into a
diamond. Three buttons had already cancelled it one by one (the font-size
buttons, notifications, the sidebar toggle).

The rule is gone, and with it those three overrides. Hover motion now moves
the icon only:
- the settings gear's icon turns 45 degrees (one tooth, so it lands on the
  same shape);
- the Tiles button's four squares spread apart, each toward its corner;
- the folder cross-fades to an open folder (a second drawing in its SVG,
  `.icon-folder-closed` / `.icon-folder-open`);
- every other icon just takes the hover colour.

Pointer devices only (`@media (hover: hover)`, so a tap cannot leave an icon
stuck mid-motion), and the transitions are off under reduced motion.

Owner request: the Tiles and folder buttons "weirdly turn" on hover.
Checked live on a dark and a light skin (rest, mid, end frames). Pinned by
test/header-icon-hover.test.ts, mutation-checked five ways (the button
rotation back, the open drawing missing, the motion not hover-gated, a
square spreading toward the wrong corner, reduced motion keeping its
transition). Gate: 507 files, 9798 tests passed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-08 06:15:19 +02:00
parent fcec77131c
commit 4bb333e9bc
4 changed files with 202 additions and 15 deletions
+1 -1
View File
@@ -189,7 +189,7 @@
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
<span class="attachment-history-badge" id="attachmentHistoryBadge" style="display:none;">0</span>
</button>
<button class="btn-icon-header btn-file-viewer" onclick="app.toggleFileBrowserButton()" title="File Viewer" aria-label="Open file viewer" aria-expanded="false"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg></button>
<button class="btn-icon-header btn-file-viewer" onclick="app.toggleFileBrowserButton()" title="File Viewer" aria-label="Open file viewer" aria-expanded="false"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path class="icon-folder-closed" d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><g class="icon-folder-open"><path d="M3 17V7a2 2 0 0 1 2-2h4l2 2h6a2 2 0 0 1 2 2v1.5"/><path d="M3 17l2.3-5.4A2 2 0 0 1 7.2 10.5H20a1.5 1.5 0 0 1 1.4 2l-1.9 5.2A2 2 0 0 1 17.6 19H5a2 2 0 0 1-2-2z"/></g></svg></button>
<button class="btn-icon-header btn-multimonitor btn-multimonitor--hidden" onclick="app.launchMultiMonitor()" title="Open Codeman across all displays" aria-label="Open Codeman across all displays"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="4" width="13" height="9" rx="1.5"/><rect x="11" y="9" width="11" height="8" rx="1.5"/></svg></button>
<button class="btn-icon-header btn-split btn-split--hidden" onclick="app.openSplitPicker(event)" title="Split: open a second session beside this one" aria-label="Split: open a second session beside this one" aria-pressed="false"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="18" rx="2"/><line x1="12" y1="3" x2="12" y2="21"/></svg></button>
<button class="btn-icon-header btn-tile-grid btn-tile-grid--hidden" onclick="app.toggleTileGrid()" oncontextmenu="app.openTileCountMenu(event)" title="Tiles: show several sessions side by side (right-click for how many)" aria-label="Tiles: show several sessions side by side (right-click for how many)" aria-pressed="false"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="8" height="8" rx="1"/><rect x="13" y="3" width="8" height="8" rx="1"/><rect x="3" y="13" width="8" height="8" rx="1"/><rect x="13" y="13" width="8" height="8" rx="1"/></svg></button>
+67 -14
View File
@@ -2450,7 +2450,73 @@ html[data-tab-orientation='vertical'].tab-rail-compact
.btn-icon-header:hover {
background: rgba(255, 255, 255, 0.07);
color: var(--text);
transform: rotate(45deg);
}
/* Hover motion moves the icon, never the button: turning the whole button used
to swing its hover background into a diamond on every header icon. Now only the
gear turns, the folder opens and the Tiles squares spread apart; every other
icon just takes the hover colour. Pointer devices only (a tap must not leave an
icon stuck mid-motion), and no motion at all under reduced motion. */
.btn-icon-header.btn-settings svg,
.btn-icon-header.btn-tile-grid svg rect,
.btn-icon-header.btn-file-viewer svg .icon-folder-closed,
.btn-icon-header.btn-file-viewer svg .icon-folder-open {
transition: transform 260ms cubic-bezier(0.34, 1.56, 0.64, 1), opacity 160ms ease;
}
.btn-icon-header.btn-tile-grid svg rect {
transform-box: fill-box;
transform-origin: center;
}
.btn-icon-header.btn-file-viewer svg .icon-folder-open {
opacity: 0;
transform: translateY(1px);
}
@media (hover: hover) {
.btn-icon-header.btn-settings:hover svg {
transform: rotate(45deg);
}
.btn-icon-header.btn-tile-grid:hover svg rect:nth-of-type(1) {
transform: translate(-1.5px, -1.5px);
}
.btn-icon-header.btn-tile-grid:hover svg rect:nth-of-type(2) {
transform: translate(1.5px, -1.5px);
}
.btn-icon-header.btn-tile-grid:hover svg rect:nth-of-type(3) {
transform: translate(-1.5px, 1.5px);
}
.btn-icon-header.btn-tile-grid:hover svg rect:nth-of-type(4) {
transform: translate(1.5px, 1.5px);
}
.btn-icon-header.btn-file-viewer:hover svg .icon-folder-closed {
opacity: 0;
}
.btn-icon-header.btn-file-viewer:hover svg .icon-folder-open {
opacity: 1;
transform: none;
}
}
@media (prefers-reduced-motion: reduce) {
.btn-icon-header.btn-settings svg,
.btn-icon-header.btn-tile-grid svg rect,
.btn-icon-header.btn-file-viewer svg .icon-folder-closed,
.btn-icon-header.btn-file-viewer svg .icon-folder-open {
transition: none;
}
.btn-icon-header.btn-settings:hover svg,
.btn-icon-header.btn-tile-grid:hover svg rect {
transform: none;
}
}
/* Admin Panel header button: only shown to admins in multi-user mode (marker
@@ -2569,10 +2635,6 @@ html[data-tab-orientation='vertical'].tab-rail-compact
font-weight: 600;
}
.btn-icon-header.btn-sm:hover {
transform: none;
}
.header-font-controls {
display: flex;
align-items: center;
@@ -9195,10 +9257,6 @@ kbd {
margin-right: 2px;
}
.btn-icon-header.btn-notifications:hover {
transform: none;
}
.notification-badge {
position: absolute;
top: 2px;
@@ -19172,11 +19230,6 @@ html[data-session-list="sidebar"][data-sidebar="collapsed"] .session-sidebar .ta
display: none !important;
}
/* .btn-icon-header:hover rotates 45deg globally — a panel glyph must not spin. */
.btn-sidebar-toggle:hover {
transform: none;
}
html[data-session-list="sidebar"][data-sidebar="collapsed"] .btn-sidebar-toggle svg {
transform: scaleX(-1);
}