mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
refactor(ui): share a short right rail strip in CSS alone
The fitTabs pass and its lane observer go: the column stands the lane's full height, which makes the strip's size definite, and the tabs take flex 1 1 0% with max-height max-content, so flexbox hands the strip's height out in equal parts, a tab's own length caps its part, and what a short label leaves goes to the longer ones. The basis is 0%, not 0, so the strip's content size, which sizes the column, still comes from the labels. Only the strip and the tool slot take the pointer; the measuring pass sets the column back to height auto so the rail's climb sees the natural column. Same heights as the JS pass at every viewport checked. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
48602e4400
commit
5cc63fefb0
3 files changed
+32
-136
No files matched your search
+1
-75
@@ -296,69 +296,12 @@ export function createRightRailTabs({
|
||||
});
|
||||
};
|
||||
|
||||
/** A length from a computed style, in pixels. */
|
||||
/** A join token from the stylesheet (--right-rail-*), in pixels. */
|
||||
const token = (computed, name, fallback) => {
|
||||
const value = parseFloat(computed?.getPropertyValue?.(name));
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
};
|
||||
|
||||
/**
|
||||
* Share a strip shorter than its tabs. The lane (--right-stack-max-height)
|
||||
* less the tool slot and the strip's gaps is divided equally; a tab shorter
|
||||
* than its share keeps its height and the rest is shared again, so the long
|
||||
* labels end up the same height and clip alike (styles/right-rail-tabs.css).
|
||||
* Tabs that fit keep their natural height.
|
||||
*/
|
||||
const heights = new WeakMap();
|
||||
const setHeight = (tab, value) => {
|
||||
if (heights.get(tab) === value) return;
|
||||
heights.set(tab, value);
|
||||
if (value) tab.style.setProperty('height', value);
|
||||
else tab.style.removeProperty('height');
|
||||
};
|
||||
function fitTabs(tabs) {
|
||||
const styleOf = (el) => documentRef.defaultView?.getComputedStyle?.(el);
|
||||
const stripStyle = styleOf(strip);
|
||||
const lane = token(styleOf(rail), '--right-stack-max-height', NaN);
|
||||
const chrome =
|
||||
token(stripStyle, 'row-gap', 0) * Math.max(0, tabs.length - 1) +
|
||||
token(stripStyle, 'padding-top', 0) +
|
||||
token(stripStyle, 'padding-bottom', 0);
|
||||
const overhead = (column.offsetHeight || 0) - (strip.offsetHeight || 0);
|
||||
const room = lane - overhead - chrome;
|
||||
const entries = tabs.map((tab) => {
|
||||
const tabStyle = styleOf(tab);
|
||||
return {
|
||||
tab,
|
||||
natural:
|
||||
(tab.firstElementChild?.offsetHeight || 0) +
|
||||
token(tabStyle, 'padding-top', 0) +
|
||||
token(tabStyle, 'padding-bottom', 0),
|
||||
};
|
||||
});
|
||||
const natural = entries.reduce((sum, entry) => sum + entry.natural, 0);
|
||||
if (
|
||||
!(room > 0) ||
|
||||
natural <= room + 0.5 ||
|
||||
entries.some((entry) => !(entry.natural > 0))
|
||||
) {
|
||||
for (const entry of entries) setHeight(entry.tab, '');
|
||||
return;
|
||||
}
|
||||
let remaining = room;
|
||||
let count = entries.length;
|
||||
for (const entry of [...entries].sort((a, b) => a.natural - b.natural)) {
|
||||
const share = remaining / count;
|
||||
if (entry.natural <= share) {
|
||||
remaining -= entry.natural;
|
||||
count -= 1;
|
||||
setHeight(entry.tab, '');
|
||||
} else {
|
||||
setHeight(entry.tab, `${Math.round(share * 100) / 100}px`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish the join: the open tab's bottom on the rail (--rail-tab-bottom,
|
||||
* rail pixels; the stylesheet sets the panel's minimum height from it) and
|
||||
@@ -442,10 +385,8 @@ export function createRightRailTabs({
|
||||
// would drop keyboard focus on every class change.
|
||||
let cursor = strip.firstElementChild;
|
||||
let open = null;
|
||||
const tabs = [];
|
||||
for (const panel of panels) {
|
||||
const { tab, label } = entryFor(panel);
|
||||
tabs.push(tab);
|
||||
if (tab !== cursor) strip.insertBefore(tab, cursor);
|
||||
else cursor = cursor.nextElementSibling;
|
||||
const collapsed = panel.classList.contains('collapsed');
|
||||
@@ -464,7 +405,6 @@ export function createRightRailTabs({
|
||||
entry.tab.remove();
|
||||
entries.delete(id);
|
||||
}
|
||||
fitTabs(tabs);
|
||||
placeJoin(open?.tab, open?.panel);
|
||||
}
|
||||
|
||||
@@ -490,19 +430,6 @@ export function createRightRailTabs({
|
||||
strip.addEventListener('click', onClick);
|
||||
strip.addEventListener('keydown', onKeydown);
|
||||
observer?.observe(rail, { childList: true });
|
||||
// Tabs holding shared heights do not resize with the lane: watch the rail's
|
||||
// style for a new lane. The join's own writes to that style change no lane.
|
||||
let seenLane = rail.style?.getPropertyValue?.('--right-stack-max-height');
|
||||
const laneObserver =
|
||||
typeof Observer === 'function'
|
||||
? new Observer(() => {
|
||||
const lane = rail.style.getPropertyValue('--right-stack-max-height');
|
||||
if (lane === seenLane) return;
|
||||
seenLane = lane;
|
||||
sync();
|
||||
})
|
||||
: null;
|
||||
laneObserver?.observe(rail, { attributes: true, attributeFilter: ['style'] });
|
||||
rail.prepend(column);
|
||||
rail.append(join);
|
||||
sync();
|
||||
@@ -517,7 +444,6 @@ export function createRightRailTabs({
|
||||
if (destroyed) return;
|
||||
destroyed = true;
|
||||
observer?.disconnect();
|
||||
laneObserver?.disconnect();
|
||||
resizer?.disconnect();
|
||||
strip.removeEventListener('click', onClick);
|
||||
strip.removeEventListener('keydown', onKeydown);
|
||||
|
||||
@@ -504,46 +504,6 @@ test('a rail panel hidden at mount gets its tab when its owner unhides it, from
|
||||
tabs.destroy();
|
||||
});
|
||||
|
||||
test('a strip shorter than its tabs is shared: short labels keep their height, the long ones get equal heights', () => {
|
||||
const f = fixture();
|
||||
f.doc.defaultView = {
|
||||
getComputedStyle: (el) => ({
|
||||
getPropertyValue: (name) => el.style.props.get(name) ?? '',
|
||||
}),
|
||||
};
|
||||
f.strip.style.setProperty('row-gap', '4px');
|
||||
const labels = {
|
||||
'pp-toggles': 49,
|
||||
'cctv-panel': 28,
|
||||
'global-context-panel': 49,
|
||||
};
|
||||
for (const [id, height] of Object.entries(labels)) {
|
||||
const tab = f.tabFor(id);
|
||||
tab.style.setProperty('padding-top', '6px');
|
||||
tab.style.setProperty('padding-bottom', '6px');
|
||||
tab.children[0].offsetHeight = height;
|
||||
}
|
||||
// The column holds the strip (160px) and a 40px tool slot; the lane is 200.
|
||||
Object.assign(f.tabs.column, { offsetHeight: 200 });
|
||||
Object.assign(f.strip, { offsetHeight: 160 });
|
||||
f.rail.style.setProperty('--right-stack-max-height', '200px');
|
||||
f.tabs.sync();
|
||||
// Room 152 for naturals 61, 40, 61: CCTV keeps 40, the other two share 112.
|
||||
assert.equal(f.tabFor('cctv-panel').style.props.has('height'), false);
|
||||
assert.equal(f.tabFor('pp-toggles').style.props.get('height'), '56px');
|
||||
assert.equal(
|
||||
f.tabFor('global-context-panel').style.props.get('height'),
|
||||
'56px',
|
||||
);
|
||||
f.rail.style.setProperty('--right-stack-max-height', '400px');
|
||||
f.tabs.sync();
|
||||
assert.equal(
|
||||
f.strip.children.some((tab) => tab.style.props.has('height')),
|
||||
false,
|
||||
'a lane with room gives every tab its natural height back',
|
||||
);
|
||||
});
|
||||
|
||||
test('a tab shrunk by a short strip joins with corners no larger than half its height', () => {
|
||||
const f = fixture();
|
||||
// The strip's offsets are relative to its column, the column's to the rail.
|
||||
@@ -639,18 +599,25 @@ test('the stylesheet keeps phones on border-box, folds the strip into narrow des
|
||||
// The tabs give way to a short strip and clip their labels at the end.
|
||||
assert.match(
|
||||
css,
|
||||
/\.right-rail-tab \{[^}]*flex: 0 1 auto;[^}]*align-items: flex-start;[^}]*min-height: 0;[^}]*overflow: hidden;[^}]*mask-image: linear-gradient\(to bottom, #000 calc\(100% - 6px\), transparent\);/,
|
||||
/\.right-rail-tab \{[^}]*flex: 1 1 0%;[^}]*align-items: flex-start;[^}]*min-height: 0;[^}]*max-height: max-content;[^}]*overflow: hidden;[^}]*mask-image: linear-gradient\(to bottom, #000 calc\(100% - 6px\), transparent\);/,
|
||||
);
|
||||
// The column stands the lane's full height so the strip's size is definite
|
||||
// and a zero basis shares it; only the strip and the slot take the pointer.
|
||||
assert.match(
|
||||
css,
|
||||
/#right-rail-column \{[^}]*height: var\(--right-stack-max-height, 70vh\);[^}]*pointer-events: none;/,
|
||||
);
|
||||
assert.match(css, /#right-rail-tabs \{[^}]*pointer-events: auto;/);
|
||||
assert.match(css, /#right-rail-tools \{[^}]*pointer-events: auto;/);
|
||||
assert.doesNotMatch(
|
||||
css,
|
||||
/#right-rail-column \{[^}]*max-height: var\(--right-stack-max-height/,
|
||||
);
|
||||
// The rail layout pass measures the column's natural height to climb above
|
||||
// its baseline before the strip has to scroll.
|
||||
assert.match(
|
||||
css,
|
||||
/#right-context-rail\[data-rail-measuring\] > #right-rail-column \{\s*max-height: none !important;/,
|
||||
);
|
||||
assert.match(
|
||||
css,
|
||||
/#right-context-rail\[data-rail-measuring\] #right-rail-tabs > \.right-rail-tab \{\s*height: auto !important;/,
|
||||
'shared tab heights are lifted for the measurement, or the climb would see a column that already fits',
|
||||
/#right-context-rail\[data-rail-measuring\] > #right-rail-column \{\s*height: auto !important;/,
|
||||
);
|
||||
assert.match(
|
||||
css,
|
||||
|
||||
@@ -58,7 +58,9 @@
|
||||
}
|
||||
|
||||
/* The strip's column: the tablist, then a tool slot that holds the compact
|
||||
Radio launcher on desktop (radioPanelPlacement.js). */
|
||||
Radio launcher on desktop (radioPanelPlacement.js). It stands the lane's
|
||||
full height, which makes the strip's size definite so its tabs can share
|
||||
it (see .right-rail-tab); only the strip and the slot take the pointer. */
|
||||
#right-rail-column {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -68,18 +70,13 @@
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
width: var(--right-rail-tabs-inset);
|
||||
max-height: var(--right-stack-max-height, 70vh);
|
||||
pointer-events: auto;
|
||||
height: var(--right-stack-max-height, 70vh);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* The layout pass reads the column's natural height with the lane clamp and
|
||||
the shared tab heights lifted, to climb when the lane is too short for it
|
||||
(rightPanelRail.js). */
|
||||
/* The layout pass reads the column's natural height, to climb when the lane
|
||||
is too short for it (rightPanelRail.js). */
|
||||
#right-context-rail[data-rail-measuring] > #right-rail-column {
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
#right-context-rail[data-rail-measuring] #right-rail-tabs > .right-rail-tab {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
@@ -96,6 +93,7 @@
|
||||
/* The rail climbs above its baseline to fit the column (rightPanelRail.js);
|
||||
a lane still shorter shrinks the tabs. The strip never scrolls. */
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#right-rail-tools {
|
||||
@@ -106,6 +104,7 @@
|
||||
box-sizing: border-box;
|
||||
margin-top: 6px;
|
||||
padding-left: var(--right-rail-tabs-gap);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#right-rail-tools:empty {
|
||||
@@ -174,18 +173,22 @@ body:not(.mobile-shell)
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
/* A short strip is shared between the tabs (rightRailTabs.js): the long
|
||||
labels end up the same height. A tab clips its label at the end, faded over
|
||||
the bottom padding. */
|
||||
/* A short strip is shared between the tabs: a zero basis hands the strip's
|
||||
height out in equal parts, a tab's own length caps its part, and what a
|
||||
short label leaves goes to the longer ones, which end up the same height. A
|
||||
strip with room gives every tab its own length. The basis is 0%, not 0:
|
||||
the strip's content size, which sizes the column, comes from the labels. A
|
||||
tab clips its label at the end, faded over the bottom padding. */
|
||||
.right-rail-tab {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 0 1 auto;
|
||||
flex: 1 1 0%;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
width: var(--right-rail-tabs-width);
|
||||
min-height: 0;
|
||||
max-height: max-content;
|
||||
padding: 6px 0;
|
||||
overflow: hidden;
|
||||
mask-image: linear-gradient(to bottom, #000 calc(100% - 6px), transparent);
|
||||
|
||||
Reference in new issue
Block a user