feat(ui): numbered context-panel surface switching with configurable prefix
- Add switch_context_surface shortcut (default Cmd/Ctrl + 1..9, 0 for the 10th surface) that opens/closes/switches context panel rail surfaces by their visible order, configurable and persisted in Settings -> Shortcuts. - Show order-number badges on rail icons while the modifier is held >500ms; dismiss on release, blur, or a number press until the next press-and-hold. - Remove the legacy mod+2/3/4 (diff/terminal/git) and switch_tab_1..9 bindings so numbered surface switching goes only through the new mechanism. - Replace the help-dialog 'Switch Project' row with the surface-switch row and update the shortcuts footer/header icons to the command icon.
This commit is contained in:
@@ -20,6 +20,13 @@ edge (`components/layout/ContextPanelRail.tsx`) and rendered by
|
||||
- Rail order is user-reorderable and persisted globally in
|
||||
`useUIStore.contextRailOrder`; `sortContextSurfaces` applies it on top of the
|
||||
registry's default order and appends any missing surfaces.
|
||||
- `getVisibleContextRailSurfaces` is the single visibility filter shared by the
|
||||
rail and the global surface-switch shortcut (`switch_context_surface` in
|
||||
`lib/shortcuts.ts`): it drops the plan surface unless plan mode is enabled,
|
||||
drops the walkthrough on VS Code and below `WALKTHROUGH_MIN_WIDTH`, and hides
|
||||
`has-content` surfaces until a tab of their mode exists. Both consumers use
|
||||
it so the digit shown on a rail badge always maps to the same surface the
|
||||
shortcut opens.
|
||||
|
||||
## Adding a surface
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import {
|
||||
CONTEXT_SURFACES,
|
||||
getVisibleContextRailSurfaces,
|
||||
WALKTHROUGH_MIN_WIDTH,
|
||||
} from './registry';
|
||||
|
||||
const baseOptions = {
|
||||
railOrder: [],
|
||||
planModeEnabled: true,
|
||||
isVSCode: false,
|
||||
screenWidth: 1200,
|
||||
tabs: [],
|
||||
} as const;
|
||||
|
||||
describe('getVisibleContextRailSurfaces', () => {
|
||||
test('hides the plan surface while plan mode is disabled', () => {
|
||||
const surfaces = getVisibleContextRailSurfaces({ ...baseOptions, planModeEnabled: false });
|
||||
expect(surfaces.some((surface) => surface.id === 'plan')).toBe(false);
|
||||
expect(surfaces.some((surface) => surface.id === 'context')).toBe(true);
|
||||
});
|
||||
|
||||
test('shows the plan surface while plan mode is enabled', () => {
|
||||
const surfaces = getVisibleContextRailSurfaces({ ...baseOptions, planModeEnabled: true });
|
||||
expect(surfaces.some((surface) => surface.id === 'plan')).toBe(true);
|
||||
});
|
||||
|
||||
test('hides the walkthrough on VS Code and below the min width', () => {
|
||||
expect(getVisibleContextRailSurfaces({ ...baseOptions, isVSCode: true }).some((s) => s.id === 'walkthrough')).toBe(false);
|
||||
expect(
|
||||
getVisibleContextRailSurfaces({ ...baseOptions, screenWidth: WALKTHROUGH_MIN_WIDTH - 1 }).some((s) => s.id === 'walkthrough'),
|
||||
).toBe(false);
|
||||
expect(
|
||||
getVisibleContextRailSurfaces({ ...baseOptions, screenWidth: WALKTHROUGH_MIN_WIDTH }).some((s) => s.id === 'walkthrough'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('hides content-driven surfaces until a matching tab exists', () => {
|
||||
const preview = CONTEXT_SURFACES.find((surface) => surface.id === 'preview');
|
||||
if (!preview) {
|
||||
throw new Error('preview surface missing from registry');
|
||||
}
|
||||
expect(preview.availability).toBe('has-content');
|
||||
expect(getVisibleContextRailSurfaces(baseOptions).some((s) => s.id === 'preview')).toBe(false);
|
||||
expect(getVisibleContextRailSurfaces({ ...baseOptions, tabs: [{ mode: preview.mode }] }).some((s) => s.id === 'preview')).toBe(true);
|
||||
});
|
||||
|
||||
test('respects the persisted user rail order', () => {
|
||||
const surfaces = getVisibleContextRailSurfaces({ ...baseOptions, railOrder: ['git', 'context'] });
|
||||
expect(surfaces.slice(0, 2).map((surface) => surface.id)).toEqual(['git', 'context']);
|
||||
});
|
||||
});
|
||||
@@ -152,6 +152,10 @@ export const CONTEXT_SURFACES: readonly ContextSurfaceDescriptor[] = [
|
||||
const SURFACE_BY_ID = new Map(CONTEXT_SURFACES.map((surface) => [surface.id, surface]));
|
||||
const FRACTION_BY_MODE = new Map(CONTEXT_SURFACES.map((surface) => [surface.mode, surface.defaultWidthFraction]));
|
||||
|
||||
// Tablet width and up: below this the walkthrough cannot show a stop and its
|
||||
// code side by side, which is the whole point of the surface.
|
||||
export const WALKTHROUGH_MIN_WIDTH = 768;
|
||||
|
||||
export const getContextSurfaceWidthFraction = (mode: ContextPanelMode): number => {
|
||||
return FRACTION_BY_MODE.get(mode) ?? 1 / 2;
|
||||
};
|
||||
@@ -187,3 +191,36 @@ export const sortContextSurfaces = (railOrder: readonly string[]): ContextSurfac
|
||||
|
||||
return ordered;
|
||||
};
|
||||
|
||||
type VisibleRailSurfacesOptions = {
|
||||
railOrder: readonly string[];
|
||||
planModeEnabled: boolean;
|
||||
isVSCode: boolean;
|
||||
screenWidth: number;
|
||||
tabs: readonly { mode: ContextPanelMode }[];
|
||||
};
|
||||
|
||||
/**
|
||||
* The context panel rail's visible, user-ordered surfaces. Shared by the rail
|
||||
* (for rendering and number badges) and the global surface-switch shortcut so
|
||||
* both agree on which surface each digit maps to.
|
||||
*
|
||||
* Content-driven surfaces are hidden (not disabled) until content exists; an
|
||||
* existing tab keeps them visible even if the content source went away.
|
||||
*/
|
||||
export const getVisibleContextRailSurfaces = (options: VisibleRailSurfacesOptions): ContextSurfaceDescriptor[] => {
|
||||
return sortContextSurfaces(options.railOrder).filter((surface) => {
|
||||
if (surface.id === 'plan' && !options.planModeEnabled) {
|
||||
return false;
|
||||
}
|
||||
// The walkthrough needs room for a stop list beside real code, and its
|
||||
// diffs come from OpenChamber's Git routes, which VS Code does not serve.
|
||||
if (surface.id === 'walkthrough' && (options.isVSCode || options.screenWidth < WALKTHROUGH_MIN_WIDTH)) {
|
||||
return false;
|
||||
}
|
||||
if (surface.availability === 'has-content') {
|
||||
return options.tabs.some((tab) => tab.mode === surface.mode);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user