From 37e2faacc768fe95a5aef7846090f9bb089a20d8 Mon Sep 17 00:00:00 2001 From: SAPHID Date: Mon, 26 Oct 2026 03:10:40 +0000 Subject: [PATCH] Hide empty Panel tab for settings-only Framey plugins --- docs/frame-plugin-author.md | 2 +- docs/framey-loader.md | 3 ++- tests/page/framey_actions.mjs | 7 ++++++- ui/framey-host.js | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/frame-plugin-author.md b/docs/frame-plugin-author.md index c2e235e..65f0c1c 100644 --- a/docs/frame-plugin-author.md +++ b/docs/frame-plugin-author.md @@ -66,7 +66,7 @@ none of these frontend controls has passed a worn-headset check yet. | `Tabs`, `SidebarNavigation`, spinners, dialog/menu wrappers, progress bars | Local tab navigation and ordinary HTML controls; no Steam navigation or controller focus tree | Host tests and catalog frontend smoke checks in local Chromium | | `SteamClient` subscriptions and `DFL.Router.MainRunningApp` | Explicit inert registration methods return disposable handles; no events or attached running game. Mutating Steam/power methods are unavailable. | Host contract tests; these are safe fallbacks, not live Steam functionality | | `DialogCheckbox`, `DialogButtonPrimary` | Native checkbox and button controls in the panel | Host unit test; Storage Cleaner empty-data browser check | -| Optional `settings`, `frameyQuickActions` | Separate settings tab; up to eight actions with result text | Host unit test; these fields are Framey additions | +| Optional `settings`, `frameyQuickActions` | Separate settings tab when content also exists; settings-only plugins have no empty Panel tab; up to eight actions with result text | Host unit test; these fields are Framey additions | | `serverAPI.call` / `callable` | Calls only that plugin's Decky Python backend via the headset service | Fake Frame e2e; real ARM64 Notebook save/reopen across backend restart and Simple Timer settings/recents persistence via local Chromium. No worn-VR claim. | | API v1/v2 `addEventListener` / `removeEventListener` | Plugin-scoped real backend events while panel is open; bounded queue, about one-second polling, subscription expiry | Real ARM64 Simple Timer countdown, start/cancel and completion toast via local Chromium; scope/bounds/reconnect tests. No closed-panel notifications. | | `toaster.toast` | Visible toast inside the plugin panel; plain text or React nodes for title, body, icon, logo and subtext; `dismiss` and optional `onClick` | Host unit test; no Steam system notification or worn VR check | diff --git a/docs/framey-loader.md b/docs/framey-loader.md index 211f7d0..cc3a084 100644 --- a/docs/framey-loader.md +++ b/docs/framey-loader.md @@ -185,7 +185,8 @@ and the fake Frame flow before asking users to install it. An API v2 plugin's normal `content` is its default VR panel. Framey also shows an optional `settings` React element under a **Settings** tab and an optional -`titleView` above it. For one-tap headset controls, return +`titleView` above it. A plugin with only `settings` opens that view directly; +Framey shows tabs only when both views exist. For one-tap headset controls, return `frameyQuickActions: [{label: 'Refresh data', onClick: refreshData}]` from the plugin initializer. Framey displays up to eight actions and reports a pending, success, or failure state. These descriptor fields are Framey additions; Decky diff --git a/tests/page/framey_actions.mjs b/tests/page/framey_actions.mjs index 5a2f177..d215fef 100644 --- a/tests/page/framey_actions.mjs +++ b/tests/page/framey_actions.mjs @@ -105,7 +105,12 @@ assert.equal(nodes(tree,'header').length,1); await buttons[2].props.onClick(); assert.equal(called,1); assert.deepEqual(states.slice(-3),[0,'Refresh data…','Fresh',null].slice(-3)); -assert.equal(nodes(window.__surface({plugin:{settings:{type:'settings'}}}),'nav').length,1); +const settingsOnly=window.__surface({plugin:{settings:{type:'settings'}}}); +assert.equal(nodes(settingsOnly,'nav').length,0); +assert.equal(nodes(settingsOnly,'settings').length,1); +const panelOnly=window.__surface({plugin:{content:{type:'content'}}}); +assert.equal(nodes(panelOnly,'nav').length,0); +assert.equal(nodes(panelOnly,'content').length,1); // API v2 backend calls remain request-scoped and errors return to the caller. const api=window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit.connect(2,'Test'); diff --git a/ui/framey-host.js b/ui/framey-host.js index 7346904..98c2464 100644 --- a/ui/framey-host.js +++ b/ui/framey-host.js @@ -435,7 +435,7 @@ return R.createElement(R.Fragment, null, plugin.titleView && R.createElement('header', null, plugin.titleView), route && R.createElement('button', {type:'button', onClick:()=>navigate(null)}, 'Back to plugin'), - plugin.settings && R.createElement('nav', {'aria-label':'Plugin views'}, + plugin.content && plugin.settings && R.createElement('nav', {'aria-label':'Plugin views'}, R.createElement('button', {type:'button', 'aria-current':view === 'panel' ? 'page' : undefined, onClick:()=>setView('panel')}, 'Panel'), R.createElement('button', {type:'button', 'aria-current':view === 'settings' ? 'page' : undefined,