Archived
feat: add Pi Web theme picker
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@jmfederico/pi-web": patch
|
||||
---
|
||||
|
||||
Add theme tokens, a theme picker, and built-in current/docs-inspired themes for the Pi Web UI.
|
||||
+39
-1
@@ -5,7 +5,45 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<title>Pi Web</title>
|
||||
<style>
|
||||
html, body { margin: 0; height: 100%; overflow: hidden; }
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--pi-bg: #0d1117;
|
||||
--pi-surface: #161b22;
|
||||
--pi-surface-hover: #21262d;
|
||||
--pi-terminal-bg: #05070a;
|
||||
--pi-terminal-text: #e6edf3;
|
||||
--pi-border: #30363d;
|
||||
--pi-border-muted: #21262d;
|
||||
--pi-text: #e6edf3;
|
||||
--pi-text-secondary: #c9d1d9;
|
||||
--pi-text-bright: #f0f6fc;
|
||||
--pi-muted: #8b949e;
|
||||
--pi-dim: #6e7681;
|
||||
--pi-accent: #58a6ff;
|
||||
--pi-accent-border: #2f81f7;
|
||||
--pi-selection-bg: #0d2847;
|
||||
--pi-success: #3fb950;
|
||||
--pi-success-border: #238636;
|
||||
--pi-success-bg: #0f1b12;
|
||||
--pi-success-surface: #0f2a16;
|
||||
--pi-success-ring: #3fb95055;
|
||||
--pi-warning: #d29922;
|
||||
--pi-warning-border: #6e5200;
|
||||
--pi-warning-surface: #1f1a10;
|
||||
--pi-danger: #ff7b72;
|
||||
--pi-purple: #d2a8ff;
|
||||
--pi-purple-border: #a371f7;
|
||||
--pi-purple-surface: #21132f;
|
||||
--pi-overlay: #0008;
|
||||
--pi-shadow-soft: #0006;
|
||||
--pi-shadow: #0008;
|
||||
--pi-shadow-strong: #000b;
|
||||
--pi-bg-overlay-soft: #0d1117dd;
|
||||
--pi-bg-overlay: #0d1117e6;
|
||||
--pi-success-bg-overlay: #0f1b12ee;
|
||||
--pi-terminal-selection: #264f78;
|
||||
}
|
||||
html, body { margin: 0; height: 100%; overflow: hidden; background: var(--pi-bg); color: var(--pi-text); }
|
||||
body { min-height: 100dvh; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface AppState {
|
||||
commandDialog: Extract<CommandResult, { type: "select" }> | undefined;
|
||||
modelDialog: { title: string; options: CommandOption[]; selectedValue?: string } | undefined;
|
||||
thinkingDialog: { title: string; options: CommandOption[]; selectedValue?: string } | undefined;
|
||||
themeDialog: { title: string; options: CommandOption[]; selectedValue?: string } | undefined;
|
||||
authDialog: AuthDialogState | undefined;
|
||||
actionPaletteOpen: boolean;
|
||||
projectDialogOpen: boolean;
|
||||
@@ -67,6 +68,7 @@ export function initialAppState(): AppState {
|
||||
commandDialog: undefined,
|
||||
modelDialog: undefined,
|
||||
thinkingDialog: undefined,
|
||||
themeDialog: undefined,
|
||||
authDialog: undefined,
|
||||
actionPaletteOpen: false,
|
||||
projectDialogOpen: false,
|
||||
|
||||
@@ -146,19 +146,19 @@ export class AuthDialog extends LitElement {
|
||||
|
||||
static override styles = [commandPickerStyles, css`
|
||||
.form { display: grid; gap: 12px; padding: 14px; overflow: auto; }
|
||||
.form p { margin: 0; color: #c9d1d9; overflow-wrap: anywhere; }
|
||||
.form a { color: #58a6ff; overflow-wrap: anywhere; }
|
||||
.form code { border: 1px solid #30363d; border-radius: 4px; background: #161b22; padding: 1px 4px; }
|
||||
label { color: #8b949e; }
|
||||
.form p { margin: 0; color: var(--pi-text-secondary); overflow-wrap: anywhere; }
|
||||
.form a { color: var(--pi-accent); overflow-wrap: anywhere; }
|
||||
.form code { border: 1px solid var(--pi-border); border-radius: 4px; background: var(--pi-surface); padding: 1px 4px; }
|
||||
label { color: var(--pi-muted); }
|
||||
.actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
.actions button, .inline-options button { border: 1px solid #30363d; border-radius: 8px; background: #161b22; color: #e6edf3; padding: 7px 9px; }
|
||||
.actions button.primary { border-color: #238636; background: #0f2a16; color: #3fb950; }
|
||||
.actions button, .inline-options button { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); color: var(--pi-text); padding: 7px 9px; }
|
||||
.actions button.primary { border-color: var(--pi-success-border); background: var(--pi-success-surface); color: var(--pi-success); }
|
||||
.actions button:disabled { opacity: .6; cursor: wait; }
|
||||
.warning { color: #d29922; }
|
||||
.error-text { color: #ff7b72; }
|
||||
.progress { margin: 0; padding-left: 18px; color: #8b949e; }
|
||||
.warning { color: var(--pi-warning); }
|
||||
.error-text { color: var(--pi-danger); }
|
||||
.progress { margin: 0; padding-left: 18px; color: var(--pi-muted); }
|
||||
.inline-options { display: grid; gap: 8px; }
|
||||
em { color: #3fb950; font-style: normal; font-size: 12px; }
|
||||
em { color: var(--pi-success); font-style: normal; font-size: 12px; }
|
||||
`];
|
||||
}
|
||||
|
||||
|
||||
@@ -70,8 +70,8 @@ export class CodeViewer extends LitElement {
|
||||
const viewerTheme = EditorView.theme({
|
||||
"&": {
|
||||
height: "100%",
|
||||
color: "#e6edf3",
|
||||
backgroundColor: "#0d1117",
|
||||
color: "var(--pi-text)",
|
||||
backgroundColor: "var(--pi-bg)",
|
||||
fontSize: "12px",
|
||||
},
|
||||
".cm-scroller": {
|
||||
@@ -79,9 +79,9 @@ const viewerTheme = EditorView.theme({
|
||||
lineHeight: "1.45",
|
||||
},
|
||||
".cm-gutters": {
|
||||
backgroundColor: "#0d1117",
|
||||
color: "#6e7681",
|
||||
borderRight: "1px solid #21262d",
|
||||
backgroundColor: "var(--pi-bg)",
|
||||
color: "var(--pi-dim)",
|
||||
borderRight: "1px solid var(--pi-border-muted)",
|
||||
},
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: "transparent",
|
||||
|
||||
@@ -12,7 +12,9 @@ import { WorkspaceController } from "../controllers/workspaceController";
|
||||
import { KeyboardShortcutDispatcher } from "../keyboardShortcuts";
|
||||
import { RealtimeSocket } from "../sessionSocket";
|
||||
import type { QualifiedContributionId, QualifiedWorkspacePanelContribution, PluginRuntimeContext, WorkspacePanelContext } from "../plugins/types";
|
||||
import { DEFAULT_THEME_ID, applyPiWebTheme, readStoredThemeId } from "../theme";
|
||||
import { corePlugin } from "../plugins/core";
|
||||
import { themePackPlugin } from "../plugins/themes";
|
||||
import { loadExternalPlugins } from "../plugins/external";
|
||||
import { PluginRegistry } from "../plugins/registry";
|
||||
import { queryNamespace, readNamespacedString } from "../namespacedQueryArgs";
|
||||
@@ -77,6 +79,8 @@ export class PiWebApp extends LitElement {
|
||||
private readonly mobileNavigationMedia = typeof window !== "undefined" && "matchMedia" in window ? window.matchMedia("(max-width: 760px)") : undefined;
|
||||
private terminalAutoStartWorkspaceId: string | undefined;
|
||||
private readonly plugins = createPluginRegistry();
|
||||
private preferredThemeId: QualifiedContributionId = readStoredThemeId() ?? DEFAULT_THEME_ID;
|
||||
@state() private activeThemeId: QualifiedContributionId = DEFAULT_THEME_ID;
|
||||
@state() private isMobileNavigationLayout = this.mobileNavigationMedia?.matches ?? false;
|
||||
@state() private expandedMobileNavigationSection: NavigationSection | "none" | undefined;
|
||||
private readonly onPopState = () => void this.withChatScrollTransition(() => this.restoreRoute(false));
|
||||
@@ -101,6 +105,7 @@ export class PiWebApp extends LitElement {
|
||||
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
||||
window.addEventListener("keydown", this.onKeyDown);
|
||||
this.mobileNavigationMedia?.addEventListener("change", this.onMobileNavigationMediaChange);
|
||||
this.applyPreferredTheme(false);
|
||||
this.connectRealtime();
|
||||
void this.loadExternalPlugins();
|
||||
void this.loadProjectsAndRestoreRoute();
|
||||
@@ -387,6 +392,7 @@ export class PiWebApp extends LitElement {
|
||||
private async loadExternalPlugins(): Promise<void> {
|
||||
try {
|
||||
for (const registration of await loadExternalPlugins()) this.plugins.register(registration);
|
||||
this.applyPreferredTheme(false);
|
||||
this.requestUpdate();
|
||||
} catch (error) {
|
||||
console.warn("Failed to load external Pi Web plugins", error);
|
||||
@@ -401,6 +407,7 @@ export class PiWebApp extends LitElement {
|
||||
addProject: () => { this.setState({ projectDialogOpen: true }); },
|
||||
configureAuth: () => this.auth.openLogin(),
|
||||
logoutAuth: () => this.auth.openLogout(),
|
||||
openThemePicker: () => { this.openThemeDialog(); },
|
||||
selectMainView: (view) => { this.selectMainView(view); },
|
||||
selectWorkspaceTool: (tool) => { this.openWorkspaceTool(tool); },
|
||||
refreshFiles: () => this.files.refreshFiles(),
|
||||
@@ -441,6 +448,40 @@ export class PiWebApp extends LitElement {
|
||||
await this.sessions.setModel(value.slice(0, slash), value.slice(slash + 1));
|
||||
}
|
||||
|
||||
private openThemeDialog() {
|
||||
const themes = this.plugins.getThemes();
|
||||
this.setState({
|
||||
themeDialog: {
|
||||
title: "Select Theme",
|
||||
selectedValue: this.activeThemeId,
|
||||
options: themes.map((theme) => ({
|
||||
value: theme.id,
|
||||
label: `${theme.name}${theme.id === this.activeThemeId ? " ✓ current" : ""}`,
|
||||
description: theme.description === undefined ? theme.colorScheme : `${theme.colorScheme} · ${theme.description}`,
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private pickTheme(value: string) {
|
||||
const theme = this.plugins.getThemes().find((candidate) => candidate.id === value);
|
||||
this.setState({ themeDialog: undefined });
|
||||
if (theme === undefined) return;
|
||||
this.preferredThemeId = theme.id;
|
||||
this.activeThemeId = theme.id;
|
||||
applyPiWebTheme(theme);
|
||||
}
|
||||
|
||||
private applyPreferredTheme(persist: boolean): void {
|
||||
const themes = this.plugins.getThemes();
|
||||
const theme = themes.find((candidate) => candidate.id === this.preferredThemeId)
|
||||
?? themes.find((candidate) => candidate.id === DEFAULT_THEME_ID)
|
||||
?? themes[0];
|
||||
if (theme === undefined) return;
|
||||
this.activeThemeId = theme.id;
|
||||
applyPiWebTheme(theme, { persist });
|
||||
}
|
||||
|
||||
private async openThinkingDialog() {
|
||||
const levels = await this.sessions.listThinkingLevels();
|
||||
const current = this.state.status?.thinkingLevel ?? "off";
|
||||
@@ -491,6 +532,7 @@ export class PiWebApp extends LitElement {
|
||||
${this.renderWorkspacePanel()}
|
||||
${state.actionPaletteOpen ? html`<action-palette .actions=${this.getActions()} .onRun=${(actionId: string) => { this.setState({ actionPaletteOpen: false }); this.runAction(actionId); }} .onCancel=${() => { this.setState({ actionPaletteOpen: false }); }}></action-palette>` : null}
|
||||
${state.projectDialogOpen ? html`<project-dialog .onSubmit=${(path: string, create: boolean) => this.projects.addProject(path, create)} .onCancel=${() => { this.setState({ projectDialogOpen: false }); }}></project-dialog>` : null}
|
||||
${state.themeDialog !== undefined ? html`<command-picker title=${state.themeDialog.title} .options=${state.themeDialog.options} .selectedValue=${state.themeDialog.selectedValue} .onPick=${(value: string) => { this.pickTheme(value); }} .onCancel=${() => { this.setState({ themeDialog: undefined }); }}></command-picker>` : null}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -501,6 +543,7 @@ export class PiWebApp extends LitElement {
|
||||
function createPluginRegistry(): PluginRegistry {
|
||||
const registry = new PluginRegistry();
|
||||
registry.register({ id: "core", plugin: corePlugin });
|
||||
registry.register({ id: "themes", plugin: themePackPlugin });
|
||||
return registry;
|
||||
}
|
||||
|
||||
|
||||
@@ -123,22 +123,22 @@ export class ProjectDialog extends LitElement {
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
:host { position: fixed; inset: 0; z-index: 30; color: #e6edf3; font: 14px system-ui, sans-serif; }
|
||||
.backdrop { display: grid; place-items: start center; width: 100%; height: 100%; padding-top: min(12vh, 90px); box-sizing: border-box; background: #0008; }
|
||||
section { width: min(720px, calc(100vw - 40px)); max-height: min(700px, calc(100vh - 40px)); display: flex; flex-direction: column; border: 1px solid #30363d; border-radius: 12px; background: #0d1117; box-shadow: 0 20px 60px #000b; overflow: hidden; }
|
||||
header, footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px; border-bottom: 1px solid #30363d; }
|
||||
footer { border-top: 1px solid #30363d; border-bottom: 0; justify-content: end; }
|
||||
:host { position: fixed; inset: 0; z-index: 30; color: var(--pi-text); font: 14px system-ui, sans-serif; }
|
||||
.backdrop { display: grid; place-items: start center; width: 100%; height: 100%; padding-top: min(12vh, 90px); box-sizing: border-box; background: var(--pi-overlay); }
|
||||
section { width: min(720px, calc(100vw - 40px)); max-height: min(700px, calc(100vh - 40px)); display: flex; flex-direction: column; border: 1px solid var(--pi-border); border-radius: 12px; background: var(--pi-bg); box-shadow: 0 20px 60px var(--pi-shadow-strong); overflow: hidden; }
|
||||
header, footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px; border-bottom: 1px solid var(--pi-border); }
|
||||
footer { border-top: 1px solid var(--pi-border); border-bottom: 0; justify-content: end; }
|
||||
.body { display: grid; gap: 12px; padding: 12px; min-height: 0; }
|
||||
label { display: grid; gap: 6px; color: #8b949e; }
|
||||
input[type="text"], input:not([type]) { box-sizing: border-box; width: 100%; border: 1px solid #30363d; border-radius: 8px; background: #0d1117; color: #e6edf3; padding: 9px; font: 14px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
.check { display: flex; grid-template-columns: auto 1fr; align-items: center; color: #e6edf3; }
|
||||
.suggestions { min-height: 90px; max-height: 320px; overflow: auto; border: 1px solid #30363d; border-radius: 8px; background: #161b22; }
|
||||
.suggestions button { display: block; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border: 0; border-bottom: 1px solid #30363d; border-radius: 0; background: transparent; color: #e6edf3; padding: 8px 10px; text-align: left; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
.suggestions button.selected, .suggestions button:hover { background: #0d2847; }
|
||||
.hint { padding: 12px; color: #8b949e; }
|
||||
button { border: 1px solid #30363d; border-radius: 8px; background: #161b22; color: #e6edf3; padding: 7px 9px; cursor: pointer; }
|
||||
header button { border: 0; background: transparent; color: #8b949e; font-size: 22px; padding: 0 8px; }
|
||||
.primary { border-color: #238636; background: #238636; }
|
||||
label { display: grid; gap: 6px; color: var(--pi-muted); }
|
||||
input[type="text"], input:not([type]) { box-sizing: border-box; width: 100%; border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-bg); color: var(--pi-text); padding: 9px; font: 14px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
.check { display: flex; grid-template-columns: auto 1fr; align-items: center; color: var(--pi-text); }
|
||||
.suggestions { min-height: 90px; max-height: 320px; overflow: auto; border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); }
|
||||
.suggestions button { display: block; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border: 0; border-bottom: 1px solid var(--pi-border); border-radius: 0; background: transparent; color: var(--pi-text); padding: 8px 10px; text-align: left; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
.suggestions button.selected, .suggestions button:hover { background: var(--pi-selection-bg); }
|
||||
.hint { padding: 12px; color: var(--pi-muted); }
|
||||
button { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); color: var(--pi-text); padding: 7px 9px; cursor: pointer; }
|
||||
header button { border: 0; background: transparent; color: var(--pi-muted); font-size: 22px; padding: 0 8px; }
|
||||
.primary { border-color: var(--pi-success-border); background: var(--pi-success-border); }
|
||||
button:disabled { opacity: .5; cursor: not-allowed; }
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import { Terminal, type ITerminalOptions } from "@xterm/xterm";
|
||||
import { Terminal, type ITerminalOptions, type ITheme } from "@xterm/xterm";
|
||||
import { FitAddon, type ITerminalDimensions } from "@xterm/addon-fit";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
import { terminalSocket, terminalsApi, type TerminalInfo, type Workspace } from "../api";
|
||||
|
||||
const TERMINAL_OPTIONS: ITerminalOptions = {
|
||||
const TERMINAL_OPTIONS_BASE: ITerminalOptions = {
|
||||
cursorBlink: true,
|
||||
convertEol: true,
|
||||
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
||||
fontSize: 13,
|
||||
theme: { background: "#05070a", foreground: "#e6edf3", cursor: "#58a6ff", selectionBackground: "#264f78" },
|
||||
};
|
||||
|
||||
const DEFAULT_TERMINAL_SIZE: TerminalSize = { cols: 100, rows: 30 };
|
||||
@@ -31,10 +30,17 @@ export class TerminalPanel extends LitElement {
|
||||
private socket: WebSocket | undefined;
|
||||
private resizeObserver: ResizeObserver | undefined;
|
||||
private intersectionObserver: IntersectionObserver | undefined;
|
||||
private themeObserver: MutationObserver | undefined;
|
||||
private suppressTerminalInput = false;
|
||||
private observedCwd: string | undefined;
|
||||
private loadedCwd: string | undefined;
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.themeObserver = new MutationObserver(() => { this.applyTerminalTheme(); });
|
||||
this.themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "style", "data-theme"] });
|
||||
}
|
||||
|
||||
override firstUpdated(): void {
|
||||
this.intersectionObserver = new IntersectionObserver((entries) => {
|
||||
this.visible = entries[0]?.isIntersecting === true;
|
||||
@@ -45,6 +51,8 @@ export class TerminalPanel extends LitElement {
|
||||
override disconnectedCallback(): void {
|
||||
this.intersectionObserver?.disconnect();
|
||||
this.intersectionObserver = undefined;
|
||||
this.themeObserver?.disconnect();
|
||||
this.themeObserver = undefined;
|
||||
this.disposeTerminalView();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
@@ -126,7 +134,7 @@ export class TerminalPanel extends LitElement {
|
||||
private ensureTerminalView(): void {
|
||||
const workspace = this.workspace;
|
||||
if (!this.visible || this.terminal !== undefined || this.selectedId === undefined || this.terminalHost === undefined || workspace === undefined) return;
|
||||
const terminal = new Terminal(TERMINAL_OPTIONS);
|
||||
const terminal = new Terminal(terminalOptions(this));
|
||||
const fitAddon = new FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.open(this.terminalHost);
|
||||
@@ -205,7 +213,7 @@ export class TerminalPanel extends LitElement {
|
||||
if (currentSize !== undefined) return currentSize;
|
||||
if (this.terminal !== undefined || this.terminalHost === undefined) return undefined;
|
||||
|
||||
const measuringTerminal = new Terminal(TERMINAL_OPTIONS);
|
||||
const measuringTerminal = new Terminal(terminalOptions(this));
|
||||
const measuringFitAddon = new FitAddon();
|
||||
measuringTerminal.loadAddon(measuringFitAddon);
|
||||
measuringTerminal.open(this.terminalHost);
|
||||
@@ -214,6 +222,10 @@ export class TerminalPanel extends LitElement {
|
||||
return size;
|
||||
}
|
||||
|
||||
private applyTerminalTheme(): void {
|
||||
if (this.terminal !== undefined) this.terminal.options.theme = terminalTheme(this);
|
||||
}
|
||||
|
||||
private send(message: { type: "input"; data: string } | { type: "resize"; cols: number; rows: number }): void {
|
||||
if (this.socket?.readyState === WebSocket.OPEN) this.socket.send(JSON.stringify(message));
|
||||
}
|
||||
@@ -249,21 +261,21 @@ export class TerminalPanel extends LitElement {
|
||||
|
||||
static override styles = css`
|
||||
:host { flex: 1 1 auto; min-height: 0; display: flex; }
|
||||
.terminal-shell { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden; background: #05070a; }
|
||||
.terminal-tabs { flex: 0 0 auto; display: flex; gap: 6px; align-items: center; padding: 6px; border-bottom: 1px solid #21262d; background: #0d1117; overflow: auto; }
|
||||
button { display: inline-flex; align-items: center; gap: 6px; min-width: 0; max-width: 180px; border: 1px solid #30363d; border-radius: 7px; background: #161b22; color: #e6edf3; padding: 5px 7px; cursor: pointer; }
|
||||
button.selected { border-color: #58a6ff; background: #0d2847; }
|
||||
button.new { flex: 0 0 auto; color: #8b949e; }
|
||||
.terminal-shell { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden; background: var(--pi-terminal-bg); }
|
||||
.terminal-tabs { flex: 0 0 auto; display: flex; gap: 6px; align-items: center; padding: 6px; border-bottom: 1px solid var(--pi-border-muted); background: var(--pi-bg); overflow: auto; }
|
||||
button { display: inline-flex; align-items: center; gap: 6px; min-width: 0; max-width: 180px; 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.new { flex: 0 0 auto; color: var(--pi-muted); }
|
||||
button span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
button small { color: #8b949e; font-size: 14px; line-height: 1; }
|
||||
button small:hover { color: #ff7b72; }
|
||||
button small { color: var(--pi-muted); font-size: 14px; line-height: 1; }
|
||||
button small:hover { color: var(--pi-danger); }
|
||||
button:disabled { opacity: .5; cursor: not-allowed; }
|
||||
.terminal-host { flex: 1 1 auto; min-height: 0; padding: 6px; box-sizing: border-box; overflow: hidden; }
|
||||
.terminal-host .xterm { height: 100%; cursor: text; position: relative; user-select: none; }
|
||||
.terminal-host .xterm.focus, .terminal-host .xterm:focus { outline: none; }
|
||||
.terminal-host .xterm-helpers { position: absolute; top: 0; z-index: 5; }
|
||||
.terminal-host .xterm-helper-textarea { position: absolute !important; left: -9999em !important; top: 0 !important; width: 0 !important; height: 0 !important; min-width: 0 !important; min-height: 0 !important; padding: 0 !important; border: 0 !important; margin: 0 !important; opacity: 0 !important; z-index: -5 !important; white-space: nowrap !important; overflow: hidden !important; resize: none !important; outline: 0 !important; appearance: none !important; }
|
||||
.terminal-host .xterm-viewport { position: absolute; inset: 0; overflow-y: scroll; cursor: default; background-color: #05070a; }
|
||||
.terminal-host .xterm-viewport { position: absolute; inset: 0; overflow-y: scroll; cursor: default; background-color: var(--pi-terminal-bg); }
|
||||
.terminal-host .xterm-screen { position: relative; }
|
||||
.terminal-host .xterm-screen canvas { position: absolute; left: 0; top: 0; }
|
||||
.terminal-host .xterm-char-measure-element { display: inline-block; visibility: hidden; position: absolute; top: 0; left: -9999em; line-height: normal; }
|
||||
@@ -272,8 +284,8 @@ export class TerminalPanel extends LitElement {
|
||||
.terminal-host .xterm-accessibility-tree { font-family: monospace; user-select: text; white-space: pre; }
|
||||
.terminal-host .xterm-accessibility-tree > div { transform-origin: left; width: fit-content; }
|
||||
.terminal-host .live-region { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
|
||||
.error { flex: 0 0 auto; margin: 0; padding: 8px; color: #ff7b72; border-bottom: 1px solid #30363d; background: #161b22; }
|
||||
.muted { margin: 10px; color: #8b949e; }
|
||||
.error { flex: 0 0 auto; margin: 0; padding: 8px; color: var(--pi-danger); border-bottom: 1px solid var(--pi-border); background: var(--pi-surface); }
|
||||
.muted { margin: 10px; color: var(--pi-muted); }
|
||||
.xterm { height: 100%; }
|
||||
`;
|
||||
}
|
||||
@@ -312,6 +324,24 @@ async function socketDataToString(data: unknown): Promise<string> {
|
||||
return String(data);
|
||||
}
|
||||
|
||||
function terminalOptions(element: HTMLElement): ITerminalOptions {
|
||||
return { ...TERMINAL_OPTIONS_BASE, theme: terminalTheme(element) };
|
||||
}
|
||||
|
||||
function terminalTheme(element: HTMLElement): ITheme {
|
||||
return {
|
||||
background: themeColor(element, "--pi-terminal-bg", "#05070a"),
|
||||
foreground: themeColor(element, "--pi-terminal-text", "#e6edf3"),
|
||||
cursor: themeColor(element, "--pi-accent", "#58a6ff"),
|
||||
selectionBackground: themeColor(element, "--pi-terminal-selection", "#264f78"),
|
||||
};
|
||||
}
|
||||
|
||||
function themeColor(element: HTMLElement, name: string, fallback: string): string {
|
||||
const value = getComputedStyle(element).getPropertyValue(name).trim();
|
||||
return value === "" ? fallback : value;
|
||||
}
|
||||
|
||||
function terminalSizeFromDimensions(dimensions: ITerminalDimensions | undefined): TerminalSize | undefined {
|
||||
if (dimensions === undefined || !isValidTerminalSize(dimensions.cols, dimensions.rows)) return undefined;
|
||||
return { cols: Math.floor(dimensions.cols), rows: Math.floor(dimensions.rows) };
|
||||
|
||||
+146
-144
@@ -30,18 +30,18 @@ export interface CompletionItem {
|
||||
}
|
||||
|
||||
export const appStyles = css`
|
||||
:host { display: block; height: 100dvh; box-sizing: border-box; padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); color: #e6edf3; background: #0d1117; font: 14px system-ui, sans-serif; }
|
||||
:host { display: block; height: 100dvh; box-sizing: border-box; padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); color: var(--pi-text); background: var(--pi-bg); font: 14px system-ui, sans-serif; }
|
||||
.shell { display: grid; grid-template-columns: 340px minmax(420px, 1fr) minmax(360px, 42vw); height: 100%; min-height: 0; }
|
||||
aside { display: flex; flex-direction: column; min-height: 0; border-right: 1px solid #30363d; overflow: hidden; }
|
||||
header { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px; border-bottom: 1px solid #30363d; }
|
||||
project-list, workspace-list { flex: 0 0 auto; max-height: 26%; overflow: auto; border-bottom: 1px solid #21262d; }
|
||||
aside { display: flex; flex-direction: column; min-height: 0; border-right: 1px solid var(--pi-border); overflow: hidden; }
|
||||
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); }
|
||||
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; }
|
||||
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 #30363d; overflow-x: auto; }
|
||||
.mobile-tabs { display: none; flex: 0 0 auto; gap: 6px; padding: 8px; border-bottom: 1px solid var(--pi-border); overflow-x: auto; }
|
||||
.mobile-navigation-tab, .mobile-navigation-panel { display: none; }
|
||||
.mobile-tabs button.selected { border-color: #58a6ff; background: #0d2847; }
|
||||
.tab-badge { display: inline-block; min-width: 14px; margin-left: 4px; border: 1px solid #238636; border-radius: 999px; background: #0f2a16; color: #3fb950; padding: 0 5px; font-size: 11px; line-height: 16px; text-align: center; }
|
||||
workspace-panel { min-width: 0; min-height: 0; border-left: 1px solid #30363d; overflow: hidden; }
|
||||
.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; }
|
||||
workspace-panel { min-width: 0; min-height: 0; border-left: 1px solid var(--pi-border); overflow: hidden; }
|
||||
@media (max-width: 1180px) {
|
||||
.shell { grid-template-columns: 340px minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
|
||||
aside { grid-row: 1 / 3; }
|
||||
@@ -72,42 +72,42 @@ export const appStyles = css`
|
||||
status-bar { flex: 0 0 auto; }
|
||||
chat-view { flex: 1 1 auto; min-height: 0; overflow: hidden; }
|
||||
prompt-editor, chat-composer { flex: 0 0 auto; }
|
||||
button { border: 1px solid #30363d; border-radius: 8px; background: #161b22; color: #e6edf3; padding: 7px 9px; cursor: pointer; }
|
||||
.empty { margin: auto; color: #8b949e; }
|
||||
.error { padding: 10px 16px; border-bottom: 1px solid #30363d; color: #ff7b72; }
|
||||
button { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); color: var(--pi-text); padding: 7px 9px; cursor: pointer; }
|
||||
.empty { margin: auto; color: var(--pi-muted); }
|
||||
.error { padding: 10px 16px; border-bottom: 1px solid var(--pi-border); color: var(--pi-danger); }
|
||||
`;
|
||||
|
||||
export const workspacePanelStyles = css`
|
||||
:host { display: flex; flex-direction: column; min-height: 0; color: #e6edf3; background: #0d1117; 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 #30363d; }
|
||||
: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); }
|
||||
.tabs { display: flex; gap: 6px; }
|
||||
button { display: inline-flex; align-items: center; gap: 5px; border: 1px solid #30363d; border-radius: 7px; background: #161b22; color: #e6edf3; padding: 5px 7px; cursor: pointer; }
|
||||
button.selected { border-color: #58a6ff; background: #0d2847; }
|
||||
.tab-badge { display: inline-block; min-width: 14px; border: 1px solid #238636; border-radius: 999px; background: #0f2a16; color: #3fb950; padding: 0 5px; font-size: 11px; line-height: 16px; text-align: center; }
|
||||
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; }
|
||||
.panel-content { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
|
||||
small, .muted { color: #8b949e; }
|
||||
small, .muted { color: var(--pi-muted); }
|
||||
header small { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
@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-base, .workspace-label-item, .workspace-label-render { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||
.workspace-label-item, .workspace-label-render, .workspace-label-separator { color: #8b949e; }
|
||||
.workspace-label-link { color: #58a6ff; text-decoration: none; }
|
||||
.workspace-label-item, .workspace-label-render, .workspace-label-separator { color: var(--pi-muted); }
|
||||
.workspace-label-link { color: var(--pi-accent); text-decoration: none; }
|
||||
.workspace-label-link:hover, .workspace-label-link:focus { text-decoration: underline; }
|
||||
.toolbar { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; padding: 8px; border-bottom: 1px solid #21262d; }
|
||||
.toolbar { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; padding: 8px; border-bottom: 1px solid var(--pi-border-muted); }
|
||||
.toolbar button { margin-left: auto; }
|
||||
.stale { border: 1px solid #6e5200; border-radius: 999px; color: #d29922; padding: 1px 6px; font-size: 12px; }
|
||||
.stale { border: 1px solid var(--pi-warning-border); border-radius: 999px; color: var(--pi-warning); padding: 1px 6px; font-size: 12px; }
|
||||
.split { flex: 1 1 auto; min-height: 0; display: grid; grid-template-rows: minmax(160px, 34%) minmax(0, 1fr); }
|
||||
.list { min-height: 0; overflow: auto; border-bottom: 1px solid #30363d; padding: 6px; }
|
||||
.list { min-height: 0; overflow: auto; border-bottom: 1px solid var(--pi-border); padding: 6px; }
|
||||
.row { display: grid; grid-template-columns: 18px minmax(0, 1fr); gap: 4px; width: 100%; border: 0; border-radius: 5px; background: transparent; text-align: left; padding: 4px 6px 4px calc(6px + var(--depth, 0) * 14px); }
|
||||
.row:hover, .row.selected { background: #0d2847; }
|
||||
.row:hover, .row.selected { background: var(--pi-selection-bg); }
|
||||
.row span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.summary { margin: 4px 6px 8px; color: #8b949e; }
|
||||
.summary { margin: 4px 6px 8px; color: var(--pi-muted); }
|
||||
.viewer { min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
|
||||
.diffs { flex: 1 1 auto; min-height: 0; overflow: auto; display: grid; grid-template-rows: minmax(120px, 1fr) minmax(120px, 1fr); }
|
||||
.diffs.single { grid-template-rows: minmax(0, 1fr); }
|
||||
.diff-section { min-height: 0; display: flex; flex-direction: column; border-bottom: 1px solid #30363d; }
|
||||
.diff-section { min-height: 0; display: flex; flex-direction: column; border-bottom: 1px solid var(--pi-border); }
|
||||
.diff-section:last-child { border-bottom: 0; }
|
||||
.viewer-header { position: sticky; top: 0; display: flex; justify-content: space-between; gap: 8px; padding: 8px; border-bottom: 1px solid #21262d; background: #0d1117; }
|
||||
.viewer-header { position: sticky; top: 0; display: flex; justify-content: space-between; gap: 8px; padding: 8px; border-bottom: 1px solid var(--pi-border-muted); background: var(--pi-bg); }
|
||||
.viewer-header strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
code-viewer { flex: 1 1 auto; min-height: 0; }
|
||||
pre { margin: 0; padding: 10px; overflow: auto; font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
@@ -115,95 +115,97 @@ export const workspacePanelStyles = css`
|
||||
`;
|
||||
|
||||
export const listStyles = css`
|
||||
:host { display: block; color: #e6edf3; font: 14px system-ui, sans-serif; }
|
||||
:host { display: block; color: var(--pi-text); font: 14px system-ui, sans-serif; }
|
||||
:host([collapsed]) { flex: 0 0 auto; min-height: auto; overflow: hidden; }
|
||||
section { padding: 10px; }
|
||||
h2 { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin: 0 0 8px; color: #8b949e; font-size: 12px; text-transform: uppercase; }
|
||||
button { border: 1px solid #30363d; border-radius: 8px; background: #161b22; color: #e6edf3; padding: 7px 9px; cursor: pointer; }
|
||||
h2 { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin: 0 0 8px; color: var(--pi-muted); font-size: 12px; text-transform: uppercase; }
|
||||
button { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); color: var(--pi-text); padding: 7px 9px; cursor: pointer; }
|
||||
section > button { display: block; width: 100%; text-align: left; margin: 6px 0; }
|
||||
.subheading { margin-top: 14px; }
|
||||
.section-toggle { display: flex; flex: 1 1 auto; min-width: 0; align-items: center; justify-content: space-between; gap: 8px; width: 100%; border: 0; background: transparent; color: inherit; padding: 0; font: inherit; text-transform: inherit; }
|
||||
.section-toggle span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.section-toggle small { display: inline; color: inherit; font-size: inherit; }
|
||||
.action-row { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto; margin: 6px 0; cursor: pointer; }
|
||||
.action-row:focus-visible { outline: 2px solid #58a6ff; outline-offset: 2px; border-radius: 8px; }
|
||||
.action-row.selected .action-main, .action-row.selected .action-menu-toggle { border-color: #58a6ff; background: #0d2847; }
|
||||
.action-row.archived .action-main { color: #8b949e; }
|
||||
.action-main { box-sizing: border-box; min-width: 0; width: 100%; border: 1px solid #30363d; border-top-right-radius: 0; border-bottom-right-radius: 0; border-top-left-radius: 8px; border-bottom-left-radius: 8px; background: #161b22; color: #e6edf3; padding: 7px 9px 7px calc(9px + var(--depth, 0) * 16px); text-align: left; }
|
||||
.action-row:not(.selected):hover .action-main { background: #21262d; }
|
||||
.action-row:focus-visible { outline: 2px solid var(--pi-accent); outline-offset: 2px; border-radius: 8px; }
|
||||
.action-row.selected .action-main, .action-row.selected .action-menu-toggle { border-color: var(--pi-accent); background: var(--pi-selection-bg); }
|
||||
.action-row.archived .action-main { color: var(--pi-muted); }
|
||||
.action-main { box-sizing: border-box; min-width: 0; width: 100%; border: 1px solid var(--pi-border); border-top-right-radius: 0; border-bottom-right-radius: 0; border-top-left-radius: 8px; border-bottom-left-radius: 8px; background: var(--pi-surface); color: var(--pi-text); padding: 7px 9px 7px calc(9px + var(--depth, 0) * 16px); text-align: left; }
|
||||
.action-row:not(.selected):hover .action-main { background: var(--pi-surface-hover); }
|
||||
.workspace-row .action-main { border-radius: 8px; }
|
||||
.tree-marker { color: #6e7681; margin-right: 5px; }
|
||||
.badge { display: inline-block; margin-left: 5px; border: 1px solid #30363d; border-radius: 999px; color: #8b949e; padding: 0 5px; font-size: 11px; font-weight: 400; }
|
||||
.tree-marker { color: var(--pi-dim); margin-right: 5px; }
|
||||
.badge { display: inline-block; margin-left: 5px; border: 1px solid var(--pi-border); border-radius: 999px; color: var(--pi-muted); padding: 0 5px; font-size: 11px; font-weight: 400; }
|
||||
.action-menu { position: relative; align-self: stretch; }
|
||||
.action-menu-toggle { display: grid; place-items: center; height: 100%; min-width: 32px; padding: 0; color: #8b949e; border-left: 0; border-top-left-radius: 0; border-bottom-left-radius: 0; }
|
||||
.action-menu-toggle:hover { color: #e6edf3; background: #21262d; }
|
||||
.action-menu-panel { position: fixed; z-index: 50; min-width: 120px; padding: 4px; border: 1px solid #30363d; border-radius: 8px; background: #161b22; box-shadow: 0 8px 24px #0008; }
|
||||
.action-menu-panel button { display: block; width: 100%; text-align: left; border: 0; background: transparent; color: #e6edf3; }
|
||||
.action-menu-panel button:hover { background: #0d2847; }
|
||||
button.selected { border-color: #58a6ff; background: #0d2847; }
|
||||
.action-menu-toggle { display: grid; place-items: center; height: 100%; min-width: 32px; padding: 0; color: var(--pi-muted); border-left: 0; border-top-left-radius: 0; border-bottom-left-radius: 0; }
|
||||
.action-menu-toggle:hover { color: var(--pi-text); background: var(--pi-surface-hover); }
|
||||
.action-menu-panel { position: fixed; z-index: 50; min-width: 120px; padding: 4px; border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); box-shadow: 0 8px 24px var(--pi-shadow); }
|
||||
.action-menu-panel button { display: block; width: 100%; text-align: left; border: 0; background: transparent; color: var(--pi-text); }
|
||||
.action-menu-panel button:hover { background: var(--pi-selection-bg); }
|
||||
button.selected { border-color: var(--pi-accent); background: var(--pi-selection-bg); }
|
||||
button:disabled { opacity: .5; cursor: not-allowed; }
|
||||
small { display: block; color: #8b949e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
small { display: block; color: var(--pi-muted); overflow: hidden; text-overflow: ellipsis; 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-item, .workspace-label-render, .workspace-label-separator { color: #8b949e; }
|
||||
.workspace-label-link { color: #58a6ff; text-decoration: none; }
|
||||
.workspace-label-item, .workspace-label-render, .workspace-label-separator { color: var(--pi-muted); }
|
||||
.workspace-label-link { color: var(--pi-accent); text-decoration: none; }
|
||||
.workspace-label-link:hover, .workspace-label-link:focus { text-decoration: underline; }
|
||||
`;
|
||||
|
||||
export const chatStyles = css`
|
||||
:host { display: flex; flex-direction: column; min-height: 0; overflow: hidden; color: #e6edf3; font: 14px system-ui, sans-serif; }
|
||||
:host { display: flex; flex-direction: column; min-height: 0; overflow: hidden; color: var(--pi-text); font: 14px system-ui, sans-serif; }
|
||||
.chat-wrap { position: relative; flex: 1 1 auto; min-height: 0; overflow: hidden; }
|
||||
.chat { height: 100%; min-height: 0; overflow: auto; overflow-anchor: none; padding: 16px 16px 64px; box-sizing: border-box; }
|
||||
.scroll-marker { display: block; height: 0; overflow: hidden; pointer-events: none; }
|
||||
.history-indicator { position: absolute; top: 10px; right: 18px; z-index: 2; display: grid; gap: 2px; max-width: min(320px, calc(100% - 36px)); border: 1px solid #30363d; border-radius: 8px; background: #0d1117dd; color: #8b949e; padding: 6px 8px; font-size: 12px; text-align: right; pointer-events: none; box-shadow: 0 8px 24px #0006; }
|
||||
.activity-dock { position: absolute; left: 16px; right: 16px; bottom: 12px; z-index: 3; display: flex; align-items: center; gap: 8px; min-width: 0; box-sizing: border-box; border: 1px solid #30363d; border-radius: 999px; background: #0d1117e6; color: #8b949e; padding: 8px 12px; font-size: 13px; pointer-events: none; box-shadow: 0 8px 28px #0008; backdrop-filter: blur(6px); }
|
||||
.activity-dock.active { border-color: #238636; color: #3fb950; background: #0f1b12ee; }
|
||||
.history-indicator { position: absolute; top: 10px; right: 18px; z-index: 2; display: grid; gap: 2px; max-width: min(320px, calc(100% - 36px)); border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-bg-overlay-soft); color: var(--pi-muted); padding: 6px 8px; font-size: 12px; text-align: right; pointer-events: none; box-shadow: 0 8px 24px var(--pi-shadow-soft); }
|
||||
.activity-dock { position: absolute; left: 16px; right: 16px; bottom: 12px; z-index: 3; display: flex; align-items: center; gap: 8px; min-width: 0; box-sizing: border-box; border: 1px solid var(--pi-border); border-radius: 999px; background: var(--pi-bg-overlay); color: var(--pi-muted); padding: 8px 12px; font-size: 13px; pointer-events: none; box-shadow: 0 8px 28px var(--pi-shadow); backdrop-filter: blur(6px); }
|
||||
.activity-dock.active { border-color: var(--pi-success-border); color: var(--pi-success); background: var(--pi-success-bg-overlay); }
|
||||
.activity-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; opacity: .45; flex: 0 0 auto; }
|
||||
.activity-dock.active .dot { animation: pulse 1s ease-in-out infinite; opacity: 1; }
|
||||
.msg { max-width: 100%; min-width: 0; box-sizing: border-box; margin: 0 0 14px; padding: 12px; border: 1px solid #30363d; border-radius: 10px; background: #161b22; overflow: hidden; }
|
||||
.msg.user { border-color: #2f81f7; background: #0d2847; }
|
||||
.msg.tool { border-color: #6e5200; background: #1f1a10; color: #d29922; }
|
||||
.msg.system { color: #ff7b72; }
|
||||
.msg.bash { border-color: #3fb950; background: #0f1b12; }
|
||||
.msg.skill { border-color: #a371f7; background: #21132f; }
|
||||
.msg.event-group { padding: 0; border-color: #30363d; background: #0d1117; color: #8b949e; }
|
||||
.msg.event-group > summary { display: flex; align-items: center; gap: 8px; padding: 8px 12px; color: #8b949e; }
|
||||
.msg { max-width: 100%; min-width: 0; box-sizing: border-box; margin: 0 0 14px; padding: 12px; border: 1px solid var(--pi-border); border-radius: 10px; background: var(--pi-surface); overflow: hidden; }
|
||||
.msg.user { border-color: var(--pi-accent-border); background: var(--pi-selection-bg); }
|
||||
.msg.tool { border-color: var(--pi-warning-border); background: var(--pi-warning-surface); color: var(--pi-warning); }
|
||||
.msg.system { color: var(--pi-danger); }
|
||||
.msg.bash { border-color: var(--pi-success); background: var(--pi-success-bg); }
|
||||
.msg.skill { border-color: var(--pi-purple-border); background: var(--pi-purple-surface); }
|
||||
.msg.event-group { padding: 0; border-color: var(--pi-border); background: var(--pi-bg); color: var(--pi-muted); }
|
||||
.msg.event-group.live { border-color: var(--pi-success-border); background: var(--pi-success-bg); }
|
||||
.msg.event-group > summary { display: flex; align-items: center; gap: 8px; padding: 8px 12px; color: var(--pi-muted); }
|
||||
.msg.event-group.live > summary { color: var(--pi-success); }
|
||||
.msg.event-group > summary .label { margin: 0; }
|
||||
.group-body { padding: 0 12px 12px; }
|
||||
.group-msg { max-width: 100%; min-width: 0; box-sizing: border-box; padding: 10px 0; border-top: 1px solid #21262d; color: #e6edf3; overflow: hidden; }
|
||||
.group-msg.tool { color: #d29922; }
|
||||
.group-msg.system { color: #ff7b72; }
|
||||
.group-msg.bash { color: #3fb950; }
|
||||
.history-boundary { display: grid; gap: 3px; justify-items: center; margin: 0 0 14px; color: #8b949e; font-size: 12px; text-align: center; }
|
||||
.history-load-button { border: 1px solid #30363d; border-radius: 999px; background: #161b22; color: #c9d1d9; padding: 5px 12px; font: 12px system-ui, sans-serif; cursor: pointer; }
|
||||
.history-load-button:hover, .history-load-button:focus { border-color: #58a6ff; color: #f0f6fc; }
|
||||
.group-msg { max-width: 100%; min-width: 0; box-sizing: border-box; padding: 10px 0; border-top: 1px solid var(--pi-border-muted); color: var(--pi-text); overflow: hidden; }
|
||||
.group-msg.tool { color: var(--pi-warning); }
|
||||
.group-msg.system { color: var(--pi-danger); }
|
||||
.group-msg.bash { color: var(--pi-success); }
|
||||
.history-boundary { display: grid; gap: 3px; justify-items: center; margin: 0 0 14px; color: var(--pi-muted); font-size: 12px; text-align: center; }
|
||||
.history-load-button { border: 1px solid var(--pi-border); border-radius: 999px; background: var(--pi-surface); color: var(--pi-text-secondary); padding: 5px 12px; font: 12px system-ui, sans-serif; cursor: pointer; }
|
||||
.history-load-button:hover, .history-load-button:focus { border-color: var(--pi-accent); color: var(--pi-text-bright); }
|
||||
.history-load-button:disabled { cursor: default; opacity: .55; }
|
||||
.queued-messages { max-width: 100%; min-width: 0; box-sizing: border-box; display: grid; gap: 8px; margin: 0 0 14px; padding: 12px; border: 1px solid #6e5200; border-radius: 10px; background: #1f1a10; color: #e6edf3; overflow: hidden; }
|
||||
.queued-messages { max-width: 100%; min-width: 0; box-sizing: border-box; display: grid; gap: 8px; margin: 0 0 14px; padding: 12px; border: 1px solid var(--pi-warning-border); border-radius: 10px; background: var(--pi-warning-surface); color: var(--pi-text); overflow: hidden; }
|
||||
.queued-header { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
|
||||
.queued-header strong { color: #d29922; }
|
||||
.queued-header small { color: #8b949e; }
|
||||
.queued-message { display: grid; gap: 4px; padding-top: 8px; border-top: 1px solid #30363d; }
|
||||
.queued-header strong { color: var(--pi-warning); }
|
||||
.queued-header small { color: var(--pi-muted); }
|
||||
.queued-message { display: grid; gap: 4px; padding-top: 8px; border-top: 1px solid var(--pi-border); }
|
||||
.queued-message:first-of-type { padding-top: 0; border-top: 0; }
|
||||
.queued-kind { color: #8b949e; font-size: 12px; text-transform: uppercase; }
|
||||
.session-activity { max-width: 100%; min-width: 0; box-sizing: border-box; display: grid; gap: 4px; margin: 0 0 14px; padding: 12px; border: 1px solid #30363d; border-radius: 10px; background: #161b22; color: #e6edf3; overflow: hidden; }
|
||||
.session-activity.compacting { border-color: #a371f7; background: #21132f; }
|
||||
.session-activity.receiving { border-color: #238636; background: #0f1b12; }
|
||||
.session-activity strong { color: #d2a8ff; }
|
||||
.session-activity.receiving strong { color: #3fb950; }
|
||||
.session-activity span, .session-activity small { color: #8b949e; }
|
||||
.history-boundary small { color: #6e7681; }
|
||||
.queued-kind { color: var(--pi-muted); font-size: 12px; text-transform: uppercase; }
|
||||
.session-activity { max-width: 100%; min-width: 0; box-sizing: border-box; display: grid; gap: 4px; margin: 0 0 14px; padding: 12px; border: 1px solid var(--pi-border); border-radius: 10px; background: var(--pi-surface); color: var(--pi-text); overflow: hidden; }
|
||||
.session-activity.compacting { border-color: var(--pi-purple-border); background: var(--pi-purple-surface); }
|
||||
.session-activity.receiving { border-color: var(--pi-success-border); background: var(--pi-success-bg); }
|
||||
.session-activity strong { color: var(--pi-purple); }
|
||||
.session-activity.receiving strong { color: var(--pi-success); }
|
||||
.session-activity span, .session-activity small { color: var(--pi-muted); }
|
||||
.history-boundary small { color: var(--pi-dim); }
|
||||
.msg-header { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
|
||||
.msg-header-trailing { min-width: 0; display: inline-flex; align-items: baseline; justify-content: flex-end; gap: 8px; }
|
||||
.msg-actions { display: inline-flex; gap: 6px; opacity: 0; transition: opacity .12s ease; }
|
||||
.msg-action { display: inline-grid; place-items: center; width: 24px; height: 24px; border: 1px solid #30363d; border-radius: 6px; background: #161b22; color: #8b949e; padding: 0; font: 14px system-ui, sans-serif; line-height: 1; cursor: pointer; }
|
||||
.msg-action:hover, .msg-action:focus { color: #e6edf3; border-color: #58a6ff; }
|
||||
.msg-action { display: inline-grid; place-items: center; width: 24px; height: 24px; border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-muted); padding: 0; font: 14px system-ui, sans-serif; line-height: 1; cursor: pointer; }
|
||||
.msg-action:hover, .msg-action:focus { color: var(--pi-text); border-color: var(--pi-accent); }
|
||||
.msg:hover > .msg-header .msg-actions, .msg:focus-within > .msg-header .msg-actions, .group-msg:hover > .msg-header .msg-actions, .group-msg:focus-within > .msg-header .msg-actions { opacity: 1; }
|
||||
.label { display: block; color: #8b949e; font-size: 12px; text-transform: uppercase; }
|
||||
.label { display: block; color: var(--pi-muted); font-size: 12px; text-transform: uppercase; }
|
||||
.msg-header .label { margin: 0; }
|
||||
.msg-meta { min-width: 0; opacity: .28; border: 0; background: transparent; color: #6e7681; padding: 0; font: 11px system-ui, sans-serif; text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: opacity .12s ease, max-width .12s ease; cursor: pointer; user-select: text; -webkit-user-select: text; }
|
||||
.msg-meta { min-width: 0; opacity: .28; border: 0; background: transparent; color: var(--pi-dim); padding: 0; font: 11px system-ui, sans-serif; text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: opacity .12s ease, max-width .12s ease; cursor: pointer; user-select: text; -webkit-user-select: text; }
|
||||
.msg:hover > .msg-header .msg-meta, .msg:focus-within > .msg-header .msg-meta, .group-msg:hover > .msg-header .msg-meta, .group-msg:focus-within > .msg-header .msg-meta, .msg-meta:focus, .msg-meta.expanded { opacity: 1; }
|
||||
.msg-meta:focus { outline: 1px solid #30363d; outline-offset: 3px; border-radius: 4px; }
|
||||
.msg-meta:focus { outline: 1px solid var(--pi-border); outline-offset: 3px; border-radius: 4px; }
|
||||
@media (hover: none) {
|
||||
.msg-actions { opacity: 1; }
|
||||
.msg-meta { opacity: .75; max-width: 26px; }
|
||||
@@ -214,16 +216,16 @@ export const chatStyles = css`
|
||||
formatted-text.part { display: block; }
|
||||
.part { max-width: 100%; min-width: 0; box-sizing: border-box; overflow: visible; }
|
||||
.part + .part { margin-top: 10px; }
|
||||
.tool-line { color: #d29922; }
|
||||
.summary { color: #8b949e; margin-left: 6px; }
|
||||
.part:is(details) { border-top: 1px solid #30363d; padding-top: 8px; }
|
||||
.tool-line { color: var(--pi-warning); }
|
||||
.summary { color: var(--pi-muted); margin-left: 6px; }
|
||||
.part:is(details) { border-top: 1px solid var(--pi-border); padding-top: 8px; }
|
||||
.part > formatted-text { display: block; max-width: 100%; min-width: 0; overflow: visible; }
|
||||
.skill-invocation, .skill-read { border: 1px solid #30363d; border-radius: 8px; background: #161b22; padding: 8px 10px; }
|
||||
.skill-invocation > summary, .skill-read > strong { color: #d2a8ff; }
|
||||
.skill-invocation > small, .skill-read > small { display: block; margin: 6px 0 0; color: #8b949e; }
|
||||
summary { cursor: pointer; color: #8b949e; }
|
||||
.skill-invocation, .skill-read { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); padding: 8px 10px; }
|
||||
.skill-invocation > summary, .skill-read > strong { color: var(--pi-purple); }
|
||||
.skill-invocation > small, .skill-read > small { display: block; margin: 6px 0 0; color: var(--pi-muted); }
|
||||
summary { cursor: pointer; color: var(--pi-muted); }
|
||||
pre { margin: 6px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
|
||||
.shell-output { color: #e6edf3; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.45; }
|
||||
.shell-output { color: var(--pi-text); font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.45; }
|
||||
@keyframes pulse { 0%, 100% { transform: scale(.75); opacity: .55; } 50% { transform: scale(1.2); opacity: 1; } }
|
||||
`;
|
||||
|
||||
@@ -234,15 +236,15 @@ export const formattedTextStyles = css`
|
||||
:is(p, ul, ol, pre, blockquote, table, .code-block-wrapper):last-child { margin-bottom: 0; }
|
||||
ul, ol { padding-left: 22px; }
|
||||
li + li { margin-top: 3px; }
|
||||
code { border: 1px solid #30363d; border-radius: 4px; background: #0d1117; padding: 1px 4px; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
code { border: 1px solid var(--pi-border); border-radius: 4px; background: var(--pi-bg); padding: 1px 4px; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
.code-block-wrapper { position: relative; }
|
||||
.code-block-wrapper pre { margin: 0; padding-right: 40px; }
|
||||
pre { border: 1px solid #30363d; border-radius: 8px; background: #0d1117; padding: 10px; overflow-x: auto; overflow-y: hidden; }
|
||||
pre { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-bg); padding: 10px; overflow-x: auto; overflow-y: hidden; }
|
||||
pre code { border: 0; padding: 0; background: transparent; }
|
||||
.code-copy-button { position: absolute; top: 6px; right: 6px; z-index: 1; display: inline-grid; place-items: center; width: 24px; height: 24px; border: 1px solid #30363d; border-radius: 6px; background: #161b22; color: #8b949e; padding: 0; font: 14px system-ui, sans-serif; line-height: 1; cursor: pointer; }
|
||||
.code-copy-button:hover, .code-copy-button:focus { color: #e6edf3; border-color: #58a6ff; }
|
||||
blockquote { border-left: 3px solid #30363d; padding-left: 10px; color: #8b949e; }
|
||||
a { color: #58a6ff; }
|
||||
.code-copy-button { position: absolute; top: 6px; right: 6px; z-index: 1; display: inline-grid; place-items: center; width: 24px; height: 24px; border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-muted); padding: 0; font: 14px system-ui, sans-serif; line-height: 1; cursor: pointer; }
|
||||
.code-copy-button:hover, .code-copy-button:focus { color: var(--pi-text); border-color: var(--pi-accent); }
|
||||
blockquote { border-left: 3px solid var(--pi-border); padding-left: 10px; color: var(--pi-muted); }
|
||||
a { color: var(--pi-accent); }
|
||||
h1, h2, h3, h4 { margin: 14px 0 8px; line-height: 1.2; }
|
||||
h1:first-child, h2:first-child, h3:first-child, h4:first-child { margin-top: 0; }
|
||||
h1 { font-size: 20px; }
|
||||
@@ -250,94 +252,94 @@ export const formattedTextStyles = css`
|
||||
h3 { font-size: 15px; }
|
||||
h4 { font-size: 14px; }
|
||||
table { border-collapse: collapse; display: block; overflow-x: auto; overflow-y: hidden; }
|
||||
th, td { border: 1px solid #30363d; padding: 4px 8px; }
|
||||
th { background: #161b22; }
|
||||
th, td { border: 1px solid var(--pi-border); padding: 4px 8px; }
|
||||
th { background: var(--pi-surface); }
|
||||
`;
|
||||
|
||||
export const statusBarStyles = css`
|
||||
:host { display: block; color: #8b949e; font: 12px system-ui, sans-serif; }
|
||||
.bar { display: flex; gap: 12px; align-items: center; min-width: 0; padding: 7px 12px; border-top: 1px solid #30363d; background: #0d1117; white-space: nowrap; overflow: hidden; }
|
||||
:host { display: block; color: var(--pi-muted); font: 12px system-ui, sans-serif; }
|
||||
.bar { display: flex; gap: 12px; align-items: center; min-width: 0; padding: 7px 12px; border-top: 1px solid var(--pi-border); background: var(--pi-bg); white-space: nowrap; overflow: hidden; }
|
||||
span { overflow: hidden; text-overflow: ellipsis; }
|
||||
.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-item, .workspace-label-render, .workspace-label-separator { color: #8b949e; }
|
||||
.workspace-label-link { color: #58a6ff; text-decoration: none; }
|
||||
.workspace-label-item, .workspace-label-render, .workspace-label-separator { color: var(--pi-muted); }
|
||||
.workspace-label-link { color: var(--pi-accent); text-decoration: none; }
|
||||
.workspace-label-link:hover, .workspace-label-link:focus { text-decoration: underline; }
|
||||
.bar > span:first-child { flex: 1 1 auto; min-width: 80px; }
|
||||
.activity { display: inline-flex; align-items: center; gap: 6px; color: #8b949e; }
|
||||
.activity.active { color: #3fb950; }
|
||||
.activity { display: inline-flex; align-items: center; gap: 6px; color: var(--pi-muted); }
|
||||
.activity.active { color: var(--pi-success); }
|
||||
.dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; opacity: .45; flex: 0 0 auto; }
|
||||
.activity.active .dot { animation: pulse 1s ease-in-out infinite; opacity: 1; }
|
||||
.muted { color: #6e7681; }
|
||||
.muted { color: var(--pi-dim); }
|
||||
@keyframes pulse { 0%, 100% { transform: scale(.75); opacity: .55; } 50% { transform: scale(1.2); opacity: 1; } }
|
||||
`;
|
||||
|
||||
export const autocompleteStyles = css`
|
||||
:host { display: block; }
|
||||
.menu { position: absolute; left: 0; right: 0; bottom: calc(100% + 6px); z-index: 10; max-height: 260px; overflow: auto; border: 1px solid #30363d; border-radius: 8px; background: #161b22; box-shadow: 0 10px 30px #0008; }
|
||||
button { display: grid; grid-template-columns: minmax(120px, 1fr) auto; gap: 4px 10px; width: 100%; border: 0; border-bottom: 1px solid #30363d; border-radius: 0; background: transparent; color: #e6edf3; padding: 8px 10px; text-align: left; cursor: pointer; }
|
||||
.menu { position: absolute; left: 0; right: 0; bottom: calc(100% + 6px); z-index: 10; max-height: 260px; overflow: auto; border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); box-shadow: 0 10px 30px var(--pi-shadow); }
|
||||
button { display: grid; grid-template-columns: minmax(120px, 1fr) auto; gap: 4px 10px; width: 100%; border: 0; border-bottom: 1px solid var(--pi-border); border-radius: 0; background: transparent; color: var(--pi-text); padding: 8px 10px; text-align: left; cursor: pointer; }
|
||||
button:last-child { border-bottom: 0; }
|
||||
button.selected, button:hover { background: #0d2847; }
|
||||
span { color: #8b949e; font-size: 12px; }
|
||||
small { grid-column: 1 / -1; color: #8b949e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
button.selected, button:hover { background: var(--pi-selection-bg); }
|
||||
span { color: var(--pi-muted); font-size: 12px; }
|
||||
small { grid-column: 1 / -1; color: var(--pi-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
`;
|
||||
|
||||
export const commandPickerStyles = css`
|
||||
:host { position: fixed; inset: 0; z-index: 10; color: #e6edf3; font: 14px system-ui, sans-serif; }
|
||||
.backdrop { display: grid; place-items: center; width: 100%; height: 100%; background: #0008; }
|
||||
section { width: min(720px, calc(100vw - 40px)); max-height: min(640px, calc(100vh - 40px)); display: flex; flex-direction: column; border: 1px solid #30363d; border-radius: 12px; background: #0d1117; box-shadow: 0 20px 60px #000b; overflow: hidden; }
|
||||
header { display: flex; align-items: center; justify-content: space-between; padding: 12px; border-bottom: 1px solid #30363d; }
|
||||
:host { position: fixed; inset: 0; z-index: 10; color: var(--pi-text); font: 14px system-ui, sans-serif; }
|
||||
.backdrop { display: grid; place-items: center; width: 100%; height: 100%; background: var(--pi-overlay); }
|
||||
section { width: min(720px, calc(100vw - 40px)); max-height: min(640px, calc(100vh - 40px)); display: flex; flex-direction: column; border: 1px solid var(--pi-border); border-radius: 12px; background: var(--pi-bg); box-shadow: 0 20px 60px var(--pi-shadow-strong); overflow: hidden; }
|
||||
header { display: flex; align-items: center; justify-content: space-between; padding: 12px; border-bottom: 1px solid var(--pi-border); }
|
||||
.options { min-height: 0; overflow: auto; outline: none; }
|
||||
button { border: 0; background: transparent; color: #e6edf3; cursor: pointer; }
|
||||
header button { font-size: 20px; color: #8b949e; }
|
||||
input { margin: 10px 12px; border: 1px solid #30363d; border-radius: 8px; background: #0d1117; color: #e6edf3; font: 14px system-ui, sans-serif; padding: 8px 10px; outline: none; }
|
||||
input:focus { border-color: #58a6ff; }
|
||||
.options button { display: block; width: 100%; padding: 10px 12px; border-bottom: 1px solid #21262d; text-align: left; }
|
||||
.options button.selected, .options button:hover { background: #0d2847; }
|
||||
small { display: block; margin-top: 4px; color: #8b949e; }
|
||||
.empty { padding: 24px; color: #8b949e; text-align: center; }
|
||||
button { border: 0; background: transparent; color: var(--pi-text); cursor: pointer; }
|
||||
header button { font-size: 20px; color: var(--pi-muted); }
|
||||
input { margin: 10px 12px; border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-bg); color: var(--pi-text); font: 14px system-ui, sans-serif; padding: 8px 10px; outline: none; }
|
||||
input:focus { border-color: var(--pi-accent); }
|
||||
.options button { display: block; width: 100%; padding: 10px 12px; border-bottom: 1px solid var(--pi-border-muted); text-align: left; }
|
||||
.options button.selected, .options button:hover { background: var(--pi-selection-bg); }
|
||||
small { display: block; margin-top: 4px; color: var(--pi-muted); }
|
||||
.empty { padding: 24px; color: var(--pi-muted); text-align: center; }
|
||||
`;
|
||||
|
||||
export const actionPaletteStyles = css`
|
||||
:host { position: fixed; inset: 0; z-index: 20; color: #e6edf3; font: 14px system-ui, sans-serif; }
|
||||
.backdrop { --palette-top: min(12dvh, 90px); --palette-bottom: max(20px, env(safe-area-inset-bottom)); display: grid; align-items: start; justify-items: center; width: 100%; height: 100dvh; background: #0008; padding: var(--palette-top) 20px var(--palette-bottom); box-sizing: border-box; overflow: hidden; }
|
||||
section { width: min(720px, 100%); max-height: min(640px, calc(100dvh - var(--palette-top) - var(--palette-bottom))); display: flex; flex-direction: column; border: 1px solid #30363d; border-radius: 12px; background: #0d1117; box-shadow: 0 20px 60px #000b; overflow: hidden; }
|
||||
header { display: grid; grid-template-columns: 1fr auto; gap: 8px; padding: 10px; border-bottom: 1px solid #30363d; }
|
||||
input { min-width: 0; border: 0; outline: none; background: transparent; color: #e6edf3; font: 16px system-ui, sans-serif; padding: 8px; }
|
||||
input::placeholder { color: #6e7681; }
|
||||
button { border: 0; background: transparent; color: #e6edf3; cursor: pointer; }
|
||||
header button { color: #8b949e; font-size: 22px; padding: 2px 8px; }
|
||||
:host { position: fixed; inset: 0; z-index: 20; color: var(--pi-text); font: 14px system-ui, sans-serif; }
|
||||
.backdrop { --palette-top: min(12dvh, 90px); --palette-bottom: max(20px, env(safe-area-inset-bottom)); display: grid; align-items: start; justify-items: center; width: 100%; height: 100dvh; background: var(--pi-overlay); padding: var(--palette-top) 20px var(--palette-bottom); box-sizing: border-box; overflow: hidden; }
|
||||
section { width: min(720px, 100%); max-height: min(640px, calc(100dvh - var(--palette-top) - var(--palette-bottom))); display: flex; flex-direction: column; border: 1px solid var(--pi-border); border-radius: 12px; background: var(--pi-bg); box-shadow: 0 20px 60px var(--pi-shadow-strong); overflow: hidden; }
|
||||
header { display: grid; grid-template-columns: 1fr auto; gap: 8px; padding: 10px; border-bottom: 1px solid var(--pi-border); }
|
||||
input { min-width: 0; border: 0; outline: none; background: transparent; color: var(--pi-text); font: 16px system-ui, sans-serif; padding: 8px; }
|
||||
input::placeholder { color: var(--pi-dim); }
|
||||
button { border: 0; background: transparent; color: var(--pi-text); cursor: pointer; }
|
||||
header button { color: var(--pi-muted); font-size: 22px; padding: 2px 8px; }
|
||||
.options { flex: 1 1 auto; min-height: 0; overflow: auto; -webkit-overflow-scrolling: touch; }
|
||||
.options button { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 3px 12px; width: 100%; padding: 10px 12px; border-bottom: 1px solid #21262d; text-align: left; }
|
||||
.options button.selected, .options button:hover { background: #0d2847; }
|
||||
.options button { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 3px 12px; width: 100%; padding: 10px 12px; border-bottom: 1px solid var(--pi-border-muted); text-align: left; }
|
||||
.options button.selected, .options button:hover { background: var(--pi-selection-bg); }
|
||||
.main { min-width: 0; }
|
||||
strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
small { display: block; color: #8b949e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
small { display: block; color: var(--pi-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.group { grid-column: 1 / -1; font-size: 12px; }
|
||||
kbd { align-self: center; border: 1px solid #30363d; border-radius: 6px; background: #161b22; color: #8b949e; padding: 2px 6px; font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: nowrap; }
|
||||
.empty { padding: 24px; color: #8b949e; text-align: center; }
|
||||
kbd { align-self: center; border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-muted); padding: 2px 6px; font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: nowrap; }
|
||||
.empty { padding: 24px; color: var(--pi-muted); text-align: center; }
|
||||
`;
|
||||
|
||||
export const promptEditorStyles = css`
|
||||
:host { position: relative; z-index: 5; display: block; color: #e6edf3; font: 14px system-ui, sans-serif; }
|
||||
footer { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; padding: 12px; border-top: 1px solid #30363d; }
|
||||
footer.shell-mode { border-top-color: #3fb950; background: #0f1b12; }
|
||||
:host { position: relative; z-index: 5; display: block; color: var(--pi-text); font: 14px system-ui, sans-serif; }
|
||||
footer { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; padding: 12px; border-top: 1px solid var(--pi-border); }
|
||||
footer.shell-mode { border-top-color: var(--pi-success); background: var(--pi-success-bg); }
|
||||
.editor-wrap { position: relative; min-width: 0; }
|
||||
.actions { display: flex; gap: 8px; align-items: center; justify-content: flex-end; flex-wrap: nowrap; white-space: nowrap; }
|
||||
.compact-status { display: flex; min-width: 0; align-items: center; gap: 6px; color: #8b949e; font-size: 12px; flex: 1 1 0; }
|
||||
.compact-status { display: flex; min-width: 0; align-items: center; gap: 6px; color: var(--pi-muted); font-size: 12px; flex: 1 1 0; }
|
||||
.compact-status > button { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||
.select-model { max-width: min(42vw, 320px); }
|
||||
.select-thinking { max-width: 110px; }
|
||||
textarea, .markdown-editor .cm-editor { box-sizing: border-box; width: 100%; min-height: 54px; max-height: 220px; resize: none; overflow: hidden; border-radius: 8px; border: 1px solid #30363d; background: #0d1117; color: #e6edf3; font: 16px/1.4 system-ui, sans-serif; }
|
||||
textarea, .markdown-editor .cm-editor { box-sizing: border-box; width: 100%; min-height: 54px; max-height: 220px; resize: none; overflow: hidden; border-radius: 8px; border: 1px solid var(--pi-border); background: var(--pi-bg); color: var(--pi-text); font: 16px/1.4 system-ui, sans-serif; }
|
||||
textarea { overflow-y: auto; padding: 8px; }
|
||||
.markdown-editor .cm-scroller { max-height: 220px; overflow-y: auto; font-family: system-ui, sans-serif; line-height: 1.4; }
|
||||
.markdown-editor .cm-content { min-height: 38px; padding: 8px; caret-color: #e6edf3; }
|
||||
.markdown-editor .cm-content { min-height: 38px; padding: 8px; caret-color: var(--pi-text); }
|
||||
.markdown-editor .cm-line { padding: 0; }
|
||||
.markdown-editor .cm-placeholder { color: #6e7681; }
|
||||
.markdown-editor .cm-placeholder { color: var(--pi-dim); }
|
||||
.markdown-editor .cm-focused { outline: none; }
|
||||
.shell-mode textarea, .shell-mode .markdown-editor .cm-editor { border-color: #3fb950; box-shadow: 0 0 0 1px #3fb95055; }
|
||||
.mode-hint { position: absolute; right: 8px; bottom: 8px; max-width: calc(100% - 16px); border: 1px solid #238636; border-radius: 999px; background: #0f2a16; color: #3fb950; padding: 2px 8px; font-size: 12px; pointer-events: none; }
|
||||
button { border: 1px solid #30363d; border-radius: 8px; background: #161b22; color: #e6edf3; padding: 7px 9px; cursor: pointer; }
|
||||
.shell-mode textarea, .shell-mode .markdown-editor .cm-editor { border-color: var(--pi-success); box-shadow: 0 0 0 1px var(--pi-success-ring); }
|
||||
.mode-hint { position: absolute; right: 8px; bottom: 8px; max-width: calc(100% - 16px); border: 1px solid var(--pi-success-border); border-radius: 999px; background: var(--pi-success-surface); color: var(--pi-success); padding: 2px 8px; font-size: 12px; pointer-events: none; }
|
||||
button { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); color: var(--pi-text); padding: 7px 9px; cursor: pointer; }
|
||||
button:disabled, textarea:disabled, .markdown-editor-disabled .cm-editor { opacity: .5; cursor: not-allowed; }
|
||||
@media (max-width: 640px) {
|
||||
footer { gap: 8px; padding: 8px; }
|
||||
|
||||
@@ -39,6 +39,13 @@ export function createCoreActions(): PluginAction[] {
|
||||
group: "General",
|
||||
run: (context) => context.logoutAuth(),
|
||||
},
|
||||
{
|
||||
id: "theme.select",
|
||||
title: "Select Theme",
|
||||
description: "Choose the Pi Web color theme",
|
||||
group: "Preferences",
|
||||
run: (context) => { context.openThemePicker(); },
|
||||
},
|
||||
{
|
||||
id: "view.chat",
|
||||
title: "Go to Chat",
|
||||
|
||||
@@ -3,7 +3,8 @@ import type { Workspace } from "../api";
|
||||
import { initialAppState, type AppState } from "../appState";
|
||||
import { corePlugin } from "./core";
|
||||
import { PluginRegistry } from "./registry";
|
||||
import type { PluginRuntimeContext } from "./types";
|
||||
import { themePackPlugin } from "./themes";
|
||||
import type { PluginRuntimeContext, ThemeTokens } from "./types";
|
||||
|
||||
function createContext(statePatch: Partial<AppState> = {}) {
|
||||
const calls: string[] = [];
|
||||
@@ -14,6 +15,7 @@ function createContext(statePatch: Partial<AppState> = {}) {
|
||||
addProject: vi.fn(() => { calls.push("addProject"); }),
|
||||
configureAuth: vi.fn(() => { calls.push("configureAuth"); }),
|
||||
logoutAuth: vi.fn(() => { calls.push("logoutAuth"); }),
|
||||
openThemePicker: vi.fn(() => { calls.push("openThemePicker"); }),
|
||||
selectMainView: vi.fn((view: AppState["mainView"]) => { calls.push(`selectMainView:${view}`); }),
|
||||
selectWorkspaceTool: vi.fn((tool: AppState["workspaceTool"]) => { calls.push(`selectWorkspaceTool:${tool}`); }),
|
||||
refreshFiles: vi.fn(() => { calls.push("refreshFiles"); }),
|
||||
@@ -81,6 +83,41 @@ describe("PluginRegistry", () => {
|
||||
expect(calls).toEqual(["refreshGit"]);
|
||||
});
|
||||
|
||||
it("collects built-in Pi Web themes from an in-app plugin", () => {
|
||||
const registry = new PluginRegistry();
|
||||
registry.register({ id: "themes", plugin: themePackPlugin });
|
||||
|
||||
expect(registry.getThemes().map((theme) => ({ id: theme.id, colorScheme: theme.colorScheme }))).toEqual([
|
||||
{ id: "themes:current", colorScheme: "dark" },
|
||||
{ id: "themes:docs-dark", colorScheme: "dark" },
|
||||
{ id: "themes:docs-light", colorScheme: "light" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("collects theme contributions in contribution order", () => {
|
||||
const registry = new PluginRegistry();
|
||||
registry.register({
|
||||
id: "example",
|
||||
plugin: {
|
||||
apiVersion: 1,
|
||||
name: "Example",
|
||||
activate: () => ({
|
||||
contributions: {
|
||||
themes: [
|
||||
{ id: "last", name: "Last", order: 20, colorScheme: "dark", tokens: testThemeTokens() },
|
||||
{ id: "first", name: "First", order: 10, colorScheme: "light", tokens: testThemeTokens() },
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.getThemes().map((theme) => ({ id: theme.id, pluginId: theme.pluginId, localId: theme.localId, name: theme.name }))).toEqual([
|
||||
{ id: "example:first", pluginId: "example", localId: "first", name: "First" },
|
||||
{ id: "example:last", pluginId: "example", localId: "last", name: "Last" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("collects workspace label items in contribution order", () => {
|
||||
const registry = new PluginRegistry();
|
||||
const workspace = testWorkspace();
|
||||
@@ -111,3 +148,43 @@ describe("PluginRegistry", () => {
|
||||
function testWorkspace(): Workspace {
|
||||
return { id: "w1", projectId: "p1", path: "/tmp/project", label: "main", isMain: true, isGitRepo: true, isGitWorktree: false };
|
||||
}
|
||||
|
||||
function testThemeTokens(): ThemeTokens {
|
||||
return {
|
||||
"--pi-bg": "#000000",
|
||||
"--pi-surface": "#000000",
|
||||
"--pi-surface-hover": "#000000",
|
||||
"--pi-terminal-bg": "#000000",
|
||||
"--pi-terminal-text": "#000000",
|
||||
"--pi-border": "#000000",
|
||||
"--pi-border-muted": "#000000",
|
||||
"--pi-text": "#000000",
|
||||
"--pi-text-secondary": "#000000",
|
||||
"--pi-text-bright": "#000000",
|
||||
"--pi-muted": "#000000",
|
||||
"--pi-dim": "#000000",
|
||||
"--pi-accent": "#000000",
|
||||
"--pi-accent-border": "#000000",
|
||||
"--pi-selection-bg": "#000000",
|
||||
"--pi-success": "#000000",
|
||||
"--pi-success-border": "#000000",
|
||||
"--pi-success-bg": "#000000",
|
||||
"--pi-success-surface": "#000000",
|
||||
"--pi-success-ring": "#000000",
|
||||
"--pi-warning": "#000000",
|
||||
"--pi-warning-border": "#000000",
|
||||
"--pi-warning-surface": "#000000",
|
||||
"--pi-danger": "#000000",
|
||||
"--pi-purple": "#000000",
|
||||
"--pi-purple-border": "#000000",
|
||||
"--pi-purple-surface": "#000000",
|
||||
"--pi-overlay": "#000000",
|
||||
"--pi-shadow-soft": "#000000",
|
||||
"--pi-shadow": "#000000",
|
||||
"--pi-shadow-strong": "#000000",
|
||||
"--pi-bg-overlay-soft": "#000000",
|
||||
"--pi-bg-overlay": "#000000",
|
||||
"--pi-success-bg-overlay": "#000000",
|
||||
"--pi-terminal-selection": "#000000",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { html } from "lit";
|
||||
import type { AppState } from "../appState";
|
||||
import type { Workspace } from "../api";
|
||||
import type { PiWebPluginRegistration, PluginAction, PluginRuntimeContext, QualifiedContributionId, QualifiedPluginAction, QualifiedWorkspaceLabelContribution, QualifiedWorkspacePanelContribution, WorkspaceLabelContribution, WorkspaceLabelItem, WorkspacePanelContribution } from "./types";
|
||||
import type { PiWebPluginRegistration, PluginAction, PluginRuntimeContext, QualifiedContributionId, QualifiedPluginAction, QualifiedThemeContribution, QualifiedWorkspaceLabelContribution, QualifiedWorkspacePanelContribution, ThemeContribution, WorkspaceLabelContribution, WorkspaceLabelItem, WorkspacePanelContribution } from "./types";
|
||||
|
||||
const idPattern = /^[a-z][a-z0-9.-]*$/u;
|
||||
const localIdPattern = /^[a-z][a-z0-9.-]*$/u;
|
||||
@@ -16,6 +16,7 @@ export class PluginRegistry {
|
||||
private readonly actions: RegisteredPluginAction[] = [];
|
||||
private readonly workspacePanels: QualifiedWorkspacePanelContribution[] = [];
|
||||
private readonly workspaceLabels: QualifiedWorkspaceLabelContribution[] = [];
|
||||
private readonly themes: QualifiedThemeContribution[] = [];
|
||||
private readonly pluginIds = new Set<string>();
|
||||
private readonly contributionIds = new Set<QualifiedContributionId>();
|
||||
|
||||
@@ -32,6 +33,7 @@ export class PluginRegistry {
|
||||
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));
|
||||
for (const contribution of contributions.workspaceLabels ?? []) this.workspaceLabels.push(this.qualifyWorkspaceLabelContribution(id, contribution));
|
||||
for (const theme of contributions.themes ?? []) this.themes.push(this.qualifyTheme(id, theme));
|
||||
}
|
||||
|
||||
getActions(context: PluginRuntimeContext): QualifiedPluginAction[] {
|
||||
@@ -56,6 +58,10 @@ export class PluginRegistry {
|
||||
return [...this.workspacePanels].sort((left, right) => (left.order ?? 1000) - (right.order ?? 1000) || left.title.localeCompare(right.title));
|
||||
}
|
||||
|
||||
getThemes(): QualifiedThemeContribution[] {
|
||||
return [...this.themes].sort((left, right) => (left.order ?? 1000) - (right.order ?? 1000) || left.name.localeCompare(right.name));
|
||||
}
|
||||
|
||||
getWorkspaceLabelItems(state: AppState, workspace: Workspace): WorkspaceLabelItem[] {
|
||||
const context = { state, workspace };
|
||||
return [...this.workspaceLabels]
|
||||
@@ -81,6 +87,11 @@ export class PluginRegistry {
|
||||
return { ...contribution, id, pluginId, localId: contribution.id };
|
||||
}
|
||||
|
||||
private qualifyTheme(pluginId: string, theme: ThemeContribution): QualifiedThemeContribution {
|
||||
const id = this.qualify(pluginId, theme.id);
|
||||
return { ...theme, id, pluginId, localId: theme.id };
|
||||
}
|
||||
|
||||
private qualify(pluginId: string, localId: string): QualifiedContributionId {
|
||||
this.validateLocalId(localId);
|
||||
const qualified: QualifiedContributionId = `${pluginId}:${localId}`;
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import type { PiWebPlugin, ThemeTokens } from "../types";
|
||||
|
||||
const currentTokens = {
|
||||
"--pi-bg": "#0d1117",
|
||||
"--pi-surface": "#161b22",
|
||||
"--pi-surface-hover": "#21262d",
|
||||
"--pi-terminal-bg": "#05070a",
|
||||
"--pi-terminal-text": "#e6edf3",
|
||||
"--pi-border": "#30363d",
|
||||
"--pi-border-muted": "#21262d",
|
||||
"--pi-text": "#e6edf3",
|
||||
"--pi-text-secondary": "#c9d1d9",
|
||||
"--pi-text-bright": "#f0f6fc",
|
||||
"--pi-muted": "#8b949e",
|
||||
"--pi-dim": "#6e7681",
|
||||
"--pi-accent": "#58a6ff",
|
||||
"--pi-accent-border": "#2f81f7",
|
||||
"--pi-selection-bg": "#0d2847",
|
||||
"--pi-success": "#3fb950",
|
||||
"--pi-success-border": "#238636",
|
||||
"--pi-success-bg": "#0f1b12",
|
||||
"--pi-success-surface": "#0f2a16",
|
||||
"--pi-success-ring": "#3fb95055",
|
||||
"--pi-warning": "#d29922",
|
||||
"--pi-warning-border": "#6e5200",
|
||||
"--pi-warning-surface": "#1f1a10",
|
||||
"--pi-danger": "#ff7b72",
|
||||
"--pi-purple": "#d2a8ff",
|
||||
"--pi-purple-border": "#a371f7",
|
||||
"--pi-purple-surface": "#21132f",
|
||||
"--pi-overlay": "#0008",
|
||||
"--pi-shadow-soft": "#0006",
|
||||
"--pi-shadow": "#0008",
|
||||
"--pi-shadow-strong": "#000b",
|
||||
"--pi-bg-overlay-soft": "#0d1117dd",
|
||||
"--pi-bg-overlay": "#0d1117e6",
|
||||
"--pi-success-bg-overlay": "#0f1b12ee",
|
||||
"--pi-terminal-selection": "#264f78",
|
||||
} satisfies ThemeTokens;
|
||||
|
||||
const docsDarkTokens = {
|
||||
"--pi-bg": "#070912",
|
||||
"--pi-surface": "#101527",
|
||||
"--pi-surface-hover": "#151b31",
|
||||
"--pi-terminal-bg": "#050710",
|
||||
"--pi-terminal-text": "#f7f4ff",
|
||||
"--pi-border": "#26304f",
|
||||
"--pi-border-muted": "#26304f",
|
||||
"--pi-text": "#f7f4ff",
|
||||
"--pi-text-secondary": "#aaa4bd",
|
||||
"--pi-text-bright": "#ffffff",
|
||||
"--pi-muted": "#aaa4bd",
|
||||
"--pi-dim": "#817a99",
|
||||
"--pi-accent": "#7c3cff",
|
||||
"--pi-accent-border": "#3d4a78",
|
||||
"--pi-selection-bg": "#151b31",
|
||||
"--pi-success": "#00f0d8",
|
||||
"--pi-success-border": "#00f0d8",
|
||||
"--pi-success-bg": "#071e22",
|
||||
"--pi-success-surface": "#092d31",
|
||||
"--pi-success-ring": "#00f0d855",
|
||||
"--pi-warning": "#ffb000",
|
||||
"--pi-warning-border": "#ffb000",
|
||||
"--pi-warning-surface": "#16140d",
|
||||
"--pi-danger": "#ff4f7b",
|
||||
"--pi-purple": "#b7a2ff",
|
||||
"--pi-purple-border": "#7c3cff",
|
||||
"--pi-purple-surface": "#181026",
|
||||
"--pi-overlay": "#0008",
|
||||
"--pi-shadow-soft": "#0006",
|
||||
"--pi-shadow": "#0008",
|
||||
"--pi-shadow-strong": "#000b",
|
||||
"--pi-bg-overlay-soft": "#070912dd",
|
||||
"--pi-bg-overlay": "#070912e6",
|
||||
"--pi-success-bg-overlay": "#071e22ee",
|
||||
"--pi-terminal-selection": "#3d4a78",
|
||||
} satisfies ThemeTokens;
|
||||
|
||||
const docsLightTokens = {
|
||||
"--pi-bg": "#f7f1e6",
|
||||
"--pi-surface": "#fff9ee",
|
||||
"--pi-surface-hover": "#f0e6d6",
|
||||
"--pi-terminal-bg": "#15131b",
|
||||
"--pi-terminal-text": "#fff9ee",
|
||||
"--pi-border": "#c9bca8",
|
||||
"--pi-border-muted": "#d8cdbc",
|
||||
"--pi-text": "#15131b",
|
||||
"--pi-text-secondary": "#15131b",
|
||||
"--pi-text-bright": "#15131b",
|
||||
"--pi-muted": "#5f586a",
|
||||
"--pi-dim": "#766f7e",
|
||||
"--pi-accent": "#6430d8",
|
||||
"--pi-accent-border": "#6430d8",
|
||||
"--pi-selection-bg": "#eadff8",
|
||||
"--pi-success": "#008c82",
|
||||
"--pi-success-border": "#008c82",
|
||||
"--pi-success-bg": "#e1f4ef",
|
||||
"--pi-success-surface": "#d7f0ec",
|
||||
"--pi-success-ring": "#008c8255",
|
||||
"--pi-warning": "#b05f00",
|
||||
"--pi-warning-border": "#b05f00",
|
||||
"--pi-warning-surface": "#fff2d2",
|
||||
"--pi-danger": "#b51d49",
|
||||
"--pi-purple": "#6430d8",
|
||||
"--pi-purple-border": "#6430d8",
|
||||
"--pi-purple-surface": "#eadff8",
|
||||
"--pi-overlay": "#15131b66",
|
||||
"--pi-shadow-soft": "#15131b22",
|
||||
"--pi-shadow": "#15131b33",
|
||||
"--pi-shadow-strong": "#15131b44",
|
||||
"--pi-bg-overlay-soft": "#f7f1e6dd",
|
||||
"--pi-bg-overlay": "#f7f1e6e6",
|
||||
"--pi-success-bg-overlay": "#e1f4efee",
|
||||
"--pi-terminal-selection": "#8d7b64",
|
||||
} satisfies ThemeTokens;
|
||||
|
||||
export const themePackPlugin: PiWebPlugin = {
|
||||
apiVersion: 1,
|
||||
name: "Pi Web Themes",
|
||||
activate: () => ({
|
||||
contributions: {
|
||||
themes: [
|
||||
{
|
||||
id: "current",
|
||||
name: "Pi Web Current",
|
||||
description: "The original Pi Web dark palette.",
|
||||
order: 10,
|
||||
colorScheme: "dark",
|
||||
tokens: currentTokens,
|
||||
},
|
||||
{
|
||||
id: "docs-dark",
|
||||
name: "Docs Dark",
|
||||
description: "Dark theme based on the Pi Web docs site.",
|
||||
order: 20,
|
||||
colorScheme: "dark",
|
||||
tokens: docsDarkTokens,
|
||||
},
|
||||
{
|
||||
id: "docs-light",
|
||||
name: "Docs Light",
|
||||
description: "Light theme based on the Pi Web docs site.",
|
||||
order: 30,
|
||||
colorScheme: "light",
|
||||
tokens: docsLightTokens,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -33,6 +33,7 @@ export interface PluginContributions {
|
||||
actions?: PluginAction[];
|
||||
workspacePanels?: WorkspacePanelContribution[];
|
||||
workspaceLabels?: WorkspaceLabelContribution[];
|
||||
themes?: ThemeContribution[];
|
||||
}
|
||||
|
||||
export interface PluginRuntimeContext {
|
||||
@@ -42,6 +43,7 @@ export interface PluginRuntimeContext {
|
||||
addProject: () => void | Promise<void>;
|
||||
configureAuth: () => void | Promise<void>;
|
||||
logoutAuth: () => void | Promise<void>;
|
||||
openThemePicker: () => void;
|
||||
selectMainView: (view: AppState["mainView"]) => void;
|
||||
selectWorkspaceTool: (tool: QualifiedContributionId) => void;
|
||||
refreshFiles: () => void | Promise<void>;
|
||||
@@ -140,6 +142,62 @@ export interface WorkspaceLabelContribution {
|
||||
items: (context: WorkspaceLabelContext) => WorkspaceLabelItem[];
|
||||
}
|
||||
|
||||
export type ThemeColorScheme = "dark" | "light";
|
||||
|
||||
export type ThemeToken =
|
||||
| "--pi-bg"
|
||||
| "--pi-surface"
|
||||
| "--pi-surface-hover"
|
||||
| "--pi-terminal-bg"
|
||||
| "--pi-terminal-text"
|
||||
| "--pi-border"
|
||||
| "--pi-border-muted"
|
||||
| "--pi-text"
|
||||
| "--pi-text-secondary"
|
||||
| "--pi-text-bright"
|
||||
| "--pi-muted"
|
||||
| "--pi-dim"
|
||||
| "--pi-accent"
|
||||
| "--pi-accent-border"
|
||||
| "--pi-selection-bg"
|
||||
| "--pi-success"
|
||||
| "--pi-success-border"
|
||||
| "--pi-success-bg"
|
||||
| "--pi-success-surface"
|
||||
| "--pi-success-ring"
|
||||
| "--pi-warning"
|
||||
| "--pi-warning-border"
|
||||
| "--pi-warning-surface"
|
||||
| "--pi-danger"
|
||||
| "--pi-purple"
|
||||
| "--pi-purple-border"
|
||||
| "--pi-purple-surface"
|
||||
| "--pi-overlay"
|
||||
| "--pi-shadow-soft"
|
||||
| "--pi-shadow"
|
||||
| "--pi-shadow-strong"
|
||||
| "--pi-bg-overlay-soft"
|
||||
| "--pi-bg-overlay"
|
||||
| "--pi-success-bg-overlay"
|
||||
| "--pi-terminal-selection";
|
||||
|
||||
export type ThemeTokens = Record<ThemeToken, string>;
|
||||
|
||||
export interface ThemeContribution {
|
||||
id: LocalContributionId;
|
||||
name: string;
|
||||
description?: string;
|
||||
order?: number;
|
||||
colorScheme: ThemeColorScheme;
|
||||
tokens: ThemeTokens;
|
||||
}
|
||||
|
||||
export interface QualifiedThemeContribution extends ThemeContribution {
|
||||
id: QualifiedContributionId;
|
||||
pluginId: PluginId;
|
||||
localId: LocalContributionId;
|
||||
}
|
||||
|
||||
export interface QualifiedWorkspaceLabelContribution extends WorkspaceLabelContribution {
|
||||
id: QualifiedContributionId;
|
||||
pluginId: PluginId;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { QualifiedContributionId, QualifiedThemeContribution, ThemeToken } from "./plugins/types";
|
||||
|
||||
export const DEFAULT_THEME_ID: QualifiedContributionId = "themes:current";
|
||||
export const THEME_STORAGE_KEY = "pi-web-app-theme";
|
||||
|
||||
export const THEME_TOKENS: ThemeToken[] = [
|
||||
"--pi-bg",
|
||||
"--pi-surface",
|
||||
"--pi-surface-hover",
|
||||
"--pi-terminal-bg",
|
||||
"--pi-terminal-text",
|
||||
"--pi-border",
|
||||
"--pi-border-muted",
|
||||
"--pi-text",
|
||||
"--pi-text-secondary",
|
||||
"--pi-text-bright",
|
||||
"--pi-muted",
|
||||
"--pi-dim",
|
||||
"--pi-accent",
|
||||
"--pi-accent-border",
|
||||
"--pi-selection-bg",
|
||||
"--pi-success",
|
||||
"--pi-success-border",
|
||||
"--pi-success-bg",
|
||||
"--pi-success-surface",
|
||||
"--pi-success-ring",
|
||||
"--pi-warning",
|
||||
"--pi-warning-border",
|
||||
"--pi-warning-surface",
|
||||
"--pi-danger",
|
||||
"--pi-purple",
|
||||
"--pi-purple-border",
|
||||
"--pi-purple-surface",
|
||||
"--pi-overlay",
|
||||
"--pi-shadow-soft",
|
||||
"--pi-shadow",
|
||||
"--pi-shadow-strong",
|
||||
"--pi-bg-overlay-soft",
|
||||
"--pi-bg-overlay",
|
||||
"--pi-success-bg-overlay",
|
||||
"--pi-terminal-selection",
|
||||
];
|
||||
|
||||
const qualifiedContributionIdPattern = /^[a-z][a-z0-9.-]*:[a-z][a-z0-9.-]*$/u;
|
||||
|
||||
export function readStoredThemeId(): QualifiedContributionId | undefined {
|
||||
try {
|
||||
const value = window.localStorage.getItem(THEME_STORAGE_KEY);
|
||||
return isQualifiedContributionId(value) ? value : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function applyPiWebTheme(theme: QualifiedThemeContribution, options: { persist?: boolean } = {}): void {
|
||||
const root = document.documentElement;
|
||||
root.dataset["piWebTheme"] = theme.id;
|
||||
root.style.colorScheme = theme.colorScheme;
|
||||
for (const token of THEME_TOKENS) {
|
||||
const value = theme.tokens[token];
|
||||
if (typeof value === "string" && value !== "") root.style.setProperty(token, value);
|
||||
else root.style.removeProperty(token);
|
||||
}
|
||||
if (options.persist === false) return;
|
||||
try {
|
||||
window.localStorage.setItem(THEME_STORAGE_KEY, theme.id);
|
||||
} catch {
|
||||
// Ignore storage failures; the selected theme can still apply for this tab.
|
||||
}
|
||||
}
|
||||
|
||||
function isQualifiedContributionId(value: string | null): value is QualifiedContributionId {
|
||||
return value !== null && qualifiedContributionIdPattern.test(value);
|
||||
}
|
||||
Reference in New Issue
Block a user