feat: make desktop navigation sections collapsible

This commit is contained in:
Federico Jaramillo Martinez
2026-06-06 00:12:38 +02:00
committed by Federico Jaramillo Martinez
parent f7eff88e00
commit 9a3f2ce64f
9 changed files with 141 additions and 42 deletions
@@ -0,0 +1,5 @@
---
"@jmfederico/pi-web": patch
---
Make navigation sections collapsible on desktop and auto-collapse completed context sections after selections.
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { defaultNavigationSection, expandedNavigationSection, isNavigationSectionCollapsed, toggleNavigationSection } from "./navigationState"; import { collapsedNavigationSectionsAfterSelection, defaultNavigationSection, expandedNavigationSection, isNavigationSectionCollapsed, toggleCollapsedNavigationSection, toggleNavigationSection } from "./navigationState";
describe("navigationState", () => { describe("navigationState", () => {
it("defaults to the first incomplete selection section", () => { it("defaults to the first incomplete selection section", () => {
@@ -16,15 +16,22 @@ describe("navigationState", () => {
expect(expandedNavigationSection("none", state)).toBeUndefined(); expect(expandedNavigationSection("none", state)).toBeUndefined();
}); });
it("only collapses sections in mobile navigation layouts", () => { it("uses the mobile accordion state on mobile layouts", () => {
const state = { selectedProject: {}, selectedWorkspace: {} }; const state = { selectedProject: {}, selectedWorkspace: {} };
expect(isNavigationSectionCollapsed("projects", { isMobileLayout: false, expanded: "sessions", state })).toBe(false);
expect(isNavigationSectionCollapsed("projects", { isMobileLayout: true, expanded: "sessions", state })).toBe(true); expect(isNavigationSectionCollapsed("projects", { isMobileLayout: true, expanded: "sessions", state })).toBe(true);
expect(isNavigationSectionCollapsed("sessions", { isMobileLayout: true, expanded: "sessions", state })).toBe(false); expect(isNavigationSectionCollapsed("sessions", { isMobileLayout: true, expanded: "sessions", state })).toBe(false);
}); });
it("toggles the effective section, including the implicit default section", () => { it("uses independent collapsed sections on desktop layouts", () => {
const state = { selectedProject: {}, selectedWorkspace: {} };
expect(isNavigationSectionCollapsed("projects", { isMobileLayout: false, expanded: "sessions", state })).toBe(false);
expect(isNavigationSectionCollapsed("projects", { isMobileLayout: false, expanded: "sessions", state, collapsedSections: ["projects"] })).toBe(true);
expect(isNavigationSectionCollapsed("sessions", { isMobileLayout: false, expanded: "sessions", state, collapsedSections: ["projects"] })).toBe(false);
});
it("toggles the effective mobile section, including the implicit default section", () => {
const state = { selectedProject: undefined, selectedWorkspace: undefined }; const state = { selectedProject: undefined, selectedWorkspace: undefined };
expect(toggleNavigationSection(undefined, "projects", { isMobileLayout: true, state })).toBe("none"); expect(toggleNavigationSection(undefined, "projects", { isMobileLayout: true, state })).toBe("none");
@@ -37,4 +44,17 @@ describe("navigationState", () => {
expect(toggleNavigationSection("projects", "projects", { isMobileLayout: false, state })).toBe("projects"); expect(toggleNavigationSection("projects", "projects", { isMobileLayout: false, state })).toBe("projects");
}); });
it("toggles desktop sections independently", () => {
expect(toggleCollapsedNavigationSection([], "projects")).toEqual(["projects"]);
expect(toggleCollapsedNavigationSection(["machines", "projects"], "projects")).toEqual(["machines"]);
expect(toggleCollapsedNavigationSection(["sessions"], "machines")).toEqual(["machines", "sessions"]);
});
it("collapses completed desktop sections and expands the next section after selection", () => {
expect(collapsedNavigationSectionsAfterSelection([], "machines")).toEqual(["machines"]);
expect(collapsedNavigationSectionsAfterSelection(["workspaces"], "projects")).toEqual(["machines", "projects"]);
expect(collapsedNavigationSectionsAfterSelection([], "workspaces")).toEqual(["machines", "projects", "workspaces"]);
expect(collapsedNavigationSectionsAfterSelection(["sessions"], "sessions")).toEqual(["machines", "projects", "workspaces"]);
});
}); });
+65 -6
View File
@@ -1,6 +1,7 @@
import type { ReactiveController, ReactiveControllerHost } from "lit"; import type { ReactiveController, ReactiveControllerHost } from "lit";
export type NavigationSection = "machines" | "projects" | "workspaces" | "sessions"; export const NAVIGATION_SECTION_ORDER = ["machines", "projects", "workspaces", "sessions"] as const;
export type NavigationSection = (typeof NAVIGATION_SECTION_ORDER)[number];
export type ExpandedNavigationSection = NavigationSection | "none" | undefined; export type ExpandedNavigationSection = NavigationSection | "none" | undefined;
export interface NavigationSelectionState { export interface NavigationSelectionState {
@@ -19,8 +20,9 @@ export function expandedNavigationSection(expanded: ExpandedNavigationSection, s
return expanded ?? defaultNavigationSection(state); return expanded ?? defaultNavigationSection(state);
} }
export function isNavigationSectionCollapsed(section: NavigationSection, options: { isMobileLayout: boolean; expanded: ExpandedNavigationSection; state: NavigationSelectionState }): boolean { export function isNavigationSectionCollapsed(section: NavigationSection, options: { isMobileLayout: boolean; expanded: ExpandedNavigationSection; state: NavigationSelectionState; collapsedSections?: readonly NavigationSection[] | undefined }): boolean {
return options.isMobileLayout && expandedNavigationSection(options.expanded, options.state) !== section; if (options.isMobileLayout) return expandedNavigationSection(options.expanded, options.state) !== section;
return options.collapsedSections?.includes(section) ?? false;
} }
export function toggleNavigationSection(expanded: ExpandedNavigationSection, section: NavigationSection, options: { isMobileLayout: boolean; state: NavigationSelectionState }): ExpandedNavigationSection { export function toggleNavigationSection(expanded: ExpandedNavigationSection, section: NavigationSection, options: { isMobileLayout: boolean; state: NavigationSelectionState }): ExpandedNavigationSection {
@@ -32,8 +34,32 @@ export function expandNavigationSection(expanded: ExpandedNavigationSection, sec
return isMobileLayout ? section : expanded; return isMobileLayout ? section : expanded;
} }
export class MobileNavigationController implements ReactiveController { export function toggleCollapsedNavigationSection(collapsedSections: readonly NavigationSection[], section: NavigationSection): NavigationSection[] {
const collapsed = new Set(collapsedSections);
if (collapsed.has(section)) collapsed.delete(section);
else collapsed.add(section);
return orderedNavigationSections(collapsed);
}
export function collapsedNavigationSectionsAfterSelection(collapsedSections: readonly NavigationSection[], selectedSection: NavigationSection): NavigationSection[] {
const selectedIndex = NAVIGATION_SECTION_ORDER.indexOf(selectedSection);
const collapsed = new Set(collapsedSections);
const collapseThroughIndex = selectedSection === "sessions" ? selectedIndex - 1 : selectedIndex;
for (const section of NAVIGATION_SECTION_ORDER.slice(0, collapseThroughIndex + 1)) collapsed.add(section);
const next = nextNavigationSection(selectedSection);
if (next !== undefined) collapsed.delete(next);
if (selectedSection === "sessions") collapsed.delete("sessions");
return orderedNavigationSections(collapsed);
}
export function nextNavigationSection(section: NavigationSection): NavigationSection | undefined {
return NAVIGATION_SECTION_ORDER[NAVIGATION_SECTION_ORDER.indexOf(section) + 1];
}
export class NavigationSectionsController implements ReactiveController {
private expanded: ExpandedNavigationSection; private expanded: ExpandedNavigationSection;
private collapsedSections: readonly NavigationSection[] = [];
hostConnected(): void { hostConnected(): void {
return; return;
@@ -56,15 +82,33 @@ export class MobileNavigationController implements ReactiveController {
isMobileLayout: this.isMobileLayout(), isMobileLayout: this.isMobileLayout(),
expanded: this.expanded, expanded: this.expanded,
state: this.getState(), state: this.getState(),
collapsedSections: this.collapsedSections,
}); });
} }
toggle(section: NavigationSection): void { toggle(section: NavigationSection): void {
this.setExpanded(toggleNavigationSection(this.expanded, section, { isMobileLayout: this.isMobileLayout(), state: this.getState() })); if (this.isMobileLayout()) {
this.setExpanded(toggleNavigationSection(this.expanded, section, { isMobileLayout: true, state: this.getState() }));
return;
}
this.setCollapsedSections(toggleCollapsedNavigationSection(this.collapsedSections, section));
} }
expand(section: NavigationSection): void { expand(section: NavigationSection): void {
this.setExpanded(expandNavigationSection(this.expanded, section, this.isMobileLayout())); if (this.isMobileLayout()) {
this.setExpanded(expandNavigationSection(this.expanded, section, true));
return;
}
this.setCollapsedSections(this.collapsedSections.filter((collapsedSection) => collapsedSection !== section));
}
advanceAfterSelection(section: NavigationSection): void {
if (this.isMobileLayout()) {
const next = nextNavigationSection(section);
if (next !== undefined) this.expand(next);
return;
}
this.setCollapsedSections(collapsedNavigationSectionsAfterSelection(this.collapsedSections, section));
} }
open(section: NavigationSection, openNavigationView: () => void): void { open(section: NavigationSection, openNavigationView: () => void): void {
@@ -78,4 +122,19 @@ export class MobileNavigationController implements ReactiveController {
this.expanded = expanded; this.expanded = expanded;
this.host.requestUpdate(); this.host.requestUpdate();
} }
private setCollapsedSections(collapsedSections: readonly NavigationSection[]): void {
if (navigationSectionListsEqual(this.collapsedSections, collapsedSections)) return;
this.collapsedSections = collapsedSections;
this.host.requestUpdate();
}
}
function orderedNavigationSections(sections: Iterable<NavigationSection>): NavigationSection[] {
const sectionSet = new Set(sections);
return NAVIGATION_SECTION_ORDER.filter((section) => sectionSet.has(section));
}
function navigationSectionListsEqual(first: readonly NavigationSection[], second: readonly NavigationSection[]): boolean {
return first.length === second.length && first.every((section, index) => section === second[index]);
} }
+1 -1
View File
@@ -107,7 +107,7 @@ export class MachineList extends LitElement {
if (!this.collapsible) return "Machines"; if (!this.collapsible) return "Machines";
const selectedSummary = this.selected?.name ?? "No machine selected"; const selectedSummary = this.selected?.name ?? "No machine selected";
const selectedTitle = this.selected?.baseUrl ?? selectedSummary; const selectedTitle = this.selected?.baseUrl ?? selectedSummary;
return html`<button class="section-toggle" aria-expanded=${String(!this.collapsed)} @click=${() => { this.onToggleCollapsed?.(); }}><span class="section-title"><span class="section-name">${this.collapsed ? "▸" : "▾"} Machines</span><small class="section-selected" title=${selectedTitle}>${selectedSummary}</small></span><small class="section-count">${this.machines.length}</small></button>`; return html`<button class="section-toggle" aria-expanded=${String(!this.collapsed)} @click=${() => { this.onToggleCollapsed?.(); }}><span class="section-title"><span class="section-name">${this.collapsed ? "▸" : "▾"} Machines</span>${this.collapsed ? html`<small class="section-selected" title=${selectedTitle}>${selectedSummary}</small>` : null}</span><small class="section-count">${this.machines.length}</small></button>`;
} }
private toggleMenu(machineId: string, target: EventTarget | null): void { private toggleMenu(machineId: string, target: EventTarget | null): void {
+28 -18
View File
@@ -27,7 +27,7 @@ import { loadExternalPlugins } from "../plugins/external";
import { PluginRegistry, installPluginRuntimeScope, installWorkspacePanelScope } from "../plugins/registry"; import { PluginRegistry, installPluginRuntimeScope, installWorkspacePanelScope } from "../plugins/registry";
import { queryNamespace, readNamespacedString, setNamespacedQueryKey } from "../namespacedQueryArgs"; import { queryNamespace, readNamespacedString, setNamespacedQueryKey } from "../namespacedQueryArgs";
import { AppShellController } from "../appShell/appShellController"; import { AppShellController } from "../appShell/appShellController";
import { MobileNavigationController, type NavigationSection } from "../appShell/navigationState"; import { NavigationSectionsController, type NavigationSection } from "../appShell/navigationState";
import { PanelCollapseController, mainViewClass } from "../appShell/panelCollapseController"; import { PanelCollapseController, mainViewClass } from "../appShell/panelCollapseController";
import { readRoute, writeRoute, type AppRoute } from "../route"; import { readRoute, writeRoute, type AppRoute } from "../route";
import { readSettingsSection, writeSettingsSection, type SettingsSection } from "../settingsRoute"; import { readSettingsSection, writeSettingsSection, type SettingsSection } from "../settingsRoute";
@@ -127,7 +127,7 @@ export class PiWebApp extends LitElement {
private readonly terminalSelection = new SessionStorageTerminalSelectionMemory(); private readonly terminalSelection = new SessionStorageTerminalSelectionMemory();
private readonly appShell = new AppShellController(this); private readonly appShell = new AppShellController(this);
private readonly panelCollapse = new PanelCollapseController(this); private readonly panelCollapse = new PanelCollapseController(this);
private readonly mobileNavigation = new MobileNavigationController( private readonly navigationSections = new NavigationSectionsController(
this, this,
() => this.state, () => this.state,
() => this.appShell.isMobileNavigationLayout, () => this.appShell.isMobileNavigationLayout,
@@ -777,10 +777,10 @@ export class PiWebApp extends LitElement {
.selectedMachine=${this.state.selectedMachine} .selectedMachine=${this.state.selectedMachine}
.machineStatuses=${this.state.machineStatuses} .machineStatuses=${this.state.machineStatuses}
.machineActivities=${this.state.machineActivities} .machineActivities=${this.state.machineActivities}
.machinesCollapsed=${this.mobileNavigation.isCollapsed("machines")} .machinesCollapsed=${this.navigationSections.isCollapsed("machines")}
.onToggleMachines=${() => { this.mobileNavigation.toggle("machines"); }} .onToggleMachines=${() => { this.navigationSections.toggle("machines"); }}
.onSelectMachine=${(machine: Machine) => this.withChatScrollTransition(async () => { .onSelectMachine=${(machine: Machine) => this.withChatScrollTransition(async () => {
this.mobileNavigation.expand("projects"); this.navigationSections.advanceAfterSelection("machines");
await this.selectMachineWithMemory(machine); await this.selectMachineWithMemory(machine);
})} })}
.onRemoveMachine=${(machine: Machine) => { void this.removeMachine(machine); }} .onRemoveMachine=${(machine: Machine) => { void this.removeMachine(machine); }}
@@ -796,32 +796,42 @@ export class PiWebApp extends LitElement {
.sessionActivities=${this.state.sessionActivities} .sessionActivities=${this.state.sessionActivities}
.selectedSession=${this.state.selectedSession} .selectedSession=${this.state.selectedSession}
.canStartSession=${!!this.state.selectedWorkspace} .canStartSession=${!!this.state.selectedWorkspace}
.collapsible=${this.appShell.isMobileNavigationLayout} .collapsible=${true}
.projectsCollapsed=${this.mobileNavigation.isCollapsed("projects")} .compact=${this.appShell.isMobileNavigationLayout}
.workspacesCollapsed=${this.mobileNavigation.isCollapsed("workspaces")} .projectsCollapsed=${this.navigationSections.isCollapsed("projects")}
.sessionsCollapsed=${this.mobileNavigation.isCollapsed("sessions")} .workspacesCollapsed=${this.navigationSections.isCollapsed("workspaces")}
.sessionsCollapsed=${this.navigationSections.isCollapsed("sessions")}
.workspaceLabelItems=${(workspace: Workspace) => this.workspaceLabelItems(workspace)} .workspaceLabelItems=${(workspace: Workspace) => this.workspaceLabelItems(workspace)}
.refreshControl=${this.appShell.shouldShowAppRefreshInHeader() ? this.renderAppRefresh() : undefined} .refreshControl=${this.appShell.shouldShowAppRefreshInHeader() ? this.renderAppRefresh() : undefined}
.onShowActions=${() => { this.setState({ actionPaletteOpen: true }); }} .onShowActions=${() => { this.setState({ actionPaletteOpen: true }); }}
.onToggleProjects=${() => { this.mobileNavigation.toggle("projects"); }} .onToggleProjects=${() => { this.navigationSections.toggle("projects"); }}
.onToggleWorkspaces=${() => { this.mobileNavigation.toggle("workspaces"); }} .onToggleWorkspaces=${() => { this.navigationSections.toggle("workspaces"); }}
.onToggleSessions=${() => { this.mobileNavigation.toggle("sessions"); }} .onToggleSessions=${() => { this.navigationSections.toggle("sessions"); }}
.onSelectProject=${(project: Project) => this.withChatScrollTransition(async () => { .onSelectProject=${(project: Project) => this.withChatScrollTransition(async () => {
this.mobileNavigation.expand("workspaces"); this.navigationSections.advanceAfterSelection("projects");
await this.workspaces.selectProject(project); await this.workspaces.selectProject(project);
})} })}
.onCloseProject=${(project: Project) => this.projects.closeProject(project.id)} .onCloseProject=${(project: Project) => this.projects.closeProject(project.id)}
.onSelectWorkspace=${(workspace: Workspace) => this.withChatScrollTransition(async () => { .onSelectWorkspace=${(workspace: Workspace) => this.withChatScrollTransition(async () => {
this.mobileNavigation.expand("sessions"); this.navigationSections.advanceAfterSelection("workspaces");
await this.workspaces.selectWorkspace(workspace); await this.workspaces.selectWorkspace(workspace);
})} })}
.onDeleteWorkspace=${(workspace: Workspace) => { void this.deleteWorkspace(workspace); }} .onDeleteWorkspace=${(workspace: Workspace) => { void this.deleteWorkspace(workspace); }}
.onArchivedCollapsed=${() => { this.sessions.clearSelectionAfterArchivedCollapse(); }} .onArchivedCollapsed=${() => { this.sessions.clearSelectionAfterArchivedCollapse(); }}
.onStartSession=${() => openChatAfter(() => this.sessions.startSession())} .onStartSession=${() => openChatAfter(() => {
.onSelectSession=${(session: SessionInfo) => openChatAfter(() => this.sessions.selectSession(session))} this.navigationSections.advanceAfterSelection("sessions");
return this.sessions.startSession();
})}
.onSelectSession=${(session: SessionInfo) => openChatAfter(() => {
this.navigationSections.advanceAfterSelection("sessions");
return this.sessions.selectSession(session);
})}
.onArchiveSession=${(session: SessionInfo) => this.sessions.archiveSession(session)} .onArchiveSession=${(session: SessionInfo) => this.sessions.archiveSession(session)}
.onArchiveSessionWithDescendants=${(session: SessionInfo) => this.sessions.archiveSessionWithDescendants(session)} .onArchiveSessionWithDescendants=${(session: SessionInfo) => this.sessions.archiveSessionWithDescendants(session)}
.onRestoreSession=${(session: SessionInfo) => openChatAfter(() => this.sessions.restoreSession(session))} .onRestoreSession=${(session: SessionInfo) => openChatAfter(() => {
this.navigationSections.advanceAfterSelection("sessions");
return this.sessions.restoreSession(session);
})}
.onDeleteCachedNewSession=${(session: SessionInfo) => this.sessions.deleteCachedNewSession(session)} .onDeleteCachedNewSession=${(session: SessionInfo) => this.sessions.deleteCachedNewSession(session)}
.onDetachParentSession=${(session: SessionInfo) => this.sessions.detachParent(session)} .onDetachParentSession=${(session: SessionInfo) => this.sessions.detachParent(session)}
></app-navigation-panel> ></app-navigation-panel>
@@ -829,7 +839,7 @@ export class PiWebApp extends LitElement {
} }
private openNavigationSection(section: NavigationSection): void { private openNavigationSection(section: NavigationSection): void {
this.mobileNavigation.open(section, () => { this.selectMainView("navigation"); }); this.navigationSections.open(section, () => { this.selectMainView("navigation"); });
} }
private visibleWorkspacePanels(): QualifiedWorkspacePanelContribution[] { private visibleWorkspacePanels(): QualifiedWorkspacePanelContribution[] {
+1 -1
View File
@@ -77,7 +77,7 @@ export class ProjectList extends LitElement {
if (!this.collapsible) return "Projects"; if (!this.collapsible) return "Projects";
const selectedSummary = this.selected?.name ?? "No project selected"; const selectedSummary = this.selected?.name ?? "No project selected";
const selectedTitle = this.selected?.path ?? selectedSummary; const selectedTitle = this.selected?.path ?? selectedSummary;
return html`<button class="section-toggle" aria-expanded=${String(!this.collapsed)} @click=${() => { this.onToggleCollapsed?.(); }}><span class="section-title"><span class="section-name">${this.collapsed ? "▸" : "▾"} Projects</span><small class="section-selected" title=${selectedTitle}>${selectedSummary}</small></span><small class="section-count">${this.projects.length}</small></button>`; return html`<button class="section-toggle" aria-expanded=${String(!this.collapsed)} @click=${() => { this.onToggleCollapsed?.(); }}><span class="section-title"><span class="section-name">${this.collapsed ? "▸" : "▾"} Projects</span>${this.collapsed ? html`<small class="section-selected" title=${selectedTitle}>${selectedSummary}</small>` : null}</span><small class="section-count">${this.projects.length}</small></button>`;
} }
private renderActivity(project: Project) { private renderActivity(project: Project) {
+1 -1
View File
@@ -95,7 +95,7 @@ export class SessionList extends LitElement {
const selectedTitle = this.selected?.path ?? selectedSummary; const selectedTitle = this.selected?.path ?? selectedSummary;
return html` return html`
<h2> <h2>
<button class="section-toggle" aria-expanded=${String(!this.collapsed)} @click=${() => { this.onToggleCollapsed?.(); }}><span class="section-title"><span class="section-name">${this.collapsed ? "▸" : "▾"} Sessions</span><small class="section-selected" title=${selectedTitle}>${selectedSummary}</small></span><small class="section-count">${sessionCount}</small></button> <button class="section-toggle" aria-expanded=${String(!this.collapsed)} @click=${() => { this.onToggleCollapsed?.(); }}><span class="section-title"><span class="section-name">${this.collapsed ? "▸" : "▾"} Sessions</span>${this.collapsed ? html`<small class="section-selected" title=${selectedTitle}>${selectedSummary}</small>` : null}</span><small class="section-count">${sessionCount}</small></button>
<button ?disabled=${!this.canStart} @click=${(event: MouseEvent) => { event.stopPropagation(); this.onStart?.(); }}>+</button> <button ?disabled=${!this.canStart} @click=${(event: MouseEvent) => { event.stopPropagation(); this.onStart?.(); }}>+</button>
</h2> </h2>
`; `;
+1 -1
View File
@@ -79,7 +79,7 @@ export class WorkspaceList extends LitElement {
if (!this.collapsible) return "Workspaces"; if (!this.collapsible) return "Workspaces";
const selectedSummary = this.selected === undefined ? "No workspace selected" : `${this.selected.label}${this.selected.isMain ? " · main" : ""} · ${this.selected.path}`; const selectedSummary = this.selected === undefined ? "No workspace selected" : `${this.selected.label}${this.selected.isMain ? " · main" : ""} · ${this.selected.path}`;
const selectedTitle = this.selected?.path ?? selectedSummary; const selectedTitle = this.selected?.path ?? selectedSummary;
return html`<button class="section-toggle" aria-expanded=${String(!this.collapsed)} @click=${() => { this.onToggleCollapsed?.(); }}><span class="section-title"><span class="section-name">${this.collapsed ? "▸" : "▾"} Workspaces</span><small class="section-selected" title=${selectedTitle}>${selectedSummary}</small></span><small class="section-count">${this.workspaces.length}</small></button>`; return html`<button class="section-toggle" aria-expanded=${String(!this.collapsed)} @click=${() => { this.onToggleCollapsed?.(); }}><span class="section-title"><span class="section-name">${this.collapsed ? "▸" : "▾"} Workspaces</span>${this.collapsed ? html`<small class="section-selected" title=${selectedTitle}>${selectedSummary}</small>` : null}</span><small class="section-count">${this.workspaces.length}</small></button>`;
} }
private renderActivity(workspace: Workspace): TemplateResult | undefined { private renderActivity(workspace: Workspace): TemplateResult | undefined {
@@ -27,6 +27,7 @@ export class AppNavigationPanel extends LitElement {
@property({ attribute: false }) workspaceLabelItems: (workspace: Workspace) => WorkspaceLabelItem[] = () => []; @property({ attribute: false }) workspaceLabelItems: (workspace: Workspace) => WorkspaceLabelItem[] = () => [];
@property({ attribute: false }) refreshControl: unknown; @property({ attribute: false }) refreshControl: unknown;
@property({ type: Boolean, reflect: true }) collapsible = false; @property({ type: Boolean, reflect: true }) collapsible = false;
@property({ type: Boolean, reflect: true }) compact = false;
@property({ type: Boolean }) machinesCollapsed = false; @property({ type: Boolean }) machinesCollapsed = false;
@property({ type: Boolean }) projectsCollapsed = false; @property({ type: Boolean }) projectsCollapsed = false;
@property({ type: Boolean }) workspacesCollapsed = false; @property({ type: Boolean }) workspacesCollapsed = false;
@@ -120,20 +121,24 @@ export class AppNavigationPanel extends LitElement {
static override styles = css` static override styles = css`
:host { display: flex; flex-direction: column; min-height: 0; overflow: hidden; } :host { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
:host([collapsible]) { flex: 1 1 auto; } :host([compact]) { flex: 1 1 auto; }
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); }
:host([collapsible]) header { display: none; } :host([compact]) header { display: none; }
.header-actions { display: flex; align-items: center; gap: 8px; } .header-actions { display: flex; align-items: center; gap: 8px; }
machine-list, project-list, workspace-list { flex: 0 0 auto; max-height: 26%; min-height: 0; overflow: hidden; border-bottom: 1px solid var(--pi-border-muted); } machine-list, project-list, workspace-list { flex: 0 0 auto; max-height: 26%; min-height: 0; overflow: hidden; border-bottom: 1px solid var(--pi-border-muted); }
session-list { flex: 1 1 auto; min-height: 0; overflow: hidden; } session-list { flex: 1 1 auto; min-height: 0; overflow: hidden; }
:host([collapsible]) machine-list, machine-list[collapsed],
:host([collapsible]) project-list, project-list[collapsed],
:host([collapsible]) workspace-list, workspace-list[collapsed],
:host([collapsible]) session-list { flex: 1 1 auto; max-height: none; min-height: 0; overflow: hidden; } session-list[collapsed] { flex: 0 0 auto; min-height: auto; overflow: hidden; }
:host([collapsible]) machine-list[collapsed], :host([compact]) machine-list,
:host([collapsible]) project-list[collapsed], :host([compact]) project-list,
:host([collapsible]) workspace-list[collapsed], :host([compact]) workspace-list,
:host([collapsible]) session-list[collapsed] { flex: 0 0 auto; min-height: auto; overflow: hidden; } :host([compact]) session-list { flex: 1 1 auto; max-height: none; min-height: 0; overflow: hidden; }
:host([compact]) machine-list[collapsed],
:host([compact]) project-list[collapsed],
:host([compact]) workspace-list[collapsed],
:host([compact]) session-list[collapsed] { flex: 0 0 auto; min-height: auto; overflow: hidden; }
button { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); color: var(--pi-text); padding: 7px 9px; cursor: pointer; } button { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); color: var(--pi-text); padding: 7px 9px; cursor: pointer; }
`; `;
} }