Archived
feat: keep workspace tab icons visible
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@jmfederico/pi-web": patch
|
||||
---
|
||||
|
||||
Keep workspace tool tab icons visible in the desktop workspace panel and collapse tab names only in compact panel widths.
|
||||
@@ -10,6 +10,8 @@ export interface WorkspacePanelEmptyState {
|
||||
body?: string;
|
||||
}
|
||||
|
||||
type WorkspacePanelBadge = string | number | TemplateResult | undefined;
|
||||
|
||||
@customElement("workspace-panel")
|
||||
export class WorkspacePanel extends LitElement {
|
||||
@property({ attribute: false }) workspace: Workspace | undefined;
|
||||
@@ -66,9 +68,16 @@ export class WorkspacePanel extends LitElement {
|
||||
<div class="workspace-header-strip" @scroll=${this.onWorkspaceHeaderScroll}>
|
||||
${this.hideToolTabs ? null : html`
|
||||
<div class="tabs">
|
||||
${visiblePanels.map((panel) => html`
|
||||
<button class=${selectedPanel?.id === panel.id ? "selected" : ""} @click=${() => { this.onSelectTool(panel.id); }}>${this.renderPanelTitle(panel, context)}</button>
|
||||
`)}
|
||||
${visiblePanels.map((panel) => {
|
||||
const selected = selectedPanel?.id === panel.id;
|
||||
const badge = panel.badge?.(context);
|
||||
const ariaLabel = this.panelTabAriaLabel(panel, badge);
|
||||
return html`
|
||||
<button class=${this.panelTabClass(panel, selected)} title=${ariaLabel} aria-label=${ariaLabel} aria-pressed=${String(selected)} @click=${() => { this.onSelectTool(panel.id); }}>
|
||||
${this.renderPanelTabContent(panel, badge)}
|
||||
</button>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
<small>${renderWorkspaceLabel(workspace.label, this.workspaceLabelItems, workspace.path)}</small>
|
||||
@@ -86,10 +95,29 @@ export class WorkspacePanel extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderPanelTitle(panel: QualifiedWorkspacePanelContribution, context: WorkspacePanelContext): TemplateResult {
|
||||
const badge = panel.badge?.(context);
|
||||
if (badge === undefined || badge === "") return html`${panel.title}`;
|
||||
return html`${panel.title} <span class="tab-badge">${badge}</span>`;
|
||||
private panelTabClass(panel: QualifiedWorkspacePanelContribution, selected: boolean): string {
|
||||
return [
|
||||
...(panel.icon === undefined ? [] : ["icon-tab"]),
|
||||
...(selected ? ["selected"] : []),
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
private panelTabAriaLabel(panel: QualifiedWorkspacePanelContribution, badge: WorkspacePanelBadge): string {
|
||||
if (typeof badge !== "string" && typeof badge !== "number") return panel.title;
|
||||
const trimmedBadge = String(badge).trim();
|
||||
return trimmedBadge === "" ? panel.title : `${panel.title}, ${trimmedBadge}`;
|
||||
}
|
||||
|
||||
private renderPanelTabContent(panel: QualifiedWorkspacePanelContribution, badge: WorkspacePanelBadge): TemplateResult {
|
||||
return html`
|
||||
${panel.icon === undefined ? null : html`<span class="tab-custom-icon" aria-hidden="true">${panel.icon}</span>`}
|
||||
<span class="tab-label">${panel.title}</span>
|
||||
${this.isEmptyBadge(badge) ? null : html`<span class="tab-badge">${badge}</span>`}
|
||||
`;
|
||||
}
|
||||
|
||||
private isEmptyBadge(badge: WorkspacePanelBadge): boolean {
|
||||
return badge === undefined || badge === "";
|
||||
}
|
||||
|
||||
private renderEmptyState(state: WorkspacePanelEmptyState): TemplateResult {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { LitElement, css, html, svg, type TemplateResult } from "lit";
|
||||
import { LitElement, css, html, type TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import type { AppState } from "../../appState";
|
||||
import { renderAppTabIcon, type AppTabBuiltinIcon } from "../tabIcons";
|
||||
|
||||
export type AppMobileMainTabBuiltinIcon = "navigation" | "chat" | "files" | "git" | "terminal";
|
||||
export type AppMobileMainTabBuiltinIcon = AppTabBuiltinIcon;
|
||||
export type AppMobileMainTabIcon = AppMobileMainTabBuiltinIcon | TemplateResult;
|
||||
|
||||
export interface AppMobileMainTab {
|
||||
@@ -85,7 +86,7 @@ export class AppMobileMainTabs extends LitElement {
|
||||
private renderTabMark(tab: AppMobileMainTab, fallbackLabels: Map<AppState["mainView"], string>) {
|
||||
return tab.icon === undefined
|
||||
? html`<span class="tab-fallback" aria-hidden="true">${fallbackLabels.get(tab.id) ?? this.initialsLabel(tab.label)}</span>`
|
||||
: this.renderIcon(tab.icon);
|
||||
: renderAppTabIcon(tab.icon);
|
||||
}
|
||||
|
||||
private fallbackLabels(): Map<AppState["mainView"], string> {
|
||||
@@ -115,56 +116,6 @@ export class AppMobileMainTabs extends LitElement {
|
||||
return trimmed === "" ? "?" : trimmed;
|
||||
}
|
||||
|
||||
private renderIcon(icon: AppMobileMainTabIcon) {
|
||||
if (typeof icon !== "string") return html`<span class="tab-custom-icon" aria-hidden="true">${icon}</span>`;
|
||||
switch (icon) {
|
||||
case "navigation":
|
||||
return svg`
|
||||
<svg class="tab-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<circle cx="6" cy="7" r="1.5"></circle>
|
||||
<path d="M10 7h8"></path>
|
||||
<circle cx="6" cy="12" r="1.5"></circle>
|
||||
<path d="M10 12h8"></path>
|
||||
<circle cx="6" cy="17" r="1.5"></circle>
|
||||
<path d="M10 17h8"></path>
|
||||
</svg>
|
||||
`;
|
||||
case "chat":
|
||||
return svg`
|
||||
<svg class="tab-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<path d="M7 5h10a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3h-6l-5 4v-4H7a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3Z"></path>
|
||||
<path d="M8 9h8"></path>
|
||||
<path d="M8 13h5"></path>
|
||||
</svg>
|
||||
`;
|
||||
case "files":
|
||||
return svg`
|
||||
<svg class="tab-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<path d="M3 7a2 2 0 0 1 2-2h4l2 2.5h8a2 2 0 0 1 2 2V17a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"></path>
|
||||
</svg>
|
||||
`;
|
||||
case "git":
|
||||
return svg`
|
||||
<svg class="tab-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<circle cx="6" cy="6" r="2"></circle>
|
||||
<circle cx="18" cy="6" r="2"></circle>
|
||||
<circle cx="12" cy="18" r="2"></circle>
|
||||
<path d="M8 6h6"></path>
|
||||
<path d="M6 8v2a6 6 0 0 0 6 6"></path>
|
||||
<path d="M18 8v2a6 6 0 0 1-6 6"></path>
|
||||
</svg>
|
||||
`;
|
||||
case "terminal":
|
||||
return svg`
|
||||
<svg class="tab-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2"></rect>
|
||||
<path d="m7 10 3 3-3 3"></path>
|
||||
<path d="M12 16h5"></path>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
private observeMobileTabs(): void {
|
||||
const mobileTabs = this.mobileTabsElement();
|
||||
if (this.observedMobileTabs === mobileTabs) return;
|
||||
|
||||
@@ -159,7 +159,7 @@ export const appStyles = css`
|
||||
`;
|
||||
|
||||
export const workspacePanelStyles = css`
|
||||
:host { display: flex; flex-direction: column; min-height: 0; color: var(--pi-text); background: var(--pi-bg); font: 13px system-ui, sans-serif; }
|
||||
:host { display: flex; flex-direction: column; min-height: 0; color: var(--pi-text); background: var(--pi-bg); font: 13px system-ui, sans-serif; container-type: inline-size; }
|
||||
header { flex: 0 0 auto; min-width: 0; border-bottom: 1px solid var(--pi-border); }
|
||||
.workspace-header-scroll-frame { position: relative; min-width: 0; background: var(--pi-bg); }
|
||||
.workspace-header-scroll-frame::before, .workspace-header-scroll-frame::after { content: ""; position: absolute; top: 0; bottom: 0; z-index: 2; width: 18px; opacity: 0; pointer-events: none; transition: opacity .15s ease; }
|
||||
@@ -169,9 +169,18 @@ export const workspacePanelStyles = css`
|
||||
.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; align-items: center; }
|
||||
.tabs button { flex: 0 0 auto; white-space: nowrap; }
|
||||
.tabs button.icon-tab { min-width: 34px; }
|
||||
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); }
|
||||
.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-icon { flex: 0 0 auto; width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
|
||||
.tab-custom-icon { flex: 0 0 auto; width: 16px; height: 16px; display: inline-grid; place-items: center; color: currentColor; pointer-events: none; }
|
||||
.tab-custom-icon svg { width: 16px; height: 16px; pointer-events: none; }
|
||||
.tab-label { min-width: 0; }
|
||||
.tab-badge { flex: 0 0 auto; 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; }
|
||||
@container (max-width: 430px) {
|
||||
.tabs button.icon-tab { justify-content: center; padding-inline: 7px; }
|
||||
.tabs button.icon-tab .tab-label { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }
|
||||
}
|
||||
.panel-content { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: auto; }
|
||||
.empty-state { box-sizing: border-box; width: min(100%, 380px); margin: auto; padding: 24px; display: grid; gap: 8px; color: var(--pi-muted); text-align: center; }
|
||||
.empty-state h2 { margin: 0; color: var(--pi-text); font-size: 15px; line-height: 1.3; }
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { html, svg, type TemplateResult } from "lit";
|
||||
|
||||
export type AppTabBuiltinIcon = "navigation" | "chat" | "files" | "git" | "terminal";
|
||||
export type AppTabIcon = AppTabBuiltinIcon | TemplateResult;
|
||||
|
||||
export function renderAppTabIcon(icon: AppTabIcon): TemplateResult {
|
||||
if (typeof icon !== "string") return html`<span class="tab-custom-icon" aria-hidden="true">${icon}</span>`;
|
||||
return renderBuiltinTabIcon(icon);
|
||||
}
|
||||
|
||||
export function renderBuiltinTabIcon(icon: AppTabBuiltinIcon): TemplateResult {
|
||||
switch (icon) {
|
||||
case "navigation":
|
||||
return svg`
|
||||
<svg class="tab-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<circle cx="6" cy="7" r="1.5"></circle>
|
||||
<path d="M10 7h8"></path>
|
||||
<circle cx="6" cy="12" r="1.5"></circle>
|
||||
<path d="M10 12h8"></path>
|
||||
<circle cx="6" cy="17" r="1.5"></circle>
|
||||
<path d="M10 17h8"></path>
|
||||
</svg>
|
||||
`;
|
||||
case "chat":
|
||||
return svg`
|
||||
<svg class="tab-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<path d="M7 5h10a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3h-6l-5 4v-4H7a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3Z"></path>
|
||||
<path d="M8 9h8"></path>
|
||||
<path d="M8 13h5"></path>
|
||||
</svg>
|
||||
`;
|
||||
case "files":
|
||||
return svg`
|
||||
<svg class="tab-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<path d="M3 7a2 2 0 0 1 2-2h4l2 2.5h8a2 2 0 0 1 2 2V17a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"></path>
|
||||
</svg>
|
||||
`;
|
||||
case "git":
|
||||
return svg`
|
||||
<svg class="tab-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<circle cx="6" cy="6" r="2"></circle>
|
||||
<circle cx="18" cy="6" r="2"></circle>
|
||||
<circle cx="12" cy="18" r="2"></circle>
|
||||
<path d="M8 6h6"></path>
|
||||
<path d="M6 8v2a6 6 0 0 0 6 6"></path>
|
||||
<path d="M18 8v2a6 6 0 0 1-6 6"></path>
|
||||
</svg>
|
||||
`;
|
||||
case "terminal":
|
||||
return svg`
|
||||
<svg class="tab-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2"></rect>
|
||||
<path d="m7 10 3 3-3 3"></path>
|
||||
<path d="M12 16h5"></path>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { html, type TemplateResult } from "lit";
|
||||
import type { FileContentResponse, FileTreeEntry, GitDiffResponse, GitStatusResponse } from "../../api";
|
||||
import { workspaceImagePreviewUrl } from "../../api/urls";
|
||||
import { MAX_IMAGE_PREVIEW_BYTES, MAX_IMAGE_PREVIEW_LABEL } from "../../../../shared/workspaceFiles";
|
||||
import { renderBuiltinTabIcon } from "../../components/tabIcons";
|
||||
import type { WorkspacePanelContribution, WorkspacePanelContext } from "../types";
|
||||
|
||||
export function createCoreWorkspacePanels(): WorkspacePanelContribution[] {
|
||||
@@ -9,12 +10,14 @@ export function createCoreWorkspacePanels(): WorkspacePanelContribution[] {
|
||||
{
|
||||
id: "workspace.files",
|
||||
title: "Files",
|
||||
icon: renderBuiltinTabIcon("files"),
|
||||
order: 10,
|
||||
render: renderFiles,
|
||||
},
|
||||
{
|
||||
id: "workspace.git",
|
||||
title: "Git",
|
||||
icon: renderBuiltinTabIcon("git"),
|
||||
order: 20,
|
||||
visible: ({ workspace }) => workspace.isGitRepo,
|
||||
render: renderGit,
|
||||
@@ -22,6 +25,7 @@ export function createCoreWorkspacePanels(): WorkspacePanelContribution[] {
|
||||
{
|
||||
id: "workspace.terminal",
|
||||
title: "Terminal",
|
||||
icon: renderBuiltinTabIcon("terminal"),
|
||||
order: 30,
|
||||
badge: (context) => context.activeTerminalCount > 0 ? context.activeTerminalCount : undefined,
|
||||
render: renderTerminal,
|
||||
|
||||
Reference in New Issue
Block a user