Archived
feat: support plugin mobile tab icons
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
"@jmfederico/pi-web": patch
|
||||
---
|
||||
|
||||
Use compact icons and initials for the mobile main tab bar so tabs are easier to fit without losing horizontal scrolling.
|
||||
Use compact icons and initials for the mobile main tab bar so tabs are easier to fit without losing horizontal scrolling, and let workspace panel plugins provide custom SVG tab icons.
|
||||
|
||||
+13
-2
@@ -100,11 +100,11 @@ Module shape excerpt:
|
||||
export default {
|
||||
apiVersion: 1,
|
||||
name: "Info Plugin",
|
||||
activate: ({ html }) => ({
|
||||
activate: ({ html, svg }) => ({
|
||||
contributions: {
|
||||
actions: [/* action definitions */],
|
||||
workspaceLabels: [/* compact label definitions */],
|
||||
workspacePanels: [/* panel definitions using html */],
|
||||
workspacePanels: [/* panel definitions using html, optional icons using svg */],
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -239,6 +239,7 @@ interface PluginActivationContext {
|
||||
apiVersion: 1;
|
||||
pluginId: string;
|
||||
html: typeof import("lit").html;
|
||||
svg: typeof import("lit").svg;
|
||||
}
|
||||
|
||||
interface PluginActivationResult {
|
||||
@@ -369,6 +370,13 @@ workspacePanels: [
|
||||
{
|
||||
id: "workspace.info",
|
||||
title: "Info",
|
||||
icon: svg`
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
<path d="M12 10v6"></path>
|
||||
<path d="M12 7h.01"></path>
|
||||
</svg>
|
||||
`,
|
||||
order: 100,
|
||||
visible: ({ workspace }) => workspace.isGitRepo,
|
||||
render: ({ workspace }) => html`
|
||||
@@ -388,6 +396,7 @@ Panel type:
|
||||
interface WorkspacePanelContribution {
|
||||
id: string;
|
||||
title: string;
|
||||
icon?: TemplateResult;
|
||||
order?: number;
|
||||
visible?: (context: { workspace: Workspace }) => boolean;
|
||||
badge?: (context: WorkspacePanelContext) => string | number | TemplateResult | undefined;
|
||||
@@ -400,6 +409,8 @@ interface WorkspacePanelContext {
|
||||
}
|
||||
```
|
||||
|
||||
`icon` is optional and is used in the compact mobile tab bar. Prefer an SVG rendered with the `svg` helper from `PluginActivationContext`; use `currentColor` so PI WEB themes can style it. If `icon` is omitted, mobile tabs fall back to initials from the panel title, or to the full title when initials collide.
|
||||
|
||||
`workspace` and `openTerminal()` are documented as stable for panel callbacks. Other fields may exist at runtime, but they are PI WEB internals and can change quickly. If a panel needs file, git, terminal, or session data beyond the helpers documented here, prefer explicit `fetch()` calls and keep them isolated.
|
||||
|
||||
Useful workspace shape:
|
||||
|
||||
Vendored
+5
@@ -4,6 +4,7 @@ export type PluginId = string;
|
||||
export type LocalContributionId = string;
|
||||
export type QualifiedContributionId = string;
|
||||
export type HtmlTemplateTag = (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult;
|
||||
export type SvgTemplateTag = (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult;
|
||||
|
||||
export interface PiWebPlugin {
|
||||
apiVersion: 1;
|
||||
@@ -15,6 +16,7 @@ export interface PluginActivationContext {
|
||||
apiVersion: 1;
|
||||
pluginId: PluginId;
|
||||
html: HtmlTemplateTag;
|
||||
svg: SvgTemplateTag;
|
||||
}
|
||||
|
||||
export interface PluginActivationResult {
|
||||
@@ -84,9 +86,12 @@ export interface WorkspacePanelContext {
|
||||
openTerminal: (options?: { terminalId?: string | undefined }) => void;
|
||||
}
|
||||
|
||||
export type WorkspacePanelIcon = TemplateResult;
|
||||
|
||||
export interface WorkspacePanelContribution {
|
||||
id: LocalContributionId;
|
||||
title: string;
|
||||
icon?: WorkspacePanelIcon;
|
||||
order?: number;
|
||||
visible?: (context: WorkspacePanelContext) => boolean;
|
||||
badge?: (context: WorkspacePanelContext) => string | number | TemplateResult | undefined;
|
||||
|
||||
@@ -1021,7 +1021,7 @@ export class PiWebApp extends LitElement {
|
||||
{ id: "navigation", label: "Sessions", icon: "navigation", className: "navigation-tab" },
|
||||
{ id: "chat", label: "Chat", icon: "chat" },
|
||||
...this.visibleWorkspacePanels().map((panel): AppMobileMainTab => {
|
||||
const icon = this.mobilePanelIcon(panel);
|
||||
const icon = panel.icon ?? this.mobilePanelIcon(panel);
|
||||
return {
|
||||
id: panel.id,
|
||||
label: panel.title,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { LitElement, css, html, svg } from "lit";
|
||||
import { LitElement, css, html, svg, type TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import type { AppState } from "../../appState";
|
||||
|
||||
export type AppMobileMainTabIcon = "navigation" | "chat" | "files" | "git" | "terminal";
|
||||
export type AppMobileMainTabBuiltinIcon = "navigation" | "chat" | "files" | "git" | "terminal";
|
||||
export type AppMobileMainTabIcon = AppMobileMainTabBuiltinIcon | TemplateResult;
|
||||
|
||||
export interface AppMobileMainTab {
|
||||
id: AppState["mainView"];
|
||||
@@ -115,6 +116,7 @@ export class AppMobileMainTabs extends LitElement {
|
||||
}
|
||||
|
||||
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`
|
||||
@@ -206,6 +208,8 @@ export class AppMobileMainTabs extends LitElement {
|
||||
.mobile-tabs .navigation-tab { display: none; }
|
||||
.mobile-tabs button.selected { border-color: var(--pi-accent); background: var(--pi-selection-bg); }
|
||||
.tab-icon { flex: 0 0 auto; width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
|
||||
.tab-custom-icon { flex: 0 0 auto; width: 18px; height: 18px; display: inline-grid; place-items: center; color: currentColor; pointer-events: none; }
|
||||
.tab-custom-icon svg { width: 18px; height: 18px; pointer-events: none; }
|
||||
.tab-fallback { display: none; font-weight: 650; letter-spacing: .01em; pointer-events: none; }
|
||||
.tab-label { min-width: 0; }
|
||||
.tab-badge { flex: 0 0 auto; display: inline-block; min-width: 14px; margin-left: 0; 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; }
|
||||
|
||||
@@ -51,6 +51,31 @@ describe("PluginRegistry", () => {
|
||||
expect(registry.getWorkspacePanels().map((panel) => panel.id)).toEqual(["core:workspace.files", "core:workspace.git", "core:workspace.terminal"]);
|
||||
});
|
||||
|
||||
it("provides html and svg helpers to plugin activation", () => {
|
||||
const registry = new PluginRegistry();
|
||||
registry.register({
|
||||
id: "example",
|
||||
plugin: {
|
||||
apiVersion: 1,
|
||||
name: "Example",
|
||||
activate: ({ html, svg }) => ({
|
||||
contributions: {
|
||||
workspacePanels: [
|
||||
{
|
||||
id: "workspace.logs",
|
||||
title: "Logs",
|
||||
icon: svg`<svg viewBox="0 0 24 24"><path d="M4 6h16"></path></svg>`,
|
||||
render: () => html`<p>Logs</p>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.getWorkspacePanels()[0]?.icon).toBeDefined();
|
||||
});
|
||||
|
||||
it("rejects duplicate ids within the same namespace", () => {
|
||||
const registry = new PluginRegistry();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html } from "lit";
|
||||
import { html, svg } from "lit";
|
||||
import type { AppState } from "../appState";
|
||||
import type { Workspace } from "../api";
|
||||
import type { PiWebPluginRegistration, PluginAction, PluginRuntimeContext, QualifiedContributionId, QualifiedPluginAction, QualifiedThemeContribution, QualifiedThemePairContribution, QualifiedWorkspaceLabelContribution, QualifiedWorkspacePanelContribution, ThemeContribution, ThemePairContribution, WorkspaceLabelContribution, WorkspaceLabelItem, WorkspacePanelContext, WorkspacePanelContribution } from "./types";
|
||||
@@ -31,7 +31,7 @@ export class PluginRegistry {
|
||||
|
||||
const apiVersion: unknown = plugin.apiVersion;
|
||||
if (apiVersion !== 1) throw new Error(`Unsupported plugin API version for ${id}: ${String(apiVersion)}`);
|
||||
const result = plugin.activate({ apiVersion: 1, pluginId: id, html });
|
||||
const result = plugin.activate({ apiVersion: 1, pluginId: id, html, svg });
|
||||
const contributions = result.contributions;
|
||||
for (const action of contributions.actions ?? []) this.actions.push(this.qualifyAction(id, action));
|
||||
for (const panel of contributions.workspacePanels ?? []) this.workspacePanels.push(this.qualifyWorkspacePanel(id, panel));
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { LocalContributionId, PluginId, QualifiedContributionId } from "./i
|
||||
|
||||
export type { LocalContributionId, PluginId, QualifiedContributionId } from "./ids";
|
||||
export type HtmlTemplateTag = (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult;
|
||||
export type SvgTemplateTag = (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult;
|
||||
|
||||
export interface PiWebPluginRegistration {
|
||||
id: PluginId;
|
||||
@@ -23,6 +24,7 @@ export interface PluginActivationContext {
|
||||
apiVersion: 1;
|
||||
pluginId: PluginId;
|
||||
html: HtmlTemplateTag;
|
||||
svg: SvgTemplateTag;
|
||||
}
|
||||
|
||||
export interface PluginActivationResult {
|
||||
@@ -118,9 +120,12 @@ export interface WorkspacePanelContext {
|
||||
onSelectTerminal: (terminalId: string | undefined, options?: { replace?: boolean | undefined }) => void;
|
||||
}
|
||||
|
||||
export type WorkspacePanelIcon = TemplateResult;
|
||||
|
||||
export interface WorkspacePanelContribution {
|
||||
id: LocalContributionId;
|
||||
title: string;
|
||||
icon?: WorkspacePanelIcon;
|
||||
order?: number;
|
||||
visible?: (context: WorkspacePanelVisibilityContext) => boolean;
|
||||
badge?: (context: WorkspacePanelContext) => string | number | TemplateResult | undefined;
|
||||
|
||||
Reference in New Issue
Block a user