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;
|
body?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WorkspacePanelBadge = string | number | TemplateResult | undefined;
|
||||||
|
|
||||||
@customElement("workspace-panel")
|
@customElement("workspace-panel")
|
||||||
export class WorkspacePanel extends LitElement {
|
export class WorkspacePanel extends LitElement {
|
||||||
@property({ attribute: false }) workspace: Workspace | undefined;
|
@property({ attribute: false }) workspace: Workspace | undefined;
|
||||||
@@ -66,9 +68,16 @@ export class WorkspacePanel extends LitElement {
|
|||||||
<div class="workspace-header-strip" @scroll=${this.onWorkspaceHeaderScroll}>
|
<div class="workspace-header-strip" @scroll=${this.onWorkspaceHeaderScroll}>
|
||||||
${this.hideToolTabs ? null : html`
|
${this.hideToolTabs ? null : html`
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
${visiblePanels.map((panel) => html`
|
${visiblePanels.map((panel) => {
|
||||||
<button class=${selectedPanel?.id === panel.id ? "selected" : ""} @click=${() => { this.onSelectTool(panel.id); }}>${this.renderPanelTitle(panel, context)}</button>
|
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>
|
</div>
|
||||||
`}
|
`}
|
||||||
<small>${renderWorkspaceLabel(workspace.label, this.workspaceLabelItems, workspace.path)}</small>
|
<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 {
|
private panelTabClass(panel: QualifiedWorkspacePanelContribution, selected: boolean): string {
|
||||||
const badge = panel.badge?.(context);
|
return [
|
||||||
if (badge === undefined || badge === "") return html`${panel.title}`;
|
...(panel.icon === undefined ? [] : ["icon-tab"]),
|
||||||
return html`${panel.title} <span class="tab-badge">${badge}</span>`;
|
...(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 {
|
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 { customElement, property, query, state } from "lit/decorators.js";
|
||||||
import type { AppState } from "../../appState";
|
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 type AppMobileMainTabIcon = AppMobileMainTabBuiltinIcon | TemplateResult;
|
||||||
|
|
||||||
export interface AppMobileMainTab {
|
export interface AppMobileMainTab {
|
||||||
@@ -85,7 +86,7 @@ export class AppMobileMainTabs extends LitElement {
|
|||||||
private renderTabMark(tab: AppMobileMainTab, fallbackLabels: Map<AppState["mainView"], string>) {
|
private renderTabMark(tab: AppMobileMainTab, fallbackLabels: Map<AppState["mainView"], string>) {
|
||||||
return tab.icon === undefined
|
return tab.icon === undefined
|
||||||
? html`<span class="tab-fallback" aria-hidden="true">${fallbackLabels.get(tab.id) ?? this.initialsLabel(tab.label)}</span>`
|
? 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> {
|
private fallbackLabels(): Map<AppState["mainView"], string> {
|
||||||
@@ -115,56 +116,6 @@ export class AppMobileMainTabs extends LitElement {
|
|||||||
return trimmed === "" ? "?" : trimmed;
|
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 {
|
private observeMobileTabs(): void {
|
||||||
const mobileTabs = this.mobileTabsElement();
|
const mobileTabs = this.mobileTabsElement();
|
||||||
if (this.observedMobileTabs === mobileTabs) return;
|
if (this.observedMobileTabs === mobileTabs) return;
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export const appStyles = css`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const workspacePanelStyles = 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); }
|
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 { 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; }
|
.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; }
|
.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 { 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; }
|
||||||
|
.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 { 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-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; }
|
.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 { 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; }
|
.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 type { FileContentResponse, FileTreeEntry, GitDiffResponse, GitStatusResponse } from "../../api";
|
||||||
import { workspaceImagePreviewUrl } from "../../api/urls";
|
import { workspaceImagePreviewUrl } from "../../api/urls";
|
||||||
import { MAX_IMAGE_PREVIEW_BYTES, MAX_IMAGE_PREVIEW_LABEL } from "../../../../shared/workspaceFiles";
|
import { MAX_IMAGE_PREVIEW_BYTES, MAX_IMAGE_PREVIEW_LABEL } from "../../../../shared/workspaceFiles";
|
||||||
|
import { renderBuiltinTabIcon } from "../../components/tabIcons";
|
||||||
import type { WorkspacePanelContribution, WorkspacePanelContext } from "../types";
|
import type { WorkspacePanelContribution, WorkspacePanelContext } from "../types";
|
||||||
|
|
||||||
export function createCoreWorkspacePanels(): WorkspacePanelContribution[] {
|
export function createCoreWorkspacePanels(): WorkspacePanelContribution[] {
|
||||||
@@ -9,12 +10,14 @@ export function createCoreWorkspacePanels(): WorkspacePanelContribution[] {
|
|||||||
{
|
{
|
||||||
id: "workspace.files",
|
id: "workspace.files",
|
||||||
title: "Files",
|
title: "Files",
|
||||||
|
icon: renderBuiltinTabIcon("files"),
|
||||||
order: 10,
|
order: 10,
|
||||||
render: renderFiles,
|
render: renderFiles,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "workspace.git",
|
id: "workspace.git",
|
||||||
title: "Git",
|
title: "Git",
|
||||||
|
icon: renderBuiltinTabIcon("git"),
|
||||||
order: 20,
|
order: 20,
|
||||||
visible: ({ workspace }) => workspace.isGitRepo,
|
visible: ({ workspace }) => workspace.isGitRepo,
|
||||||
render: renderGit,
|
render: renderGit,
|
||||||
@@ -22,6 +25,7 @@ export function createCoreWorkspacePanels(): WorkspacePanelContribution[] {
|
|||||||
{
|
{
|
||||||
id: "workspace.terminal",
|
id: "workspace.terminal",
|
||||||
title: "Terminal",
|
title: "Terminal",
|
||||||
|
icon: renderBuiltinTabIcon("terminal"),
|
||||||
order: 30,
|
order: 30,
|
||||||
badge: (context) => context.activeTerminalCount > 0 ? context.activeTerminalCount : undefined,
|
badge: (context) => context.activeTerminalCount > 0 ? context.activeTerminalCount : undefined,
|
||||||
render: renderTerminal,
|
render: renderTerminal,
|
||||||
|
|||||||
Reference in New Issue
Block a user