mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 09:19:42 +02:00
fix(tabs): state rows start under the brand, labels left-aligned (#426)
In the desktop header strip grouped by state, the brand leaves the flow and sits over the strip's top-left corner, so every row after the first starts at the left edge under "Codeman" instead of leaving that corner empty. Labels are left-aligned in the measured column; the first row's heading takes its natural width beside the brand (--tab-triage-brand, kept by a ResizeObserver so no render pass reads layout for it). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+47
-6
@@ -4890,6 +4890,9 @@ class CodemanApp {
|
||||
}
|
||||
const count = String(group.count);
|
||||
if (head.lastElementChild.textContent !== count) head.lastElementChild.textContent = count;
|
||||
// The first row's heading is the one that starts beside the brand in the
|
||||
// header strip (styles.css); every later row starts under it.
|
||||
head.classList.toggle('tab-triage-head--lead', group === triage.groups[0]);
|
||||
const headOrder = String(group.headOrder);
|
||||
if (head.style.order !== headOrder) head.style.order = headOrder;
|
||||
const brk = ensure('tab-triage-break', group.key);
|
||||
@@ -4905,20 +4908,34 @@ class CodemanApp {
|
||||
}
|
||||
|
||||
/**
|
||||
* Size the header strip's label column to the widest label on screen, so no
|
||||
* row carries a fixed gutter's worth of empty space before its tabs (labels
|
||||
* are right-aligned in it, styles.css). Measured only when the label text
|
||||
* changes (a group appears, goes, or its count gains a digit), and once more
|
||||
* when the web fonts finish loading, since that changes every width. The
|
||||
* vertical lists do not use the gutter, so they are never measured.
|
||||
* Size the header strip's two measured lengths (styles.css, "Header strip,
|
||||
* wrapping"): `--tab-triage-gutter`, the label column, as wide as the widest
|
||||
* label on screen so a row never carries a fixed gutter's worth of empty
|
||||
* space; and `--tab-triage-brand`, the brand's width, because the brand sits
|
||||
* over the strip's top-left corner and only the FIRST row starts beside it,
|
||||
* every later row starting under it.
|
||||
*
|
||||
* The labels are measured only when their text changes (a group appears,
|
||||
* goes, or its count gains a digit) and once more when the web fonts finish
|
||||
* loading. The brand is watched by a ResizeObserver (a display-name change,
|
||||
* the sidebar toggle appearing), so a render pass never forces a layout read
|
||||
* for it. The vertical lists use neither length and are never measured.
|
||||
*/
|
||||
_sizeTabTriageGutter(container, triage) {
|
||||
const inHeader = !!container.parentElement?.classList.contains('session-tabs-host');
|
||||
if (!triage || !inHeader) {
|
||||
if (container.style.getPropertyValue('--tab-triage-gutter')) container.style.removeProperty('--tab-triage-gutter');
|
||||
if (container.style.getPropertyValue('--tab-triage-brand')) container.style.removeProperty('--tab-triage-brand');
|
||||
this._tabTriageGutterKey = null;
|
||||
return;
|
||||
}
|
||||
this._watchTabTriageBrand(container);
|
||||
if (Number.isFinite(this._tabTriageBrandWidth)) {
|
||||
const brand = `${this._tabTriageBrandWidth}px`;
|
||||
if (container.style.getPropertyValue('--tab-triage-brand') !== brand) {
|
||||
container.style.setProperty('--tab-triage-brand', brand);
|
||||
}
|
||||
}
|
||||
const key = triage.groups.map((g) => `${g.key}:${g.count}`).join('|');
|
||||
if (key === this._tabTriageGutterKey) return;
|
||||
let widest = 0;
|
||||
@@ -4940,6 +4957,30 @@ class CodemanApp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep `_tabTriageBrandWidth` (the header brand plus the gap after it) in
|
||||
* step with the brand, once per page. The first observation arrives right
|
||||
* after `observe()`, so the width is known from the first frame on.
|
||||
*/
|
||||
_watchTabTriageBrand(container) {
|
||||
if (this._tabTriageBrandObserver !== undefined) return;
|
||||
const brand = container.closest('.header')?.querySelector(':scope > .header-brand');
|
||||
if (!brand || typeof ResizeObserver !== 'function') {
|
||||
this._tabTriageBrandObserver = null;
|
||||
return;
|
||||
}
|
||||
const gap = 8;
|
||||
this._tabTriageBrandWidth = Math.ceil(brand.getBoundingClientRect().width + gap);
|
||||
this._tabTriageBrandObserver = new ResizeObserver((entries) => {
|
||||
const box = entries[0]?.borderBoxSize?.[0];
|
||||
const width = Math.ceil((box ? box.inlineSize : brand.getBoundingClientRect().width) + gap);
|
||||
if (width === this._tabTriageBrandWidth) return;
|
||||
this._tabTriageBrandWidth = width;
|
||||
this._sizeTabTriageGutter(this.$('sessionTabs'), this._lastTabTriage);
|
||||
});
|
||||
this._tabTriageBrandObserver.observe(brand);
|
||||
}
|
||||
|
||||
/**
|
||||
* A drag in a grouped strip (by state or by case) may only reorder WITHIN a
|
||||
* group. Inside a group the rows sit in tab order, so a drop there moves the
|
||||
|
||||
@@ -19848,21 +19848,40 @@ html .toolbar .btn-git-status[aria-expanded='true'] {
|
||||
/* Header strip, wrapping (desktop): one row per state. Each heading sits in a
|
||||
left gutter, so a group too long for one line continues under its own tabs
|
||||
rather than under its heading, and each row ends with a full-width break. */
|
||||
/* The gutter is as wide as the widest label on screen (app.js measures it into
|
||||
--tab-triage-gutter whenever the labels change; 92px is only the first-paint
|
||||
fallback), and every label is right-aligned in it, so each one sits right
|
||||
next to its own tabs while the rows still start on one line. */
|
||||
/* The label column is as wide as the widest label on screen (app.js measures
|
||||
it into --tab-triage-gutter whenever the labels change; 92px is only the
|
||||
first-paint fallback) and the labels are left-aligned in it.
|
||||
|
||||
The brand leaves the flow and sits over the strip's top-left corner, so the
|
||||
strip starts at the header's left edge: the first row's heading (the lead)
|
||||
is pushed past the brand by --tab-triage-brand, and every later row starts
|
||||
at the edge, under "Codeman", instead of leaving that corner empty. The
|
||||
rows under the first one line up with each other in the label column. */
|
||||
.header:has(> .session-tabs-host > .session-tabs.tabs-triage:is(.tabs-auto-wrap, .tabs-two-rows)) > .header-brand {
|
||||
position: absolute;
|
||||
top: 0.35rem;
|
||||
left: 0.75rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-triage:is(.tabs-auto-wrap, .tabs-two-rows) {
|
||||
padding-left: calc(0.25rem + var(--tab-triage-gutter, 92px));
|
||||
padding-left: var(--tab-triage-gutter, 92px);
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-triage:is(.tabs-auto-wrap, .tabs-two-rows) > .tab-triage-head {
|
||||
justify-content: flex-end;
|
||||
justify-content: flex-start;
|
||||
width: var(--tab-triage-gutter, 92px);
|
||||
margin-left: calc(-1 * var(--tab-triage-gutter, 92px));
|
||||
padding: 0 8px 0 0;
|
||||
}
|
||||
|
||||
/* The first row starts beside the brand and lines up with nothing else, so its
|
||||
label takes its own width and its tabs follow it directly. */
|
||||
.session-tabs-host > .session-tabs.tabs-triage:is(.tabs-auto-wrap, .tabs-two-rows) > .tab-triage-head--lead {
|
||||
width: auto;
|
||||
margin-left: calc(var(--tab-triage-brand, 100px) - var(--tab-triage-gutter, 92px));
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-triage:is(.tabs-auto-wrap, .tabs-two-rows) > .tab-triage-break {
|
||||
display: block;
|
||||
flex: 0 0 100%;
|
||||
|
||||
Reference in New Issue
Block a user