mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +02:00
feat(tiles): the grid holds at most 6 tiles (owner decision 7)
Six was tested smooth on the owner's desktop; nine missed the headless frame bar and is untested on real hardware. TILE_GRID_MAX (constants.js) is now 6 and stays the one cap every limit reads; the layout table gets its own bound, TILE_LAYOUT_MAX = 9, so the 7 to 9 layouts keep working (unreachable) and going back to nine is that one line. Every way in stops at the cap: opening, addTile, a tile's +, a session Run makes, Ctrl/Cmd+click, the picker, "Open group as tiles", and a stored grid with more ids (it comes back as its first six, focus kept only if it survives, a dropped zoom cleared, row fractions that no longer match the 3x2 reset). The limits now go through one helper, _tileGridLimit(), whose texts say which limit binds: "Up to 6 tiles" / "The grid holds at most 6 tiles" when it is the cap, "This window fits N" when it is the window. Docs: decision 7 and an as-built entry in the spec, CLAUDE.md, the invariants, the wiki's Tile Grid and Dashboard pages. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* @fileoverview The grid holds at most TILE_GRID_MAX tiles: 6, owner decision 7
|
||||
* (docs/tile-grid-plan.md). Six was tested smooth on a real desktop; nine missed
|
||||
* the headless frame bar. The layout table still covers 7 to 9 (unreachable).
|
||||
*
|
||||
* Every way into the grid stops at the cap even where the window would fit
|
||||
* nine (the harness window is 2400x1200): opening, adding, a tile's +, a
|
||||
* session Run makes, Ctrl/Cmd+click, the picker and "Open group as tiles".
|
||||
* The texts say which limit binds: "at most 6" for the cap, "what this window
|
||||
* fits" for a smaller window.
|
||||
*
|
||||
* Real code via the shared vm harness (test/mocks/tile-grid-vm.ts). Port: N/A.
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { FakeEl, body, bySelector, makeGridApp, resetGridHarness, type GridApp } from './mocks/tile-grid-vm.js';
|
||||
|
||||
const EIGHT = Array.from({ length: 8 }, (_, i) => `s-${i + 1}`);
|
||||
const SIX = EIGHT.slice(0, 6);
|
||||
const AT_MOST = 'The grid holds at most 6 tiles';
|
||||
|
||||
let wrapRect = { width: 2400, height: 1200 };
|
||||
beforeEach(() => {
|
||||
resetGridHarness();
|
||||
wrapRect = { width: 2400, height: 1200 };
|
||||
const wrap = new FakeEl();
|
||||
wrap.getBoundingClientRect = () => ({ ...wrapRect, top: 0, left: 0, right: wrapRect.width, bottom: wrapRect.height });
|
||||
bySelector.set('.terminal-wrap', wrap);
|
||||
});
|
||||
|
||||
function fullGrid(): GridApp {
|
||||
const app = makeGridApp(EIGHT);
|
||||
app.openTileGrid(EIGHT);
|
||||
return app;
|
||||
}
|
||||
|
||||
describe('the cap', () => {
|
||||
it('is 6, worded as the cap where the window would fit more', () => {
|
||||
const app = makeGridApp(EIGHT);
|
||||
expect(app._tileGridLimit()).toEqual({ capacity: 6, hint: 'Up to 6 tiles', full: AT_MOST });
|
||||
});
|
||||
|
||||
it('a smaller window is worded by the window', () => {
|
||||
const app = makeGridApp(EIGHT);
|
||||
wrapRect = { width: 1200, height: 900 };
|
||||
expect(app._tileGridLimit()).toEqual({
|
||||
capacity: 4,
|
||||
hint: 'This window fits 4 tiles',
|
||||
full: 'The grid already holds what this window fits (4)',
|
||||
});
|
||||
});
|
||||
|
||||
it('opening on eight sessions shows the first six', () => {
|
||||
const app = fullGrid();
|
||||
expect(app._tileGrid.ids).toEqual(SIX);
|
||||
expect(app._tileGrid.tiles.size).toBe(6);
|
||||
});
|
||||
|
||||
it('a full grid takes no more tiles', () => {
|
||||
const app = fullGrid();
|
||||
expect(app.addTile('s-7')).toBe(false);
|
||||
expect(app._tileGrid.ids).toEqual(SIX);
|
||||
});
|
||||
});
|
||||
|
||||
describe('every way in stops at the cap', () => {
|
||||
it("a tile's + offers the rest, disabled, and says why", () => {
|
||||
const app = fullGrid();
|
||||
app.openTileAddMenu({ stopPropagation: vi.fn(), preventDefault: vi.fn(), currentTarget: null }, 's-1');
|
||||
const menu = body.children.find((c) => c.className.includes('tile-add-menu'))!;
|
||||
const items = menu.children.filter((c) => c.textContent === 's-7' || c.textContent === 's-8');
|
||||
expect(items).toHaveLength(2);
|
||||
for (const item of items) {
|
||||
expect(item.disabled).toBe(true);
|
||||
expect(item.title).toBe(AT_MOST);
|
||||
}
|
||||
expect(menu.children.find((c) => c.className === 'tile-add-new')!.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('a session Run makes opens on its own, with a toast', () => {
|
||||
const app = fullGrid();
|
||||
expect(app._joinTileGridFromRun('s-7')).toBe(false);
|
||||
expect(app._tileGrid.ids).toEqual(SIX);
|
||||
expect(app.showToast).toHaveBeenCalledWith(`${AT_MOST}: the new session opens on its own`, 'info');
|
||||
});
|
||||
|
||||
it('Ctrl/Cmd+click on another tab says the grid is full', () => {
|
||||
const app = fullGrid();
|
||||
expect(app.addSessionToTiles('s-7')).toBe(true);
|
||||
expect(app._tileGrid.ids).toEqual(SIX);
|
||||
expect(app.showToast).toHaveBeenCalledWith(AT_MOST, 'info');
|
||||
});
|
||||
|
||||
it('the picker greys out the seventh box', () => {
|
||||
const app = makeGridApp(EIGHT);
|
||||
app.openTilePicker({ stopPropagation: vi.fn() });
|
||||
const picker = body.children.find((c) => c.id === 'tilePickerMenu')!;
|
||||
const boxes = picker.children[0].children.map((row) => row.children[0]);
|
||||
expect(picker.children[1].children[0].textContent).toBe('Up to 6 tiles');
|
||||
for (const box of boxes) {
|
||||
// A disabled box cannot be ticked (the browser ignores the click).
|
||||
if (!EIGHT.includes(box.value) || box.checked || box.disabled) continue;
|
||||
box.checked = true;
|
||||
box.dispatch('change');
|
||||
}
|
||||
const checked = boxes.filter((b) => b.checked).map((b) => b.value);
|
||||
expect(checked).toHaveLength(6);
|
||||
const left = boxes.filter((b) => !b.checked);
|
||||
expect(left.length).toBeGreaterThan(0);
|
||||
for (const box of left) {
|
||||
expect(box.disabled).toBe(true);
|
||||
expect(box.title).toBe(AT_MOST);
|
||||
}
|
||||
});
|
||||
|
||||
it('"Open group as tiles" shows the first six of a larger group', () => {
|
||||
const app = makeGridApp(EIGHT);
|
||||
app.tabLayout = { groups: [{ id: 'g', name: 'G', refs: EIGHT.map((id) => ({ kind: 'session', id })) }] };
|
||||
app.openGroupAsTiles('g');
|
||||
expect(app._tileGrid.ids).toEqual(SIX);
|
||||
});
|
||||
});
|
||||
@@ -26,6 +26,7 @@ type TileGrid = {
|
||||
tileInDirection(ids: string[], focused: string, dir: string, cols: number): string | null;
|
||||
cycleTile(ids: string[], focused: string, delta: number): string | null;
|
||||
TILE_GRID_MAX: number;
|
||||
TILE_LAYOUT_MAX: number;
|
||||
TILE_MIN_W: number;
|
||||
TILE_MIN_H: number;
|
||||
TILE_SCROLLBACK: number;
|
||||
@@ -61,7 +62,8 @@ describe('computeTileLayout', () => {
|
||||
expect(T.computeTileLayout({ count: 3, width: 1799, height: 900 })).toMatchObject({ cols: 2, rows: 2 });
|
||||
});
|
||||
|
||||
it('caps the count at 9 and treats nothing as an empty grid', () => {
|
||||
it('lays out up to 9 (past the cap, unreachable but kept) and treats nothing as an empty grid', () => {
|
||||
expect(T.TILE_LAYOUT_MAX).toBe(9);
|
||||
expect(T.computeTileLayout({ count: 12, ...BIG })).toMatchObject({ cols: 3, rows: 3 });
|
||||
expect(T.computeTileLayout({ count: 0, ...BIG })).toMatchObject({ cols: 0, rows: 0 });
|
||||
});
|
||||
@@ -75,8 +77,10 @@ describe('computeTileLayout', () => {
|
||||
});
|
||||
|
||||
describe('tileGridCapacity', () => {
|
||||
it('holds all nine on a large monitor', () => {
|
||||
expect(T.tileGridCapacity(BIG)).toBe(T.TILE_GRID_MAX);
|
||||
it('never holds more than the cap of 6 (owner decision 7), even where nine would fit', () => {
|
||||
expect(T.TILE_GRID_MAX).toBe(6);
|
||||
expect(T.computeTileLayout({ count: 9, ...BIG }).fits).toBe(true);
|
||||
expect(T.tileGridCapacity(BIG)).toBe(6);
|
||||
});
|
||||
|
||||
it('stops at the first count whose layout does not fit', () => {
|
||||
@@ -137,10 +141,18 @@ describe('sanitizeTileGridState', () => {
|
||||
expect(out).toMatchObject({ open: false, ids: [], focused: null });
|
||||
});
|
||||
|
||||
it('caps the list at nine tiles', () => {
|
||||
it('caps the list at the cap (6): the extras are dropped', () => {
|
||||
const many = Array.from({ length: 12 }, (_, i) => `s${i}`);
|
||||
const out = T.sanitizeTileGridState({ v: 1, open: true, ids: many }, many);
|
||||
expect(out?.ids).toEqual(many.slice(0, 9));
|
||||
expect(out?.ids).toEqual(many.slice(0, 6));
|
||||
});
|
||||
|
||||
it('a stored 3x3 keeps its focus if it survives the cap, and loses a zoom that did not', () => {
|
||||
const nine = Array.from({ length: 9 }, (_, i) => `s${i}`);
|
||||
const kept = T.sanitizeTileGridState({ v: 1, open: true, ids: nine, focused: 's4', zoomed: 's7' }, nine);
|
||||
expect(kept).toMatchObject({ ids: nine.slice(0, 6), focused: 's4', zoomed: null });
|
||||
const lost = T.sanitizeTileGridState({ v: 1, open: true, ids: nine, focused: 's8', zoomed: 's2' }, nine);
|
||||
expect(lost).toMatchObject({ focused: 's0', zoomed: 's2' });
|
||||
});
|
||||
|
||||
it('drops malformed track fractions', () => {
|
||||
|
||||
@@ -152,6 +152,20 @@ describe('page load with a stored open grid', () => {
|
||||
expect(app.activeSessionId).toBe('s-b');
|
||||
});
|
||||
|
||||
it('a stored 3x3 (before the cap of 6) comes back as its first six, focus kept, a dropped zoom cleared', () => {
|
||||
const nine = Array.from({ length: 9 }, (_, i) => `n-${i + 1}`);
|
||||
storeGrid({ ids: nine, focused: 'n-5', zoomed: 'n-8', colFr: [2, 1, 1], rowFr: [1, 1, 1] });
|
||||
const app = pageLoad(nine);
|
||||
expect(app._tileGrid.ids).toEqual(nine.slice(0, 6));
|
||||
expect(FakeTile.all.filter((t) => !t._destroyed)).toHaveLength(6);
|
||||
expect(app.activeSessionId).toBe('n-5');
|
||||
expect(app._tileGrid.zoomedId).toBeNull();
|
||||
// 3x2 now: the columns still match, the three stored rows do not.
|
||||
expect(app._tileGrid.colFr).toEqual([2, 1, 1]);
|
||||
expect(app._tileGrid.rowFr).toEqual([1, 1]);
|
||||
expect(stored().ids).toEqual(nine.slice(0, 6));
|
||||
});
|
||||
|
||||
it('a stored closed grid leaves the single view, and the Tiles toggle brings it back', () => {
|
||||
localStore.set(KEY, JSON.stringify({ v: 1, open: false, ids: ['s-b', 's-c'], focused: 's-c' }));
|
||||
const app = pageLoad(IDS, (a) => localStore.set('codeman-active-session', 's-a'));
|
||||
|
||||
Reference in New Issue
Block a user