Files
openchamber/packages/ui/src/lib/surfaces/registry.ts
T

190 lines
5.1 KiB
TypeScript

import type { IconName } from '@/components/icon/icons';
import type { I18nKey } from '@/lib/i18n';
import type { ContextPanelMode } from '@/stores/useUIStore';
export type ContextSurfaceId =
| 'editor'
| 'git'
| 'pr'
| 'diff'
| 'walkthrough'
| 'terminal'
| 'plan'
| 'notes'
| 'context'
| 'browser'
| 'preview'
| 'chat';
export type ContextSurfaceDescriptor = {
id: ContextSurfaceId;
/** The context panel tab mode this surface activates. 1:1 in the current model. */
mode: ContextPanelMode;
icon: IconName;
labelKey: I18nKey;
/**
* 'always' surfaces can be opened empty from the rail.
* 'has-content' surfaces are content-driven: they need an existing tab of
* their mode (a preview URL emitted, a split session) and stay hidden on
* the rail until one exists.
*/
availability: 'always' | 'has-content';
/** Short tooltip explanation shown on the rail. */
descriptionKey: I18nKey;
/**
* Default panel width as a fraction of the available content area, used
* until the user manually resizes this surface.
*/
defaultWidthFraction: number;
};
export const CONTEXT_SURFACES: readonly ContextSurfaceDescriptor[] = [
{
id: 'context',
descriptionKey: 'contextRail.surface.context.description',
defaultWidthFraction: 0.45,
mode: 'context',
icon: 'donut-chart-fill',
labelKey: 'contextPanel.mode.context',
availability: 'always',
},
{
id: 'git',
descriptionKey: 'contextRail.surface.git.description',
defaultWidthFraction: 2 / 5,
mode: 'git',
icon: 'git-branch',
labelKey: 'layout.rightSidebar.git',
availability: 'always',
},
{
id: 'pr',
descriptionKey: 'contextRail.surface.pr.description',
defaultWidthFraction: 0.45,
mode: 'pr',
icon: 'github',
labelKey: 'contextPanel.mode.pr',
availability: 'always',
},
{
id: 'diff',
descriptionKey: 'contextRail.surface.diff.description',
defaultWidthFraction: 3 / 5,
mode: 'diff',
icon: 'arrow-left-right',
labelKey: 'contextPanel.mode.diff',
availability: 'always',
},
{
id: 'walkthrough',
descriptionKey: 'contextRail.surface.walkthrough.description',
defaultWidthFraction: 3 / 5,
mode: 'walkthrough',
icon: 'route',
labelKey: 'contextPanel.mode.walkthrough',
availability: 'always',
},
{
id: 'editor',
descriptionKey: 'contextRail.surface.editor.description',
defaultWidthFraction: 3 / 5,
mode: 'file',
icon: 'braces',
labelKey: 'contextPanel.mode.files',
availability: 'always',
},
{
id: 'terminal',
descriptionKey: 'contextRail.surface.terminal.description',
defaultWidthFraction: 3 / 5,
mode: 'terminal',
icon: 'terminal-box',
labelKey: 'layout.mainTab.terminal',
availability: 'always',
},
{
id: 'notes',
descriptionKey: 'contextRail.surface.notes.description',
defaultWidthFraction: 1 / 3,
mode: 'notes',
icon: 'sticky-note',
labelKey: 'contextRail.surface.notes',
availability: 'always',
},
{
id: 'plan',
descriptionKey: 'contextRail.surface.plan.description',
defaultWidthFraction: 0.45,
mode: 'plan',
icon: 'file-text',
labelKey: 'contextPanel.mode.plan',
availability: 'always',
},
{
id: 'browser',
descriptionKey: 'contextRail.surface.browser.description',
defaultWidthFraction: 0.45,
mode: 'browser',
icon: 'global',
labelKey: 'contextPanel.mode.browser',
availability: 'always',
},
{
id: 'preview',
descriptionKey: 'contextRail.surface.preview.description',
defaultWidthFraction: 0.45,
mode: 'preview',
icon: 'window',
labelKey: 'contextPanel.mode.preview',
availability: 'has-content',
},
{
id: 'chat',
descriptionKey: 'contextRail.surface.chat.description',
defaultWidthFraction: 0.45,
mode: 'chat',
icon: 'chat-4',
labelKey: 'contextPanel.mode.chat',
availability: 'has-content',
},
];
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]));
export const getContextSurfaceWidthFraction = (mode: ContextPanelMode): number => {
return FRACTION_BY_MODE.get(mode) ?? 1 / 2;
};
const isContextSurfaceId = (value: unknown): value is ContextSurfaceId => {
return typeof value === 'string' && SURFACE_BY_ID.has(value as ContextSurfaceId);
};
/**
* Applies a persisted user reorder on top of the default registry order:
* unknown ids are dropped, missing surfaces are appended in default order.
*/
export const sortContextSurfaces = (railOrder: readonly string[]): ContextSurfaceDescriptor[] => {
const ordered: ContextSurfaceDescriptor[] = [];
const seen = new Set<ContextSurfaceId>();
for (const id of railOrder) {
if (!isContextSurfaceId(id) || seen.has(id)) {
continue;
}
const surface = SURFACE_BY_ID.get(id);
if (surface) {
seen.add(id);
ordered.push(surface);
}
}
for (const surface of CONTEXT_SURFACES) {
if (!seen.has(surface.id)) {
ordered.push(surface);
}
}
return ordered;
};