mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 11:19:24 +00:00
feat(plugins): full-screen (immersive) plugin screens via manifest opt-in (#590)
DAW-style plugin UIs (e.g. a practice studio) need the whole viewport, not a scrolling content page below the v3 topbar — embedded in the shell they get cut off at the bottom with excess padding up top. Add an opt-in top-level `"fullscreen": true` plugin.json field, surfaced as the `fullscreen` boolean on /api/plugins (mirrors the settings_category plumbing in plugins/__init__.py). When a fullscreen plugin's screen is active, static/v3/ shell.js toggles `html.fb-immersive` from syncActive() so it tracks every navigation incl. deep-link; static/v3/v3.css then hides the topbar, collapses the sidebar to a functional icon rail (kept reachable — Escape is bound only on player/settings scopes, so a fully hidden sidebar would trap the user), and lets the active plugin screen fill #v3-main. Mirrors the existing ss-follower-pre chrome-hide pattern. Additive + opt-in: plugins without the flag are unaffected. Test: tests/test_plugins.py::test_fullscreen_flag_parsed_from_manifest Claude-Session: https://claude.ai/code/session_01BmWopMsRjdZyD6RwmZAQBv Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
byrongamatos
parent
8b4c9b0050
commit
4c3ec2ff66
+24
-2
@@ -17,6 +17,13 @@
|
||||
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => (
|
||||
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
|
||||
// Plugin ids whose manifest declared `"fullscreen": true`. Populated from
|
||||
// /api/plugins in renderPromotedNav(); read by syncActive() to toggle the
|
||||
// immersive (chrome-collapsed) shell whenever such a plugin's screen is the
|
||||
// active one. Empty until plugins resolve — the worst case is one extra
|
||||
// syncActive() once the fetch lands, which re-applies the class.
|
||||
const FULLSCREEN_PLUGIN_IDS = new Set();
|
||||
|
||||
// ── Navigation registry ────────────────────────────────────────────────
|
||||
// Each entry maps a stable hash key → a screen id (showScreen target) and a
|
||||
// label. Legacy screens are reused: "Songs" = #home (library), "Favorites"
|
||||
@@ -112,6 +119,15 @@
|
||||
el.classList.toggle('text-fb-textDim', !on);
|
||||
});
|
||||
setTopbarTitle(titleFor(screenId));
|
||||
// Immersive (full-screen) plugin screens: when the active screen belongs
|
||||
// to a plugin that opted in via `"fullscreen": true`, collapse the host
|
||||
// chrome (topbar hidden, sidebar → icon rail; see v3.css) and let the
|
||||
// plugin own the content area. Toggled here so it tracks every
|
||||
// navigation, including deep-link load and programmatic showScreen().
|
||||
const fsPluginId = (screenId && screenId.indexOf('plugin-') === 0)
|
||||
? screenId.slice('plugin-'.length) : null;
|
||||
const immersive = !!(fsPluginId && FULLSCREEN_PLUGIN_IDS.has(fsPluginId));
|
||||
document.documentElement.classList.toggle('fb-immersive', immersive);
|
||||
// Show the song search only on the library screen. Everywhere else the
|
||||
// box is irrelevant (and would silently no-op against v3Songs.search).
|
||||
const searchWrap = document.getElementById('v3-search-wrap');
|
||||
@@ -144,7 +160,7 @@
|
||||
return '<a href="#/' + entry.key + '" data-v3-nav="' + entry.key + '" ' +
|
||||
'class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-fb-textDim ' +
|
||||
'hover:text-fb-text hover:bg-fb-card/50 transition-colors">' +
|
||||
iconSvg(entry.icon) + '<span class="truncate">' + esc(labelOverride != null ? labelOverride : entry.label) + '</span></a>';
|
||||
iconSvg(entry.icon) + '<span class="truncate v3-nav-label">' + esc(labelOverride != null ? labelOverride : entry.label) + '</span></a>';
|
||||
}
|
||||
// Empty slot for a promoted plugin, anchored after a nav item. Filled by
|
||||
// renderPromotedNav() only when the plugin is installed, so an absent
|
||||
@@ -161,7 +177,7 @@
|
||||
const items = NAV.filter((n) => n.group === group);
|
||||
if (!items.length) continue;
|
||||
const itemsHTML = items.map((it) => navItemHTML(it) + promotedSlotHTML(it.key)).join('');
|
||||
html += '<div><div class="px-3 mb-1 text-[10px] uppercase tracking-wider font-semibold text-fb-textDim/70">' +
|
||||
html += '<div><div class="v3-nav-group px-3 mb-1 text-[10px] uppercase tracking-wider font-semibold text-fb-textDim/70">' +
|
||||
group + '</div><div class="space-y-0.5">' + itemsHTML + '</div></div>';
|
||||
}
|
||||
nav.innerHTML = html;
|
||||
@@ -270,6 +286,12 @@
|
||||
if (res.ok) plugins = await res.json();
|
||||
} catch (e) { return; } // degrade: no promoted slots
|
||||
const list = Array.isArray(plugins) ? plugins : [];
|
||||
// Record which installed plugins requested immersive (full-screen)
|
||||
// screens, then re-sync the active screen so the chrome collapses even
|
||||
// if we navigated to a fullscreen plugin before /api/plugins resolved.
|
||||
FULLSCREEN_PLUGIN_IDS.clear();
|
||||
for (const p of list) { if (p && p.fullscreen && p.id) FULLSCREEN_PLUGIN_IDS.add(p.id); }
|
||||
try { syncActive(currentScreenId()); } catch (e) { /* non-fatal */ }
|
||||
for (const promo of PROMOTED_PLUGINS) {
|
||||
const host = document.getElementById(promo.slotId);
|
||||
const entry = byKey(promo.navKey);
|
||||
|
||||
@@ -1074,3 +1074,39 @@ body.font-display { font-family: Rubik, system-ui, sans-serif; }
|
||||
}
|
||||
.fb-settings-note { font-size: .75rem; color: #64748b; margin-top: 1rem; }
|
||||
.fb-tabpanel-empty { font-size: .8rem; color: #64748b; padding: .5rem 0; }
|
||||
|
||||
/* ── Immersive (full-screen) plugin screens ──────────────────────────────────
|
||||
A plugin that declares `"fullscreen": true` in its manifest gets the whole
|
||||
content area when its screen is active. shell.js toggles `html.fb-immersive`
|
||||
on navigation (see syncActive). DAW-style plugin UIs (e.g. a practice studio)
|
||||
need the viewport, not a scrolling content page below the topbar — the cause
|
||||
of the "cut off at the bottom / too much top padding" reports on the embedded
|
||||
layout. Mirrors the proven chrome-hide pattern in static/v3/index.html
|
||||
(`html.ss-follower-pre …`), but keeps the sidebar as a functional icon rail
|
||||
so the user is never trapped (Escape is bound only on player/settings
|
||||
scopes, not plugin screens). */
|
||||
html.fb-immersive #v3-topbar { display: none !important; }
|
||||
|
||||
/* Sidebar → icon rail: narrow it, drop the wordmark + group headers + labels,
|
||||
center the remaining icons. The .v3-nav-label / .v3-nav-group hooks are added
|
||||
by shell.js so these rules don't depend on Tailwind utility class strings. */
|
||||
html.fb-immersive #v3-sidebar { width: 4.5rem; }
|
||||
html.fb-immersive #v3-brand { display: none; }
|
||||
html.fb-immersive #v3-nav { padding-left: .5rem; padding-right: .5rem; }
|
||||
html.fb-immersive #v3-nav .v3-nav-group,
|
||||
html.fb-immersive #v3-nav .v3-nav-label { display: none; }
|
||||
html.fb-immersive #v3-nav a {
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
/* The active plugin screen fills the now-topbar-less content area. #v3-main is
|
||||
position:relative, so pinning the screen avoids relying on the auto-height
|
||||
block flow that made a 100vh plugin overflow #v3-main and force a scroll. */
|
||||
html.fb-immersive #v3-main > .screen.active {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user