mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
feat(tabs): L-shaped header strip, wrapped rows run under the toolbar
A wrapped classic tab strip stopped at the toolbar's left edge on every row, so the space under the header chips stayed empty and long tabs took a row each. Once the strip wraps, the toolbar now sits over the strip's top-right corner and the strip lays its tabs out inline around a float sized to the toolbar's box (_syncTabStripCorner), so only the rows beside the toolbar stop short of it and every later row runs the header's full width. - Wrap is still measured with the toolbar in flow (the corner is dropped before the measure), so the single-row strip is unchanged. - A ResizeObserver on the toolbar re-decides wrap + corner and redraws the lineage routes when a reading or the divider changes its size. - Falls back to the plain strip when the first tab cannot fit beside the toolbar (it would otherwise leave an empty band). - Classic arrangement, desktop, header host only; state/case/ledger, the sidebar and the tab rail keep their own layouts.
This commit is contained in:
1 parent
3e06f3c141
commit
3dafc89095
2 files changed
+118
-1
No files matched your search
+80
-1
@@ -5910,7 +5910,7 @@ class CodemanApp {
|
||||
// tabs-auto-wrap is measured, not derived from settings — updateTabOverflowMode()
|
||||
// drops it in sidebar mode, but drop it here too so nothing paints wrapped
|
||||
// for a frame before the next measure.
|
||||
if (sidebar) tabsEl.classList.remove('tabs-auto-wrap');
|
||||
if (sidebar) tabsEl.classList.remove('tabs-auto-wrap', 'tabs-corner');
|
||||
// Collapse/expand changes whether the filter is reachable, so re-evaluate it
|
||||
// here too — not only at the render tails.
|
||||
this.applySidebarFilter(this._sidebarFilter);
|
||||
@@ -6901,7 +6901,14 @@ class CodemanApp {
|
||||
updateTabOverflowMode() {
|
||||
const container = this.$('sessionTabs');
|
||||
if (!container) return;
|
||||
// The corner takes the toolbar out of the header's flow, which widens the
|
||||
// strip, so it goes before the wrap is measured and comes back once decided.
|
||||
container.classList.remove('tabs-corner');
|
||||
this._decideTabOverflow(container);
|
||||
this._syncTabStripCorner(container);
|
||||
}
|
||||
|
||||
_decideTabOverflow(container) {
|
||||
// Lineage routing room (session-lineage.js) changes the strip's padding and
|
||||
// row gap, so it is decided before the wrap is measured below.
|
||||
this._syncLineageGutter?.();
|
||||
@@ -6963,6 +6970,78 @@ class CodemanApp {
|
||||
container.classList.toggle('tabs-auto-wrap', shouldWrap);
|
||||
}
|
||||
|
||||
/**
|
||||
* L-shaped header strip: once the classic strip wraps, the toolbar
|
||||
* (#headerRight) sits over the strip's top-right corner (styles.css,
|
||||
* `.tabs-corner`) and the rows under the first one run the header's full
|
||||
* width beneath it. The strip keeps the toolbar's corner clear with a float
|
||||
* sized here, from the toolbar's own box, so a toolbar that wraps onto more
|
||||
* lines (header-divider.js) or gains a reading keeps every tab out from
|
||||
* under it. The grouped arrangements (state, case, ledger) lay their rows
|
||||
* out themselves and keep the plain strip.
|
||||
*/
|
||||
_syncTabStripCorner(container) {
|
||||
const right = this.$('headerRight');
|
||||
const on =
|
||||
!!right &&
|
||||
container.offsetParent !== null &&
|
||||
!!container.parentElement?.classList.contains('session-tabs-host') &&
|
||||
(container.classList.contains('tabs-auto-wrap') || container.classList.contains('tabs-two-rows')) &&
|
||||
!container.classList.contains('tabs-triage') &&
|
||||
!container.classList.contains('tabs-clusters') &&
|
||||
!container.classList.contains('tabs-ledger') &&
|
||||
MobileDetection.getDeviceType() === 'desktop';
|
||||
const off = () => {
|
||||
container.classList.remove('tabs-corner');
|
||||
container.style.removeProperty('--tab-corner-width');
|
||||
container.style.removeProperty('--tab-corner-height');
|
||||
};
|
||||
if (!on) {
|
||||
off();
|
||||
return;
|
||||
}
|
||||
container.classList.add('tabs-corner');
|
||||
if (!this._tabCornerObserver && typeof ResizeObserver !== 'undefined') {
|
||||
let frame = 0;
|
||||
// A toolbar that changes size (a reading arrives, the divider settles)
|
||||
// moves the tabs without a render: re-decide the wrap and the corner, and
|
||||
// redraw the lineage routes that follow the tabs.
|
||||
this._tabCornerObserver = new ResizeObserver(() => {
|
||||
if (frame) return;
|
||||
frame = requestAnimationFrame(() => {
|
||||
frame = 0;
|
||||
const strip = this.$('sessionTabs');
|
||||
if (!strip || !/\btabs-(corner|auto-wrap|two-rows)\b/.test(strip.className)) return;
|
||||
this.updateTabOverflowMode();
|
||||
this.updateConnectionLines?.();
|
||||
});
|
||||
});
|
||||
this._tabCornerObserver.observe(right);
|
||||
}
|
||||
const style = getComputedStyle(container);
|
||||
const strip = container.getBoundingClientRect();
|
||||
const box = right.getBoundingClientRect();
|
||||
// The float sits in the strip's scrolled content, at its padding edge.
|
||||
const contentTop = strip.top + parseFloat(style.paddingTop) - container.scrollTop;
|
||||
const contentLeft = strip.left + container.clientLeft + parseFloat(style.paddingLeft);
|
||||
const contentRight = strip.left + container.clientLeft + container.clientWidth - parseFloat(style.paddingRight);
|
||||
// A first tab that cannot fit beside the toolbar would drop the whole first
|
||||
// line below it and leave an empty band: keep the plain strip instead.
|
||||
const firstTab = container.querySelector(':scope > .session-tab');
|
||||
if (firstTab && box.left - 8 - contentLeft < firstTab.offsetWidth + 2) {
|
||||
off();
|
||||
return;
|
||||
}
|
||||
const width = `${Math.max(0, Math.ceil(contentRight - box.left + 8))}px`;
|
||||
const height = `${Math.max(0, Math.ceil(box.bottom - contentTop))}px`;
|
||||
if (container.style.getPropertyValue('--tab-corner-width') !== width) {
|
||||
container.style.setProperty('--tab-corner-width', width);
|
||||
}
|
||||
if (container.style.getPropertyValue('--tab-corner-height') !== height) {
|
||||
container.style.setProperty('--tab-corner-height', height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a case cluster in the header strip wraps inside its own box: a case
|
||||
* wider than the whole strip (styles.css caps a box at the strip's width). The
|
||||
|
||||
@@ -616,6 +616,44 @@ body {
|
||||
padding-left: var(--lineage-spine-channel);
|
||||
}
|
||||
|
||||
/* L-shaped strip (`_syncTabStripCorner()`, app.js): a wrapped classic strip
|
||||
runs the header's full width, the toolbar sits over its top-right corner,
|
||||
and only the rows beside the toolbar stop short of it. A flex line cannot
|
||||
be shortened on its own, so the strip lays its tabs out inline, around a
|
||||
float the size of the toolbar's box (measured into --tab-corner-width and
|
||||
--tab-corner-height). The strip scrolls, so it contains the float and is
|
||||
never shorter than the toolbar it now holds up. Inline tabs space their
|
||||
rows with a bottom margin (the old row-gap, lineage's included); the strip
|
||||
carries no font size of its own, so the whitespace between tabs is 0 wide. */
|
||||
.header:has(> .session-tabs-host > .session-tabs.tabs-corner) > .header-right {
|
||||
position: absolute;
|
||||
top: 0.35rem;
|
||||
right: 0.75rem;
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-corner {
|
||||
display: block;
|
||||
font-size: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-corner::before {
|
||||
content: '';
|
||||
float: right;
|
||||
width: var(--tab-corner-width, 0px);
|
||||
height: var(--tab-corner-height, 0px);
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.tabs-corner > .session-tab {
|
||||
display: inline-flex;
|
||||
vertical-align: top;
|
||||
margin: 0 2px var(--lineage-row-gap, 4px) 0;
|
||||
}
|
||||
|
||||
.session-tabs-host > .session-tabs.lineage-tree.tabs-corner {
|
||||
padding-bottom: calc(var(--lineage-bottom-room) - var(--lineage-row-gap));
|
||||
}
|
||||
|
||||
.session-tabs::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 0;
|
||||
|
||||
Reference in new issue
Block a user