Archived
fix: make workspace navigation bars scrollable
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@jmfederico/pi-web": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Make workspace navigation bars horizontally scrollable on desktop and mobile, with side shadows showing when more items are available.
|
||||||
@@ -43,6 +43,7 @@ export class PiWebApp extends LitElement {
|
|||||||
@state() private state: AppState = initialAppState();
|
@state() private state: AppState = initialAppState();
|
||||||
@query("chat-view") private chatView?: ChatView;
|
@query("chat-view") private chatView?: ChatView;
|
||||||
@query("prompt-editor") private promptEditor?: PromptEditor;
|
@query("prompt-editor") private promptEditor?: PromptEditor;
|
||||||
|
@query(".mobile-tabs") private mobileTabs?: HTMLElement;
|
||||||
|
|
||||||
private readonly sessions = new SessionController(
|
private readonly sessions = new SessionController(
|
||||||
() => this.state,
|
() => this.state,
|
||||||
@@ -79,6 +80,8 @@ export class PiWebApp extends LitElement {
|
|||||||
private readonly realtime = new RealtimeSocket();
|
private readonly realtime = new RealtimeSocket();
|
||||||
private readonly activeTerminalIds = new Set<string>();
|
private readonly activeTerminalIds = new Set<string>();
|
||||||
private readonly mobileNavigationMedia = typeof window !== "undefined" && "matchMedia" in window ? window.matchMedia("(max-width: 760px)") : undefined;
|
private readonly mobileNavigationMedia = typeof window !== "undefined" && "matchMedia" in window ? window.matchMedia("(max-width: 760px)") : undefined;
|
||||||
|
private observedMobileTabs: HTMLElement | undefined;
|
||||||
|
private mobileTabsResizeObserver: ResizeObserver | undefined;
|
||||||
private terminalAutoStartWorkspaceId: string | undefined;
|
private terminalAutoStartWorkspaceId: string | undefined;
|
||||||
private piWebStatusTimer: number | undefined;
|
private piWebStatusTimer: number | undefined;
|
||||||
private readonly plugins = createPluginRegistry();
|
private readonly plugins = createPluginRegistry();
|
||||||
@@ -86,6 +89,8 @@ export class PiWebApp extends LitElement {
|
|||||||
@state() private activeThemeId: QualifiedContributionId = DEFAULT_THEME_ID;
|
@state() private activeThemeId: QualifiedContributionId = DEFAULT_THEME_ID;
|
||||||
@state() private isMobileNavigationLayout = this.mobileNavigationMedia?.matches ?? false;
|
@state() private isMobileNavigationLayout = this.mobileNavigationMedia?.matches ?? false;
|
||||||
@state() private expandedMobileNavigationSection: NavigationSection | "none" | undefined;
|
@state() private expandedMobileNavigationSection: NavigationSection | "none" | undefined;
|
||||||
|
@state() private mobileTabsCanScrollLeft = false;
|
||||||
|
@state() private mobileTabsCanScrollRight = false;
|
||||||
private readonly onPopState = () => void this.withChatScrollTransition(() => this.restoreRoute(false));
|
private readonly onPopState = () => void this.withChatScrollTransition(() => this.restoreRoute(false));
|
||||||
private readonly onFocus = () => {
|
private readonly onFocus = () => {
|
||||||
void this.sessions.refreshSelectedSession();
|
void this.sessions.refreshSelectedSession();
|
||||||
@@ -99,6 +104,10 @@ export class PiWebApp extends LitElement {
|
|||||||
};
|
};
|
||||||
private readonly onMobileNavigationMediaChange = (event: MediaQueryListEvent) => {
|
private readonly onMobileNavigationMediaChange = (event: MediaQueryListEvent) => {
|
||||||
this.isMobileNavigationLayout = event.matches;
|
this.isMobileNavigationLayout = event.matches;
|
||||||
|
this.updateMobileTabsScrollState();
|
||||||
|
};
|
||||||
|
private readonly onMobileTabsScroll = () => {
|
||||||
|
this.updateMobileTabsScrollState();
|
||||||
};
|
};
|
||||||
private readonly onKeyDown = (event: KeyboardEvent) => {
|
private readonly onKeyDown = (event: KeyboardEvent) => {
|
||||||
if (this.keyboard.handle(event, this.getActions())) {
|
if (this.keyboard.handle(event, this.getActions())) {
|
||||||
@@ -135,9 +144,22 @@ export class PiWebApp extends LitElement {
|
|||||||
this.git.dispose();
|
this.git.dispose();
|
||||||
if (this.piWebStatusTimer !== undefined) window.clearInterval(this.piWebStatusTimer);
|
if (this.piWebStatusTimer !== undefined) window.clearInterval(this.piWebStatusTimer);
|
||||||
this.piWebStatusTimer = undefined;
|
this.piWebStatusTimer = undefined;
|
||||||
|
this.mobileTabsResizeObserver?.disconnect();
|
||||||
|
this.mobileTabsResizeObserver = undefined;
|
||||||
|
this.observedMobileTabs = undefined;
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override firstUpdated(): void {
|
||||||
|
this.observeMobileTabs();
|
||||||
|
this.updateMobileTabsScrollState();
|
||||||
|
}
|
||||||
|
|
||||||
|
override updated(): void {
|
||||||
|
this.observeMobileTabs();
|
||||||
|
this.updateMobileTabsScrollState();
|
||||||
|
}
|
||||||
|
|
||||||
private setState(patch: Partial<AppState>) {
|
private setState(patch: Partial<AppState>) {
|
||||||
if (!patchChangesState(this.state, patch)) return;
|
if (!patchChangesState(this.state, patch)) return;
|
||||||
const previous = this.state;
|
const previous = this.state;
|
||||||
@@ -527,19 +549,47 @@ export class PiWebApp extends LitElement {
|
|||||||
void this.sessions.send(text, streamingBehavior);
|
void this.sessions.send(text, streamingBehavior);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private mobileTabsFrameClass(): string {
|
||||||
|
return `mobile-tabs-frame${this.mobileTabsCanScrollLeft ? " can-scroll-left" : ""}${this.mobileTabsCanScrollRight ? " can-scroll-right" : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private observeMobileTabs(): void {
|
||||||
|
const mobileTabs = this.mobileTabs;
|
||||||
|
if (this.observedMobileTabs === mobileTabs) return;
|
||||||
|
this.mobileTabsResizeObserver?.disconnect();
|
||||||
|
this.observedMobileTabs = mobileTabs;
|
||||||
|
this.mobileTabsResizeObserver = undefined;
|
||||||
|
if (mobileTabs === undefined || typeof ResizeObserver === "undefined") return;
|
||||||
|
this.mobileTabsResizeObserver = new ResizeObserver(() => {
|
||||||
|
this.updateMobileTabsScrollState();
|
||||||
|
});
|
||||||
|
this.mobileTabsResizeObserver.observe(mobileTabs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateMobileTabsScrollState(): void {
|
||||||
|
const mobileTabs = this.mobileTabs;
|
||||||
|
const maxScrollLeft = mobileTabs === undefined ? 0 : Math.max(0, mobileTabs.scrollWidth - mobileTabs.clientWidth);
|
||||||
|
const canScrollLeft = mobileTabs !== undefined && mobileTabs.scrollLeft > 1;
|
||||||
|
const canScrollRight = mobileTabs !== undefined && maxScrollLeft - mobileTabs.scrollLeft > 1;
|
||||||
|
if (this.mobileTabsCanScrollLeft !== canScrollLeft) this.mobileTabsCanScrollLeft = canScrollLeft;
|
||||||
|
if (this.mobileTabsCanScrollRight !== canScrollRight) this.mobileTabsCanScrollRight = canScrollRight;
|
||||||
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
const state = this.state;
|
const state = this.state;
|
||||||
return html`
|
return html`
|
||||||
<div class=${`shell ${state.mainView === "navigation" ? "navigation-view" : state.mainView === "chat" ? "chat-view" : "workspace-view"}`}>
|
<div class=${`shell ${state.mainView === "navigation" ? "navigation-view" : state.mainView === "chat" ? "chat-view" : "workspace-view"}`}>
|
||||||
<aside>${this.isMobileNavigationLayout ? null : this.renderNavigationPanel(false)}</aside>
|
<aside>${this.isMobileNavigationLayout ? null : this.renderNavigationPanel(false)}</aside>
|
||||||
<main class=${state.mainView === "chat" ? "chat-view" : state.mainView === "navigation" ? "navigation-view" : "workspace-view"}>
|
<main class=${state.mainView === "chat" ? "chat-view" : state.mainView === "navigation" ? "navigation-view" : "workspace-view"}>
|
||||||
<div class="mobile-tabs">
|
<div class=${this.mobileTabsFrameClass()}>
|
||||||
|
<div class="mobile-tabs" @scroll=${this.onMobileTabsScroll}>
|
||||||
<button class=${state.mainView === "navigation" ? "mobile-navigation-tab selected" : "mobile-navigation-tab"} @click=${() => { this.selectMainView("navigation"); }}>Sessions</button>
|
<button class=${state.mainView === "navigation" ? "mobile-navigation-tab selected" : "mobile-navigation-tab"} @click=${() => { this.selectMainView("navigation"); }}>Sessions</button>
|
||||||
<button class=${state.mainView === "chat" ? "selected" : ""} @click=${() => { this.selectMainView("chat"); }}>Chat</button>
|
<button class=${state.mainView === "chat" ? "selected" : ""} @click=${() => { this.selectMainView("chat"); }}>Chat</button>
|
||||||
${this.visibleWorkspacePanels().map((panel) => html`
|
${this.visibleWorkspacePanels().map((panel) => html`
|
||||||
<button class=${state.mainView === panel.id ? "selected" : ""} @click=${() => { this.openWorkspaceTool(panel.id); }}>${this.renderMobilePanelTitle(panel)}</button>
|
<button class=${state.mainView === panel.id ? "selected" : ""} @click=${() => { this.openWorkspaceTool(panel.id); }}>${this.renderMobilePanelTitle(panel)}</button>
|
||||||
`)}
|
`)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
${state.error ? html`<div class="error">${state.error}</div>` : null}
|
${state.error ? html`<div class="error">${state.error}</div>` : null}
|
||||||
<div class="mobile-navigation-panel">${this.isMobileNavigationLayout ? this.renderNavigationPanel(true) : null}</div>
|
<div class="mobile-navigation-panel">${this.isMobileNavigationLayout ? this.renderNavigationPanel(true) : null}</div>
|
||||||
${state.selectedSession ? html`
|
${state.selectedSession ? html`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LitElement, html, type TemplateResult } from "lit";
|
import { LitElement, html, type TemplateResult } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property, query, state } from "lit/decorators.js";
|
||||||
import type { FileContentResponse, FileTreeEntry, GitDiffResponse, GitStatusResponse, Workspace } from "../api";
|
import type { FileContentResponse, FileTreeEntry, GitDiffResponse, GitStatusResponse, Workspace } from "../api";
|
||||||
import type { AppState } from "../appState";
|
import type { AppState } from "../appState";
|
||||||
import type { QualifiedContributionId, QualifiedWorkspacePanelContribution, WorkspaceLabelItem, WorkspacePanelContext } from "../plugins/types";
|
import type { QualifiedContributionId, QualifiedWorkspacePanelContribution, WorkspaceLabelItem, WorkspacePanelContext } from "../plugins/types";
|
||||||
@@ -32,6 +32,32 @@ export class WorkspacePanel extends LitElement {
|
|||||||
@property({ attribute: false }) onSelectDiff: (path: string) => void = () => undefined;
|
@property({ attribute: false }) onSelectDiff: (path: string) => void = () => undefined;
|
||||||
@property({ type: Number }) activeTerminalCount = 0;
|
@property({ type: Number }) activeTerminalCount = 0;
|
||||||
@property({ type: Boolean }) terminalAutoStart = false;
|
@property({ type: Boolean }) terminalAutoStart = false;
|
||||||
|
@query(".workspace-header-strip") private workspaceHeaderStrip?: HTMLElement;
|
||||||
|
@state() private workspaceHeaderCanScrollLeft = false;
|
||||||
|
@state() private workspaceHeaderCanScrollRight = false;
|
||||||
|
|
||||||
|
private observedWorkspaceHeaderStrip: HTMLElement | undefined;
|
||||||
|
private workspaceHeaderResizeObserver: ResizeObserver | undefined;
|
||||||
|
private readonly onWorkspaceHeaderScroll = () => {
|
||||||
|
this.updateWorkspaceHeaderScrollState();
|
||||||
|
};
|
||||||
|
|
||||||
|
override firstUpdated(): void {
|
||||||
|
this.observeWorkspaceHeaderStrip();
|
||||||
|
this.updateWorkspaceHeaderScrollState();
|
||||||
|
}
|
||||||
|
|
||||||
|
override updated(): void {
|
||||||
|
this.observeWorkspaceHeaderStrip();
|
||||||
|
this.updateWorkspaceHeaderScrollState();
|
||||||
|
}
|
||||||
|
|
||||||
|
override disconnectedCallback(): void {
|
||||||
|
this.workspaceHeaderResizeObserver?.disconnect();
|
||||||
|
this.workspaceHeaderResizeObserver = undefined;
|
||||||
|
this.observedWorkspaceHeaderStrip = undefined;
|
||||||
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
const workspace = this.workspace;
|
const workspace = this.workspace;
|
||||||
@@ -41,6 +67,8 @@ export class WorkspacePanel extends LitElement {
|
|||||||
const context = this.createPanelContext(workspace);
|
const context = this.createPanelContext(workspace);
|
||||||
return html`
|
return html`
|
||||||
<header>
|
<header>
|
||||||
|
<div class=${this.workspaceHeaderFrameClass()}>
|
||||||
|
<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) => html`
|
||||||
@@ -49,6 +77,8 @@ export class WorkspacePanel extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
<small>${renderWorkspaceLabel(workspace.label, this.workspaceLabelItems, workspace.path)}</small>
|
<small>${renderWorkspaceLabel(workspace.label, this.workspaceLabelItems, workspace.path)}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
${selectedPanel === undefined ? html`<section class="empty">No workspace panels registered.</section>` : html`
|
${selectedPanel === undefined ? html`<section class="empty">No workspace panels registered.</section>` : html`
|
||||||
<div class="panel-content">
|
<div class="panel-content">
|
||||||
@@ -64,6 +94,32 @@ export class WorkspacePanel extends LitElement {
|
|||||||
return html`${panel.title} <span class="tab-badge">${badge}</span>`;
|
return html`${panel.title} <span class="tab-badge">${badge}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private workspaceHeaderFrameClass(): string {
|
||||||
|
return `workspace-header-scroll-frame${this.workspaceHeaderCanScrollLeft ? " can-scroll-left" : ""}${this.workspaceHeaderCanScrollRight ? " can-scroll-right" : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private observeWorkspaceHeaderStrip(): void {
|
||||||
|
const strip = this.workspaceHeaderStrip;
|
||||||
|
if (this.observedWorkspaceHeaderStrip === strip) return;
|
||||||
|
this.workspaceHeaderResizeObserver?.disconnect();
|
||||||
|
this.observedWorkspaceHeaderStrip = strip;
|
||||||
|
this.workspaceHeaderResizeObserver = undefined;
|
||||||
|
if (strip === undefined || typeof ResizeObserver === "undefined") return;
|
||||||
|
this.workspaceHeaderResizeObserver = new ResizeObserver(() => {
|
||||||
|
this.updateWorkspaceHeaderScrollState();
|
||||||
|
});
|
||||||
|
this.workspaceHeaderResizeObserver.observe(strip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateWorkspaceHeaderScrollState(): void {
|
||||||
|
const strip = this.workspaceHeaderStrip;
|
||||||
|
const maxScrollLeft = strip === undefined ? 0 : Math.max(0, strip.scrollWidth - strip.clientWidth);
|
||||||
|
const canScrollLeft = strip !== undefined && strip.scrollLeft > 1;
|
||||||
|
const canScrollRight = strip !== undefined && maxScrollLeft - strip.scrollLeft > 1;
|
||||||
|
if (this.workspaceHeaderCanScrollLeft !== canScrollLeft) this.workspaceHeaderCanScrollLeft = canScrollLeft;
|
||||||
|
if (this.workspaceHeaderCanScrollRight !== canScrollRight) this.workspaceHeaderCanScrollRight = canScrollRight;
|
||||||
|
}
|
||||||
|
|
||||||
private createPanelContext(workspace: Workspace): WorkspacePanelContext {
|
private createPanelContext(workspace: Workspace): WorkspacePanelContext {
|
||||||
return {
|
return {
|
||||||
workspace,
|
workspace,
|
||||||
|
|||||||
@@ -57,7 +57,13 @@ export const appStyles = css`
|
|||||||
project-list, workspace-list { flex: 0 0 auto; max-height: 26%; overflow: auto; border-bottom: 1px solid var(--pi-border-muted); }
|
project-list, workspace-list { flex: 0 0 auto; max-height: 26%; overflow: auto; border-bottom: 1px solid var(--pi-border-muted); }
|
||||||
session-list { flex: 1 1 auto; min-height: 0; overflow: auto; }
|
session-list { flex: 1 1 auto; min-height: 0; overflow: auto; }
|
||||||
main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
|
main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
|
||||||
.mobile-tabs { display: none; flex: 0 0 auto; gap: 6px; padding: 8px; border-bottom: 1px solid var(--pi-border); overflow-x: auto; }
|
.mobile-tabs-frame { position: relative; display: none; flex: 0 0 auto; min-width: 0; border-bottom: 1px solid var(--pi-border); background: var(--pi-bg); }
|
||||||
|
.mobile-tabs-frame::before, .mobile-tabs-frame::after { content: ""; position: absolute; top: 0; bottom: 0; z-index: 2; width: 20px; opacity: 0; pointer-events: none; transition: opacity .15s ease; }
|
||||||
|
.mobile-tabs-frame::before { left: 0; background: linear-gradient(90deg, color-mix(in srgb, var(--pi-shadow-strong) 55%, transparent) 0%, transparent 100%); }
|
||||||
|
.mobile-tabs-frame::after { right: 0; background: linear-gradient(270deg, color-mix(in srgb, var(--pi-shadow-strong) 55%, transparent) 0%, transparent 100%); }
|
||||||
|
.mobile-tabs-frame.can-scroll-left::before, .mobile-tabs-frame.can-scroll-right::after { opacity: 1; }
|
||||||
|
.mobile-tabs { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 6px; padding: 8px; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scrollbar-width: thin; -webkit-overflow-scrolling: touch; }
|
||||||
|
.mobile-tabs button { flex: 0 0 auto; white-space: nowrap; }
|
||||||
.mobile-navigation-tab, .mobile-navigation-panel { display: none; }
|
.mobile-navigation-tab, .mobile-navigation-panel { display: none; }
|
||||||
.mobile-tabs button.selected { border-color: var(--pi-accent); background: var(--pi-selection-bg); }
|
.mobile-tabs button.selected { border-color: var(--pi-accent); background: var(--pi-selection-bg); }
|
||||||
.tab-badge { display: inline-block; min-width: 14px; margin-left: 4px; border: 1px solid var(--pi-success-border); border-radius: 999px; background: var(--pi-success-surface); color: var(--pi-success); padding: 0 5px; font-size: 11px; line-height: 16px; text-align: center; }
|
.tab-badge { display: inline-block; min-width: 14px; margin-left: 4px; border: 1px solid var(--pi-success-border); border-radius: 999px; background: var(--pi-success-surface); color: var(--pi-success); padding: 0 5px; font-size: 11px; line-height: 16px; text-align: center; }
|
||||||
@@ -66,7 +72,7 @@ export const appStyles = css`
|
|||||||
.shell { grid-template-columns: 340px minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
|
.shell { grid-template-columns: 340px minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
|
||||||
aside { grid-row: 1 / 3; }
|
aside { grid-row: 1 / 3; }
|
||||||
main { grid-column: 2; grid-row: 1 / 3; }
|
main { grid-column: 2; grid-row: 1 / 3; }
|
||||||
.mobile-tabs { display: flex; }
|
.mobile-tabs-frame { display: flex; }
|
||||||
.shell.workspace-view main { grid-row: 1; min-height: auto; }
|
.shell.workspace-view main { grid-row: 1; min-height: auto; }
|
||||||
.shell.workspace-view > workspace-panel { grid-column: 2; grid-row: 2; display: flex; border-left: 0; }
|
.shell.workspace-view > workspace-panel { grid-column: 2; grid-row: 2; display: flex; border-left: 0; }
|
||||||
.shell:not(.workspace-view) > workspace-panel { display: none; }
|
.shell:not(.workspace-view) > workspace-panel { display: none; }
|
||||||
@@ -99,14 +105,23 @@ 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; }
|
||||||
header { flex: 0 0 auto; display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 8px; border-bottom: 1px solid var(--pi-border); }
|
header { flex: 0 0 auto; min-width: 0; border-bottom: 1px solid var(--pi-border); }
|
||||||
.tabs { display: flex; gap: 6px; }
|
.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 { left: 0; background: linear-gradient(90deg, color-mix(in srgb, var(--pi-shadow-strong) 55%, transparent) 0%, transparent 100%); }
|
||||||
|
.workspace-header-scroll-frame::after { right: 0; background: linear-gradient(270deg, color-mix(in srgb, var(--pi-shadow-strong) 55%, transparent) 0%, transparent 100%); }
|
||||||
|
.workspace-header-scroll-frame.can-scroll-left::before, .workspace-header-scroll-frame.can-scroll-right::after { opacity: 1; }
|
||||||
|
.workspace-header-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; -webkit-overflow-scrolling: touch; }
|
||||||
|
.tabs { flex: 0 0 auto; display: flex; gap: 6px; }
|
||||||
|
.tabs button { flex: 0 0 auto; white-space: nowrap; }
|
||||||
button { display: inline-flex; align-items: center; gap: 5px; border: 1px solid var(--pi-border); border-radius: 7px; background: var(--pi-surface); color: var(--pi-text); padding: 5px 7px; cursor: pointer; }
|
button { display: inline-flex; align-items: center; gap: 5px; border: 1px solid var(--pi-border); border-radius: 7px; background: var(--pi-surface); color: var(--pi-text); padding: 5px 7px; cursor: pointer; }
|
||||||
button.selected { border-color: var(--pi-accent); background: var(--pi-selection-bg); }
|
button.selected { border-color: var(--pi-accent); background: var(--pi-selection-bg); }
|
||||||
.tab-badge { display: inline-block; min-width: 14px; border: 1px solid var(--pi-success-border); border-radius: 999px; background: var(--pi-success-surface); color: var(--pi-success); padding: 0 5px; font-size: 11px; line-height: 16px; text-align: center; }
|
.tab-badge { display: inline-block; min-width: 14px; border: 1px solid var(--pi-success-border); border-radius: 999px; background: var(--pi-success-surface); color: var(--pi-success); padding: 0 5px; font-size: 11px; line-height: 16px; text-align: center; }
|
||||||
.panel-content { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
|
.panel-content { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
|
||||||
small, .muted { color: var(--pi-muted); }
|
small, .muted { color: var(--pi-muted); }
|
||||||
header small { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
header small { flex: 0 0 auto; min-width: max-content; overflow: visible; text-overflow: clip; white-space: nowrap; }
|
||||||
|
header .workspace-label { width: max-content; max-width: none; overflow: visible; }
|
||||||
|
header .workspace-label-base, header .workspace-label-item, header .workspace-label-render { overflow: visible; text-overflow: clip; }
|
||||||
@media (max-width: 1180px) { .tabs { display: none; } }
|
@media (max-width: 1180px) { .tabs { display: none; } }
|
||||||
.workspace-label { min-width: 0; display: inline-flex; align-items: baseline; gap: 5px; max-width: 100%; overflow: hidden; white-space: nowrap; }
|
.workspace-label { min-width: 0; display: inline-flex; align-items: baseline; gap: 5px; max-width: 100%; overflow: hidden; white-space: nowrap; }
|
||||||
.workspace-label-base, .workspace-label-item, .workspace-label-render { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
.workspace-label-base, .workspace-label-item, .workspace-label-render { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
|||||||
Reference in New Issue
Block a user