diff --git a/.changeset/full-reload-refresh-control.md b/.changeset/full-reload-refresh-control.md new file mode 100644 index 0000000..596c4e7 --- /dev/null +++ b/.changeset/full-reload-refresh-control.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Make the app refresh control perform a full page reload directly instead of opening refresh-data options. diff --git a/src/client/src/components/PiWebApp.ts b/src/client/src/components/PiWebApp.ts index 230e423..093b59c 100644 --- a/src/client/src/components/PiWebApp.ts +++ b/src/client/src/components/PiWebApp.ts @@ -1364,7 +1364,7 @@ export class PiWebApp extends LitElement { } private renderAppRefresh() { - return html` this.refreshAppData()} .onReload=${() => { this.hardReloadApp(); }}>`; + return html` { this.hardReloadApp(); }}>`; } override render() { diff --git a/src/client/src/components/appShell/AppRefreshControl.ts b/src/client/src/components/appShell/AppRefreshControl.ts index 90a050d..a37f710 100644 --- a/src/client/src/components/appShell/AppRefreshControl.ts +++ b/src/client/src/components/appShell/AppRefreshControl.ts @@ -1,51 +1,18 @@ import { LitElement, css, html } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; -import { actionMenuPanelStyle } from "../actionMenu"; - -const REFRESH_LONG_PRESS_MS = 550; -const REFRESH_MENU_PORTAL_STYLE_ID = "pi-web-app-refresh-menu-portal-style"; +import { customElement, property } from "lit/decorators.js"; @customElement("app-refresh-control") export class AppRefreshControl extends LitElement { - @property({ type: Boolean }) isRefreshing = false; - @property({ attribute: false }) onRefresh?: () => void | Promise; @property({ attribute: false }) onReload?: () => void; - @state() private menuOpen = false; - private menuStyle = ""; - private menuPortal: HTMLDivElement | undefined; - private longPressTimer: number | undefined; - private suppressNextClick = false; - - override connectedCallback(): void { - super.connectedCallback(); - document.addEventListener("click", this.onDocumentClick); - document.addEventListener("keydown", this.onDocumentKeyDown); - } - - override disconnectedCallback(): void { - document.removeEventListener("click", this.onDocumentClick); - document.removeEventListener("keydown", this.onDocumentKeyDown); - this.clearLongPressTimer(); - this.removePortalMenu(); - super.disconnectedCallback(); - } override render() { - const label = this.isRefreshing ? "Refreshing app data. Long-press for reload options." : "Refresh app data. Long-press for reload options."; + const label = "Full page reload"; return html` `; } @@ -61,114 +28,9 @@ export class AppRefreshControl extends LitElement { `; } - private readonly onRefreshClick = (event: MouseEvent): void => { + private readonly onReloadClick = (event: MouseEvent): void => { event.stopPropagation(); - if (this.suppressNextClick) { - this.suppressNextClick = false; - return; - } - this.refresh(); - }; - - private readonly onRefreshPointerDown = (event: PointerEvent): void => { - if (!event.isPrimary || event.button !== 0) return; - const target = event.currentTarget; - if (!(target instanceof HTMLElement)) return; - this.clearLongPressTimer(); - this.suppressNextClick = false; - this.longPressTimer = window.setTimeout(() => { - this.longPressTimer = undefined; - this.suppressNextClick = true; - this.openMenu(target); - }, REFRESH_LONG_PRESS_MS); - }; - - private readonly onRefreshContextMenu = (event: MouseEvent): void => { - event.preventDefault(); - event.stopPropagation(); - this.clearLongPressTimer(); - this.suppressNextClick = true; - this.openMenu(event.currentTarget); - }; - - private readonly onDocumentClick = (event: MouseEvent): void => { - const path = event.composedPath(); - if (path.includes(this) || (this.menuPortal !== undefined && path.includes(this.menuPortal))) return; - this.closeMenu(); - }; - - private readonly onDocumentKeyDown = (event: KeyboardEvent): void => { - if (event.key !== "Escape" || !this.menuOpen) return; - event.preventDefault(); - event.stopPropagation(); - this.closeMenu(); - }; - - private openMenu(target: EventTarget | null): void { - this.menuStyle = actionMenuPanelStyle(target, { constrainTo: "viewport" }); - this.menuOpen = true; - this.renderPortalMenu(); - } - - private closeMenu(): void { - this.menuOpen = false; - this.suppressNextClick = false; - this.removePortalMenu(); - } - - private refresh(): void { - this.closeMenu(); - void this.onRefresh?.(); - } - - private reload(): void { - this.closeMenu(); this.onReload?.(); - } - - private clearLongPressTimer(): void { - if (this.longPressTimer === undefined) return; - window.clearTimeout(this.longPressTimer); - this.longPressTimer = undefined; - } - - private renderPortalMenu(): void { - const ownerDocument = this.ownerDocument; - ensurePortalMenuStyles(ownerDocument); - - const menu = this.menuPortal ?? ownerDocument.createElement("div"); - this.menuPortal = menu; - menu.className = "pi-web-app-refresh-menu-portal"; - menu.setAttribute("role", "menu"); - menu.setAttribute("style", this.menuStyle); - menu.replaceChildren( - this.createPortalMenuButton("Refresh app data", () => { this.refresh(); }), - this.createPortalMenuButton("Full page reload", () => { this.reload(); }), - ); - menu.addEventListener("click", this.onPortalMenuClick); - if (!menu.isConnected) ownerDocument.body.append(menu); - } - - private createPortalMenuButton(label: string, onClick: () => void): HTMLButtonElement { - const button = this.ownerDocument.createElement("button"); - button.type = "button"; - button.setAttribute("role", "menuitem"); - button.textContent = label; - button.addEventListener("click", (event) => { - event.stopPropagation(); - onClick(); - }); - return button; - } - - private removePortalMenu(): void { - this.menuPortal?.removeEventListener("click", this.onPortalMenuClick); - this.menuPortal?.remove(); - this.menuPortal = undefined; - } - - private readonly onPortalMenuClick = (event: MouseEvent): void => { - event.stopPropagation(); }; static override styles = css` @@ -176,52 +38,5 @@ export class AppRefreshControl extends LitElement { :host, :host * { -webkit-user-select: none; user-select: none; } .app-refresh-button { box-sizing: border-box; width: 36px; height: 36px; display: grid; place-items: center; border: 1px solid var(--pi-border); border-radius: 999px; background: var(--pi-surface); color: var(--pi-text); padding: 0; line-height: 1; cursor: pointer; touch-action: manipulation; -webkit-touch-callout: none; } .app-refresh-icon { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; } - .app-refresh-button.refreshing .app-refresh-icon { animation: app-refresh-spin .8s linear infinite; } - @keyframes app-refresh-spin { to { transform: rotate(360deg); } } `; } - -function ensurePortalMenuStyles(ownerDocument: Document): void { - if (ownerDocument.getElementById(REFRESH_MENU_PORTAL_STYLE_ID) !== null) return; - const style = ownerDocument.createElement("style"); - style.id = REFRESH_MENU_PORTAL_STYLE_ID; - style.textContent = ` - .pi-web-app-refresh-menu-portal { - position: fixed; - z-index: 2147483647; - box-sizing: border-box; - min-width: min(170px, calc(100vw - 16px)); - overflow: auto; - padding: 4px; - border: 1px solid var(--pi-border); - border-radius: 8px; - background: var(--pi-surface); - color: var(--pi-text); - box-shadow: 0 8px 24px var(--pi-shadow); - overflow-wrap: anywhere; - font: 14px system-ui, sans-serif; - -webkit-touch-callout: none; - -webkit-user-select: none; - user-select: none; - } - .pi-web-app-refresh-menu-portal button { - display: block; - width: 100%; - border: 0; - border-radius: 8px; - background: transparent; - color: var(--pi-text); - padding: 7px 9px; - text-align: left; - white-space: normal; - overflow-wrap: anywhere; - font: inherit; - cursor: pointer; - } - .pi-web-app-refresh-menu-portal button:hover, - .pi-web-app-refresh-menu-portal button:focus { - background: var(--pi-selection-bg); - } - `; - ownerDocument.head.append(style); -} diff --git a/src/client/src/components/shared.ts b/src/client/src/components/shared.ts index c4a78f6..495a32b 100644 --- a/src/client/src/components/shared.ts +++ b/src/client/src/components/shared.ts @@ -75,14 +75,6 @@ export const appStyles = css` .context-item { flex: 0 0 auto; min-width: 0; display: flex; } .context-actions { position: absolute; top: 6px; right: 0; bottom: 6px; z-index: 3; display: flex; align-items: center; padding: 0 8px 0 0; pointer-events: none; } .context-actions::after { content: ""; position: absolute; top: 0; right: 0; bottom: 0; z-index: 0; width: 26px; background: var(--pi-bg); pointer-events: none; } - .app-refresh { position: relative; z-index: 1; display: flex; align-items: center; pointer-events: auto; -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; } - .app-refresh, .app-refresh * { -webkit-user-select: none; user-select: none; } - .app-refresh-button { box-sizing: border-box; width: 36px; height: 36px; display: grid; place-items: center; border-radius: 999px; padding: 0; line-height: 1; touch-action: manipulation; -webkit-touch-callout: none; } - .app-refresh-icon { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; } - .app-refresh-button.refreshing .app-refresh-icon { animation: app-refresh-spin .8s linear infinite; } - .app-refresh-menu { position: fixed; z-index: 10000; box-sizing: border-box; min-width: min(170px, calc(100vw - 16px)); overflow: auto; padding: 4px; border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); box-shadow: 0 8px 24px var(--pi-shadow); overflow-wrap: anywhere; } - .app-refresh-menu button { display: block; width: 100%; border: 0; background: transparent; color: var(--pi-text); text-align: left; white-space: normal; overflow-wrap: anywhere; } - .app-refresh-menu button:hover, .app-refresh-menu button:focus { background: var(--pi-selection-bg); } .context-chip { flex: 0 0 auto; min-width: 0; display: inline-flex; align-items: baseline; gap: 5px; border: 1px solid var(--pi-border-muted); border-radius: 999px; background: var(--pi-surface); color: var(--pi-text); padding: 4px 8px; font: inherit; text-align: left; } .context-chip:hover { background: var(--pi-surface-hover); } .context-chip:focus-visible { outline: 2px solid var(--pi-accent); outline-offset: 2px; } @@ -155,7 +147,6 @@ export const appStyles = css` 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); } - @keyframes app-refresh-spin { to { transform: rotate(360deg); } } `; export const workspacePanelStyles = css` diff --git a/src/client/src/plugins/core/actions.ts b/src/client/src/plugins/core/actions.ts index 9e07c51..77e7d3a 100644 --- a/src/client/src/plugins/core/actions.ts +++ b/src/client/src/plugins/core/actions.ts @@ -87,13 +87,6 @@ export function createCoreActions(): PluginAction[] { group: "Preferences", run: (context) => { context.piWebUnstable?.openSettings?.(); }, }, - { - id: "app.refresh-data", - title: "Refresh App Data", - description: "Refresh session, status, activity, and the current workspace surface without reloading the page", - group: "General", - run: (context) => context.refreshAppData(), - }, { id: "app.reload-page", title: "Full Page Reload", diff --git a/src/client/src/plugins/registry.test.ts b/src/client/src/plugins/registry.test.ts index bbe7312..1b5f64b 100644 --- a/src/client/src/plugins/registry.test.ts +++ b/src/client/src/plugins/registry.test.ts @@ -174,17 +174,17 @@ describe("PluginRegistry", () => { expect(calls).toEqual(["refreshGit"]); }); - it("routes app refresh, reload, and settings actions through the runtime context", () => { + it("routes app reload and settings actions through the runtime context", () => { const registry = new PluginRegistry(); registry.register({ id: "core", plugin: corePlugin }); const { context, calls } = createContext(); const actions = registry.getActions(context); - void actions.find((candidate) => candidate.id === "core:app.refresh-data")?.run(); + expect(actions.some((candidate) => candidate.id === "core:app.refresh-data")).toBe(false); void actions.find((candidate) => candidate.id === "core:app.reload-page")?.run(); void actions.find((candidate) => candidate.id === "core:settings.open")?.run(); - expect(calls).toEqual(["refreshAppData", "reloadPage", "openSettings"]); + expect(calls).toEqual(["reloadPage", "openSettings"]); }); it("exposes terminal navigation as a shortcut-backed action", () => {