Skip to the content

Capsid menu

components/admin-shell/examples.tsx

221 lines and 59 functions in DrDustinEdwards/capsomer, at commit d3966cc on 2026-10-24. This file on GitHub.

Functions

The functions of examples.tsx
FunctionLinesComplexityCoveredCRAPMutation
svg8 to 121No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
onSearch34 to 341No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
onHelp36 to 361No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
onClick41 to 411No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
onSignOut42 to 421No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
onRefresh46 to 461No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
Page50 to 551No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
key57 to 571No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
remember58 to 642No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
Shell66 to 732No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
showName76 to 821No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:77>77 to 802No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:81>81 to 811No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
Tipped83 to 861No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:84>84 to 841No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
CollapsedTip89 to 921No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:90>90 to 901No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
AccountOpen95 to 1011No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:96>96 to 991No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:97>97 to 971No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:98>98 to 981No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
SheetOpen103 to 1091No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:104>104 to 1071No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:105>105 to 1051No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:106>106 to 1061No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:117>117 to 1171No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
onHelp140 to 1401No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
onSignOut146 to 1461No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
Transitioning152 to 1861No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
go154 to 1541No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:154>154 to 1541No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:154>154 to 1541No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:155>155 to 1551No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:160>160 to 1601No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:163>163 to 1751No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:166>166 to 1712No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:180>180 to 1801No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
FoxhoundMarks189 to 2021No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:190>190 to 1901No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:191>191 to 1912No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:194>194 to 1991No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
expanded205 to 2051No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
collapsed206 to 2061No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
tip207 to 2071No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:208>208 to 2081No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
trail209 to 2091No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:209>209 to 2091No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
account210 to 2101No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
reader211 to 2131No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
user214 to 2141No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:215>215 to 2151No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
comfortable216 to 2201No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
transition221 to 2211No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
minimal223 to 2271No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:228>228 to 2281No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
phone229 to 2291No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:230>230 to 2301No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:231>231 to 2311No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:231>231 to 2312No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet

Imports

It imports components/admin-shell/admin-shell.react.tsx, components/tooltip/tooltip.ts. No module of this repo imports it.

Source

// Mounts the admin shell specimens. Every element with data-mount="<state>" gets that state.
import { StrictMode, useEffect, useState, type ReactNode } from "react";
import { createRoot } from "react-dom/client";
import { flushSync } from "react-dom";
import { showTip } from "../tooltip/tooltip.ts";
import { AdminShell, FAMILY_APPS, FIRST_PAGE, FOXHOUND_MARKS, startPageTransition, type AdminEntry, type AdminShellProps } from "./admin-shell.react.tsx";

const svg = (d: string) => (
  <svg className="cap-admin-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
    <path d={d} />
  </svg>
);
const GRID = svg("M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z");
const GLOBE = svg("M12 3a9 9 0 1 0 0 18a9 9 0 1 0 0-18zM3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18");
const ALERT = svg("M12 3l9.5 17h-19zM12 10v4M12 17h.01");
const LIST = svg("M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01");
const CLOCK = svg("M12 3a9 9 0 1 0 0 18a9 9 0 1 0 0-18zM12 7v5l3 3");
const FOLDER = svg("M3 7h6l2 2h10v10H3z");

const nav: AdminEntry[] = [
  { id: "overview", label: FIRST_PAGE, href: "#overview", icon: GRID, current: true, group: "Watch" },
  { id: "sites", label: "Sites", href: "#sites", icon: GLOBE, count: 11, group: "Watch" },
  { id: "incidents", label: "Incidents", href: "#incidents", icon: ALERT, count: 4, countNote: "open", tone: "need", group: "Watch" },
  { id: "queue", label: "Queue", href: "#queue", icon: LIST, count: 5, countNote: "blocked", tone: "need", group: "Work" },
  { id: "activity", label: "Activity", href: "#activity", icon: CLOCK, count: 120, group: "Records" },
  { id: "namespaces", label: "Namespaces", href: "#namespaces", icon: FOLDER, count: 11, group: "Records" },
];

const base: Omit<AdminShellProps, "children"> = {
  app: "portal",
  badges: { portal: { count: 3 }, carrel: { count: 2, countNote: "AI drafts waiting" }, germomics: { count: 1, countNote: "check failing" } },
  nav,
  page: FIRST_PAGE,
  onSearch: () => undefined,
  inbox: { count: 7, countNote: "need you", href: "#inbox" },
  onHelp: () => undefined,
  viewSite: "#site",
  account: {
    role: "Owner",
    email: "dustin@example.test",
    appLinks: [{ label: "Portal settings", href: "#settings" }, { label: "Keyboard shortcuts", onClick: () => undefined }],
    onSignOut: () => undefined,
  },
  // Six minutes before the page was opened, so the specimen always reads "6 minutes ago".
  updated: { at: Date.now() - 6 * 60_000, auto: true },
  onRefresh: () => undefined,
  refreshShortcut: "R",
};

const Page = ({ children }: { children?: ReactNode }) => (
  <div className="cap-admin-page">
    <h1>{FIRST_PAGE}</h1>
    <p className="cap-muted">The page's own content sits here, and the document scrolls. {children}</p>
  </div>
);

const key = (name: string) => `cap-admin-specimen-${name}`;
const remember = (name: string, value: string) => {
  try {
    localStorage.setItem(key(name), value);
  } catch {
    // A blocked store: the specimen shows its default.
  }
};

function Shell({ name, collapsed, ...rest }: { name: string; collapsed?: boolean } & Partial<AdminShellProps>) {
  if (collapsed) remember(name, "collapsed");
  return (
    <AdminShell {...base} prefKey={key(name)} {...rest}>
      <Page />
    </AdminShell>
  );
}

// Shows the name a hovered control would show, for the picture; the spec checks real hover and focus.
const showName = (sel: string, i: number) => {
  const frame = requestAnimationFrame(() => {
    const el = document.querySelectorAll<HTMLElement>(sel)[i];
    if (el) showTip(el);
  });
  return () => cancelAnimationFrame(frame);
};
function Tipped() {
  useEffect(() => showName(".cap-admin-tile", 1), []);
  return <Shell name="tip" />;
}

// Collapsed, with a menu icon's name shown, for the picture.
function CollapsedTip() {
  useEffect(() => showName(".cap-admin-menu a", 1), []);
  return <Shell name="collapsed-tip" collapsed />;
}

// Opens the header's account menu, for the picture: a press with no pointer, as a key gives.
function AccountOpen() {
  useEffect(() => {
    const frame = requestAnimationFrame(() => document.querySelector<HTMLButtonElement>(".cap-site-header-avatar")?.click());
    return () => cancelAnimationFrame(frame);
  }, []);
  return <Shell name="account" />;
}

function SheetOpen() {
  useEffect(() => {
    const frame = requestAnimationFrame(() => document.querySelector<HTMLButtonElement>(".cap-site-header-menu")?.click());
    return () => cancelAnimationFrame(frame);
  }, []);
  return <Shell name="phone-sheet" />;
}

// A reader's own area on a site outside the family: the user variant (no strip, the 288px menu),
// the site's sections in the menu, a centred reading column.
const reader: Partial<AdminShellProps> = {
  variant: "user",
  app: undefined,
  title: "dustinedwards.info",
  logo: { href: "#home", label: "dustinedwards.info", mark: FAMILY_APPS.find((a) => a.id === "info")?.logo },
  nav: [
    { id: "research", label: "Research", href: "#research", icon: FOLDER, current: true },
    { id: "protocols", label: "Protocols", href: "#protocols", icon: LIST },
    { id: "episodes", label: "Episodes", href: "#episodes", icon: CLOCK },
  ],
  inbox: undefined,
  account: undefined,
  updated: undefined,
  onRefresh: undefined,
  page: "Research",
};

// A customer's own dashboard (the user variant): no strip, the 288px menu, so its page starts at
// the same x as your admin's; the business and product switchers sit in the header.
const dashboard: Partial<AdminShellProps> = {
  variant: "user",
  app: undefined,
  title: "Acme Supply Co.",
  logo: { href: "#home", label: "Acme Supply Co.", mark: GRID },
  badges: undefined,
  onSearch: undefined,
  inbox: undefined,
  onHelp: () => undefined,
  viewSite: undefined,
  switchers: [
    { id: "business", label: "Acme Supply Co.", name: "Business: Acme Supply Co.", tile: "A", sheetLabel: "Businesses", groups: [{ links: [{ label: "Acme Supply Co.", href: "#acme", current: true }, { label: "Northwind Goods", href: "#northwind" }] }] },
    { id: "product", label: "Recovery", name: "Product: Recovery", sheetLabel: "Products", groups: [{ links: [{ label: "Console", href: "#console" }, { label: "Recovery", href: "#recovery", current: true }, { label: "Disputes", href: "#disputes" }] }] },
  ],
  account: { name: "Sam Rivera", initials: "SR", email: "sam@example.com", appLinks: [{ label: "Settings", href: "#settings" }, { label: "Billing", href: "#billing" }], onSignOut: () => undefined },
};

// Page transitions: the menu links and the button change the page inside startPageTransition, so
// the content cross-fades and the strip and menu stay put.
const PAGES = ["overview", "sites", "queue"];
function Transitioning() {
  const [page, setPage] = useState(0);
  const go = (to: number) => void startPageTransition(() => flushSync(() => setPage(to)));
  const entries = nav.map((e) => ({ ...e, current: e.id === PAGES[page] }));
  return (
    <AdminShell
      {...base}
      nav={entries}
      page={entries.find((e) => e.current)?.label}
      transition
      prefKey={key("transition")}
      renderLink={({ children, ...rest }) => (
        <a
          {...rest}
          onClick={(e) => {
            const to = PAGES.indexOf(rest.href.slice(1));
            if (to < 0) return;
            e.preventDefault();
            go(to);
          }}
        >
          {children}
        </a>
      )}
    >
      <div className="cap-admin-page" key={page}>
        <h1 data-testid="page-title">{PAGES[page]}</h1>
        <p className="cap-muted">Page {page + 1} of {PAGES.length}.</p>
        <button type="button" className="cap-btn" onClick={() => go((page + 1) % PAGES.length)}>
          Next page
        </button>
      </div>
    </AdminShell>
  );
}

// Foxhound's mark: today's x beside the two alternatives, for Dustin to pick from.
function FoxhoundMarks() {
  const today = FAMILY_APPS.find((a) => a.id === "foxhound")?.logo;
  const options: [string, string, ReactNode][] = [["today", "Today: the favicon's x", today], ...Object.entries(FOXHOUND_MARKS).map(([k, v]): [string, string, ReactNode] => [k, k === "track" ? "Alternative: a paw print" : "Alternative: a fox's head", v])];
  return (
    <ul className="cap-admin-marks" style={{ display: "flex", gap: "2rem", listStyle: "none", margin: 0, padding: "1rem" }}>
      {options.map(([id, label, logo]) => (
        <li key={id} data-mark={id} style={{ display: "grid", justifyItems: "center", gap: ".5rem", color: "var(--text)" }}>
          <span style={{ display: "grid", placeItems: "center", width: 56, height: 56, borderRadius: 12, background: "var(--ground)", border: "1px solid var(--line)", color: "var(--text)" }}><span style={{ display: "grid", width: 28, height: 28 }}>{logo}</span></span>
          <span>{label}</span>
        </li>
      ))}
    </ul>
  );
}

const SPECIMENS: Record<string, () => ReactNode> = {
  expanded: () => <Shell name="expanded" />,
  collapsed: () => <Shell name="collapsed" collapsed />,
  tip: () => <Tipped />,
  "collapsed-tip": () => <CollapsedTip />,
  trail: () => <Shell name="trail" nav={nav.map((e) => ({ ...e, current: e.id === "sites" }))} page="fieldnotes.example" trail={[{ label: "Sites", href: "#sites" }]} />,
  account: () => <AccountOpen />,
  reader: () => (
    <AdminShell {...base} {...reader} prefKey={key("reader")} children={<div className="cap-admin-page" data-measure="reading"><h1>Research</h1><p>A reading column holds a comfortable line length.</p></div>} />
  ),
  user: () => <Shell name="user" {...dashboard} />,
  "user-collapsed": () => <Shell name="user-collapsed" collapsed {...dashboard} />,
  comfortable: () => (
    <div data-density="comfortable">
      <Shell name="comfortable" />
    </div>
  ),
  transition: () => <Transitioning />,
  // An app that passes only `app`: the strip still lists all seven, each with its real mark.
  minimal: () => (
    <AdminShell app="foxhound" nav={nav} page={FIRST_PAGE} prefKey={key("minimal")}>
      <Page />
    </AdminShell>
  ),
  "foxhound-marks": () => <FoxhoundMarks />,
  phone: () => <Shell name="phone" />,
  "phone-sheet": () => <SheetOpen />,
  "phone-counts": () => <Shell name="phone-counts" nav={nav.map((e) => (e.id === "queue" ? { ...e, count: 120 } : e))} />,
};

for (const el of Array.from(document.querySelectorAll<HTMLElement>("[data-mount]"))) {
  const render = SPECIMENS[el.dataset.mount ?? ""];
  if (render) createRoot(el).render(<StrictMode>{render()}</StrictMode>);
}