112 lines
4.7 KiB
TypeScript
112 lines
4.7 KiB
TypeScript
/**
|
|
* Where the editor toolbar strip goes: on the header's brand row or on its
|
|
* own bar below, labelled or icon-only.
|
|
*
|
|
* The complaint this fixes: with the AI chat docked at 1440px, a board with
|
|
* many canvas controls (XIAO: display + camera + mic + scope) wrapped the
|
|
* strip INSIDE the header — canvas controls orphaned on a second line next
|
|
* to a black hole — because the drop-below fallback and the label collapse
|
|
* fired on guessed thresholds instead of the strip's real width. These
|
|
* lock the rule: measured needed width vs measured available width, one
|
|
* row preferred over two, labels preferred over icons.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
decideStripLayout,
|
|
stripAvailableBelow,
|
|
stripAvailableInline,
|
|
stripNeededWidth,
|
|
} from '../components/layout/headerStripFit';
|
|
|
|
const base = {
|
|
contentWidth: 1044, // 1440 minus 16px header padding minus 380px docked chat
|
|
leftWidth: 352, // brand + File/Edit/View/Account/Help
|
|
hostMarginXInline: 20, // .header-editor-toolbar { margin: 0 10px }
|
|
hostMarginXBelow: -32, // own bar bleeds into the header padding
|
|
stripPaddingX: 6, // in-header strip pads 6px right
|
|
};
|
|
|
|
// Zone widths per candidate layout: [view-mode toggle, editor zone, canvas
|
|
// controls]. On the narrow brand row the toggle is hidden by its own
|
|
// container query (0); on the wide own-bar it shows (139).
|
|
const uno = {
|
|
inlineZoneWidths: [0, 382, 268],
|
|
inlineCompactZoneWidths: [0, 350, 240],
|
|
belowZoneWidths: [139, 395, 268],
|
|
};
|
|
// Display + camera + mic + scope + add: ~640px of labelled canvas controls,
|
|
// ~344px icon-only; the editor zone carries "Libraries" (~530 / ~420).
|
|
const xiao = {
|
|
inlineZoneWidths: [0, 530, 640],
|
|
inlineCompactZoneWidths: [0, 420, 344],
|
|
belowZoneWidths: [139, 530, 640],
|
|
};
|
|
|
|
describe('header strip layout', () => {
|
|
it('sums the zones plus the strip padding; available subtracts brand + margins', () => {
|
|
const i = { ...base, ...uno };
|
|
expect(stripNeededWidth(i, 'inline')).toBe(656);
|
|
expect(stripNeededWidth(i, 'inline-compact')).toBe(596);
|
|
expect(stripNeededWidth(i, 'below')).toBe(808);
|
|
expect(stripAvailableInline(i)).toBe(672);
|
|
expect(stripAvailableBelow(i)).toBe(1076);
|
|
});
|
|
|
|
it('keeps an Uno-sized strip labelled on the brand row at 1440 with the chat docked', () => {
|
|
expect(decideStripLayout({ ...base, ...uno })).toEqual({ below: false, compact: false });
|
|
});
|
|
|
|
it('goes icon-only on the brand row before dropping below', () => {
|
|
// 1300px window with the chat: 904 content, 532 for the strip.
|
|
const i = { ...base, contentWidth: 904, ...uno };
|
|
expect(stripAvailableInline(i)).toBe(532);
|
|
// labelled 656 does not fit, compact 596 does not either -> below
|
|
expect(decideStripLayout(i)).toEqual({ below: true, compact: false });
|
|
// a slightly narrower compact strip fits inline -> one row, icons
|
|
expect(decideStripLayout({ ...i, inlineCompactZoneWidths: [0, 300, 220] })).toEqual({
|
|
below: false,
|
|
compact: true,
|
|
});
|
|
});
|
|
|
|
it('drops a XIAO-sized strip below AND compacts it when the full bar cannot hold the labels', () => {
|
|
// 1440 with the chat: 1076px bar, 1315px labelled on that bar -> compact.
|
|
expect(decideStripLayout({ ...base, ...xiao })).toEqual({ below: true, compact: true });
|
|
// The same board on a 1920 window with the chat: 1524 content ->
|
|
// 1152 inline (labels 1176 no, compact 770 yes) -> one row, icons.
|
|
expect(decideStripLayout({ ...base, contentWidth: 1524, ...xiao })).toEqual({
|
|
below: false,
|
|
compact: true,
|
|
});
|
|
});
|
|
|
|
it('judges the own-bar by the widths the strip has THERE (the view toggle reappears)', () => {
|
|
// 1300 with the chat: 532 inline (no), 936 own-bar; measured on the brand
|
|
// row the strip would seem to fit the bar (656 <= 936) — but there the
|
|
// hidden view-mode toggle comes back and the bar wraps in two.
|
|
const i = { ...base, contentWidth: 904, ...uno, belowZoneWidths: [139, 530, 268] };
|
|
expect(decideStripLayout(i)).toEqual({ below: true, compact: true });
|
|
});
|
|
|
|
it('is decided by the real widths, not a viewport threshold', () => {
|
|
// 1000px window without the chat: the old media query forced two rows
|
|
// below 1040px; a strip whose labels fit stays on one row.
|
|
const noChat = { ...base, contentWidth: 968 };
|
|
expect(
|
|
decideStripLayout({
|
|
...noChat,
|
|
inlineZoneWidths: [0, 322, 268],
|
|
inlineCompactZoneWidths: [0, 300, 240],
|
|
belowZoneWidths: [139, 395, 268],
|
|
}),
|
|
).toEqual({ below: false, compact: false });
|
|
});
|
|
|
|
it('treats an exact fit as fitting', () => {
|
|
expect(decideStripLayout({ ...base, ...uno, inlineZoneWidths: [0, 398, 268] })).toEqual({
|
|
below: false,
|
|
compact: false,
|
|
});
|
|
});
|
|
});
|