Archived
feat: add collapse/expand toggle for workspace panel
Adds a chevron-right collapse button after the last tab (Files, Git, Terminal, Info) in the workspace panel header. Clicking it hides the workspace panel and lets the main chat column fill the full width. When collapsed, an expand strip with a chevron-left button appears below the context bar to re-open the workspace panel. Changes: - Add workspacePanelCollapsed boolean to AppState - Add collapse button to WorkspacePanel header tabs - Add expand strip with toggle button in main column when collapsed - Adjust grid layout to hide panel and stretch main when collapsed - Handle collapsed state at ≤1180px responsive breakpoint
This commit is contained in:
@@ -45,6 +45,7 @@ export interface AppState {
|
|||||||
activeTerminalCount: number;
|
activeTerminalCount: number;
|
||||||
selectedTerminalId: string | undefined;
|
selectedTerminalId: string | undefined;
|
||||||
piWebStatus: PiWebStatusResponse | undefined;
|
piWebStatus: PiWebStatusResponse | undefined;
|
||||||
|
workspacePanelCollapsed: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +134,7 @@ export function initialAppState(): AppState {
|
|||||||
activeTerminalCount: 0,
|
activeTerminalCount: 0,
|
||||||
selectedTerminalId: undefined,
|
selectedTerminalId: undefined,
|
||||||
piWebStatus: undefined,
|
piWebStatus: undefined,
|
||||||
|
workspacePanelCollapsed: false,
|
||||||
error: "",
|
error: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -511,7 +511,11 @@ export class PiWebApp extends LitElement {
|
|||||||
const panelContext = workspace === undefined ? undefined : this.createWorkspacePanelContext(workspace);
|
const panelContext = workspace === undefined ? undefined : this.createWorkspacePanelContext(workspace);
|
||||||
const workspaceLabelItems = workspace === undefined ? [] : this.plugins.getWorkspaceLabelItems(this.state, workspace);
|
const workspaceLabelItems = workspace === undefined ? [] : this.plugins.getWorkspaceLabelItems(this.state, workspace);
|
||||||
const emptyState = workspace === undefined ? this.workspacePanelEmptyState() : undefined;
|
const emptyState = workspace === undefined ? this.workspacePanelEmptyState() : undefined;
|
||||||
return html`<workspace-panel .workspace=${workspace} .panelContext=${panelContext} .emptyState=${emptyState} .tool=${this.state.workspaceTool} .panels=${this.visibleWorkspacePanels()} .workspaceLabelItems=${workspaceLabelItems} .onSelectTool=${(tool: QualifiedContributionId) => { this.openWorkspaceTool(tool); }}></workspace-panel>`;
|
return html`<workspace-panel .workspace=${workspace} .panelContext=${panelContext} .emptyState=${emptyState} .tool=${this.state.workspaceTool} .panels=${this.visibleWorkspacePanels()} .workspaceLabelItems=${workspaceLabelItems} .collapsed=${this.state.workspacePanelCollapsed} .onSelectTool=${(tool: QualifiedContributionId) => { this.openWorkspaceTool(tool); }} .onToggleCollapse=${() => { this.toggleWorkspacePanelCollapse(); }}></workspace-panel>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleWorkspacePanelCollapse(): void {
|
||||||
|
this.setState({ workspacePanelCollapsed: !this.state.workspacePanelCollapsed });
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderNavigationPanel(autoSwitchToChat: boolean) {
|
private renderNavigationPanel(autoSwitchToChat: boolean) {
|
||||||
@@ -1192,10 +1196,11 @@ export class PiWebApp extends LitElement {
|
|||||||
override render() {
|
override render() {
|
||||||
const state = this.state;
|
const state = this.state;
|
||||||
return html`
|
return html`
|
||||||
<div class=${`shell ${state.mainView === "navigation" ? "navigation-view" : state.mainView === "chat" ? "chat-view" : "workspace-view"}`}>
|
<div class=${`shell ${state.mainView === "navigation" ? "navigation-view" : state.mainView === "chat" ? "chat-view" : "workspace-view"}${state.workspacePanelCollapsed ? " workspace-panel-collapsed" : ""}`}>
|
||||||
<aside>${this.isMobileNavigationLayout ? null : this.renderNavigationPanel(false)}</aside>
|
<aside>${this.isMobileNavigationLayout ? null : this.renderNavigationPanel(false)}</aside>
|
||||||
<main class=${state.mainView === "chat" ? "chat-view" : state.mainView === "navigation" ? "navigation-view" : "workspace-view"}>
|
<main class=${state.mainView === "chat" ? "chat-view" : state.mainView === "navigation" ? "navigation-view" : "workspace-view"}>
|
||||||
${this.renderContextBar()}
|
${this.renderContextBar()}
|
||||||
|
${state.workspacePanelCollapsed ? html`<div class="expand-panel-strip"><button class="expand-workspace-panel-button" title="Expand workspace panel" aria-label="Expand workspace panel" @click=${() => { this.toggleWorkspacePanelCollapse(); }}><svg class="expand-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15 18l-6-6 6-6"/></svg></button></div>` : null}
|
||||||
<div class=${this.mobileTabsFrameClass()}>
|
<div class=${this.mobileTabsFrameClass()}>
|
||||||
<div class="mobile-tabs" @scroll=${this.onMobileTabsScroll}>
|
<div class="mobile-tabs" @scroll=${this.onMobileTabsScroll}>
|
||||||
<button class=${state.mainView === "navigation" ? "mobile-navigation-tab selected" : "mobile-navigation-tab"} @click=${() => { this.selectMainView("navigation"); }}>Sessions</button>
|
<button class=${state.mainView === "navigation" ? "mobile-navigation-tab selected" : "mobile-navigation-tab"} @click=${() => { this.selectMainView("navigation"); }}>Sessions</button>
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ export class WorkspacePanel extends LitElement {
|
|||||||
@property({ attribute: false }) panels: QualifiedWorkspacePanelContribution[] = [];
|
@property({ attribute: false }) panels: QualifiedWorkspacePanelContribution[] = [];
|
||||||
@property({ attribute: false }) workspaceLabelItems: WorkspaceLabelItem[] = [];
|
@property({ attribute: false }) workspaceLabelItems: WorkspaceLabelItem[] = [];
|
||||||
@property({ type: Boolean }) hideToolTabs = false;
|
@property({ type: Boolean }) hideToolTabs = false;
|
||||||
|
@property({ type: Boolean }) collapsed = false;
|
||||||
@property({ attribute: false }) onSelectTool: (tool: QualifiedContributionId) => void = () => undefined;
|
@property({ attribute: false }) onSelectTool: (tool: QualifiedContributionId) => void = () => undefined;
|
||||||
|
@property({ attribute: false }) onToggleCollapse: () => void = () => undefined;
|
||||||
@query(".workspace-header-strip") private workspaceHeaderStrip?: HTMLElement | null;
|
@query(".workspace-header-strip") private workspaceHeaderStrip?: HTMLElement | null;
|
||||||
@state() private workspaceHeaderCanScrollLeft = false;
|
@state() private workspaceHeaderCanScrollLeft = false;
|
||||||
@state() private workspaceHeaderCanScrollRight = false;
|
@state() private workspaceHeaderCanScrollRight = false;
|
||||||
@@ -69,6 +71,7 @@ export class WorkspacePanel extends LitElement {
|
|||||||
${visiblePanels.map((panel) => html`
|
${visiblePanels.map((panel) => html`
|
||||||
<button class=${selectedPanel?.id === panel.id ? "selected" : ""} @click=${() => { this.onSelectTool(panel.id); }}>${this.renderPanelTitle(panel, context)}</button>
|
<button class=${selectedPanel?.id === panel.id ? "selected" : ""} @click=${() => { this.onSelectTool(panel.id); }}>${this.renderPanelTitle(panel, context)}</button>
|
||||||
`)}
|
`)}
|
||||||
|
<button class="collapse-button" title="Collapse panel" aria-label="Collapse panel" @click=${() => { this.onToggleCollapse(); }}>${this.renderCollapseIcon()}</button>
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
<small>${renderWorkspaceLabel(workspace.label, this.workspaceLabelItems, workspace.path)}</small>
|
<small>${renderWorkspaceLabel(workspace.label, this.workspaceLabelItems, workspace.path)}</small>
|
||||||
@@ -92,6 +95,10 @@ export class WorkspacePanel extends LitElement {
|
|||||||
return html`${panel.title} <span class="tab-badge">${badge}</span>`;
|
return html`${panel.title} <span class="tab-badge">${badge}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderCollapseIcon(): TemplateResult {
|
||||||
|
return html`<svg class="collapse-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 18l6-6-6-6"/></svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
private renderEmptyState(state: WorkspacePanelEmptyState): TemplateResult {
|
private renderEmptyState(state: WorkspacePanelEmptyState): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<section class="empty-state" role="status">
|
<section class="empty-state" role="status">
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export interface CompletionItem {
|
|||||||
export const appStyles = css`
|
export const appStyles = css`
|
||||||
:host { display: block; height: 100dvh; box-sizing: border-box; padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); color: var(--pi-text); background: var(--pi-bg); font: 14px system-ui, sans-serif; }
|
:host { display: block; height: 100dvh; box-sizing: border-box; padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); color: var(--pi-text); background: var(--pi-bg); font: 14px system-ui, sans-serif; }
|
||||||
.shell { display: grid; grid-template-columns: 340px minmax(420px, 1fr) minmax(360px, 42vw); height: 100%; min-height: 0; }
|
.shell { display: grid; grid-template-columns: 340px minmax(420px, 1fr) minmax(360px, 42vw); height: 100%; min-height: 0; }
|
||||||
|
.shell.workspace-panel-collapsed { grid-template-columns: 340px minmax(420px, 1fr); }
|
||||||
|
.shell.workspace-panel-collapsed > workspace-panel { display: none; }
|
||||||
aside { display: flex; flex-direction: column; min-height: 0; border-right: 1px solid var(--pi-border); overflow: hidden; }
|
aside { display: flex; flex-direction: column; min-height: 0; border-right: 1px solid var(--pi-border); overflow: hidden; }
|
||||||
header { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px; border-bottom: 1px solid var(--pi-border); }
|
header { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px; border-bottom: 1px solid var(--pi-border); }
|
||||||
.header-actions { display: flex; align-items: center; gap: 8px; }
|
.header-actions { display: flex; align-items: center; gap: 8px; }
|
||||||
@@ -93,6 +95,9 @@ export const appStyles = css`
|
|||||||
.mobile-navigation-tab, .mobile-navigation-panel { display: none; }
|
.mobile-navigation-tab, .mobile-navigation-panel { display: none; }
|
||||||
.mobile-tabs button.selected { border-color: var(--pi-accent); background: var(--pi-selection-bg); }
|
.mobile-tabs button.selected { border-color: var(--pi-accent); background: var(--pi-selection-bg); }
|
||||||
.tab-badge { display: inline-block; min-width: 14px; margin-left: 4px; border: 1px solid var(--pi-success-border); border-radius: 999px; background: var(--pi-success-surface); color: var(--pi-success); padding: 0 5px; font-size: 11px; line-height: 16px; text-align: center; }
|
.tab-badge { display: inline-block; min-width: 14px; margin-left: 4px; border: 1px solid var(--pi-success-border); border-radius: 999px; background: var(--pi-success-surface); color: var(--pi-success); padding: 0 5px; font-size: 11px; line-height: 16px; text-align: center; }
|
||||||
|
.expand-panel-strip { flex: 0 0 auto; display: flex; align-items: center; padding: 4px 8px; border-bottom: 1px solid var(--pi-border-muted); }
|
||||||
|
.expand-workspace-panel-button { display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; padding: 0; border-radius: 6px; }
|
||||||
|
.expand-workspace-panel-button .expand-icon { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
|
||||||
workspace-panel { min-width: 0; min-height: 0; border-left: 1px solid var(--pi-border); overflow: hidden; }
|
workspace-panel { min-width: 0; min-height: 0; border-left: 1px solid var(--pi-border); overflow: hidden; }
|
||||||
@media (max-width: 1180px) {
|
@media (max-width: 1180px) {
|
||||||
.shell { grid-template-columns: 340px minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
|
.shell { grid-template-columns: 340px minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
|
||||||
@@ -102,6 +107,8 @@ export const appStyles = css`
|
|||||||
.shell.workspace-view main { grid-row: 1; min-height: auto; }
|
.shell.workspace-view main { grid-row: 1; min-height: auto; }
|
||||||
.shell.workspace-view > workspace-panel { grid-column: 2; grid-row: 2; display: flex; border-left: 0; }
|
.shell.workspace-view > workspace-panel { grid-column: 2; grid-row: 2; display: flex; border-left: 0; }
|
||||||
.shell:not(.workspace-view) > workspace-panel { display: none; }
|
.shell:not(.workspace-view) > workspace-panel { display: none; }
|
||||||
|
.shell.workspace-panel-collapsed > workspace-panel { display: none; }
|
||||||
|
.shell.workspace-panel-collapsed .expand-panel-strip { display: flex; }
|
||||||
main.workspace-view chat-view, main.workspace-view prompt-editor, main.workspace-view status-bar,
|
main.workspace-view chat-view, main.workspace-view prompt-editor, main.workspace-view status-bar,
|
||||||
main.workspace-view .empty { display: none; }
|
main.workspace-view .empty { display: none; }
|
||||||
main.workspace-view { overflow: hidden; }
|
main.workspace-view { overflow: hidden; }
|
||||||
@@ -140,8 +147,10 @@ export const workspacePanelStyles = css`
|
|||||||
.workspace-header-scroll-frame::after { right: 0; background: linear-gradient(270deg, color-mix(in srgb, var(--pi-shadow-strong) 55%, transparent) 0%, transparent 100%); }
|
.workspace-header-scroll-frame::after { right: 0; background: linear-gradient(270deg, color-mix(in srgb, var(--pi-shadow-strong) 55%, transparent) 0%, transparent 100%); }
|
||||||
.workspace-header-scroll-frame.can-scroll-left::before, .workspace-header-scroll-frame.can-scroll-right::after { opacity: 1; }
|
.workspace-header-scroll-frame.can-scroll-left::before, .workspace-header-scroll-frame.can-scroll-right::after { opacity: 1; }
|
||||||
.workspace-header-strip { display: flex; justify-content: space-between; align-items: center; gap: 8px; min-width: 0; padding: 8px; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scrollbar-width: thin; }
|
.workspace-header-strip { display: flex; justify-content: space-between; align-items: center; gap: 8px; min-width: 0; padding: 8px; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scrollbar-width: thin; }
|
||||||
.tabs { flex: 0 0 auto; display: flex; gap: 6px; }
|
.tabs { flex: 0 0 auto; display: flex; gap: 6px; align-items: center; }
|
||||||
.tabs button { flex: 0 0 auto; white-space: nowrap; }
|
.tabs button { flex: 0 0 auto; white-space: nowrap; }
|
||||||
|
.collapse-button { display: inline-flex; align-items: center; justify-content: center; width: auto; height: auto; padding: 5px 7px; }
|
||||||
|
.collapse-icon { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
|
||||||
button { display: inline-flex; align-items: center; gap: 5px; border: 1px solid var(--pi-border); border-radius: 7px; background: var(--pi-surface); color: var(--pi-text); padding: 5px 7px; cursor: pointer; }
|
button { display: inline-flex; align-items: center; gap: 5px; border: 1px solid var(--pi-border); border-radius: 7px; background: var(--pi-surface); color: var(--pi-text); padding: 5px 7px; cursor: pointer; }
|
||||||
button.selected { border-color: var(--pi-accent); background: var(--pi-selection-bg); }
|
button.selected { border-color: var(--pi-accent); background: var(--pi-selection-bg); }
|
||||||
.tab-badge { display: inline-block; min-width: 14px; border: 1px solid var(--pi-success-border); border-radius: 999px; background: var(--pi-success-surface); color: var(--pi-success); padding: 0 5px; font-size: 11px; line-height: 16px; text-align: center; }
|
.tab-badge { display: inline-block; min-width: 14px; border: 1px solid var(--pi-success-border); border-radius: 999px; background: var(--pi-success-surface); color: var(--pi-success); padding: 0 5px; font-size: 11px; line-height: 16px; text-align: center; }
|
||||||
|
|||||||
Reference in New Issue
Block a user