swarm-ui: a start page at / and the hive directory at /hives
/ is now the swarm's name over the digital-rain backdrop the hive dashboard's home has, and the swarm name in the header links to it. The hive directory moves to /hives, where the nav's hives tab points. The swarm name is read once per page load by a shared hook, the header and the start page both use it. Closes #5252
This commit is contained in:
parent
be2e64fa89
commit
417125b33c
10 changed files with 240 additions and 37 deletions
|
|
@ -36,6 +36,7 @@ import { HideDebugSetting } from "@hive/shared/hide-debug-setting.js";
|
|||
import { useApplyThemeOverride } from "@hive/shared/theme-apply.js";
|
||||
import { useApplyMotionOverride } from "@hive/shared/motion-apply.js";
|
||||
import { useDynamicTabs } from "./useDynamicTabs.js";
|
||||
import { useSwarmName } from "./useSwarmName.js";
|
||||
import "./Shell.css";
|
||||
|
||||
// Storage keys this app owns for `@hive/shared`'s settings mechanism —
|
||||
|
|
@ -47,7 +48,7 @@ const THEME_KEY = "swarm-ui:theme-override";
|
|||
const MOTION_KEY = "swarm-ui:motion-override";
|
||||
|
||||
const NAV_ITEMS: { href: string; label: string; accent: string }[] = [
|
||||
{ href: "/", label: "hives", accent: "var(--purple)" },
|
||||
{ href: "/hives", label: "hives", accent: "var(--purple)" },
|
||||
{ href: "/agents", label: "agents", accent: "var(--green)" },
|
||||
{ href: "/jobs", label: "jobs", accent: "var(--pink)" },
|
||||
{ href: "/schedules", label: "schedules", accent: "var(--cyan)" },
|
||||
|
|
@ -79,8 +80,10 @@ const WIDE_BODY_ROUTES = new Set([
|
|||
const FULL_BLEED_ROUTE_PATTERN =
|
||||
/^\/agents\/[^/]+(\/subagents\/[^/]+)?\/terminal$/;
|
||||
|
||||
// The start page (`/`) is full-bleed too: its rain backdrop fills the whole
|
||||
// body below the header.
|
||||
function isFullBleedRoute(location: string): boolean {
|
||||
return FULL_BLEED_ROUTE_PATTERN.test(location);
|
||||
return location === "/" || FULL_BLEED_ROUTE_PATTERN.test(location);
|
||||
}
|
||||
|
||||
function isWideBodyRoute(location: string): boolean {
|
||||
|
|
@ -206,7 +209,7 @@ function DynamicTabLink({
|
|||
}
|
||||
|
||||
export function Shell({ children }: { children: ComponentChildren }) {
|
||||
const [swarmName, setSwarmName] = useState<string | null>(null);
|
||||
const swarmName = useSwarmName();
|
||||
const [location] = useLocation();
|
||||
const navRef = useRef<HTMLElement | null>(null);
|
||||
const textRefs = useRef<Record<string, HTMLSpanElement | null>>({});
|
||||
|
|
@ -340,23 +343,8 @@ export function Shell({ children }: { children: ComponentChildren }) {
|
|||
setIndicator(measureIndex(settledIndexRef.current));
|
||||
}, [tabs.length]);
|
||||
|
||||
// Fetched once here, not per-page: every route mounts inside one
|
||||
// `<Shell>`, and the swarm's name doesn't change within a page
|
||||
// visit. A fetch failure is silently ignored — `swarmName` just stays
|
||||
// `null` and the header/title fall back to `DEFAULT_BRAND`, same as
|
||||
// an operator who never configured a name; this label is cosmetic,
|
||||
// not worth an `ApiErrorPanel`.
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const r = await fetch("/api/swarm");
|
||||
if (!r.ok) return;
|
||||
const data = (await r.json()) as { name: string | null };
|
||||
if (data.name) setSwarmName(data.name);
|
||||
})().catch(() => {
|
||||
/* cosmetic only — see comment above */
|
||||
});
|
||||
}, []);
|
||||
|
||||
// `swarmName` stays `null` on a failed fetch or an unconfigured name, and
|
||||
// the header/title fall back to `DEFAULT_BRAND`; see `useSwarmName`.
|
||||
const brand = swarmName ?? DEFAULT_BRAND;
|
||||
// Per-route label so browser tabs of different pages tell apart: a
|
||||
// top-level route takes its nav label, a terminal route takes the
|
||||
|
|
@ -378,20 +366,22 @@ export function Shell({ children }: { children: ComponentChildren }) {
|
|||
return (
|
||||
<div class={"shell" + (isFullBleedRoute(location) ? " shell-full" : "")}>
|
||||
<header class="shell-header">
|
||||
<span
|
||||
class="shell-brand"
|
||||
// Same glow recipe as the active nav tab / panel titles, riding
|
||||
// `navAccent` — the same value `color` already uses here — so
|
||||
// the brand's glow tracks the identical hop sequence as its own
|
||||
// text colour instead of introducing a second, disagreeing
|
||||
// accent source.
|
||||
style={{
|
||||
color: navAccent,
|
||||
textShadow: `0 0 8px color-mix(in srgb, ${navAccent} 45%, transparent)`,
|
||||
}}
|
||||
>
|
||||
{brand}
|
||||
</span>
|
||||
<Link href="/" class="shell-brand-link">
|
||||
<span
|
||||
class="shell-brand"
|
||||
// Same glow recipe as the active nav tab / panel titles, riding
|
||||
// `navAccent` — the same value `color` already uses here — so
|
||||
// the brand's glow tracks the identical hop sequence as its own
|
||||
// text colour instead of introducing a second, disagreeing
|
||||
// accent source.
|
||||
style={{
|
||||
color: navAccent,
|
||||
textShadow: `0 0 8px color-mix(in srgb, ${navAccent} 45%, transparent)`,
|
||||
}}
|
||||
>
|
||||
{brand}
|
||||
</span>
|
||||
</Link>
|
||||
<nav class="shell-nav" ref={navRef}>
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<NavLink
|
||||
|
|
|
|||
Loading…
Reference in a new issue