mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 01:09:43 +02:00
feat(defaults): Compact header, Tiles button on, Classic tab layout
The owner's picks after testing the 1.36.0 beta: - Header Stats Style defaults to Compact (two pills with rings) instead of Tiles. The resolver, the pre-paint stamp and the App Settings option all agree; an unknown value now reads as compact. - The Tiles header button is ON by default everywhere but handhelds (their defaults object keeps it off, and the button still needs a 1180px window). The Ctrl+Shift+G gate in tileShortcutFor() now resolves an absent key through the device defaults too, so the chord and the button cannot disagree; before, it required a stored true. - Tab Layout defaults to Classic (the single strip, as before). By state, By case and Ledger stay available as opt-ins. The tile-grid beta the owner used last had only this layout, and it is the one they wanted back. zh-CN option labels follow the new "(default)" markers; docs and wiki updated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* @fileoverview The header-stats styles (`headerStatsStyle`, Discussion #426
|
||||
* option G): 'classic' (as before), 'compact' (one system pill with rings
|
||||
* plus a plan-ring pill) and 'tiles' (label over value, bar underneath; the
|
||||
* default).
|
||||
* plus a plan-ring pill; the default, the owner's pick on the 1.36.0 beta) and
|
||||
* 'tiles' (label over value, bar underneath).
|
||||
*
|
||||
* Pinned here:
|
||||
* - The default is 'tiles', and only the three known values are honoured.
|
||||
* - The default is 'compact', and only the three known values are honoured.
|
||||
* - The two clustered styles move the connection indicator INTO the system
|
||||
* stats pill and the plan chip right after it, on the REAL header markup
|
||||
* from index.html, and 'classic' puts both back exactly where the template
|
||||
@@ -69,8 +69,7 @@ function makeApp() {
|
||||
}
|
||||
|
||||
/** Element children of the header's right side, by id (or first class). */
|
||||
const rightSide = () =>
|
||||
[...document.getElementById('headerRight')!.children].map((el) => el.id || el.classList[0]);
|
||||
const rightSide = () => [...document.getElementById('headerRight')!.children].map((el) => el.id || el.classList[0]);
|
||||
const statsChildren = () =>
|
||||
[...document.getElementById('headerSystemStats')!.children].map((el) => el.id || el.classList[0]);
|
||||
|
||||
@@ -80,14 +79,27 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe('resolveHeaderStatsStyle', () => {
|
||||
it("defaults to 'tiles' and honours only the three known styles", () => {
|
||||
it("defaults to 'compact' and honours only the three known styles", () => {
|
||||
const app = makeApp();
|
||||
expect(app.resolveHeaderStatsStyle({})).toBe('tiles');
|
||||
expect(app.resolveHeaderStatsStyle(undefined)).toBe('tiles');
|
||||
expect(app.resolveHeaderStatsStyle({})).toBe('compact');
|
||||
expect(app.resolveHeaderStatsStyle(undefined)).toBe('compact');
|
||||
expect(app.resolveHeaderStatsStyle({ headerStatsStyle: 'classic' })).toBe('classic');
|
||||
expect(app.resolveHeaderStatsStyle({ headerStatsStyle: 'compact' })).toBe('compact');
|
||||
expect(app.resolveHeaderStatsStyle({ headerStatsStyle: 'tiles' })).toBe('tiles');
|
||||
expect(app.resolveHeaderStatsStyle({ headerStatsStyle: 'neon' })).toBe('tiles');
|
||||
expect(app.resolveHeaderStatsStyle({ headerStatsStyle: 'neon' })).toBe('compact');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveTabArrangement', () => {
|
||||
it("defaults to 'classic' and honours only the four known layouts", () => {
|
||||
// Lives here because this file already loads settings-ui.js's resolvers.
|
||||
const app = makeApp();
|
||||
expect(app.resolveTabArrangement({})).toBe('classic');
|
||||
expect(app.resolveTabArrangement(undefined)).toBe('classic');
|
||||
for (const value of ['state', 'case', 'ledger', 'classic']) {
|
||||
expect(app.resolveTabArrangement({ tabArrangement: value })).toBe(value);
|
||||
}
|
||||
expect(app.resolveTabArrangement({ tabArrangement: 'neon' })).toBe('classic');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -260,15 +272,16 @@ describe('header stats wiring (static)', () => {
|
||||
expect([...found.keys()].filter((s) => /(tiles|split)-open/.test(s) && s.includes(':is('))).toEqual([]);
|
||||
});
|
||||
|
||||
it('stamps data-header-stats before first paint, tiles by default and classic on narrow screens', () => {
|
||||
it('stamps data-header-stats before first paint, compact by default and classic on narrow screens', () => {
|
||||
expect(INDEX).toContain(
|
||||
"dataset.headerStats=(window.innerWidth<768||solo)?'classic':(H==='classic'||H==='compact')?H:'tiles'"
|
||||
"dataset.headerStats=(window.innerWidth<768||solo)?'classic':(H==='classic'||H==='tiles')?H:'compact'"
|
||||
);
|
||||
});
|
||||
|
||||
it('offers the three styles with tiles marked as the default', () => {
|
||||
it('offers the three styles with compact marked as the default', () => {
|
||||
expect(INDEX).toMatch(
|
||||
/<select id="appSettingsHeaderStatsStyle"[^>]*>\s*<option value="classic">[^<]+<\/option>\s*<option value="compact">[^<]+<\/option>\s*<option value="tiles">Tiles \(default\)<\/option>/
|
||||
/<select id="appSettingsHeaderStatsStyle"[^>]*>\s*<option value="classic">[^<]+<\/option>\s*<option value="compact">Compact \(default\)<\/option>\s*<option value="tiles">[^<]+<\/option>/
|
||||
);
|
||||
expect(INDEX).not.toContain('Tiles (default)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,7 +71,8 @@ afterAll(() => {
|
||||
describe('the Tab Layout settings rows in zh-CN', () => {
|
||||
it('finds the three rows and all their options (the check is not vacuous)', () => {
|
||||
expect(english.size).toBe(15);
|
||||
expect(english.get('appSettingsHeaderStatsStyle option tiles')).toBe('Tiles (default)');
|
||||
expect(english.get('appSettingsHeaderStatsStyle option compact')).toBe('Compact (default)');
|
||||
expect(english.get('appSettingsHeaderStatsStyle option tiles')).toBe('Tiles (label over value)');
|
||||
expect(english.get('appSettingsTabStateOrder option urgent-last')).toBe('Needs you at the bottom');
|
||||
});
|
||||
|
||||
|
||||
@@ -615,17 +615,19 @@ describe('tab grouping wiring (static)', () => {
|
||||
const mobileCss = read('mobile.css');
|
||||
|
||||
it('stamps data-tab-arrangement and data-tab-state-order before first paint', () => {
|
||||
expect(html).toContain("dataset.tabArrangement=(T==='case'||T==='ledger'||T==='classic')?T:'state'");
|
||||
expect(html).toContain("dataset.tabArrangement=(T==='state'||T==='case'||T==='ledger')?T:'classic'");
|
||||
expect(html).toContain("dataset.tabStateOrder=(A.tabStateOrder==='urgent-last')?'urgent-last':'urgent-first'");
|
||||
// The catch branch (localStorage threw) must set both too.
|
||||
expect(html).toContain(
|
||||
"document.documentElement.dataset.tabArrangement='state';document.documentElement.dataset.tabStateOrder='urgent-first';"
|
||||
"document.documentElement.dataset.tabArrangement='classic';document.documentElement.dataset.tabStateOrder='urgent-first';"
|
||||
);
|
||||
});
|
||||
|
||||
it('offers the four layouts with "By state" as the default, and the state order', () => {
|
||||
it('offers the four layouts with "Classic" as the default, and the state order', () => {
|
||||
// Classic (the single strip, as before) is the default: the owner's pick on
|
||||
// the 1.36.0 beta. By state, By case and Ledger are opt-in.
|
||||
expect(html).toMatch(
|
||||
/<select id="appSettingsTabArrangement"[^>]*>\s*<option value="state">By state \(default\)<\/option>\s*<option value="case">[^<]+<\/option>\s*<option value="ledger">[^<]+<\/option>\s*<option value="classic">Classic \(as before\)<\/option>/
|
||||
/<select id="appSettingsTabArrangement"[^>]*>\s*<option value="state">By state \(rows per state\)<\/option>\s*<option value="case">[^<]+<\/option>\s*<option value="ledger">[^<]+<\/option>\s*<option value="classic">Classic \(default\)<\/option>/
|
||||
);
|
||||
expect(html).toMatch(
|
||||
/<select id="appSettingsTabStateOrder"[^>]*>\s*<option value="urgent-first">Needs you on top \(default\)<\/option>\s*<option value="urgent-last">/
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Port: none (pure static analysis, runs in CI, no browser/server).
|
||||
//
|
||||
// `showTileGridButton` (the header Tiles button, and the Ctrl+Shift+G chord) is
|
||||
// a PER-DEVICE setting, default OFF, with the same three-way rule as
|
||||
// a PER-DEVICE setting (default ON on desktop, OFF on handhelds) with the same three-way rule as
|
||||
// showSplitButton (test/split-pane-per-device-setting.test.ts, read it for the
|
||||
// history): in settings-ui.js's displayKeys merge policy, stripped out of the
|
||||
// object saveAppSettings() PUTs (SettingsUpdateSchema is .strict(), so sending
|
||||
@@ -38,10 +38,16 @@ describe('showTileGridButton stays per-device: display key, stripped from the PU
|
||||
expect(schemas).not.toContain('showTileGridButton');
|
||||
});
|
||||
|
||||
it('defaults OFF on handhelds (and reads as OFF when absent everywhere)', () => {
|
||||
it('defaults ON on desktop (an absent key reads as ON) and OFF on handhelds', () => {
|
||||
// Owner's pick on the 1.36.0 beta: the Tiles button ships visible on desktop.
|
||||
// Handhelds keep it OFF in their defaults object, and the button never shows
|
||||
// below 1180px wide anyway.
|
||||
expect(settingsUi).toMatch(/showSplitButton: false,\s*showTileGridButton: false,/);
|
||||
expect(settingsUi).toContain(
|
||||
"document.getElementById('appSettingsShowTileGridButton').checked = settings.showTileGridButton ?? defaults.showTileGridButton ?? false;"
|
||||
"document.getElementById('appSettingsShowTileGridButton').checked = settings.showTileGridButton ?? defaults.showTileGridButton ?? true;"
|
||||
);
|
||||
expect(settingsUi).toContain(
|
||||
'const showTileGridButton = settings.showTileGridButton ?? defaults.showTileGridButton ?? true;'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -51,6 +51,11 @@ const withTilesSetting = (app: GridApp) => {
|
||||
app.loadAppSettingsFromStorage = () => ({ showTileGridButton: true });
|
||||
return app;
|
||||
};
|
||||
/** The per-device Tiles setting explicitly off on this device. */
|
||||
const withTilesSettingOff = (app: GridApp) => {
|
||||
app.loadAppSettingsFromStorage = () => ({ showTileGridButton: false });
|
||||
return app;
|
||||
};
|
||||
const RIGHT = { key: 'ArrowRight', code: 'ArrowRight', altKey: true, shiftKey: true };
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -88,13 +93,27 @@ describe('when a chord applies', () => {
|
||||
expect(app.tileShortcutFor(chord(TOGGLE))).toBe('toggle-tile-grid');
|
||||
});
|
||||
|
||||
it('with the Tiles setting off (the default) the toggle is inert, like an unbound key', () => {
|
||||
const app = makeGridApp(IDS);
|
||||
it('with the Tiles setting off the toggle is inert, like an unbound key', () => {
|
||||
const app = withTilesSettingOff(makeGridApp(IDS));
|
||||
expect(app.tileShortcutFor(chord(TOGGLE))).toBeNull();
|
||||
});
|
||||
|
||||
it('with no stored value the device default decides: ON on desktop, OFF on a handheld', () => {
|
||||
// Desktop: nothing stored and no device default, so the chord follows the
|
||||
// button's own `?? true` (settings-ui.js) and applies.
|
||||
const desktop = makeGridApp(IDS);
|
||||
desktop.loadAppSettingsFromStorage = () => ({});
|
||||
desktop.getDefaultSettings = () => ({});
|
||||
expect(desktop.tileShortcutFor(chord(TOGGLE))).toBe('toggle-tile-grid');
|
||||
// Handheld: the device defaults say OFF, so the chord stays inert.
|
||||
const handheld = makeGridApp(IDS);
|
||||
handheld.loadAppSettingsFromStorage = () => ({});
|
||||
handheld.getDefaultSettings = () => ({ showTileGridButton: false });
|
||||
expect(handheld.tileShortcutFor(chord(TOGGLE))).toBeNull();
|
||||
});
|
||||
|
||||
it('with the setting off, a grid opened another way (Ctrl+click, a drop) still has its chords, toggle included', () => {
|
||||
const app = makeGridApp(IDS);
|
||||
const app = withTilesSettingOff(makeGridApp(IDS));
|
||||
app.openTileGrid(IDS);
|
||||
expect(app.tileShortcutFor(chord(RIGHT))).toBe('focus-tile-right');
|
||||
expect(app.tileShortcutFor(chord(TOGGLE))).toBe('toggle-tile-grid');
|
||||
@@ -176,7 +195,7 @@ describe('the capture-phase handler', () => {
|
||||
}
|
||||
|
||||
it('setting off: Ctrl+Shift+G is left alone (no preventDefault), the grid stays closed', () => {
|
||||
const app = makeGridApp(IDS);
|
||||
const app = withTilesSettingOff(makeGridApp(IDS));
|
||||
const onKeydown = handlerFor(app);
|
||||
const e = chord(TOGGLE);
|
||||
onKeydown(e);
|
||||
|
||||
Reference in New Issue
Block a user