Fourth chord for the launcher, and the last move is a return: ctrl-space → shift-space → alt-space → ctrl-space. Every one was the owner naming a real collision, and this one is the interesting direction — alt-space is the GNOME window menu, he rebound the desktop to keep it, disliked what he rebound it to, and would rather move the app than the window manager. ctrl-space was ruled out on 2026-08-20 as "focuses the address bar in Brave". That observation was made before anything preventDefault'ed it, and the trade has changed: rebinding a browser is cheaper than rebinding a desktop. Written down in the file, because if the address bar still wins the key never reaches the handler and nothing in this repository can help — the fix would be in Brave. `!ev.shiftKey && !ev.altKey` matters: ctrl+SHIFT+space is the theme browser, and one chord opening both is the bug that needed a capture-phase listener to fix on the theme side. Verified in the built bundle that the two conditions are disjoint. CTRL+? renders every shortcut from a registry, ported in shape from vimazing's gameInfo.ts — controls declared as data, help screen renders them. Its renderer was a recursive `any` walk, right for a game whose help also carries rules and scoring; here the content is one shape, so the renderer is typed and boring and the value is in state/shortcuts.ts. The registry is deliberately NOT authoritative yet, and says so: the handlers still hold their own conditions, so changing a chord here does not move it. Each entry names the file that implements it, so the pair is one search apart. Making it the source of truth is the configurable-shortcuts work, and doing it now would rewrite three handlers for nothing visible. Also removed a line I put in the theme dropdown yesterday promising "alt+space to search every theme by name". Themes are not Spotlight items — that was an instruction for a feature nobody built, sitting in the UI as though it worked. It now points at ctrl+?, which exists. Two comments naming ctrl+alt+space for the theme browser were corrected to ctrl+shift+space with the older chord noted as still accepted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
83 lines
4.8 KiB
TypeScript
83 lines
4.8 KiB
TypeScript
import { useMemo, useRef } from 'react';
|
|
import { useLocation } from 'react-router';
|
|
import { useDock, usePanelFullscreen, useChatNotifications } from 'officerdev';
|
|
import { usePermissions } from 'hooks/usePermissions';
|
|
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
|
import { ScreenErrorFallback } from './ScreenErrorFallback';
|
|
import { Background } from './Background';
|
|
import { Header } from './Header';
|
|
import { Dock, CORE_DOCK_ITEMS, dockItemsFromPlugins, defaultDockPaths } from './Dock';
|
|
import { Spotlight } from './Spotlight/Spotlight';
|
|
import { ShortcutsModal } from './ShortcutsModal';
|
|
import { useIsTouch } from './useIsTouch';
|
|
import { usePageTitleSync } from '@/state/usePageTitle';
|
|
import { RouteGate } from './RouteGate';
|
|
|
|
type DashboardLayoutProps = {
|
|
children?: React.ReactNode;
|
|
};
|
|
export function DashboardLayout({ children }: DashboardLayoutProps) {
|
|
const { canVisit, plugins } = usePermissions();
|
|
// Filtered BEFORE useDock, so a member's saved dock order cannot resurrect an icon their role no longer
|
|
// reaches, and so the pinned-item defaults fall back to something they can actually open. Cosmetic
|
|
// either way — every one of these routes is refused server-side too — but an app that offers a door it
|
|
// will then slam is worse than one that never showed it.
|
|
// The shell's own items plus whatever the installed sidecars contribute. `plugins` already excludes
|
|
// anything uninstalled or disabled, so an absent feature has no tile at all rather than a dead one.
|
|
const permitted = useMemo(
|
|
() => [...CORE_DOCK_ITEMS, ...dockItemsFromPlugins(plugins)].filter((item) => canVisit(item.to)),
|
|
[canVisit, plugins],
|
|
);
|
|
const { items: visibleItems } = useDock(
|
|
permitted,
|
|
useMemo(() => defaultDockPaths(plugins), [plugins]),
|
|
);
|
|
const isTouch = useIsTouch();
|
|
const { pathname } = useLocation();
|
|
usePageTitleSync();
|
|
useChatNotifications();
|
|
// A panel can maximize into the content region on its own, but it cannot paint over this header — the
|
|
// region below is an `absolute z-2` stacking context and the header is a `fixed z-10` sibling of it. So
|
|
// "full screen" is cooperative: the panel asks, and the chrome steps aside.
|
|
const panelFullscreen = usePanelFullscreen();
|
|
// The nav dock measures its reveal boundary from this element, so it sits just above whatever is at the
|
|
// bottom of the content region. Written for the in-flow music bar, which used to be rendered here and
|
|
// left with `plugins/music/` on 2026-08-15; the measurement is not music-specific and still holds for
|
|
// anything a screen puts at its foot.
|
|
const regionRef = useRef<HTMLElement | null>(null);
|
|
|
|
return (
|
|
<div className="relative flex h-dvh flex-col overflow-hidden outline-none inset-0">
|
|
<Header dockItems={visibleItems} hidden={panelFullscreen} />
|
|
{/* Outside the content region on purpose, like the header and the dock: whatever a screen is doing
|
|
— maximised panel, error boundary, mid-navigation — ctrl-space still gets you off it. */}
|
|
<Spotlight />
|
|
{/* Outside the content region for the same reason Spotlight is: ctrl+? has to work from a maximised
|
|
panel, an error boundary, or mid-navigation. */}
|
|
<ShortcutsModal />
|
|
{/* overflow-CLIP, not hidden: `hidden` still makes this a scroll container, and the nav Dock —
|
|
absolute, parked below the bottom edge by translateY while hidden — adds its transformed box to
|
|
the scrollable overflow. So clicking a link let the browser scroll this section ~70px to reveal
|
|
the focused element: content slid up under the fixed header and the hidden dock slid into view.
|
|
`clip` clips identically but is not scrollable, so nothing can ever scroll it. */}
|
|
<section ref={regionRef} className="relative min-h-0 flex-1 snap-start overflow-clip">
|
|
<Background />
|
|
{!isTouch && <Dock items={visibleItems} boundaryRef={regionRef} className="hidden md:flex" />}
|
|
<div className="absolute inset-0 z-2 pt-[52px] md:pt-[64px] pb-2 overflow-y-auto">
|
|
{/* Chrome stays outside: whatever broke, the dock and the header still navigate you off it.
|
|
Keyed on the pathname so leaving a broken screen is itself a recovery. */}
|
|
<ErrorBoundary
|
|
resetKeys={[pathname]}
|
|
fallback={({ error, reset }) => <ScreenErrorFallback error={error} reset={reset} />}
|
|
>
|
|
{/* Inside the boundary and around every screen, so one place decides whether a route exists for
|
|
this account on this server. Filtering the dock was never enough: the tile was hidden and the
|
|
route still rendered for anyone who typed it, followed an old link or restored a tab. */}
|
|
<RouteGate>{children}</RouteGate>
|
|
</ErrorBoundary>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|