Archived
feat: make desktop navigation sections collapsible
This commit is contained in:
committed by
Federico Jaramillo Martinez
parent
f7eff88e00
commit
9a3f2ce64f
@@ -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"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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[] {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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; }
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user