cortena-ui 1.16.1 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -586,7 +586,11 @@ function AppShell({ className, style, extension, user, nav, headerSlot, menuItem
586
586
  }) : null, /* @__PURE__ */ jsx("main", {
587
587
  "data-slot": "app-shell-main",
588
588
  className: "min-w-0 flex-1",
589
- children
589
+ children: /* @__PURE__ */ jsx("div", {
590
+ "data-slot": "app-shell-content",
591
+ className: "mx-auto w-full max-w-[var(--ds-layout-content)]",
592
+ children
593
+ })
590
594
  })]
591
595
  }),
592
596
  nav && navOpen ? /* @__PURE__ */ jsxs("div", {
@@ -1 +1 @@
1
- {"version":3,"file":"app-shell.js","names":["Icon"],"sources":["../../src/components/app-shell.tsx"],"sourcesContent":["\"use client\";\n\nimport { getMark } from \"cortena-design/marks\";\nimport {\n CircleQuestionMark,\n LogOut,\n type LucideIcon,\n Menu,\n Monitor,\n Moon,\n Settings,\n Sun,\n User,\n X,\n} from \"lucide-react\";\nimport * as React from \"react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/avatar\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/components/dropdown-menu\";\nimport { type CortenaTheme, useCortenaTheme } from \"@/hooks/use-cortena-theme\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * AppShell — the chrome every Cortena extension wears, so that moving between\n * two of them is moving between two screens of one product rather than between\n * two applications (how-to-create-a-cortena-extension §9, audit rule P-10).\n *\n * Four things are fixed, and they are fixed because an extension that places\n * them itself places them somewhere slightly different:\n *\n * top left the extension's name and its registered mark, 20px inside a\n * 32px square. The mark comes from `cortena-design/marks` by\n * `extension.id` — the same file the Apps tile, the MCP app\n * card, the agent pop-up pill and the favicon draw (§9.1), not\n * an SVG in this app's web/public.\n * top right the avatar menu. It is the ONLY settings entry point:\n * Profile, Settings, Log out. Not a gear in a sidebar as well\n * (§9.2).\n * bottom right one fixed cluster, theme toggle then help button, left to\n * right, 24px from both viewport edges, 40px each, 12px apart\n * (§9.3). The agent pop-up pill stacks above it.\n * everywhere the theme is `useCortenaTheme`, so the choice persists and a\n * host `postMessage` still wins.\n *\n * The help panel itself is EXTBP-24 and does not exist yet, so `helpPanel` is a\n * slot: pass a node or a render function and the shell owns the button, the\n * open state and the corner. When HelpPanel ships, `help.source` is what it\n * reads — the extension's functional document (§12) — and the default panel\n * replaces the slot without a call-site change.\n */\n\n/* ── the mark ────────────────────────────────────────────────────────────── */\n\nexport interface BrandMarkProps extends Omit<React.ComponentProps<\"span\">, \"children\"> {\n /** Registered mark id — what the extension puts in `x-cortena.icon`. */\n id: string;\n /** The extension name. Labels the mark, and supplies the fallback initial. */\n name: string;\n /** Rendered size of the glyph in pixels. 20 in the shell, 24 in the Apps tile, 16 on the pill. */\n size?: number;\n /** Set when the mark is decorative because the name is already beside it. */\n decorative?: boolean;\n}\n\n/**\n * The presentation attributes a mark's root may carry into the DOM, mapped\n * from the SVG spelling to the JSX one.\n *\n * The same allow-list cortena-design's `validateMarkup` enforces at build\n * time, repeated here because this component spreads values that came from a\n * package the consumer resolves — an older `cortena-design`, a patched one, a\n * hand-edited `dist/marks.js` — straight onto an element.\n *\n * The mapping is the fix for DESIGN-88. A registered mark's root is\n * `{ \"stroke-width\": \"2.5\", \"stroke-linecap\": \"round\", … }` — the SVG\n * attribute names, which is what an SVG file carries — and spreading those\n * into JSX verbatim made React log three errors per page (\"Invalid DOM\n * property `stroke-width`. Did you mean `strokeWidth`?\"). React still applied\n * them, so nothing looked wrong; every screen of every extension just opened\n * with three console errors, which is how a real one gets missed.\n */\nconst MARK_ROOT_ATTRIBUTES: Readonly<Record<string, string>> = {\n fill: \"fill\",\n stroke: \"stroke\",\n \"stroke-width\": \"strokeWidth\",\n \"stroke-linecap\": \"strokeLinecap\",\n \"stroke-linejoin\": \"strokeLinejoin\",\n transform: \"transform\",\n opacity: \"opacity\",\n};\n\n/** `root`, with anything outside the allow-list dropped and the rest camelCased. */\nfunction safeRootAttributes(root: Record<string, string> | undefined): Record<string, string> {\n const safe: Record<string, string> = {};\n for (const [attribute, property] of Object.entries(MARK_ROOT_ATTRIBUTES)) {\n const value = root?.[attribute];\n if (typeof value === \"string\") {\n safe[property] = value;\n }\n }\n return safe;\n}\n\n/**\n * The registered mark for `id`, or the extension's initial when nothing is\n * registered. The fallback matters: an unregistered id used to render an empty\n * box, which reads as a broken image rather than as \"this extension has not\n * registered a mark yet\".\n */\nfunction BrandMark({ className, id, name, size = 20, decorative, ...props }: BrandMarkProps) {\n const mark = getMark(id);\n const label = decorative ? undefined : name;\n return (\n <span\n data-slot=\"brand-mark\"\n data-mark={mark ? mark.id : \"fallback\"}\n className={cn(\"inline-flex shrink-0 items-center justify-center\", className)}\n style={{ width: size, height: size }}\n {...props}\n >\n {mark ? (\n <svg\n viewBox={mark.viewBox}\n width={size}\n height={size}\n role={decorative ? undefined : \"img\"}\n aria-label={label}\n aria-hidden={decorative ? true : undefined}\n focusable=\"false\"\n // The body is generated by cortena-design's build from a file in this\n // monorepo, where `validateMarkup` accepts an allow-list of elements\n // and attributes — no <script>, <style>, <use>, <image> or\n // <foreignObject>, no href and no on* handler — and paints only in\n // currentColor. It is not user input, and it is not sanitised here:\n // the guarantee is the build's, so a mark that would need sanitising\n // fails `pnpm build` instead of being quietly cleaned up.\n dangerouslySetInnerHTML={{ __html: mark.body }}\n // `root` is filtered again on the way in. A consumer can be running\n // against an older or hand-edited `cortena-design/marks`, and this\n // spread is the one place its values reach the DOM as attributes.\n {...safeRootAttributes(mark.root)}\n />\n ) : (\n <span\n aria-label={label}\n aria-hidden={decorative ? true : undefined}\n role={decorative ? undefined : \"img\"}\n className={cn(\n \"font-display font-bold uppercase leading-none\",\n \"text-[length:var(--ds-text-caption-lg)]\",\n )}\n >\n {name.trim().charAt(0) || \"?\"}\n </span>\n )}\n </span>\n );\n}\n\n/* ── bottom-right cluster ────────────────────────────────────────────────── */\n\nexport interface BottomRightClusterProps extends React.ComponentProps<\"div\"> {\n /** The agent pop-up pill (§17). Stacks above the row, never beside it. */\n agentSlot?: React.ReactNode;\n}\n\n/**\n * The one fixed container in the bottom-right corner. It exists so that no\n * extension positions a floating control itself — that is how a third one\n * appears and covers one of the first two.\n *\n * Its distance from the bottom edge is 24px PLUS `--ds-shell-bottom-inset`,\n * which `AppShell` sets from its `bottomInset` prop or from a `data-bottom-bar`\n * element it finds in the page. On a phone with a bottom tab bar the cluster\n * used to sit on top of two of the tabs; Tasks worked around it with\n * `className=\"bottom-22 lg:bottom-6\"` on its own cluster, which the agent\n * pop-up inside `agentSlot` had no equivalent of (DESIGN-88).\n */\nfunction BottomRightCluster({\n className,\n style,\n agentSlot,\n children,\n ...props\n}: BottomRightClusterProps) {\n return (\n <div\n data-slot=\"bottom-right-cluster\"\n className={cn(\"fixed right-6 z-40 flex flex-col items-end gap-3\", className)}\n // Inline rather than a utility: the value is `calc()` over a variable a\n // consumer may not have set, and `bottom-6` would have to be beaten by\n // specificity rather than replaced.\n style={{ bottom: `calc(${CLUSTER_INSET}px + var(${BOTTOM_INSET_VAR}, 0px))`, ...style }}\n {...props}\n >\n {agentSlot ? (\n <div data-slot=\"bottom-right-cluster-agent\" className=\"flex items-center justify-end\">\n {agentSlot}\n </div>\n ) : null}\n <div data-slot=\"bottom-right-cluster-row\" className=\"flex items-center gap-3\">\n {children}\n </div>\n </div>\n );\n}\n\n/** The cluster's distance from both viewport edges (§9.3). `right-6` / `bottom-6`. */\nconst CLUSTER_INSET = 24;\n/** What the help panel leaves between itself and the cluster, and below the header. */\nconst HELP_PANEL_GAP = 12;\n/**\n * The header's height, in pixels, declared once. The header takes it as an\n * inline height rather than as a utility class, because the sidebar's `top`,\n * the help panel's `top` and `--ds-shell-header-height` all have to agree with\n * it and a class is a second place for the number to live. What the shell\n * publishes is the *measured* height of the header element — this value is\n * what it starts from, before the first measurement and where there is no DOM.\n */\nconst HEADER_HEIGHT = 64;\n\n/**\n * The two custom properties the shell publishes.\n *\n * They are the least-coupled way to tell something the shell does not own —\n * the agent pop-up in `agentSlot`, a consumer's own floating panel — where the\n * chrome ends. Both are written on the shell's own element AND on\n * `<html>`, because a pop-up may portal out of the shell's subtree.\n */\n/** What the drawer's focus trap cycles through. */\nconst FOCUSABLE_SELECTOR = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"textarea:not([disabled])\",\n \"[tabindex]:not([tabindex='-1'])\",\n].join(\", \");\n\nconst BOTTOM_INSET_VAR = \"--ds-shell-bottom-inset\";\nconst HEADER_HEIGHT_VAR = \"--ds-shell-header-height\";\n\nconst clusterButtonClassName = [\n \"inline-flex size-10 shrink-0 items-center justify-center rounded-[var(--ds-radius-full)]\",\n \"border border-[var(--ds-border)] bg-[var(--ds-card)] text-[color:var(--ds-foreground)]\",\n \"shadow-[var(--ds-shadow-md)] outline-none\",\n \"transition-colors duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]\",\n \"hover:bg-[var(--ds-hover)]\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--ds-ring)] focus-visible:ring-offset-2\",\n \"focus-visible:ring-offset-[var(--ds-background)]\",\n].join(\" \");\n\n/** light → dark → system → light. Three states, one control, no menu to open. */\nconst THEME_ORDER: readonly CortenaTheme[] = [\"light\", \"dark\", \"system\"];\n\nconst THEME_LABEL: Record<CortenaTheme, string> = {\n light: \"Light\",\n dark: \"Dark\",\n system: \"System\",\n};\n\nexport interface ThemeToggleProps extends Omit<React.ComponentProps<\"button\">, \"onClick\"> {\n /** localStorage key, forwarded to `useCortenaTheme`. */\n storageKey?: string;\n}\n\n/**\n * The theme selector. It lives here rather than in the avatar menu because a\n * user changing theme is usually reacting to the room they are in, and that is\n * a one-tap decision, not a settings visit.\n */\nfunction ThemeToggle({ className, storageKey, ...props }: ThemeToggleProps) {\n const { theme, resolvedTheme, setTheme } = useCortenaTheme(\n storageKey === undefined ? {} : { storageKey },\n );\n const next = THEME_ORDER[(THEME_ORDER.indexOf(theme) + 1) % THEME_ORDER.length] ?? \"light\";\n const Icon = theme === \"system\" ? Monitor : theme === \"dark\" ? Moon : Sun;\n return (\n <button\n type=\"button\"\n data-slot=\"theme-toggle\"\n data-theme-choice={theme}\n data-theme-resolved={resolvedTheme}\n aria-label={`Theme: ${THEME_LABEL[theme]}. Switch to ${THEME_LABEL[next].toLowerCase()}.`}\n className={cn(clusterButtonClassName, className)}\n onClick={() => setTheme(next)}\n {...props}\n >\n <Icon className=\"size-5\" aria-hidden />\n </button>\n );\n}\n\n/** The id the shell gives its help region, and the button's `aria-controls`. */\nexport const HELP_PANEL_ID = \"cortena-help-panel\";\n\n/** The id the shell gives its mobile nav drawer, and the menu button's `aria-controls`. */\nexport const NAV_DRAWER_ID = \"cortena-nav-drawer\";\n\nexport interface HelpButtonProps extends React.ComponentProps<\"button\"> {\n /** Whether the panel it controls is open. */\n open?: boolean;\n}\n\n/** The help affordance. Right of the theme toggle, always — order is fixed (§9.3). */\nfunction HelpButton({ className, open, ...props }: HelpButtonProps) {\n return (\n <button\n type=\"button\"\n data-slot=\"help-button\"\n aria-label=\"Help\"\n aria-expanded={open}\n // `aria-expanded` alone says \"something is open\" without saying what.\n // A screen reader user then has to go and find it — and an id that names\n // an element which is not on the page is worse than saying nothing, so\n // it is set only while the panel is mounted.\n aria-controls={open ? HELP_PANEL_ID : undefined}\n data-open={open ? \"\" : undefined}\n className={cn(clusterButtonClassName, open && \"bg-[var(--ds-hover)]\", className)}\n {...props}\n >\n <CircleQuestionMark className=\"size-5\" aria-hidden />\n </button>\n );\n}\n\n/* ── navigation ──────────────────────────────────────────────────────────── */\n\n/**\n * A nav item's glyph: a lucide icon component, or the id of a mark registered\n * in `cortena-design/marks` (so a nav row can wear another extension's mark\n * without importing its SVG).\n */\nexport type AppShellNavIcon = LucideIcon | string;\n\nexport interface AppShellNavItem {\n /** Stable id. What `activeId` names, and what `data-nav-item` carries. */\n id: string;\n label: string;\n icon?: AppShellNavIcon;\n /** The destination. Rendered as a real `href`, so middle-click and copy-link work. */\n href: string;\n /** A count or a short word. Rendered from tokens; `0` and `\"\"` draw nothing. */\n badge?: number | string;\n /** Match `pathname` exactly rather than as a prefix. Ignored under `activeId`/`isActive`. */\n exact?: boolean;\n}\n\nexport interface AppShellNavGroup {\n /** Section heading. A group with no label is separated by a rule instead. */\n label?: string;\n items: AppShellNavItem[];\n}\n\nexport interface AppShellNav {\n groups: AppShellNavGroup[];\n /**\n * Which item is active, decided in one of three ways, in this order:\n *\n * `activeId` the item's own id. The simplest, and the only one that\n * needs nothing from the router.\n * `isActive` a callback per item. Use it when \"active\" is not a path\n * comparison — a query parameter, a nested layout route.\n * `pathname` the current path, compared to each `href`: a prefix match,\n * or an exact one for an item marked `exact`.\n *\n * cortena-ui has no router dependency and never will, so one of the three\n * has to come from the consumer. With react-router:\n *\n * ```tsx\n * const { pathname } = useLocation();\n * const navigate = useNavigate();\n *\n * <AppShell\n * nav={{\n * groups,\n * pathname,\n * // Keep it a client-side navigation; the href stays real for\n * // middle-click, \"open in new tab\" and copy-link, and the shell never\n * // calls this for a modified or middle click.\n * onNavigate: (item, event) => {\n * event.preventDefault();\n * navigate(item.href);\n * },\n * }}\n * />\n * ```\n *\n * Give none of the three and nothing is active — deliberately, because the\n * alternative is reading `window.location` here, which would be right on\n * first paint and then silently stale on every navigation after it.\n */\n activeId?: string;\n isActive?: (href: string, item: AppShellNavItem) => boolean;\n pathname?: string;\n /** Intercept a click. Called before the browser follows the `href`. */\n onNavigate?: (item: AppShellNavItem, event: React.MouseEvent<HTMLAnchorElement>) => void;\n /**\n * Pinned under the items: File Vault's storage meter, Flux's stream status.\n * The one thing in the sidebar that is neither navigation nor chrome.\n */\n footer?: React.ReactNode;\n /** The landmark's accessible name. Default \"Sections\". */\n label?: string;\n}\n\n/** Which of the three signals decides, in order. See `AppShellNav`. */\nfunction isNavItemActive(nav: AppShellNav, item: AppShellNavItem): boolean {\n if (nav.activeId !== undefined) {\n return item.id === nav.activeId;\n }\n if (nav.isActive) {\n return nav.isActive(item.href, item);\n }\n if (nav.pathname === undefined) {\n return false;\n }\n if (item.exact || item.href === \"/\") {\n return nav.pathname === item.href;\n }\n const base = item.href.endsWith(\"/\") ? item.href.slice(0, -1) : item.href;\n return nav.pathname === base || nav.pathname.startsWith(`${base}/`);\n}\n\n/** A lucide component, or a registered mark drawn at the same 16px. */\nfunction NavIcon({ icon, label }: { icon: AppShellNavIcon; label: string }) {\n if (typeof icon === \"string\") {\n return <BrandMark id={icon} name={label} size={16} decorative />;\n }\n const Icon = icon;\n return <Icon className=\"size-4 shrink-0\" aria-hidden />;\n}\n\n/** A click the browser would answer with a plain same-tab navigation. */\nfunction isPlainLeftClick(event: React.MouseEvent): boolean {\n return (\n event.button === 0 &&\n !event.metaKey &&\n !event.ctrlKey &&\n !event.shiftKey &&\n !event.altKey\n );\n}\n\ninterface NavLinkProps {\n item: AppShellNavItem;\n active: boolean;\n onNavigate?: AppShellNav[\"onNavigate\"];\n onFollow?: () => void;\n}\n\n/**\n * One row. An `<a>` with a real `href`, because a nav item that is a `<button>`\n * cannot be middle-clicked, opened in a new tab or copied — and every one of\n * these is a route.\n */\nfunction NavLink({ item, active, onNavigate, onFollow }: NavLinkProps) {\n const badge = item.badge;\n const hasBadge = badge !== undefined && badge !== \"\" && badge !== 0;\n return (\n <a\n data-slot=\"app-shell-nav-item\"\n data-nav-item={item.id}\n data-active={active ? \"\" : undefined}\n href={item.href}\n // `aria-current=\"page\"` is the active state a screen reader hears. The\n // colour is the one a sighted user sees; neither stands in for the other.\n aria-current={active ? \"page\" : undefined}\n onClick={(event) => {\n // A cmd/ctrl-click is \"open this in a new tab\", a shift-click is a new\n // window, an alt-click is a download, and a middle-click is a\n // background tab. Every one of those is the browser's answer to this\n // `href`, so the shell hands none of them to `onNavigate` and does not\n // close a drawer whose screen the user never left. Without this,\n // `onNavigate` fires on all of them and each consumer has to write the\n // same four-condition guard before its `preventDefault()`.\n if (!isPlainLeftClick(event)) {\n return;\n }\n onFollow?.();\n onNavigate?.(item, event);\n }}\n className={cn(\n \"flex items-center gap-2.5 rounded-[var(--ds-radius-md)] px-3 py-2\",\n \"text-[length:var(--ds-text-caption-lg)] no-underline outline-none\",\n \"transition-colors duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--ds-ring)]\",\n active\n ? \"bg-[var(--ds-primary-soft)] font-semibold text-[color:var(--ds-primary)]\"\n : cn(\n \"font-medium text-[color:var(--ds-muted-foreground)]\",\n \"hover:bg-[var(--ds-hover)] hover:text-[color:var(--ds-foreground)]\",\n ),\n )}\n >\n {item.icon ? <NavIcon icon={item.icon} label={item.label} /> : null}\n <span className=\"truncate\">{item.label}</span>\n {hasBadge ? (\n <span\n data-slot=\"app-shell-nav-badge\"\n className={cn(\n \"ml-auto inline-flex min-w-5 shrink-0 items-center justify-center\",\n \"rounded-[var(--ds-radius-full)] px-1.5\",\n \"text-[length:var(--ds-text-caption-sm)] font-semibold tabular-nums\",\n active\n ? \"bg-[var(--ds-primary)] text-[color:var(--ds-primary-foreground)]\"\n : \"bg-[var(--ds-hover)] text-[color:var(--ds-muted-foreground)]\",\n )}\n >\n {badge}\n </span>\n ) : null}\n </a>\n );\n}\n\n/** The items themselves. Shared by the sidebar and the mobile drawer, unchanged. */\nfunction NavItems({ nav, onFollow }: { nav: AppShellNav; onFollow?: () => void }) {\n return (\n <nav\n data-slot=\"app-shell-nav-list\"\n aria-label={nav.label ?? \"Sections\"}\n className=\"flex min-h-0 flex-1 flex-col gap-1\"\n >\n {nav.groups.map((group, index) => (\n <div\n key={group.label ?? `group-${index}`}\n data-slot=\"app-shell-nav-group\"\n data-group-label={group.label}\n className={cn(\n \"flex flex-col gap-1\",\n // A labelled group is separated by its label; an unlabelled one\n // has nothing to separate it, so it gets a rule.\n index > 0 && !group.label && \"mt-2 border-t border-[var(--ds-border-subtle)] pt-2\",\n )}\n >\n {group.label ? (\n <span\n data-slot=\"app-shell-nav-group-label\"\n className={cn(\n \"px-3 pt-3 pb-1 text-[length:var(--ds-text-eyebrow)] uppercase\",\n \"tracking-[var(--ds-text-eyebrow--letter-spacing)]\",\n \"text-[color:var(--ds-muted-foreground)]\",\n )}\n >\n {group.label}\n </span>\n ) : null}\n {group.items.map((item) => (\n <NavLink\n key={item.id}\n item={item}\n active={isNavItemActive(nav, item)}\n onNavigate={nav.onNavigate}\n onFollow={onFollow}\n />\n ))}\n </div>\n ))}\n {nav.footer ? (\n <div data-slot=\"app-shell-nav-footer\" className=\"mt-auto pt-3\">\n {nav.footer}\n </div>\n ) : null}\n </nav>\n );\n}\n\n/* ── the shell ───────────────────────────────────────────────────────────── */\n\nexport interface AppShellExtension {\n /** Registered mark id, from `x-cortena.icon` in the OpenAPI document. */\n id: string;\n /** Display name. Also the first half of the document title, \"<name> · Cortena\". */\n name: string;\n}\n\nexport interface AppShellUser {\n name: string;\n avatarUrl?: string;\n}\n\nexport interface AppShellHelp {\n /**\n * The extension's functional document — the thing the help panel answers\n * from (§12). Giving that document a runtime consumer is the point: a stale\n * doc becomes a visibly wrong help panel rather than a file nobody opens.\n */\n source: string;\n}\n\nexport interface HelpPanelSlotProps {\n open: boolean;\n source?: string;\n onClose: () => void;\n}\n\nexport interface AppShellMenuItem {\n id: string;\n label: string;\n icon?: LucideIcon;\n onSelect: () => void;\n}\n\nexport interface AppShellProps extends Omit<React.ComponentProps<\"div\">, \"title\"> {\n extension: AppShellExtension;\n user: AppShellUser;\n /**\n * The left sidebar. `null` (or omitted) renders no sidebar and no menu\n * button — a legal state, and the right one for an extension whose whole\n * navigation is inside one page.\n */\n nav?: AppShellNav | null;\n /**\n * Between the extension name and the avatar. For state, not for controls:\n * a live-status pill, the org or workspace the screen is scoped to, or one\n * secondary action. Navigation, theme, help and settings do NOT go here —\n * they have exactly one home each and a second one is how two extensions\n * stop looking like one product (§10).\n */\n headerSlot?: React.ReactNode;\n /**\n * Extra avatar-menu items, inserted between Settings and Log out. For a\n * destination that is the user's, not the screen's — Assure's Permissions,\n * a Report-issue dialog. Not a second way to reach something already in the\n * shell.\n */\n menuItems?: AppShellMenuItem[];\n /**\n * How far the bottom-right cluster (and anything in `agentSlot`) lifts off\n * the bottom edge, on top of its own 24px. A number is pixels.\n *\n * Omit it and the shell measures the first `[data-bottom-bar]` element\n * inside itself instead, which is 0 when that element is hidden — so a\n * `lg:hidden` mobile tab bar lifts the cluster on a phone and nowhere else,\n * with nothing to keep in sync.\n */\n bottomInset?: string | number;\n /** Opens the extension's own settings route. The avatar menu is the only way in. */\n onSettings?: () => void;\n onProfile?: () => void;\n onLogout?: () => void;\n help?: AppShellHelp;\n /**\n * The help panel, until HelpPanel ships (EXTBP-24). A render function gets\n * the open state and owns its own markup; a node is mounted in a right-hand\n * region while open.\n */\n helpPanel?: React.ReactNode | ((props: HelpPanelSlotProps) => React.ReactNode);\n /** The agent pop-up pill. Stacks above the bottom-right cluster. */\n agentSlot?: React.ReactNode;\n /** localStorage key for the theme choice, forwarded to `useCortenaTheme`. */\n themeStorageKey?: string;\n}\n\n/**\n * How many shells are mounted. `<html>` has one set of these two variables and\n * any number of shells may be on the page at once — the token guide mounts\n * three — so writing them on every mount is last-writer-wins, and removing\n * them on every unmount takes them away from the shells still on screen.\n *\n * The first shell to mount owns them; the last to unmount clears them. A\n * second shell publishes the same two values on its own element either way,\n * which is what anything inside its subtree reads.\n */\nlet mountedShells = 0;\n\nfunction useOwnsRootVars(): boolean {\n const [owns, setOwns] = React.useState(false);\n React.useEffect(() => {\n mountedShells += 1;\n setOwns(mountedShells === 1);\n return () => {\n mountedShells -= 1;\n if (mountedShells === 0) {\n const root = document.documentElement;\n root.style.removeProperty(BOTTOM_INSET_VAR);\n root.style.removeProperty(HEADER_HEIGHT_VAR);\n }\n };\n }, []);\n return owns;\n}\n\nfunction AppShell({\n className,\n style,\n extension,\n user,\n nav,\n headerSlot,\n menuItems,\n bottomInset,\n onSettings,\n onProfile,\n onLogout,\n help,\n helpPanel,\n agentSlot,\n themeStorageKey,\n children,\n ...props\n}: AppShellProps) {\n const [helpOpen, setHelpOpen] = React.useState(false);\n const helpButtonRef = React.useRef<HTMLButtonElement>(null);\n const closeHelp = React.useCallback(() => {\n setHelpOpen(false);\n // Focus goes back where it came from. A panel closed with Escape or its\n // own X otherwise left focus on a removed node, which the browser resets\n // to <body> — the next Tab starts from the top of the page, and a\n // keyboard user has to walk the whole shell to get back.\n helpButtonRef.current?.focus();\n }, []);\n\n /**\n * The help panel stops 12px above the bottom-right cluster, so the button\n * that opened it stays visible and clickable. The gap was hard-coded at\n * 76px — one 40px control plus the two margins — and the cluster is not\n * always that tall: the agent pop-up pill stacks ABOVE the row, which makes\n * it 92px, and the panel then covered the pill it was sitting on.\n */\n //\n // Every observer below is keyed on the DOM node, held in state through a\n // callback ref, and never on a React element. `agentSlot`, `children` and\n // the rest are fresh objects on every render of the consumer's page, so an\n // effect that lists them tore down and rebuilt its ResizeObserver on every\n // keystroke in the page underneath. The observer is what notices a size\n // change; it does not need the element identity to do it.\n const [clusterNode, setClusterNode] = React.useState<HTMLDivElement | null>(null);\n const [clusterHeight, setClusterHeight] = React.useState(40);\n React.useEffect(() => {\n if (!clusterNode || typeof ResizeObserver === \"undefined\") {\n return;\n }\n const measure = () => setClusterHeight(clusterNode.getBoundingClientRect().height);\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(clusterNode);\n return () => observer.disconnect();\n }, [clusterNode]);\n\n /**\n * The header's height as the browser laid it out, which is what the sidebar,\n * the help panel and `--ds-shell-header-height` all read. `HEADER_HEIGHT` is\n * where the number is declared; this is where it is confirmed.\n */\n const [headerNode, setHeaderNode] = React.useState<HTMLElement | null>(null);\n const [headerHeight, setHeaderHeight] = React.useState(HEADER_HEIGHT);\n React.useEffect(() => {\n if (!headerNode || typeof ResizeObserver === \"undefined\") {\n return;\n }\n const measure = () => setHeaderHeight(headerNode.getBoundingClientRect().height);\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(headerNode);\n return () => observer.disconnect();\n }, [headerNode]);\n /**\n * The bottom inset. Given explicitly, or measured from the first\n * `[data-bottom-bar]` element in the page — which reports 0 while it is\n * hidden, so a `lg:hidden` tab bar lifts the cluster on a phone and nowhere\n * else without the app repeating its own breakpoint here.\n */\n const shellRef = React.useRef<HTMLDivElement>(null);\n const [barNode, setBarNode] = React.useState<HTMLElement | null>(null);\n const [detectedInset, setDetectedInset] = React.useState(0);\n // The bar belongs to the consumer's `children`, so there is no ref to give\n // it: it is found by query. Finding it is a `querySelector` after each\n // render — cheap, and the only way a bar that arrives with the data is\n // noticed at all. Observing it is keyed on the node it found, so the\n // observer survives renders that did not change which element it is.\n React.useEffect(() => {\n if (bottomInset !== undefined) {\n return;\n }\n setBarNode(shellRef.current?.querySelector<HTMLElement>(\"[data-bottom-bar]\") ?? null);\n });\n React.useEffect(() => {\n if (bottomInset !== undefined) {\n return;\n }\n if (!barNode || typeof ResizeObserver === \"undefined\") {\n setDetectedInset(0);\n return;\n }\n const measure = () => setDetectedInset(barNode.getBoundingClientRect().height);\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(barNode);\n return () => observer.disconnect();\n }, [barNode, bottomInset]);\n const resolvedInset =\n bottomInset === undefined\n ? `${detectedInset}px`\n : typeof bottomInset === \"number\"\n ? `${bottomInset}px`\n : bottomInset;\n\n /**\n * The same two values on `<html>`. The shell's own element covers everything\n * it renders; the agent pop-up may portal out of that subtree, and a\n * variable it cannot see is a variable it cannot honour.\n *\n * Only the first shell on the page writes them; see `useOwnsRootVars`.\n */\n const ownsRootVars = useOwnsRootVars();\n React.useEffect(() => {\n if (!ownsRootVars) {\n return;\n }\n const root = document.documentElement;\n root.style.setProperty(BOTTOM_INSET_VAR, resolvedInset);\n root.style.setProperty(HEADER_HEIGHT_VAR, `${headerHeight}px`);\n }, [ownsRootVars, resolvedInset, headerHeight]);\n\n /** The mobile nav drawer. Nothing below `lg` has room for a 224px sidebar. */\n const [navOpen, setNavOpen] = React.useState(false);\n const navToggleRef = React.useRef<HTMLButtonElement>(null);\n const navOverlayRef = React.useRef<HTMLDivElement>(null);\n const navDrawerRef = React.useRef<HTMLDivElement>(null);\n const navCloseRef = React.useRef<HTMLButtonElement>(null);\n const closeNav = React.useCallback(() => setNavOpen(false), []);\n /**\n * What `role=\"dialog\" aria-modal=\"true\"` promises, done rather than\n * declared: focus starts inside, Tab stays inside, the shell behind the\n * scrim is out of reach, and the page under it does not scroll.\n *\n * `inert` goes on the shell's own children rather than on `<body>` so that\n * the overlay — which is one of those children — stays live, and so that\n * anything a consumer portals outside the shell is untouched.\n */\n React.useEffect(() => {\n if (!navOpen) {\n return;\n }\n const shell = shellRef.current;\n const behind = shell\n ? [...shell.children].filter((el) => el !== navOverlayRef.current)\n : [];\n for (const el of behind) {\n el.setAttribute(\"inert\", \"\");\n }\n const { body } = document;\n const scroll = body.style.overflow;\n body.style.overflow = \"hidden\";\n // The close button, not the drawer: focus on a container is focus a\n // screen reader announces and a Tab immediately leaves.\n navCloseRef.current?.focus();\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") {\n closeNav();\n return;\n }\n if (event.key !== \"Tab\") {\n return;\n }\n const drawer = navDrawerRef.current;\n const focusable = drawer\n ? [...drawer.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)]\n : [];\n const first = focusable[0];\n const last = focusable.at(-1);\n if (!drawer || !first || !last) {\n return;\n }\n const active = document.activeElement;\n const leaving = event.shiftKey ? active === first || !drawer.contains(active) : active === last;\n if (leaving) {\n event.preventDefault();\n (event.shiftKey ? last : first).focus();\n }\n };\n document.addEventListener(\"keydown\", onKeyDown);\n return () => {\n document.removeEventListener(\"keydown\", onKeyDown);\n for (const el of behind) {\n el.removeAttribute(\"inert\");\n }\n body.style.overflow = scroll;\n // Focus goes back to the button that opened it, and it goes back here\n // rather than in `closeNav` because the toggle is inside `behind`: while\n // the drawer is open it is inert, and focusing an inert element does\n // nothing at all. Left where it was, focus falls to <body> and the next\n // Tab starts from the top of the page.\n navToggleRef.current?.focus();\n };\n }, [navOpen, closeNav]);\n\n const hasHelp = help !== undefined || helpPanel !== undefined;\n const panel =\n typeof helpPanel === \"function\"\n ? helpPanel({ open: helpOpen, source: help?.source, onClose: closeHelp })\n : helpOpen\n ? helpPanel\n : null;\n\n return (\n <div\n ref={shellRef}\n data-slot=\"app-shell\"\n className={cn(\n \"flex min-h-screen w-full flex-col bg-[var(--ds-background)] text-[color:var(--ds-foreground)]\",\n className,\n )}\n style={\n {\n [HEADER_HEIGHT_VAR]: `${headerHeight}px`,\n [BOTTOM_INSET_VAR]: resolvedInset,\n ...style,\n } as React.CSSProperties\n }\n {...props}\n >\n <header\n data-slot=\"app-shell-header\"\n ref={setHeaderNode}\n className={cn(\n \"sticky top-0 z-30 flex w-full shrink-0 items-center justify-between gap-3\",\n \"border-b border-[var(--ds-border-subtle)] bg-[var(--ds-background)] px-4 sm:px-6\",\n )}\n style={{ height: HEADER_HEIGHT }}\n >\n <div data-slot=\"app-shell-brand\" className=\"flex min-w-0 items-center gap-2.5\">\n {nav ? (\n <button\n ref={navToggleRef}\n type=\"button\"\n data-slot=\"app-shell-nav-toggle\"\n aria-label=\"Menu\"\n aria-expanded={navOpen}\n // Only while the drawer exists; see `HelpButton`.\n aria-controls={navOpen ? NAV_DRAWER_ID : undefined}\n onClick={() => setNavOpen(true)}\n className={cn(\n \"-ml-1 inline-flex size-9 shrink-0 items-center justify-center lg:hidden\",\n \"rounded-[var(--ds-radius-md)] text-[color:var(--ds-muted-foreground)] outline-none\",\n \"transition-colors duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]\",\n \"hover:bg-[var(--ds-hover)] hover:text-[color:var(--ds-foreground)]\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--ds-ring)]\",\n )}\n >\n <Menu className=\"size-5\" aria-hidden />\n </button>\n ) : null}\n <span\n data-slot=\"app-shell-brand-square\"\n className={cn(\n \"flex size-8 shrink-0 items-center justify-center rounded-[var(--ds-radius-md)]\",\n \"bg-[var(--ds-primary-soft)] text-[color:var(--ds-primary)]\",\n )}\n >\n <BrandMark id={extension.id} name={extension.name} size={20} decorative />\n </span>\n <span\n data-slot=\"app-shell-brand-name\"\n className={cn(\n \"truncate font-display text-[length:var(--ds-text-body-md)] font-bold\",\n \"leading-[var(--ds-text-body-md--line-height)] text-[color:var(--ds-foreground)]\",\n )}\n >\n {extension.name}\n </span>\n </div>\n\n {headerSlot ? (\n <div\n data-slot=\"app-shell-header-slot\"\n className=\"flex min-w-0 flex-1 items-center gap-2\"\n >\n {headerSlot}\n </div>\n ) : null}\n\n <DropdownMenu>\n <DropdownMenuTrigger\n data-slot=\"app-shell-avatar-trigger\"\n aria-label={`Account: ${user.name}`}\n className={cn(\n \"flex shrink-0 items-center rounded-[var(--ds-radius-full)] outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--ds-ring)] focus-visible:ring-offset-2\",\n \"focus-visible:ring-offset-[var(--ds-background)]\",\n )}\n >\n <Avatar>\n {user.avatarUrl ? <AvatarImage src={user.avatarUrl} alt=\"\" /> : null}\n <AvatarFallback>{initials(user.name)}</AvatarFallback>\n </Avatar>\n </DropdownMenuTrigger>\n {/* Profile, Settings, Log out. Nothing else, and no second settings\n entry point anywhere in the extension (§9.2). */}\n <DropdownMenuContent align=\"end\" sideOffset={8} className=\"min-w-48\">\n <DropdownMenuItem\n data-item=\"profile\"\n disabled={!onProfile}\n onClick={onProfile}\n >\n <User /> Profile\n </DropdownMenuItem>\n <DropdownMenuItem\n data-item=\"settings\"\n disabled={!onSettings}\n onClick={onSettings}\n >\n <Settings /> Settings\n </DropdownMenuItem>\n {menuItems?.map((item) => {\n const Icon = item.icon;\n return (\n <DropdownMenuItem key={item.id} data-item={item.id} onClick={item.onSelect}>\n {Icon ? <Icon /> : null} {item.label}\n </DropdownMenuItem>\n );\n })}\n <DropdownMenuSeparator />\n <DropdownMenuItem\n data-item=\"logout\"\n variant=\"destructive\"\n disabled={!onLogout}\n onClick={onLogout}\n >\n <LogOut /> Log out\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </header>\n\n <div data-slot=\"app-shell-body\" className=\"flex min-h-0 w-full flex-1\">\n {nav ? (\n <aside\n data-slot=\"app-shell-nav\"\n className={cn(\n \"sticky hidden w-56 shrink-0 flex-col overflow-y-auto p-3 lg:flex\",\n \"border-r border-[var(--ds-border-subtle)] bg-[var(--ds-background)]\",\n )}\n // The sidebar starts under the header and never scrolls past the\n // viewport. `calc()` over a length, so it is written here rather\n // than as an arbitrary Tailwind value, which would need escaping\n // and would fail silently if it were got wrong.\n style={{ top: headerHeight, maxHeight: `calc(100vh - ${headerHeight}px)` }}\n >\n <NavItems nav={nav} />\n </aside>\n ) : null}\n\n <main data-slot=\"app-shell-main\" className=\"min-w-0 flex-1\">\n {children}\n </main>\n </div>\n\n {nav && navOpen ? (\n <div\n ref={navOverlayRef}\n data-slot=\"app-shell-nav-overlay\"\n className=\"fixed inset-0 z-50 lg:hidden\"\n >\n {/* A button rather than a div: dismissing an overlay by clicking\n outside it has to be reachable from the keyboard too. */}\n <button\n type=\"button\"\n data-slot=\"app-shell-nav-scrim\"\n aria-label=\"Close menu\"\n onClick={closeNav}\n className={cn(\n \"absolute inset-0 backdrop-blur-[2px]\",\n \"bg-[color-mix(in_oklab,var(--ds-ink-950)_70%,transparent)]\",\n )}\n />\n <div\n ref={navDrawerRef}\n id={NAV_DRAWER_ID}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={`${extension.name} navigation`}\n data-slot=\"app-shell-nav-drawer\"\n className={cn(\n \"absolute inset-y-0 left-0 flex w-72 max-w-[85vw] flex-col gap-1 overflow-y-auto p-3\",\n \"border-r border-[var(--ds-border-subtle)] bg-[var(--ds-background)]\",\n \"shadow-[var(--ds-shadow-lg)]\",\n )}\n >\n <div className=\"flex items-center justify-between gap-2 pb-2\">\n <span className=\"flex size-8 shrink-0 items-center justify-center rounded-[var(--ds-radius-md)] bg-[var(--ds-primary-soft)] text-[color:var(--ds-primary)]\">\n <BrandMark id={extension.id} name={extension.name} size={20} decorative />\n </span>\n <button\n ref={navCloseRef}\n type=\"button\"\n data-slot=\"app-shell-nav-drawer-close\"\n aria-label=\"Close menu\"\n onClick={closeNav}\n className={cn(\n \"inline-flex size-9 shrink-0 items-center justify-center\",\n \"rounded-[var(--ds-radius-md)] text-[color:var(--ds-muted-foreground)] outline-none\",\n \"transition-colors duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]\",\n \"hover:bg-[var(--ds-hover)] hover:text-[color:var(--ds-foreground)]\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--ds-ring)]\",\n )}\n >\n <X className=\"size-5\" aria-hidden />\n </button>\n </div>\n {/* Following a link closes the drawer: it covers the screen the\n link just navigated to. */}\n <NavItems nav={nav} onFollow={closeNav} />\n </div>\n </div>\n ) : null}\n\n {panel ? (\n <aside\n id={HELP_PANEL_ID}\n data-slot=\"app-shell-help-panel\"\n aria-label={`${extension.name} help`}\n // Measured, not assumed: 24px of viewport margin, whatever a bottom\n // bar has claimed, the cluster the panel must clear, and 12px\n // between the two. At the top it starts below the 64px header — it\n // was `top-6`, which put it over the header and over the avatar\n // trigger (DESIGN-88).\n style={{\n top: `calc(var(${HEADER_HEIGHT_VAR}, ${headerHeight}px) + ${HELP_PANEL_GAP}px)`,\n bottom: `calc(${CLUSTER_INSET + clusterHeight + HELP_PANEL_GAP}px + var(${BOTTOM_INSET_VAR}, 0px))`,\n }}\n className={cn(\n // Anchored to the same corner. A panel that covers its own trigger\n // leaves no way back except Escape.\n \"fixed right-6 z-40 flex w-[min(24rem,calc(100vw-3rem))] flex-col\",\n \"overflow-hidden rounded-[var(--ds-radius-lg)] border border-[var(--ds-border)]\",\n \"bg-[var(--ds-card)] shadow-[var(--ds-shadow-lg)]\",\n )}\n >\n {panel}\n </aside>\n ) : null}\n\n <BottomRightCluster ref={setClusterNode} agentSlot={agentSlot}>\n <ThemeToggle storageKey={themeStorageKey} />\n {hasHelp ? (\n <HelpButton\n ref={helpButtonRef}\n open={helpOpen}\n // `helpPanel` as a render function may mount its own markup while\n // closed, or none while open; the region below is the one that\n // carries the id, so it is the one that decides.\n aria-controls={panel ? HELP_PANEL_ID : undefined}\n onClick={() => setHelpOpen((open) => !open)}\n />\n ) : null}\n </BottomRightCluster>\n </div>\n );\n}\n\n/** One or two letters. Two words give two initials; anything else gives one. */\nfunction initials(name: string): string {\n const words = name.trim().split(/\\s+/).filter(Boolean);\n if (words.length === 0) {\n return \"?\";\n }\n const first = words[0]?.charAt(0) ?? \"\";\n const last = words.length > 1 ? (words.at(-1)?.charAt(0) ?? \"\") : \"\";\n return (first + last).toUpperCase();\n}\n\n/**\n * `<title>` for an extension: the extension first, because that is what\n * distinguishes one tab from another, and the product second so a row of tabs\n * reads as one product (§9.5, audit rule P-38).\n */\nfunction documentTitle(extensionName: string, page?: string): string {\n return page ? `${page} · ${extensionName} · Cortena` : `${extensionName} · Cortena`;\n}\n\nexport { AppShell, BottomRightCluster, BrandMark, HelpButton, ThemeToggle, documentTitle };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsFA,MAAM,uBAAyD;CAC7D,MAAM;CACN,QAAQ;CACR,gBAAgB;CAChB,kBAAkB;CAClB,mBAAmB;CACnB,WAAW;CACX,SAAS;AACX;;AAGA,SAAS,mBAAmB,MAAkE;CAC5F,MAAM,OAA+B,CAAC;CACtC,KAAK,MAAM,CAAC,WAAW,aAAa,OAAO,QAAQ,oBAAoB,GAAG;EACxE,MAAM,QAAQ,OAAO;EACrB,IAAI,OAAO,UAAU,UACnB,KAAK,YAAY;CAErB;CACA,OAAO;AACT;;;;;;;AAQA,SAAS,UAAU,EAAE,WAAW,IAAI,MAAM,OAAO,IAAI,YAAY,GAAG,SAAyB;CAC3F,MAAM,OAAO,QAAQ,EAAE;CACvB,MAAM,QAAQ,aAAa,KAAA,IAAY;CACvC,OACE,oBAAC,QAAD;EACE,aAAU;EACV,aAAW,OAAO,KAAK,KAAK;EAC5B,WAAW,GAAG,oDAAoD,SAAS;EAC3E,OAAO;GAAE,OAAO;GAAM,QAAQ;EAAK;EACnC,GAAI;EAEH,UAAA,OACC,oBAAC,OAAD;GACE,SAAS,KAAK;GACd,OAAO;GACP,QAAQ;GACR,MAAM,aAAa,KAAA,IAAY;GAC/B,cAAY;GACZ,eAAa,aAAa,OAAO,KAAA;GACjC,WAAU;GAQV,yBAAyB,EAAE,QAAQ,KAAK,KAAK;GAI7C,GAAI,mBAAmB,KAAK,IAAI;EACjC,CAAA,IAED,oBAAC,QAAD;GACE,cAAY;GACZ,eAAa,aAAa,OAAO,KAAA;GACjC,MAAM,aAAa,KAAA,IAAY;GAC/B,WAAW,GACT,iDACA,yCACF;GAEC,UAAA,KAAK,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK;EACtB,CAAA;CAEJ,CAAA;AAEV;;;;;;;;;;;;;AAqBA,SAAS,mBAAmB,EAC1B,WACA,OACA,WACA,UACA,GAAG,SACuB;CAC1B,OACE,qBAAC,OAAD;EACE,aAAU;EACV,WAAW,GAAG,oDAAoD,SAAS;EAI3E,OAAO;GAAE,QAAQ,QAAQ,cAAc,WAAW,iBAAiB;GAAU,GAAG;EAAM;EACtF,GAAI;EAPN,UAAA,CASG,YACC,oBAAC,OAAD;GAAK,aAAU;GAA6B,WAAU;GACnD,UAAA;EACE,CAAA,IACH,MACJ,oBAAC,OAAD;GAAK,aAAU;GAA2B,WAAU;GACjD;EACE,CAAA,CACF;;AAET;;AAGA,MAAM,gBAAgB;;AAEtB,MAAM,iBAAiB;;;;;;;;;AASvB,MAAM,gBAAgB;;;;;;;;;;AAWtB,MAAM,qBAAqB;CACzB;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,IAAI;AAEX,MAAM,mBAAmB;AACzB,MAAM,oBAAoB;AAE1B,MAAM,yBAAyB;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG;;AAGV,MAAM,cAAuC;CAAC;CAAS;CAAQ;AAAQ;AAEvE,MAAM,cAA4C;CAChD,OAAO;CACP,MAAM;CACN,QAAQ;AACV;;;;;;AAYA,SAAS,YAAY,EAAE,WAAW,YAAY,GAAG,SAA2B;CAC1E,MAAM,EAAE,OAAO,eAAe,aAAa,gBACzC,eAAe,KAAA,IAAY,CAAC,IAAI,EAAE,WAAW,CAC/C;CACA,MAAM,OAAO,aAAa,YAAY,QAAQ,KAAK,IAAI,KAAK,YAAY,WAAW;CACnF,MAAM,OAAO,UAAU,WAAW,UAAU,UAAU,SAAS,OAAO;CACtE,OACE,oBAAC,UAAD;EACE,MAAK;EACL,aAAU;EACV,qBAAmB;EACnB,uBAAqB;EACrB,cAAY,UAAU,YAAY,OAAO,cAAc,YAAY,KAAK,CAAC,YAAY,EAAE;EACvF,WAAW,GAAG,wBAAwB,SAAS;EAC/C,eAAe,SAAS,IAAI;EAC5B,GAAI;EAEJ,UAAA,oBAAC,MAAD;GAAM,WAAU;GAAS,eAAA;EAAa,CAAA;CAChC,CAAA;AAEZ;;AAGA,MAAa,gBAAgB;;AAG7B,MAAa,gBAAgB;;AAQ7B,SAAS,WAAW,EAAE,WAAW,MAAM,GAAG,SAA0B;CAClE,OACE,oBAAC,UAAD;EACE,MAAK;EACL,aAAU;EACV,cAAW;EACX,iBAAe;EAKf,iBAAe,OAAO,gBAAgB,KAAA;EACtC,aAAW,OAAO,KAAK,KAAA;EACvB,WAAW,GAAG,wBAAwB,QAAQ,wBAAwB,SAAS;EAC/E,GAAI;EAEJ,UAAA,oBAAC,oBAAD;GAAoB,WAAU;GAAS,eAAA;EAAa,CAAA;CAC9C,CAAA;AAEZ;;AAmFA,SAAS,gBAAgB,KAAkB,MAAgC;CACzE,IAAI,IAAI,aAAa,KAAA,GACnB,OAAO,KAAK,OAAO,IAAI;CAEzB,IAAI,IAAI,UACN,OAAO,IAAI,SAAS,KAAK,MAAM,IAAI;CAErC,IAAI,IAAI,aAAa,KAAA,GACnB,OAAO;CAET,IAAI,KAAK,SAAS,KAAK,SAAS,KAC9B,OAAO,IAAI,aAAa,KAAK;CAE/B,MAAM,OAAO,KAAK,KAAK,SAAS,GAAG,IAAI,KAAK,KAAK,MAAM,GAAG,EAAE,IAAI,KAAK;CACrE,OAAO,IAAI,aAAa,QAAQ,IAAI,SAAS,WAAW,GAAG,KAAK,EAAE;AACpE;;AAGA,SAAS,QAAQ,EAAE,MAAM,SAAmD;CAC1E,IAAI,OAAO,SAAS,UAClB,OAAO,oBAAC,WAAD;EAAW,IAAI;EAAM,MAAM;EAAO,MAAM;EAAI,YAAA;CAAY,CAAA;CAGjE,OAAO,oBAACA,MAAD;EAAM,WAAU;EAAkB,eAAA;CAAa,CAAA;AACxD;;AAGA,SAAS,iBAAiB,OAAkC;CAC1D,OACE,MAAM,WAAW,KACjB,CAAC,MAAM,WACP,CAAC,MAAM,WACP,CAAC,MAAM,YACP,CAAC,MAAM;AAEX;;;;;;AAcA,SAAS,QAAQ,EAAE,MAAM,QAAQ,YAAY,YAA0B;CACrE,MAAM,QAAQ,KAAK;CACnB,MAAM,WAAW,UAAU,KAAA,KAAa,UAAU,MAAM,UAAU;CAClE,OACE,qBAAC,KAAD;EACE,aAAU;EACV,iBAAe,KAAK;EACpB,eAAa,SAAS,KAAK,KAAA;EAC3B,MAAM,KAAK;EAGX,gBAAc,SAAS,SAAS,KAAA;EAChC,UAAU,UAAU;GAQlB,IAAI,CAAC,iBAAiB,KAAK,GACzB;GAEF,WAAW;GACX,aAAa,MAAM,KAAK;EAC1B;EACA,WAAW,GACT,qEACA,qEACA,kFACA,4DACA,SACI,6EACA,GACE,uDACA,oEACF,CACN;EAjCF,UAAA;GAmCG,KAAK,OAAO,oBAAC,SAAD;IAAS,MAAM,KAAK;IAAM,OAAO,KAAK;GAAQ,CAAA,IAAI;GAC/D,oBAAC,QAAD;IAAM,WAAU;IAAY,UAAA,KAAK;GAAY,CAAA;GAC5C,WACC,oBAAC,QAAD;IACE,aAAU;IACV,WAAW,GACT,oEACA,0CACA,sEACA,SACI,qEACA,8DACN;IAEC,UAAA;GACG,CAAA,IACJ;EACH;;AAEP;;AAGA,SAAS,SAAS,EAAE,KAAK,YAAyD;CAChF,OACE,qBAAC,OAAD;EACE,aAAU;EACV,cAAY,IAAI,SAAS;EACzB,WAAU;EAHZ,UAAA,CAKG,IAAI,OAAO,KAAK,OAAO,UACtB,qBAAC,OAAD;GAEE,aAAU;GACV,oBAAkB,MAAM;GACxB,WAAW,GACT,uBAGA,QAAQ,KAAK,CAAC,MAAM,SAAS,qDAC/B;GATF,UAAA,CAWG,MAAM,QACL,oBAAC,QAAD;IACE,aAAU;IACV,WAAW,GACT,iEACA,qDACA,yCACF;IAEC,UAAA,MAAM;GACH,CAAA,IACJ,MACH,MAAM,MAAM,KAAK,SAChB,oBAAC,SAAD;IAEQ;IACN,QAAQ,gBAAgB,KAAK,IAAI;IACjC,YAAY,IAAI;IACN;GACX,GALM,KAAK,EAKX,CACF,CACE;EA/BE,GAAA,MAAM,SAAS,SAAS,OA+B1B,CACN,GACA,IAAI,SACH,oBAAC,OAAD;GAAK,aAAU;GAAuB,WAAU;GAC7C,UAAA,IAAI;EACF,CAAA,IACH,IACD;;AAET;;;;;;;;;;;AAmGA,IAAI,gBAAgB;AAEpB,SAAS,kBAA2B;CAClC,MAAM,CAAC,MAAM,WAAW,MAAM,SAAS,KAAK;CAC5C,MAAM,gBAAgB;EACpB,iBAAiB;EACjB,QAAQ,kBAAkB,CAAC;EAC3B,aAAa;GACX,iBAAiB;GACjB,IAAI,kBAAkB,GAAG;IACvB,MAAM,OAAO,SAAS;IACtB,KAAK,MAAM,eAAe,gBAAgB;IAC1C,KAAK,MAAM,eAAe,iBAAiB;GAC7C;EACF;CACF,GAAG,CAAC,CAAC;CACL,OAAO;AACT;AAEA,SAAS,SAAS,EAChB,WACA,OACA,WACA,MACA,KACA,YACA,WACA,aACA,YACA,WACA,UACA,MACA,WACA,WACA,iBACA,UACA,GAAG,SACa;CAChB,MAAM,CAAC,UAAU,eAAe,MAAM,SAAS,KAAK;CACpD,MAAM,gBAAgB,MAAM,OAA0B,IAAI;CAC1D,MAAM,YAAY,MAAM,kBAAkB;EACxC,YAAY,KAAK;EAKjB,cAAc,SAAS,MAAM;CAC/B,GAAG,CAAC,CAAC;;;;;;;;CAgBL,MAAM,CAAC,aAAa,kBAAkB,MAAM,SAAgC,IAAI;CAChF,MAAM,CAAC,eAAe,oBAAoB,MAAM,SAAS,EAAE;CAC3D,MAAM,gBAAgB;EACpB,IAAI,CAAC,eAAe,OAAO,mBAAmB,aAC5C;EAEF,MAAM,gBAAgB,iBAAiB,YAAY,sBAAsB,CAAC,CAAC,MAAM;EACjF,QAAQ;EACR,MAAM,WAAW,IAAI,eAAe,OAAO;EAC3C,SAAS,QAAQ,WAAW;EAC5B,aAAa,SAAS,WAAW;CACnC,GAAG,CAAC,WAAW,CAAC;;;;;;CAOhB,MAAM,CAAC,YAAY,iBAAiB,MAAM,SAA6B,IAAI;CAC3E,MAAM,CAAC,cAAc,mBAAmB,MAAM,SAAS,aAAa;CACpE,MAAM,gBAAgB;EACpB,IAAI,CAAC,cAAc,OAAO,mBAAmB,aAC3C;EAEF,MAAM,gBAAgB,gBAAgB,WAAW,sBAAsB,CAAC,CAAC,MAAM;EAC/E,QAAQ;EACR,MAAM,WAAW,IAAI,eAAe,OAAO;EAC3C,SAAS,QAAQ,UAAU;EAC3B,aAAa,SAAS,WAAW;CACnC,GAAG,CAAC,UAAU,CAAC;;;;;;;CAOf,MAAM,WAAW,MAAM,OAAuB,IAAI;CAClD,MAAM,CAAC,SAAS,cAAc,MAAM,SAA6B,IAAI;CACrE,MAAM,CAAC,eAAe,oBAAoB,MAAM,SAAS,CAAC;CAM1D,MAAM,gBAAgB;EACpB,IAAI,gBAAgB,KAAA,GAClB;EAEF,WAAW,SAAS,SAAS,cAA2B,mBAAmB,KAAK,IAAI;CACtF,CAAC;CACD,MAAM,gBAAgB;EACpB,IAAI,gBAAgB,KAAA,GAClB;EAEF,IAAI,CAAC,WAAW,OAAO,mBAAmB,aAAa;GACrD,iBAAiB,CAAC;GAClB;EACF;EACA,MAAM,gBAAgB,iBAAiB,QAAQ,sBAAsB,CAAC,CAAC,MAAM;EAC7E,QAAQ;EACR,MAAM,WAAW,IAAI,eAAe,OAAO;EAC3C,SAAS,QAAQ,OAAO;EACxB,aAAa,SAAS,WAAW;CACnC,GAAG,CAAC,SAAS,WAAW,CAAC;CACzB,MAAM,gBACJ,gBAAgB,KAAA,IACZ,GAAG,cAAc,MACjB,OAAO,gBAAgB,WACrB,GAAG,YAAY,MACf;;;;;;;;CASR,MAAM,eAAe,gBAAgB;CACrC,MAAM,gBAAgB;EACpB,IAAI,CAAC,cACH;EAEF,MAAM,OAAO,SAAS;EACtB,KAAK,MAAM,YAAY,kBAAkB,aAAa;EACtD,KAAK,MAAM,YAAY,mBAAmB,GAAG,aAAa,GAAG;CAC/D,GAAG;EAAC;EAAc;EAAe;CAAY,CAAC;;CAG9C,MAAM,CAAC,SAAS,cAAc,MAAM,SAAS,KAAK;CAClD,MAAM,eAAe,MAAM,OAA0B,IAAI;CACzD,MAAM,gBAAgB,MAAM,OAAuB,IAAI;CACvD,MAAM,eAAe,MAAM,OAAuB,IAAI;CACtD,MAAM,cAAc,MAAM,OAA0B,IAAI;CACxD,MAAM,WAAW,MAAM,kBAAkB,WAAW,KAAK,GAAG,CAAC,CAAC;;;;;;;;;;CAU9D,MAAM,gBAAgB;EACpB,IAAI,CAAC,SACH;EAEF,MAAM,QAAQ,SAAS;EACvB,MAAM,SAAS,QACX,CAAC,GAAG,MAAM,QAAQ,CAAC,CAAC,QAAQ,OAAO,OAAO,cAAc,OAAO,IAC/D,CAAC;EACL,KAAK,MAAM,MAAM,QACf,GAAG,aAAa,SAAS,EAAE;EAE7B,MAAM,EAAE,SAAS;EACjB,MAAM,SAAS,KAAK,MAAM;EAC1B,KAAK,MAAM,WAAW;EAGtB,YAAY,SAAS,MAAM;EAE3B,MAAM,aAAa,UAAyB;GAC1C,IAAI,MAAM,QAAQ,UAAU;IAC1B,SAAS;IACT;GACF;GACA,IAAI,MAAM,QAAQ,OAChB;GAEF,MAAM,SAAS,aAAa;GAC5B,MAAM,YAAY,SACd,CAAC,GAAG,OAAO,iBAA8B,kBAAkB,CAAC,IAC5D,CAAC;GACL,MAAM,QAAQ,UAAU;GACxB,MAAM,OAAO,UAAU,GAAG,EAAE;GAC5B,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,MACxB;GAEF,MAAM,SAAS,SAAS;GAExB,IADgB,MAAM,WAAW,WAAW,SAAS,CAAC,OAAO,SAAS,MAAM,IAAI,WAAW,MAC9E;IACX,MAAM,eAAe;IACrB,CAAC,MAAM,WAAW,OAAO,MAAA,CAAO,MAAM;GACxC;EACF;EACA,SAAS,iBAAiB,WAAW,SAAS;EAC9C,aAAa;GACX,SAAS,oBAAoB,WAAW,SAAS;GACjD,KAAK,MAAM,MAAM,QACf,GAAG,gBAAgB,OAAO;GAE5B,KAAK,MAAM,WAAW;GAMtB,aAAa,SAAS,MAAM;EAC9B;CACF,GAAG,CAAC,SAAS,QAAQ,CAAC;CAEtB,MAAM,UAAU,SAAS,KAAA,KAAa,cAAc,KAAA;CACpD,MAAM,QACJ,OAAO,cAAc,aACjB,UAAU;EAAE,MAAM;EAAU,QAAQ,MAAM;EAAQ,SAAS;CAAU,CAAC,IACtE,WACE,YACA;CAER,OACE,qBAAC,OAAD;EACE,KAAK;EACL,aAAU;EACV,WAAW,GACT,iGACA,SACF;EACA,OACE;IACG,oBAAoB,GAAG,aAAa;IACpC,mBAAmB;GACpB,GAAG;EACL;EAEF,GAAI;EAdN,UAAA;GAgBE,qBAAC,UAAD;IACE,aAAU;IACV,KAAK;IACL,WAAW,GACT,6EACA,kFACF;IACA,OAAO,EAAE,QAAQ,cAAc;IAPjC,UAAA;KASE,qBAAC,OAAD;MAAK,aAAU;MAAkB,WAAU;MAA3C,UAAA;OACG,MACC,oBAAC,UAAD;QACE,KAAK;QACL,MAAK;QACL,aAAU;QACV,cAAW;QACX,iBAAe;QAEf,iBAAe,UAAU,gBAAgB,KAAA;QACzC,eAAe,WAAW,IAAI;QAC9B,WAAW,GACT,2EACA,sFACA,kFACA,sEACA,0DACF;QAEA,UAAA,oBAAC,MAAD;SAAM,WAAU;SAAS,eAAA;QAAa,CAAA;OAChC,CAAA,IACN;OACJ,oBAAC,QAAD;QACE,aAAU;QACV,WAAW,GACT,kFACA,4DACF;QAEA,UAAA,oBAAC,WAAD;SAAW,IAAI,UAAU;SAAI,MAAM,UAAU;SAAM,MAAM;SAAI,YAAA;QAAY,CAAA;OACrE,CAAA;OACN,oBAAC,QAAD;QACE,aAAU;QACV,WAAW,GACT,wEACA,iFACF;QAEC,UAAA,UAAU;OACP,CAAA;MACH;;KAEJ,aACC,oBAAC,OAAD;MACE,aAAU;MACV,WAAU;MAET,UAAA;KACE,CAAA,IACH;KAEJ,qBAAC,cAAD,EAAA,UAAA,CACE,oBAAC,qBAAD;MACE,aAAU;MACV,cAAY,YAAY,KAAK;MAC7B,WAAW,GACT,2EACA,wFACA,kDACF;MAEA,UAAA,qBAAC,QAAD,EAAA,UAAA,CACG,KAAK,YAAY,oBAAC,aAAD;OAAa,KAAK,KAAK;OAAW,KAAI;MAAI,CAAA,IAAI,MAChE,oBAAC,gBAAD,EAAA,UAAiB,SAAS,KAAK,IAAI,EAAkB,CAAA,CAC/C,EAAA,CAAA;KACW,CAAA,GAGrB,qBAAC,qBAAD;MAAqB,OAAM;MAAM,YAAY;MAAG,WAAU;MAA1D,UAAA;OACE,qBAAC,kBAAD;QACE,aAAU;QACV,UAAU,CAAC;QACX,SAAS;QAHX,UAAA,CAKE,oBAAC,MAAD,CAAO,CAAA,GAAC,UACQ;;OAClB,qBAAC,kBAAD;QACE,aAAU;QACV,UAAU,CAAC;QACX,SAAS;QAHX,UAAA,CAKE,oBAAC,UAAD,CAAW,CAAA,GAAC,WACI;;OACjB,WAAW,KAAK,SAAS;QACxB,MAAM,OAAO,KAAK;QAClB,OACE,qBAAC,kBAAD;SAAgC,aAAW,KAAK;SAAI,SAAS,KAAK;SAAlE,UAAA;UACG,OAAO,oBAAC,MAAD,CAAO,CAAA,IAAI;UAAK;UAAE,KAAK;SACf;QAFK,GAAA,KAAK,EAEV;OAEtB,CAAC;OACD,oBAAC,uBAAD,CAAwB,CAAA;OACxB,qBAAC,kBAAD;QACE,aAAU;QACV,SAAQ;QACR,UAAU,CAAC;QACX,SAAS;QAJX,UAAA,CAME,oBAAC,QAAD,CAAS,CAAA,GAAC,UACM;;MACC;KACT,CAAA,CAAA,EAAA,CAAA;IACR;;GAER,qBAAC,OAAD;IAAK,aAAU;IAAiB,WAAU;IAA1C,UAAA,CACG,MACC,oBAAC,SAAD;KACE,aAAU;KACV,WAAW,GACT,oEACA,qEACF;KAKA,OAAO;MAAE,KAAK;MAAc,WAAW,gBAAgB,aAAa;KAAK;KAEzE,UAAA,oBAAC,UAAD,EAAe,IAAM,CAAA;IAChB,CAAA,IACL,MAEJ,oBAAC,QAAD;KAAM,aAAU;KAAiB,WAAU;KACxC;IACG,CAAA,CACH;;GAEJ,OAAO,UACN,qBAAC,OAAD;IACE,KAAK;IACL,aAAU;IACV,WAAU;IAHZ,UAAA,CAOE,oBAAC,UAAD;KACE,MAAK;KACL,aAAU;KACV,cAAW;KACX,SAAS;KACT,WAAW,GACT,wCACA,4DACF;IACD,CAAA,GACD,qBAAC,OAAD;KACE,KAAK;KACL,IAAI;KACJ,MAAK;KACL,cAAW;KACX,cAAY,GAAG,UAAU,KAAK;KAC9B,aAAU;KACV,WAAW,GACT,uFACA,uEACA,8BACF;KAXF,UAAA,CAaE,qBAAC,OAAD;MAAK,WAAU;MAAf,UAAA,CACE,oBAAC,QAAD;OAAM,WAAU;OACd,UAAA,oBAAC,WAAD;QAAW,IAAI,UAAU;QAAI,MAAM,UAAU;QAAM,MAAM;QAAI,YAAA;OAAY,CAAA;MACrE,CAAA,GACN,oBAAC,UAAD;OACE,KAAK;OACL,MAAK;OACL,aAAU;OACV,cAAW;OACX,SAAS;OACT,WAAW,GACT,2DACA,sFACA,kFACA,sEACA,0DACF;OAEA,UAAA,oBAAC,GAAD;QAAG,WAAU;QAAS,eAAA;OAAa,CAAA;MAC7B,CAAA,CACL;KAGL,CAAA,GAAA,oBAAC,UAAD;MAAe;MAAK,UAAU;KAAW,CAAA,CACtC;IACF,CAAA,CAAA;GACH,CAAA,IAAA;GAEH,QACC,oBAAC,SAAD;IACE,IAAI;IACJ,aAAU;IACV,cAAY,GAAG,UAAU,KAAK;IAM9B,OAAO;KACL,KAAK,YAAY,kBAAkB,IAAI,aAAa,QAAQ,eAAe;KAC3E,QAAQ,QAAQ,gBAAgB,gBAAgB,eAAe,WAAW,iBAAiB;IAC7F;IACA,WAAW,GAGT,oEACA,kFACA,kDACF;IAEC,UAAA;GACI,CAAA,IACL;GAEJ,qBAAC,oBAAD;IAAoB,KAAK;IAA2B;IAApD,UAAA,CACE,oBAAC,aAAD,EAAa,YAAY,gBAAkB,CAAA,GAC1C,UACC,oBAAC,YAAD;KACE,KAAK;KACL,MAAM;KAIN,iBAAe,QAAQ,gBAAgB,KAAA;KACvC,eAAe,aAAa,SAAS,CAAC,IAAI;IAC3C,CAAA,IACC,IACc;;EACjB;;AAET;;AAGA,SAAS,SAAS,MAAsB;CACtC,MAAM,QAAQ,KAAK,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,OAAO,OAAO;CACrD,IAAI,MAAM,WAAW,GACnB,OAAO;CAIT,SAFc,MAAM,EAAE,EAAE,OAAO,CAAC,KAAK,OACxB,MAAM,SAAS,IAAK,MAAM,GAAG,EAAE,CAAC,EAAE,OAAO,CAAC,KAAK,KAAM,IAAA,CAC5C,YAAY;AACpC;;;;;;AAOA,SAAS,cAAc,eAAuB,MAAuB;CACnE,OAAO,OAAO,GAAG,KAAK,KAAK,cAAc,cAAc,GAAG,cAAc;AAC1E"}
1
+ {"version":3,"file":"app-shell.js","names":["Icon"],"sources":["../../src/components/app-shell.tsx"],"sourcesContent":["\"use client\";\n\nimport { getMark } from \"cortena-design/marks\";\nimport {\n CircleQuestionMark,\n LogOut,\n type LucideIcon,\n Menu,\n Monitor,\n Moon,\n Settings,\n Sun,\n User,\n X,\n} from \"lucide-react\";\nimport * as React from \"react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/avatar\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/components/dropdown-menu\";\nimport { type CortenaTheme, useCortenaTheme } from \"@/hooks/use-cortena-theme\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * AppShell — the chrome every Cortena extension wears, so that moving between\n * two of them is moving between two screens of one product rather than between\n * two applications (how-to-create-a-cortena-extension §9, audit rule P-10).\n *\n * Four things are fixed, and they are fixed because an extension that places\n * them itself places them somewhere slightly different:\n *\n * top left the extension's name and its registered mark, 20px inside a\n * 32px square. The mark comes from `cortena-design/marks` by\n * `extension.id` — the same file the Apps tile, the MCP app\n * card, the agent pop-up pill and the favicon draw (§9.1), not\n * an SVG in this app's web/public.\n * top right the avatar menu. It is the ONLY settings entry point:\n * Profile, Settings, Log out. Not a gear in a sidebar as well\n * (§9.2).\n * bottom right one fixed cluster, theme toggle then help button, left to\n * right, 24px from both viewport edges, 40px each, 12px apart\n * (§9.3). The agent pop-up pill stacks above it.\n * everywhere the theme is `useCortenaTheme`, so the choice persists and a\n * host `postMessage` still wins.\n *\n * The help panel itself is EXTBP-24 and does not exist yet, so `helpPanel` is a\n * slot: pass a node or a render function and the shell owns the button, the\n * open state and the corner. When HelpPanel ships, `help.source` is what it\n * reads — the extension's functional document (§12) — and the default panel\n * replaces the slot without a call-site change.\n */\n\n/* ── the mark ────────────────────────────────────────────────────────────── */\n\nexport interface BrandMarkProps extends Omit<React.ComponentProps<\"span\">, \"children\"> {\n /** Registered mark id — what the extension puts in `x-cortena.icon`. */\n id: string;\n /** The extension name. Labels the mark, and supplies the fallback initial. */\n name: string;\n /** Rendered size of the glyph in pixels. 20 in the shell, 24 in the Apps tile, 16 on the pill. */\n size?: number;\n /** Set when the mark is decorative because the name is already beside it. */\n decorative?: boolean;\n}\n\n/**\n * The presentation attributes a mark's root may carry into the DOM, mapped\n * from the SVG spelling to the JSX one.\n *\n * The same allow-list cortena-design's `validateMarkup` enforces at build\n * time, repeated here because this component spreads values that came from a\n * package the consumer resolves — an older `cortena-design`, a patched one, a\n * hand-edited `dist/marks.js` — straight onto an element.\n *\n * The mapping is the fix for DESIGN-88. A registered mark's root is\n * `{ \"stroke-width\": \"2.5\", \"stroke-linecap\": \"round\", … }` — the SVG\n * attribute names, which is what an SVG file carries — and spreading those\n * into JSX verbatim made React log three errors per page (\"Invalid DOM\n * property `stroke-width`. Did you mean `strokeWidth`?\"). React still applied\n * them, so nothing looked wrong; every screen of every extension just opened\n * with three console errors, which is how a real one gets missed.\n */\nconst MARK_ROOT_ATTRIBUTES: Readonly<Record<string, string>> = {\n fill: \"fill\",\n stroke: \"stroke\",\n \"stroke-width\": \"strokeWidth\",\n \"stroke-linecap\": \"strokeLinecap\",\n \"stroke-linejoin\": \"strokeLinejoin\",\n transform: \"transform\",\n opacity: \"opacity\",\n};\n\n/** `root`, with anything outside the allow-list dropped and the rest camelCased. */\nfunction safeRootAttributes(root: Record<string, string> | undefined): Record<string, string> {\n const safe: Record<string, string> = {};\n for (const [attribute, property] of Object.entries(MARK_ROOT_ATTRIBUTES)) {\n const value = root?.[attribute];\n if (typeof value === \"string\") {\n safe[property] = value;\n }\n }\n return safe;\n}\n\n/**\n * The registered mark for `id`, or the extension's initial when nothing is\n * registered. The fallback matters: an unregistered id used to render an empty\n * box, which reads as a broken image rather than as \"this extension has not\n * registered a mark yet\".\n */\nfunction BrandMark({ className, id, name, size = 20, decorative, ...props }: BrandMarkProps) {\n const mark = getMark(id);\n const label = decorative ? undefined : name;\n return (\n <span\n data-slot=\"brand-mark\"\n data-mark={mark ? mark.id : \"fallback\"}\n className={cn(\"inline-flex shrink-0 items-center justify-center\", className)}\n style={{ width: size, height: size }}\n {...props}\n >\n {mark ? (\n <svg\n viewBox={mark.viewBox}\n width={size}\n height={size}\n role={decorative ? undefined : \"img\"}\n aria-label={label}\n aria-hidden={decorative ? true : undefined}\n focusable=\"false\"\n // The body is generated by cortena-design's build from a file in this\n // monorepo, where `validateMarkup` accepts an allow-list of elements\n // and attributes — no <script>, <style>, <use>, <image> or\n // <foreignObject>, no href and no on* handler — and paints only in\n // currentColor. It is not user input, and it is not sanitised here:\n // the guarantee is the build's, so a mark that would need sanitising\n // fails `pnpm build` instead of being quietly cleaned up.\n dangerouslySetInnerHTML={{ __html: mark.body }}\n // `root` is filtered again on the way in. A consumer can be running\n // against an older or hand-edited `cortena-design/marks`, and this\n // spread is the one place its values reach the DOM as attributes.\n {...safeRootAttributes(mark.root)}\n />\n ) : (\n <span\n aria-label={label}\n aria-hidden={decorative ? true : undefined}\n role={decorative ? undefined : \"img\"}\n className={cn(\n \"font-display font-bold uppercase leading-none\",\n \"text-[length:var(--ds-text-caption-lg)]\",\n )}\n >\n {name.trim().charAt(0) || \"?\"}\n </span>\n )}\n </span>\n );\n}\n\n/* ── bottom-right cluster ────────────────────────────────────────────────── */\n\nexport interface BottomRightClusterProps extends React.ComponentProps<\"div\"> {\n /** The agent pop-up pill (§17). Stacks above the row, never beside it. */\n agentSlot?: React.ReactNode;\n}\n\n/**\n * The one fixed container in the bottom-right corner. It exists so that no\n * extension positions a floating control itself — that is how a third one\n * appears and covers one of the first two.\n *\n * Its distance from the bottom edge is 24px PLUS `--ds-shell-bottom-inset`,\n * which `AppShell` sets from its `bottomInset` prop or from a `data-bottom-bar`\n * element it finds in the page. On a phone with a bottom tab bar the cluster\n * used to sit on top of two of the tabs; Tasks worked around it with\n * `className=\"bottom-22 lg:bottom-6\"` on its own cluster, which the agent\n * pop-up inside `agentSlot` had no equivalent of (DESIGN-88).\n */\nfunction BottomRightCluster({\n className,\n style,\n agentSlot,\n children,\n ...props\n}: BottomRightClusterProps) {\n return (\n <div\n data-slot=\"bottom-right-cluster\"\n className={cn(\"fixed right-6 z-40 flex flex-col items-end gap-3\", className)}\n // Inline rather than a utility: the value is `calc()` over a variable a\n // consumer may not have set, and `bottom-6` would have to be beaten by\n // specificity rather than replaced.\n style={{ bottom: `calc(${CLUSTER_INSET}px + var(${BOTTOM_INSET_VAR}, 0px))`, ...style }}\n {...props}\n >\n {agentSlot ? (\n <div data-slot=\"bottom-right-cluster-agent\" className=\"flex items-center justify-end\">\n {agentSlot}\n </div>\n ) : null}\n <div data-slot=\"bottom-right-cluster-row\" className=\"flex items-center gap-3\">\n {children}\n </div>\n </div>\n );\n}\n\n/** The cluster's distance from both viewport edges (§9.3). `right-6` / `bottom-6`. */\nconst CLUSTER_INSET = 24;\n/** What the help panel leaves between itself and the cluster, and below the header. */\nconst HELP_PANEL_GAP = 12;\n/**\n * The header's height, in pixels, declared once. The header takes it as an\n * inline height rather than as a utility class, because the sidebar's `top`,\n * the help panel's `top` and `--ds-shell-header-height` all have to agree with\n * it and a class is a second place for the number to live. What the shell\n * publishes is the *measured* height of the header element — this value is\n * what it starts from, before the first measurement and where there is no DOM.\n */\nconst HEADER_HEIGHT = 64;\n\n/**\n * The two custom properties the shell publishes.\n *\n * They are the least-coupled way to tell something the shell does not own —\n * the agent pop-up in `agentSlot`, a consumer's own floating panel — where the\n * chrome ends. Both are written on the shell's own element AND on\n * `<html>`, because a pop-up may portal out of the shell's subtree.\n */\n/** What the drawer's focus trap cycles through. */\nconst FOCUSABLE_SELECTOR = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"textarea:not([disabled])\",\n \"[tabindex]:not([tabindex='-1'])\",\n].join(\", \");\n\nconst BOTTOM_INSET_VAR = \"--ds-shell-bottom-inset\";\nconst HEADER_HEIGHT_VAR = \"--ds-shell-header-height\";\n\nconst clusterButtonClassName = [\n \"inline-flex size-10 shrink-0 items-center justify-center rounded-[var(--ds-radius-full)]\",\n \"border border-[var(--ds-border)] bg-[var(--ds-card)] text-[color:var(--ds-foreground)]\",\n \"shadow-[var(--ds-shadow-md)] outline-none\",\n \"transition-colors duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]\",\n \"hover:bg-[var(--ds-hover)]\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--ds-ring)] focus-visible:ring-offset-2\",\n \"focus-visible:ring-offset-[var(--ds-background)]\",\n].join(\" \");\n\n/** light → dark → system → light. Three states, one control, no menu to open. */\nconst THEME_ORDER: readonly CortenaTheme[] = [\"light\", \"dark\", \"system\"];\n\nconst THEME_LABEL: Record<CortenaTheme, string> = {\n light: \"Light\",\n dark: \"Dark\",\n system: \"System\",\n};\n\nexport interface ThemeToggleProps extends Omit<React.ComponentProps<\"button\">, \"onClick\"> {\n /** localStorage key, forwarded to `useCortenaTheme`. */\n storageKey?: string;\n}\n\n/**\n * The theme selector. It lives here rather than in the avatar menu because a\n * user changing theme is usually reacting to the room they are in, and that is\n * a one-tap decision, not a settings visit.\n */\nfunction ThemeToggle({ className, storageKey, ...props }: ThemeToggleProps) {\n const { theme, resolvedTheme, setTheme } = useCortenaTheme(\n storageKey === undefined ? {} : { storageKey },\n );\n const next = THEME_ORDER[(THEME_ORDER.indexOf(theme) + 1) % THEME_ORDER.length] ?? \"light\";\n const Icon = theme === \"system\" ? Monitor : theme === \"dark\" ? Moon : Sun;\n return (\n <button\n type=\"button\"\n data-slot=\"theme-toggle\"\n data-theme-choice={theme}\n data-theme-resolved={resolvedTheme}\n aria-label={`Theme: ${THEME_LABEL[theme]}. Switch to ${THEME_LABEL[next].toLowerCase()}.`}\n className={cn(clusterButtonClassName, className)}\n onClick={() => setTheme(next)}\n {...props}\n >\n <Icon className=\"size-5\" aria-hidden />\n </button>\n );\n}\n\n/** The id the shell gives its help region, and the button's `aria-controls`. */\nexport const HELP_PANEL_ID = \"cortena-help-panel\";\n\n/** The id the shell gives its mobile nav drawer, and the menu button's `aria-controls`. */\nexport const NAV_DRAWER_ID = \"cortena-nav-drawer\";\n\nexport interface HelpButtonProps extends React.ComponentProps<\"button\"> {\n /** Whether the panel it controls is open. */\n open?: boolean;\n}\n\n/** The help affordance. Right of the theme toggle, always — order is fixed (§9.3). */\nfunction HelpButton({ className, open, ...props }: HelpButtonProps) {\n return (\n <button\n type=\"button\"\n data-slot=\"help-button\"\n aria-label=\"Help\"\n aria-expanded={open}\n // `aria-expanded` alone says \"something is open\" without saying what.\n // A screen reader user then has to go and find it — and an id that names\n // an element which is not on the page is worse than saying nothing, so\n // it is set only while the panel is mounted.\n aria-controls={open ? HELP_PANEL_ID : undefined}\n data-open={open ? \"\" : undefined}\n className={cn(clusterButtonClassName, open && \"bg-[var(--ds-hover)]\", className)}\n {...props}\n >\n <CircleQuestionMark className=\"size-5\" aria-hidden />\n </button>\n );\n}\n\n/* ── navigation ──────────────────────────────────────────────────────────── */\n\n/**\n * A nav item's glyph: a lucide icon component, or the id of a mark registered\n * in `cortena-design/marks` (so a nav row can wear another extension's mark\n * without importing its SVG).\n */\nexport type AppShellNavIcon = LucideIcon | string;\n\nexport interface AppShellNavItem {\n /** Stable id. What `activeId` names, and what `data-nav-item` carries. */\n id: string;\n label: string;\n icon?: AppShellNavIcon;\n /** The destination. Rendered as a real `href`, so middle-click and copy-link work. */\n href: string;\n /** A count or a short word. Rendered from tokens; `0` and `\"\"` draw nothing. */\n badge?: number | string;\n /** Match `pathname` exactly rather than as a prefix. Ignored under `activeId`/`isActive`. */\n exact?: boolean;\n}\n\nexport interface AppShellNavGroup {\n /** Section heading. A group with no label is separated by a rule instead. */\n label?: string;\n items: AppShellNavItem[];\n}\n\nexport interface AppShellNav {\n groups: AppShellNavGroup[];\n /**\n * Which item is active, decided in one of three ways, in this order:\n *\n * `activeId` the item's own id. The simplest, and the only one that\n * needs nothing from the router.\n * `isActive` a callback per item. Use it when \"active\" is not a path\n * comparison — a query parameter, a nested layout route.\n * `pathname` the current path, compared to each `href`: a prefix match,\n * or an exact one for an item marked `exact`.\n *\n * cortena-ui has no router dependency and never will, so one of the three\n * has to come from the consumer. With react-router:\n *\n * ```tsx\n * const { pathname } = useLocation();\n * const navigate = useNavigate();\n *\n * <AppShell\n * nav={{\n * groups,\n * pathname,\n * // Keep it a client-side navigation; the href stays real for\n * // middle-click, \"open in new tab\" and copy-link, and the shell never\n * // calls this for a modified or middle click.\n * onNavigate: (item, event) => {\n * event.preventDefault();\n * navigate(item.href);\n * },\n * }}\n * />\n * ```\n *\n * Give none of the three and nothing is active — deliberately, because the\n * alternative is reading `window.location` here, which would be right on\n * first paint and then silently stale on every navigation after it.\n */\n activeId?: string;\n isActive?: (href: string, item: AppShellNavItem) => boolean;\n pathname?: string;\n /** Intercept a click. Called before the browser follows the `href`. */\n onNavigate?: (item: AppShellNavItem, event: React.MouseEvent<HTMLAnchorElement>) => void;\n /**\n * Pinned under the items: File Vault's storage meter, Flux's stream status.\n * The one thing in the sidebar that is neither navigation nor chrome.\n */\n footer?: React.ReactNode;\n /** The landmark's accessible name. Default \"Sections\". */\n label?: string;\n}\n\n/** Which of the three signals decides, in order. See `AppShellNav`. */\nfunction isNavItemActive(nav: AppShellNav, item: AppShellNavItem): boolean {\n if (nav.activeId !== undefined) {\n return item.id === nav.activeId;\n }\n if (nav.isActive) {\n return nav.isActive(item.href, item);\n }\n if (nav.pathname === undefined) {\n return false;\n }\n if (item.exact || item.href === \"/\") {\n return nav.pathname === item.href;\n }\n const base = item.href.endsWith(\"/\") ? item.href.slice(0, -1) : item.href;\n return nav.pathname === base || nav.pathname.startsWith(`${base}/`);\n}\n\n/** A lucide component, or a registered mark drawn at the same 16px. */\nfunction NavIcon({ icon, label }: { icon: AppShellNavIcon; label: string }) {\n if (typeof icon === \"string\") {\n return <BrandMark id={icon} name={label} size={16} decorative />;\n }\n const Icon = icon;\n return <Icon className=\"size-4 shrink-0\" aria-hidden />;\n}\n\n/** A click the browser would answer with a plain same-tab navigation. */\nfunction isPlainLeftClick(event: React.MouseEvent): boolean {\n return (\n event.button === 0 &&\n !event.metaKey &&\n !event.ctrlKey &&\n !event.shiftKey &&\n !event.altKey\n );\n}\n\ninterface NavLinkProps {\n item: AppShellNavItem;\n active: boolean;\n onNavigate?: AppShellNav[\"onNavigate\"];\n onFollow?: () => void;\n}\n\n/**\n * One row. An `<a>` with a real `href`, because a nav item that is a `<button>`\n * cannot be middle-clicked, opened in a new tab or copied — and every one of\n * these is a route.\n */\nfunction NavLink({ item, active, onNavigate, onFollow }: NavLinkProps) {\n const badge = item.badge;\n const hasBadge = badge !== undefined && badge !== \"\" && badge !== 0;\n return (\n <a\n data-slot=\"app-shell-nav-item\"\n data-nav-item={item.id}\n data-active={active ? \"\" : undefined}\n href={item.href}\n // `aria-current=\"page\"` is the active state a screen reader hears. The\n // colour is the one a sighted user sees; neither stands in for the other.\n aria-current={active ? \"page\" : undefined}\n onClick={(event) => {\n // A cmd/ctrl-click is \"open this in a new tab\", a shift-click is a new\n // window, an alt-click is a download, and a middle-click is a\n // background tab. Every one of those is the browser's answer to this\n // `href`, so the shell hands none of them to `onNavigate` and does not\n // close a drawer whose screen the user never left. Without this,\n // `onNavigate` fires on all of them and each consumer has to write the\n // same four-condition guard before its `preventDefault()`.\n if (!isPlainLeftClick(event)) {\n return;\n }\n onFollow?.();\n onNavigate?.(item, event);\n }}\n className={cn(\n \"flex items-center gap-2.5 rounded-[var(--ds-radius-md)] px-3 py-2\",\n \"text-[length:var(--ds-text-caption-lg)] no-underline outline-none\",\n \"transition-colors duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--ds-ring)]\",\n active\n ? \"bg-[var(--ds-primary-soft)] font-semibold text-[color:var(--ds-primary)]\"\n : cn(\n \"font-medium text-[color:var(--ds-muted-foreground)]\",\n \"hover:bg-[var(--ds-hover)] hover:text-[color:var(--ds-foreground)]\",\n ),\n )}\n >\n {item.icon ? <NavIcon icon={item.icon} label={item.label} /> : null}\n <span className=\"truncate\">{item.label}</span>\n {hasBadge ? (\n <span\n data-slot=\"app-shell-nav-badge\"\n className={cn(\n \"ml-auto inline-flex min-w-5 shrink-0 items-center justify-center\",\n \"rounded-[var(--ds-radius-full)] px-1.5\",\n \"text-[length:var(--ds-text-caption-sm)] font-semibold tabular-nums\",\n active\n ? \"bg-[var(--ds-primary)] text-[color:var(--ds-primary-foreground)]\"\n : \"bg-[var(--ds-hover)] text-[color:var(--ds-muted-foreground)]\",\n )}\n >\n {badge}\n </span>\n ) : null}\n </a>\n );\n}\n\n/** The items themselves. Shared by the sidebar and the mobile drawer, unchanged. */\nfunction NavItems({ nav, onFollow }: { nav: AppShellNav; onFollow?: () => void }) {\n return (\n <nav\n data-slot=\"app-shell-nav-list\"\n aria-label={nav.label ?? \"Sections\"}\n className=\"flex min-h-0 flex-1 flex-col gap-1\"\n >\n {nav.groups.map((group, index) => (\n <div\n key={group.label ?? `group-${index}`}\n data-slot=\"app-shell-nav-group\"\n data-group-label={group.label}\n className={cn(\n \"flex flex-col gap-1\",\n // A labelled group is separated by its label; an unlabelled one\n // has nothing to separate it, so it gets a rule.\n index > 0 && !group.label && \"mt-2 border-t border-[var(--ds-border-subtle)] pt-2\",\n )}\n >\n {group.label ? (\n <span\n data-slot=\"app-shell-nav-group-label\"\n className={cn(\n \"px-3 pt-3 pb-1 text-[length:var(--ds-text-eyebrow)] uppercase\",\n \"tracking-[var(--ds-text-eyebrow--letter-spacing)]\",\n \"text-[color:var(--ds-muted-foreground)]\",\n )}\n >\n {group.label}\n </span>\n ) : null}\n {group.items.map((item) => (\n <NavLink\n key={item.id}\n item={item}\n active={isNavItemActive(nav, item)}\n onNavigate={nav.onNavigate}\n onFollow={onFollow}\n />\n ))}\n </div>\n ))}\n {nav.footer ? (\n <div data-slot=\"app-shell-nav-footer\" className=\"mt-auto pt-3\">\n {nav.footer}\n </div>\n ) : null}\n </nav>\n );\n}\n\n/* ── the shell ───────────────────────────────────────────────────────────── */\n\nexport interface AppShellExtension {\n /** Registered mark id, from `x-cortena.icon` in the OpenAPI document. */\n id: string;\n /** Display name. Also the first half of the document title, \"<name> · Cortena\". */\n name: string;\n}\n\nexport interface AppShellUser {\n name: string;\n avatarUrl?: string;\n}\n\nexport interface AppShellHelp {\n /**\n * The extension's functional document — the thing the help panel answers\n * from (§12). Giving that document a runtime consumer is the point: a stale\n * doc becomes a visibly wrong help panel rather than a file nobody opens.\n */\n source: string;\n}\n\nexport interface HelpPanelSlotProps {\n open: boolean;\n source?: string;\n onClose: () => void;\n}\n\nexport interface AppShellMenuItem {\n id: string;\n label: string;\n icon?: LucideIcon;\n onSelect: () => void;\n}\n\nexport interface AppShellProps extends Omit<React.ComponentProps<\"div\">, \"title\"> {\n extension: AppShellExtension;\n user: AppShellUser;\n /**\n * The left sidebar. `null` (or omitted) renders no sidebar and no menu\n * button — a legal state, and the right one for an extension whose whole\n * navigation is inside one page.\n */\n nav?: AppShellNav | null;\n /**\n * Between the extension name and the avatar. For state, not for controls:\n * a live-status pill, the org or workspace the screen is scoped to, or one\n * secondary action. Navigation, theme, help and settings do NOT go here —\n * they have exactly one home each and a second one is how two extensions\n * stop looking like one product (§10).\n */\n headerSlot?: React.ReactNode;\n /**\n * Extra avatar-menu items, inserted between Settings and Log out. For a\n * destination that is the user's, not the screen's — Assure's Permissions,\n * a Report-issue dialog. Not a second way to reach something already in the\n * shell.\n */\n menuItems?: AppShellMenuItem[];\n /**\n * How far the bottom-right cluster (and anything in `agentSlot`) lifts off\n * the bottom edge, on top of its own 24px. A number is pixels.\n *\n * Omit it and the shell measures the first `[data-bottom-bar]` element\n * inside itself instead, which is 0 when that element is hidden — so a\n * `lg:hidden` mobile tab bar lifts the cluster on a phone and nowhere else,\n * with nothing to keep in sync.\n */\n bottomInset?: string | number;\n /** Opens the extension's own settings route. The avatar menu is the only way in. */\n onSettings?: () => void;\n onProfile?: () => void;\n onLogout?: () => void;\n help?: AppShellHelp;\n /**\n * The help panel, until HelpPanel ships (EXTBP-24). A render function gets\n * the open state and owns its own markup; a node is mounted in a right-hand\n * region while open.\n */\n helpPanel?: React.ReactNode | ((props: HelpPanelSlotProps) => React.ReactNode);\n /** The agent pop-up pill. Stacks above the bottom-right cluster. */\n agentSlot?: React.ReactNode;\n /** localStorage key for the theme choice, forwarded to `useCortenaTheme`. */\n themeStorageKey?: string;\n}\n\n/**\n * How many shells are mounted. `<html>` has one set of these two variables and\n * any number of shells may be on the page at once — the token guide mounts\n * three — so writing them on every mount is last-writer-wins, and removing\n * them on every unmount takes them away from the shells still on screen.\n *\n * The first shell to mount owns them; the last to unmount clears them. A\n * second shell publishes the same two values on its own element either way,\n * which is what anything inside its subtree reads.\n */\nlet mountedShells = 0;\n\nfunction useOwnsRootVars(): boolean {\n const [owns, setOwns] = React.useState(false);\n React.useEffect(() => {\n mountedShells += 1;\n setOwns(mountedShells === 1);\n return () => {\n mountedShells -= 1;\n if (mountedShells === 0) {\n const root = document.documentElement;\n root.style.removeProperty(BOTTOM_INSET_VAR);\n root.style.removeProperty(HEADER_HEIGHT_VAR);\n }\n };\n }, []);\n return owns;\n}\n\nfunction AppShell({\n className,\n style,\n extension,\n user,\n nav,\n headerSlot,\n menuItems,\n bottomInset,\n onSettings,\n onProfile,\n onLogout,\n help,\n helpPanel,\n agentSlot,\n themeStorageKey,\n children,\n ...props\n}: AppShellProps) {\n const [helpOpen, setHelpOpen] = React.useState(false);\n const helpButtonRef = React.useRef<HTMLButtonElement>(null);\n const closeHelp = React.useCallback(() => {\n setHelpOpen(false);\n // Focus goes back where it came from. A panel closed with Escape or its\n // own X otherwise left focus on a removed node, which the browser resets\n // to <body> — the next Tab starts from the top of the page, and a\n // keyboard user has to walk the whole shell to get back.\n helpButtonRef.current?.focus();\n }, []);\n\n /**\n * The help panel stops 12px above the bottom-right cluster, so the button\n * that opened it stays visible and clickable. The gap was hard-coded at\n * 76px — one 40px control plus the two margins — and the cluster is not\n * always that tall: the agent pop-up pill stacks ABOVE the row, which makes\n * it 92px, and the panel then covered the pill it was sitting on.\n */\n //\n // Every observer below is keyed on the DOM node, held in state through a\n // callback ref, and never on a React element. `agentSlot`, `children` and\n // the rest are fresh objects on every render of the consumer's page, so an\n // effect that lists them tore down and rebuilt its ResizeObserver on every\n // keystroke in the page underneath. The observer is what notices a size\n // change; it does not need the element identity to do it.\n const [clusterNode, setClusterNode] = React.useState<HTMLDivElement | null>(null);\n const [clusterHeight, setClusterHeight] = React.useState(40);\n React.useEffect(() => {\n if (!clusterNode || typeof ResizeObserver === \"undefined\") {\n return;\n }\n const measure = () => setClusterHeight(clusterNode.getBoundingClientRect().height);\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(clusterNode);\n return () => observer.disconnect();\n }, [clusterNode]);\n\n /**\n * The header's height as the browser laid it out, which is what the sidebar,\n * the help panel and `--ds-shell-header-height` all read. `HEADER_HEIGHT` is\n * where the number is declared; this is where it is confirmed.\n */\n const [headerNode, setHeaderNode] = React.useState<HTMLElement | null>(null);\n const [headerHeight, setHeaderHeight] = React.useState(HEADER_HEIGHT);\n React.useEffect(() => {\n if (!headerNode || typeof ResizeObserver === \"undefined\") {\n return;\n }\n const measure = () => setHeaderHeight(headerNode.getBoundingClientRect().height);\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(headerNode);\n return () => observer.disconnect();\n }, [headerNode]);\n /**\n * The bottom inset. Given explicitly, or measured from the first\n * `[data-bottom-bar]` element in the page — which reports 0 while it is\n * hidden, so a `lg:hidden` tab bar lifts the cluster on a phone and nowhere\n * else without the app repeating its own breakpoint here.\n */\n const shellRef = React.useRef<HTMLDivElement>(null);\n const [barNode, setBarNode] = React.useState<HTMLElement | null>(null);\n const [detectedInset, setDetectedInset] = React.useState(0);\n // The bar belongs to the consumer's `children`, so there is no ref to give\n // it: it is found by query. Finding it is a `querySelector` after each\n // render — cheap, and the only way a bar that arrives with the data is\n // noticed at all. Observing it is keyed on the node it found, so the\n // observer survives renders that did not change which element it is.\n React.useEffect(() => {\n if (bottomInset !== undefined) {\n return;\n }\n setBarNode(shellRef.current?.querySelector<HTMLElement>(\"[data-bottom-bar]\") ?? null);\n });\n React.useEffect(() => {\n if (bottomInset !== undefined) {\n return;\n }\n if (!barNode || typeof ResizeObserver === \"undefined\") {\n setDetectedInset(0);\n return;\n }\n const measure = () => setDetectedInset(barNode.getBoundingClientRect().height);\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(barNode);\n return () => observer.disconnect();\n }, [barNode, bottomInset]);\n const resolvedInset =\n bottomInset === undefined\n ? `${detectedInset}px`\n : typeof bottomInset === \"number\"\n ? `${bottomInset}px`\n : bottomInset;\n\n /**\n * The same two values on `<html>`. The shell's own element covers everything\n * it renders; the agent pop-up may portal out of that subtree, and a\n * variable it cannot see is a variable it cannot honour.\n *\n * Only the first shell on the page writes them; see `useOwnsRootVars`.\n */\n const ownsRootVars = useOwnsRootVars();\n React.useEffect(() => {\n if (!ownsRootVars) {\n return;\n }\n const root = document.documentElement;\n root.style.setProperty(BOTTOM_INSET_VAR, resolvedInset);\n root.style.setProperty(HEADER_HEIGHT_VAR, `${headerHeight}px`);\n }, [ownsRootVars, resolvedInset, headerHeight]);\n\n /** The mobile nav drawer. Nothing below `lg` has room for a 224px sidebar. */\n const [navOpen, setNavOpen] = React.useState(false);\n const navToggleRef = React.useRef<HTMLButtonElement>(null);\n const navOverlayRef = React.useRef<HTMLDivElement>(null);\n const navDrawerRef = React.useRef<HTMLDivElement>(null);\n const navCloseRef = React.useRef<HTMLButtonElement>(null);\n const closeNav = React.useCallback(() => setNavOpen(false), []);\n /**\n * What `role=\"dialog\" aria-modal=\"true\"` promises, done rather than\n * declared: focus starts inside, Tab stays inside, the shell behind the\n * scrim is out of reach, and the page under it does not scroll.\n *\n * `inert` goes on the shell's own children rather than on `<body>` so that\n * the overlay — which is one of those children — stays live, and so that\n * anything a consumer portals outside the shell is untouched.\n */\n React.useEffect(() => {\n if (!navOpen) {\n return;\n }\n const shell = shellRef.current;\n const behind = shell\n ? [...shell.children].filter((el) => el !== navOverlayRef.current)\n : [];\n for (const el of behind) {\n el.setAttribute(\"inert\", \"\");\n }\n const { body } = document;\n const scroll = body.style.overflow;\n body.style.overflow = \"hidden\";\n // The close button, not the drawer: focus on a container is focus a\n // screen reader announces and a Tab immediately leaves.\n navCloseRef.current?.focus();\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") {\n closeNav();\n return;\n }\n if (event.key !== \"Tab\") {\n return;\n }\n const drawer = navDrawerRef.current;\n const focusable = drawer\n ? [...drawer.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)]\n : [];\n const first = focusable[0];\n const last = focusable.at(-1);\n if (!drawer || !first || !last) {\n return;\n }\n const active = document.activeElement;\n const leaving = event.shiftKey ? active === first || !drawer.contains(active) : active === last;\n if (leaving) {\n event.preventDefault();\n (event.shiftKey ? last : first).focus();\n }\n };\n document.addEventListener(\"keydown\", onKeyDown);\n return () => {\n document.removeEventListener(\"keydown\", onKeyDown);\n for (const el of behind) {\n el.removeAttribute(\"inert\");\n }\n body.style.overflow = scroll;\n // Focus goes back to the button that opened it, and it goes back here\n // rather than in `closeNav` because the toggle is inside `behind`: while\n // the drawer is open it is inert, and focusing an inert element does\n // nothing at all. Left where it was, focus falls to <body> and the next\n // Tab starts from the top of the page.\n navToggleRef.current?.focus();\n };\n }, [navOpen, closeNav]);\n\n const hasHelp = help !== undefined || helpPanel !== undefined;\n const panel =\n typeof helpPanel === \"function\"\n ? helpPanel({ open: helpOpen, source: help?.source, onClose: closeHelp })\n : helpOpen\n ? helpPanel\n : null;\n\n return (\n <div\n ref={shellRef}\n data-slot=\"app-shell\"\n className={cn(\n \"flex min-h-screen w-full flex-col bg-[var(--ds-background)] text-[color:var(--ds-foreground)]\",\n className,\n )}\n style={\n {\n [HEADER_HEIGHT_VAR]: `${headerHeight}px`,\n [BOTTOM_INSET_VAR]: resolvedInset,\n ...style,\n } as React.CSSProperties\n }\n {...props}\n >\n <header\n data-slot=\"app-shell-header\"\n ref={setHeaderNode}\n className={cn(\n \"sticky top-0 z-30 flex w-full shrink-0 items-center justify-between gap-3\",\n \"border-b border-[var(--ds-border-subtle)] bg-[var(--ds-background)] px-4 sm:px-6\",\n )}\n style={{ height: HEADER_HEIGHT }}\n >\n <div data-slot=\"app-shell-brand\" className=\"flex min-w-0 items-center gap-2.5\">\n {nav ? (\n <button\n ref={navToggleRef}\n type=\"button\"\n data-slot=\"app-shell-nav-toggle\"\n aria-label=\"Menu\"\n aria-expanded={navOpen}\n // Only while the drawer exists; see `HelpButton`.\n aria-controls={navOpen ? NAV_DRAWER_ID : undefined}\n onClick={() => setNavOpen(true)}\n className={cn(\n \"-ml-1 inline-flex size-9 shrink-0 items-center justify-center lg:hidden\",\n \"rounded-[var(--ds-radius-md)] text-[color:var(--ds-muted-foreground)] outline-none\",\n \"transition-colors duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]\",\n \"hover:bg-[var(--ds-hover)] hover:text-[color:var(--ds-foreground)]\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--ds-ring)]\",\n )}\n >\n <Menu className=\"size-5\" aria-hidden />\n </button>\n ) : null}\n <span\n data-slot=\"app-shell-brand-square\"\n className={cn(\n \"flex size-8 shrink-0 items-center justify-center rounded-[var(--ds-radius-md)]\",\n \"bg-[var(--ds-primary-soft)] text-[color:var(--ds-primary)]\",\n )}\n >\n <BrandMark id={extension.id} name={extension.name} size={20} decorative />\n </span>\n <span\n data-slot=\"app-shell-brand-name\"\n className={cn(\n \"truncate font-display text-[length:var(--ds-text-body-md)] font-bold\",\n \"leading-[var(--ds-text-body-md--line-height)] text-[color:var(--ds-foreground)]\",\n )}\n >\n {extension.name}\n </span>\n </div>\n\n {headerSlot ? (\n <div\n data-slot=\"app-shell-header-slot\"\n className=\"flex min-w-0 flex-1 items-center gap-2\"\n >\n {headerSlot}\n </div>\n ) : null}\n\n <DropdownMenu>\n <DropdownMenuTrigger\n data-slot=\"app-shell-avatar-trigger\"\n aria-label={`Account: ${user.name}`}\n className={cn(\n \"flex shrink-0 items-center rounded-[var(--ds-radius-full)] outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--ds-ring)] focus-visible:ring-offset-2\",\n \"focus-visible:ring-offset-[var(--ds-background)]\",\n )}\n >\n <Avatar>\n {user.avatarUrl ? <AvatarImage src={user.avatarUrl} alt=\"\" /> : null}\n <AvatarFallback>{initials(user.name)}</AvatarFallback>\n </Avatar>\n </DropdownMenuTrigger>\n {/* Profile, Settings, Log out. Nothing else, and no second settings\n entry point anywhere in the extension (§9.2). */}\n <DropdownMenuContent align=\"end\" sideOffset={8} className=\"min-w-48\">\n <DropdownMenuItem\n data-item=\"profile\"\n disabled={!onProfile}\n onClick={onProfile}\n >\n <User /> Profile\n </DropdownMenuItem>\n <DropdownMenuItem\n data-item=\"settings\"\n disabled={!onSettings}\n onClick={onSettings}\n >\n <Settings /> Settings\n </DropdownMenuItem>\n {menuItems?.map((item) => {\n const Icon = item.icon;\n return (\n <DropdownMenuItem key={item.id} data-item={item.id} onClick={item.onSelect}>\n {Icon ? <Icon /> : null} {item.label}\n </DropdownMenuItem>\n );\n })}\n <DropdownMenuSeparator />\n <DropdownMenuItem\n data-item=\"logout\"\n variant=\"destructive\"\n disabled={!onLogout}\n onClick={onLogout}\n >\n <LogOut /> Log out\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </header>\n\n <div data-slot=\"app-shell-body\" className=\"flex min-h-0 w-full flex-1\">\n {nav ? (\n <aside\n data-slot=\"app-shell-nav\"\n className={cn(\n \"sticky hidden w-56 shrink-0 flex-col overflow-y-auto p-3 lg:flex\",\n \"border-r border-[var(--ds-border-subtle)] bg-[var(--ds-background)]\",\n )}\n // The sidebar starts under the header and never scrolls past the\n // viewport. `calc()` over a length, so it is written here rather\n // than as an arbitrary Tailwind value, which would need escaping\n // and would fail silently if it were got wrong.\n style={{ top: headerHeight, maxHeight: `calc(100vh - ${headerHeight}px)` }}\n >\n <NavItems nav={nav} />\n </aside>\n ) : null}\n\n {/*\n * The content measure (DESIGN-140).\n *\n * This was `min-w-0 flex-1` and nothing else, so a page grew to\n * whatever the window was. On a wide monitor that is not generous, it\n * is unreadable: a DataTable's fill column took the slack, so the\n * first cell sat on the far left, every other column was pushed to\n * the far right, and one row had to be read corner to corner.\n *\n * `max-width` alone would have been the other half of the same bug —\n * the skill's own example is content pinned to the left edge of a\n * wide screen — so the margin is here with it and neither is written\n * without the other.\n *\n * The cap is on an inner element rather than on `main`, which keeps\n * `flex-1` doing its job: `main` still fills the row beside the\n * sidebar, so a page that wants the full width for a canvas or a map\n * can still reach it, and only this measure is constrained.\n */}\n <main data-slot=\"app-shell-main\" className=\"min-w-0 flex-1\">\n <div\n data-slot=\"app-shell-content\"\n className=\"mx-auto w-full max-w-[var(--ds-layout-content)]\"\n >\n {children}\n </div>\n </main>\n </div>\n\n {nav && navOpen ? (\n <div\n ref={navOverlayRef}\n data-slot=\"app-shell-nav-overlay\"\n className=\"fixed inset-0 z-50 lg:hidden\"\n >\n {/* A button rather than a div: dismissing an overlay by clicking\n outside it has to be reachable from the keyboard too. */}\n <button\n type=\"button\"\n data-slot=\"app-shell-nav-scrim\"\n aria-label=\"Close menu\"\n onClick={closeNav}\n className={cn(\n \"absolute inset-0 backdrop-blur-[2px]\",\n \"bg-[color-mix(in_oklab,var(--ds-ink-950)_70%,transparent)]\",\n )}\n />\n <div\n ref={navDrawerRef}\n id={NAV_DRAWER_ID}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={`${extension.name} navigation`}\n data-slot=\"app-shell-nav-drawer\"\n className={cn(\n \"absolute inset-y-0 left-0 flex w-72 max-w-[85vw] flex-col gap-1 overflow-y-auto p-3\",\n \"border-r border-[var(--ds-border-subtle)] bg-[var(--ds-background)]\",\n \"shadow-[var(--ds-shadow-lg)]\",\n )}\n >\n <div className=\"flex items-center justify-between gap-2 pb-2\">\n <span className=\"flex size-8 shrink-0 items-center justify-center rounded-[var(--ds-radius-md)] bg-[var(--ds-primary-soft)] text-[color:var(--ds-primary)]\">\n <BrandMark id={extension.id} name={extension.name} size={20} decorative />\n </span>\n <button\n ref={navCloseRef}\n type=\"button\"\n data-slot=\"app-shell-nav-drawer-close\"\n aria-label=\"Close menu\"\n onClick={closeNav}\n className={cn(\n \"inline-flex size-9 shrink-0 items-center justify-center\",\n \"rounded-[var(--ds-radius-md)] text-[color:var(--ds-muted-foreground)] outline-none\",\n \"transition-colors duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]\",\n \"hover:bg-[var(--ds-hover)] hover:text-[color:var(--ds-foreground)]\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--ds-ring)]\",\n )}\n >\n <X className=\"size-5\" aria-hidden />\n </button>\n </div>\n {/* Following a link closes the drawer: it covers the screen the\n link just navigated to. */}\n <NavItems nav={nav} onFollow={closeNav} />\n </div>\n </div>\n ) : null}\n\n {panel ? (\n <aside\n id={HELP_PANEL_ID}\n data-slot=\"app-shell-help-panel\"\n aria-label={`${extension.name} help`}\n // Measured, not assumed: 24px of viewport margin, whatever a bottom\n // bar has claimed, the cluster the panel must clear, and 12px\n // between the two. At the top it starts below the 64px header — it\n // was `top-6`, which put it over the header and over the avatar\n // trigger (DESIGN-88).\n style={{\n top: `calc(var(${HEADER_HEIGHT_VAR}, ${headerHeight}px) + ${HELP_PANEL_GAP}px)`,\n bottom: `calc(${CLUSTER_INSET + clusterHeight + HELP_PANEL_GAP}px + var(${BOTTOM_INSET_VAR}, 0px))`,\n }}\n className={cn(\n // Anchored to the same corner. A panel that covers its own trigger\n // leaves no way back except Escape.\n \"fixed right-6 z-40 flex w-[min(24rem,calc(100vw-3rem))] flex-col\",\n \"overflow-hidden rounded-[var(--ds-radius-lg)] border border-[var(--ds-border)]\",\n \"bg-[var(--ds-card)] shadow-[var(--ds-shadow-lg)]\",\n )}\n >\n {panel}\n </aside>\n ) : null}\n\n <BottomRightCluster ref={setClusterNode} agentSlot={agentSlot}>\n <ThemeToggle storageKey={themeStorageKey} />\n {hasHelp ? (\n <HelpButton\n ref={helpButtonRef}\n open={helpOpen}\n // `helpPanel` as a render function may mount its own markup while\n // closed, or none while open; the region below is the one that\n // carries the id, so it is the one that decides.\n aria-controls={panel ? HELP_PANEL_ID : undefined}\n onClick={() => setHelpOpen((open) => !open)}\n />\n ) : null}\n </BottomRightCluster>\n </div>\n );\n}\n\n/** One or two letters. Two words give two initials; anything else gives one. */\nfunction initials(name: string): string {\n const words = name.trim().split(/\\s+/).filter(Boolean);\n if (words.length === 0) {\n return \"?\";\n }\n const first = words[0]?.charAt(0) ?? \"\";\n const last = words.length > 1 ? (words.at(-1)?.charAt(0) ?? \"\") : \"\";\n return (first + last).toUpperCase();\n}\n\n/**\n * `<title>` for an extension: the extension first, because that is what\n * distinguishes one tab from another, and the product second so a row of tabs\n * reads as one product (§9.5, audit rule P-38).\n */\nfunction documentTitle(extensionName: string, page?: string): string {\n return page ? `${page} · ${extensionName} · Cortena` : `${extensionName} · Cortena`;\n}\n\nexport { AppShell, BottomRightCluster, BrandMark, HelpButton, ThemeToggle, documentTitle };\n"],"mappings":"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"}
@@ -3,7 +3,7 @@ import { VariantProps } from "class-variance-authority";
3
3
  import * as React from "react";
4
4
  //#region src/components/badge.d.ts
5
5
  declare const badgeVariants: (props?: ({
6
- variant?: "default" | "success" | "warn" | "destructive" | "danger" | "info" | "warning" | "neutral" | "secondary" | "outline" | null | undefined;
6
+ variant?: "default" | "success" | "warning" | "warn" | "destructive" | "danger" | "info" | "neutral" | "secondary" | "outline" | null | undefined;
7
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
8
  export interface BadgeProps extends React.ComponentProps<"span">, VariantProps<typeof badgeVariants> {}
9
9
  declare function Badge({ className, variant, ...props }: BadgeProps): React.JSX.Element;
@@ -401,7 +401,7 @@ function DataTableView({ instance, className, maxHeight, virtualize, rowHeight =
401
401
  rowId: row.id,
402
402
  columnId: column.id
403
403
  }) : void 0,
404
- className: cn("relative align-middle outline-none", isEditing ? "px-1.5 py-1" : cellPadding, alignClass[meta?.align ?? "start"], "focus-visible:ring-[2px] focus-visible:ring-inset focus-visible:ring-[var(--ds-ring)]", pin.pinned && "z-[1] bg-[var(--ds-card)] group-hover/row:bg-[linear-gradient(var(--ds-hover),var(--ds-hover))] group-data-[selected]/row:bg-[linear-gradient(var(--ds-primary-soft),var(--ds-primary-soft))] group-data-[active]/row:bg-[linear-gradient(var(--ds-primary-soft),var(--ds-primary-soft))]", pin.pinned === "start" && pin.edge && "shadow-[inset_-1px_0_0_var(--ds-border)]", pin.pinned === "end" && pin.edge && "shadow-[inset_1px_0_0_var(--ds-border)]", !isEditing && "truncate", meta?.cellClassName),
404
+ className: cn("relative align-middle outline-none", isEditing ? "px-1.5 py-1" : cellPadding, alignClass[meta?.align ?? "start"], "focus-visible:ring-[2px] focus-visible:ring-inset focus-visible:ring-[var(--ds-ring)]", pin.pinned && "z-[1] bg-[var(--ds-background)] group-hover/row:bg-[linear-gradient(var(--ds-hover),var(--ds-hover))] group-data-[selected]/row:bg-[linear-gradient(var(--ds-primary-soft),var(--ds-primary-soft))] group-data-[active]/row:bg-[linear-gradient(var(--ds-primary-soft),var(--ds-primary-soft))]", pin.pinned === "start" && pin.edge && "shadow-[inset_-1px_0_0_var(--ds-border)]", pin.pinned === "end" && pin.edge && "shadow-[inset_1px_0_0_var(--ds-border)]", !isEditing && "truncate", meta?.cellClassName),
405
405
  children: isEditing ? /* @__PURE__ */ jsx(EditableCell, {
406
406
  cell,
407
407
  instance,
@@ -483,7 +483,7 @@ function DataTableView({ instance, className, maxHeight, virtualize, rowHeight =
483
483
  "data-slot": "data-table-container",
484
484
  onScroll,
485
485
  style: { maxHeight: resolvedHeight },
486
- className: cn("relative w-full overflow-auto rounded-[var(--ds-radius-md)] border border-[var(--ds-border)] bg-[var(--ds-card)]"),
486
+ className: cn("relative w-full overflow-auto rounded-[var(--ds-radius-md)] border border-[var(--ds-border)] bg-[var(--ds-background)]"),
487
487
  children: [/* @__PURE__ */ jsxs("table", {
488
488
  ref: tableRef,
489
489
  role: "grid",
@@ -1 +1 @@
1
- {"version":3,"file":"data-table.js","names":[],"sources":["../../../src/components/data-table/data-table.tsx"],"sourcesContent":["\"use client\";\n\nimport type { RowData } from \"@tanstack/react-table\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { Inbox } from \"lucide-react\";\nimport * as React from \"react\";\nimport { ErrorBanner } from \"@/components/error-banner\";\nimport { Input } from \"@/components/input\";\nimport { Skeleton } from \"@/components/skeleton\";\nimport { Spinner } from \"@/components/spinner\";\nimport { cn } from \"@/lib/cn\";\nimport { DataTableColumnHeader, DataTablePagination, DataTableToolbar } from \"./parts\";\nimport type {\n DataTableCell,\n DataTableColumn,\n DataTableInstance,\n DataTableRow,\n DataTableTable,\n UseDataTableOptions,\n} from \"./types\";\nimport { useDataTable } from \"./use-data-table\";\n\n/**\n * DataTable — the composite from cortenaUI&Design/04-data-tables.md.\n *\n * `<DataTable columns={columns} dataSource={{ kind: \"client\", rows }} />`\n * renders a complete table: toolbar with search, faceted filters, view and\n * export menus; sortable, pinnable, hideable, resizable columns; sticky\n * header; pager; and, past 100 rows, virtualised rows. A server source\n * changes nothing in the markup and everything in the data flow.\n *\n * Markup is a real `<table>` with `role=\"grid\"` so screen readers get rows\n * and columns, and keyboard users get one tab stop with arrow-key movement\n * between cells (roving tabindex), Enter to edit an editable cell, Escape to\n * cancel, Space to toggle selection.\n *\n * Virtualised bodies keep the table element: rows outside the viewport are\n * replaced by two spacer rows sized from the virtualizer, which keeps column\n * widths, sticky headers and pinned columns working without absolute\n * positioning.\n */\n\nconst VIRTUALIZE_ABOVE = 100;\nconst DEFAULT_VIRTUAL_HEIGHT = 600;\nconst INFINITE_THRESHOLD_PX = 240;\n\n/** What a `ref` on `DataTable` hands back. */\nexport interface DataTableHandle<Row extends RowData> {\n /**\n * Refetch the page on screen through the same query the auto-refresh\n * interval drives — never a second fetch path. A table subscribed to a\n * notification stream calls this from the stream and leaves the interval as\n * the fallback for when the stream is unavailable.\n */\n refresh: () => void;\n /** Everything else the table knows, for a caller that needs more than a refresh. */\n instance: DataTableInstance<Row>;\n}\n\nexport interface DataTableViewProps<Row extends RowData> {\n className?: string;\n /** Imperative handle: `ref.current.refresh()`. */\n ref?: React.Ref<DataTableHandle<Row>>;\n /** Height of the scrolling body. Required for virtualisation to mean anything; defaults to 600px when virtualised. */\n maxHeight?: number | string;\n /** Force virtualisation on or off; undefined virtualises above 100 rows. */\n virtualize?: boolean;\n /** Estimated row height in px for the virtualizer. @default 40 */\n rowHeight?: number;\n /** @default true */\n stickyHeader?: boolean;\n /** @default \"default\" */\n density?: \"default\" | \"compact\";\n /** Detail panel under an expanded row; enables the expander column. */\n renderSubComponent?: (row: DataTableRow<Row>) => React.ReactNode;\n /** `false` hides the toolbar; a function replaces it. */\n toolbar?: boolean | ((ctx: { instance: DataTableInstance<Row> }) => React.ReactNode);\n /** Rendered in the toolbar while rows are selected. */\n bulkActions?: (ctx: { rows: Row[]; table: DataTableTable<Row> }) => React.ReactNode;\n /** Extra toolbar controls (right side). */\n toolbarActions?: React.ReactNode;\n searchPlaceholder?: string;\n emptyState?: React.ReactNode;\n /** @default \"No results\" */\n emptyMessage?: string;\n pageSizeOptions?: number[];\n /** @default true */\n enableExport?: boolean;\n /** @default \"export\" */\n exportFileName?: string;\n /** Show the loading skeleton regardless of the source's own state. */\n loading?: boolean;\n /** Error to show above the table, in addition to the server source's. */\n error?: string | Error | null;\n onRowClick?: (row: Row, event: React.MouseEvent<HTMLTableRowElement>) => void;\n /**\n * The row a detail pane is currently showing, matched against the row id\n * (`getRowId`, the row index when that is not set). It gets `data-active`\n * and the `--ds-primary-soft` tint, independent of checkbox selection — a\n * master/detail list highlights one row without selecting it, and a table\n * with both can show a selected set and one open row at once.\n */\n activeRowId?: string | null;\n /** Extra classes per row, e.g. to dim rows the app considers stale. */\n rowClassName?: (row: Row) => string | undefined;\n}\n\nexport type DataTableProps<Row extends RowData> = DataTableViewProps<Row> &\n ({ instance: DataTableInstance<Row> } | UseDataTableOptions<Row>);\n\nfunction DataTable<Row extends RowData>(props: DataTableProps<Row>) {\n if (\"instance\" in props) {\n const { instance, ...view } = props;\n return <DataTableView instance={instance} {...view} />;\n }\n return <DataTableOwned {...props} />;\n}\n\nfunction DataTableOwned<Row extends RowData>(props: DataTableViewProps<Row> & UseDataTableOptions<Row>) {\n const {\n className,\n maxHeight,\n virtualize,\n rowHeight,\n stickyHeader,\n density,\n renderSubComponent,\n toolbar,\n bulkActions,\n toolbarActions,\n searchPlaceholder,\n emptyState,\n emptyMessage,\n pageSizeOptions,\n enableExport,\n exportFileName,\n loading,\n error,\n onRowClick,\n activeRowId,\n rowClassName,\n ref,\n ...options\n } = props;\n const instance = useDataTable<Row>({\n ...options,\n enableExpanding: options.enableExpanding ?? Boolean(renderSubComponent),\n });\n return (\n <DataTableView\n instance={instance}\n className={className}\n maxHeight={maxHeight}\n virtualize={virtualize}\n rowHeight={rowHeight}\n stickyHeader={stickyHeader}\n density={density}\n renderSubComponent={renderSubComponent}\n toolbar={toolbar}\n bulkActions={bulkActions}\n toolbarActions={toolbarActions}\n searchPlaceholder={searchPlaceholder}\n emptyState={emptyState}\n emptyMessage={emptyMessage}\n pageSizeOptions={pageSizeOptions}\n enableExport={enableExport}\n exportFileName={exportFileName}\n loading={loading}\n error={error}\n onRowClick={onRowClick}\n activeRowId={activeRowId}\n rowClassName={rowClassName}\n ref={ref}\n />\n );\n}\n\n/* ------------------------------------------------------------------------ */\n/* Pinning */\n/* ------------------------------------------------------------------------ */\n\nfunction pinAttrs<Row extends RowData>(\n column: DataTableColumn<Row, any>,\n table: DataTableTable<Row>,\n): { style: React.CSSProperties; pinned: \"start\" | \"end\" | undefined; edge: boolean } {\n const pinned = column.getIsPinned();\n if (!pinned) return { style: {}, pinned: undefined, edge: false };\n if (pinned === \"start\") {\n const last = table.getStartVisibleLeafColumns().at(-1);\n return {\n style: { position: \"sticky\", left: column.getStart(\"start\") },\n pinned,\n edge: last?.id === column.id,\n };\n }\n const first = table.getEndVisibleLeafColumns()[0];\n return {\n style: { position: \"sticky\", right: column.getAfter(\"end\") },\n pinned,\n edge: first?.id === column.id,\n };\n}\n\nconst alignClass = { start: \"text-left\", center: \"text-center\", end: \"text-right\" } as const;\n\n/* ------------------------------------------------------------------------ */\n/* Column widths and hover text (DESIGN-77) */\n/* ------------------------------------------------------------------------ */\n\n/**\n * Full text for a cell's `title`, or undefined for no title.\n *\n * Cells truncate and never wrap, so the only way back to the full text is the\n * pointer. A column that renders its own value can be read straight off the\n * value; a column with a `cell` renderer draws a node the table cannot turn\n * into a sentence, and guessing produces titles like \"[object Object]\" on a\n * row of buttons. Such a column says what its hover text is with\n * `meta.hoverText`, or has none.\n */\nfunction hoverTextFor<Row extends RowData>(cell: DataTableCell<Row, any>): string | undefined {\n const def = cell.column.columnDef;\n const meta = def.meta;\n if (meta?.hoverText) return meta.hoverText(cell.row.original) || undefined;\n if (!meta?.plainCell) return undefined;\n const value = cell.getValue();\n // `|| undefined` on the way out too: an empty string is a `title=\"\"`, which\n // is a pointer that stops on an empty tooltip rather than no tooltip.\n return typeof value === \"string\" || typeof value === \"number\"\n ? String(value) || undefined\n : undefined;\n}\n\n/* ------------------------------------------------------------------------ */\n/* Editable cell */\n/* ------------------------------------------------------------------------ */\n\nfunction EditableCell<Row extends RowData>({\n cell,\n instance,\n onDone,\n}: {\n cell: DataTableCell<Row, any>;\n instance: DataTableInstance<Row>;\n onDone: () => void;\n}) {\n const editable = cell.column.columnDef.meta?.editable;\n const config = typeof editable === \"object\" ? editable : {};\n const initial = cell.getValue();\n const [draft, setDraft] = React.useState(initial == null ? \"\" : String(initial));\n const done = React.useRef(false);\n\n const finish = () => {\n done.current = true;\n instance.editing.stop();\n onDone();\n };\n const commit = () => {\n if (done.current) return;\n const value = config.parse ? config.parse(draft) : config.type === \"number\" ? Number(draft) : draft;\n void instance.editing.commit(cell.row, cell.column.id, value);\n finish();\n };\n const cancel = () => {\n if (done.current) return;\n finish();\n };\n\n return (\n <Input\n data-slot=\"data-table-editor\"\n autoFocus\n type={config.type === \"number\" ? \"number\" : \"text\"}\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n onFocus={(e) => e.target.select()}\n onBlur={commit}\n onKeyDown={(e) => {\n // The editor owns its keys; the grid must not move the focus cell.\n e.stopPropagation();\n if (e.key === \"Enter\") {\n e.preventDefault();\n commit();\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n cancel();\n }\n }}\n className=\"h-7 px-2 py-0 text-[length:var(--ds-text-body-sm)]\"\n />\n );\n}\n\n/* ------------------------------------------------------------------------ */\n/* View */\n/* ------------------------------------------------------------------------ */\n\ntype CellCoord = { r: number; c: number };\n\nfunction DataTableView<Row extends RowData>({\n instance,\n className,\n maxHeight,\n virtualize,\n rowHeight = 40,\n stickyHeader = true,\n density = \"default\",\n renderSubComponent,\n toolbar = true,\n bulkActions,\n toolbarActions,\n searchPlaceholder,\n emptyState,\n emptyMessage = \"No results\",\n pageSizeOptions,\n enableExport = true,\n exportFileName = \"export\",\n loading,\n error,\n onRowClick,\n activeRowId,\n rowClassName,\n ref,\n}: DataTableViewProps<Row> & { instance: DataTableInstance<Row> }) {\n const { table, paginationMode } = instance;\n const rows = table.getRowModel().rows;\n const leafColumns = table.getVisibleLeafColumns();\n // The one column drawn with no fixed width, so the space the sized columns\n // leave lands on it instead of on the filler cell (DESIGN-77). First visible\n // one wins; hide it and the table falls back to the old filler behaviour.\n // A pinned column is skipped: pinning positions it with a `left`/`right`\n // offset read off the widths of the columns beside it, and a column with no\n // width of its own would leave every pinned neighbour at the wrong offset.\n const fillColumnId = leafColumns.find(\n (column) => column.columnDef.meta?.fill && !column.getIsPinned(),\n )?.id;\n const widthFor = (column: DataTableColumn<Row, any>, size: number) =>\n column.id === fillColumnId ? undefined : size;\n // A width the fill column would otherwise have to share the remainder with.\n const fillerStyle = fillColumnId ? ({ width: 0 } as const) : undefined;\n const colCount = leafColumns.length;\n const headerGroups = table.getHeaderGroups();\n const isVirtual = virtualize ?? rows.length > VIRTUALIZE_ABOVE;\n const isLoading = loading || instance.isLoading;\n const showSkeleton = isLoading && rows.length === 0;\n const showEmpty = !isLoading && rows.length === 0;\n const resolvedHeight = maxHeight ?? (isVirtual ? DEFAULT_VIRTUAL_HEIGHT : undefined);\n const errorMessage = error instanceof Error ? error.message : (error ?? instance.error?.message ?? \"\");\n\n const scrollRef = React.useRef<HTMLDivElement>(null);\n const tableRef = React.useRef<HTMLTableElement>(null);\n\n const refresh = instance.refetch;\n React.useImperativeHandle(ref, () => ({ refresh, instance }), [refresh, instance]);\n\n // Mandatory on every server-backed table (P-39): a Refresh button beside the\n // search box and a stamp saying which moment these rows are. The switch only\n // appears where the table asked for `autoRefresh`.\n const refreshProps =\n instance.dataSource.kind === \"server\"\n ? {\n onRefresh: refresh,\n isFetching: instance.isFetching,\n lastUpdatedAt: instance.lastUpdatedAt,\n lastErrorAt: instance.lastErrorAt,\n autoRefresh: instance.autoRefresh,\n }\n : undefined;\n\n /* Virtualisation ------------------------------------------------------ */\n\n const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({\n count: rows.length,\n enabled: isVirtual,\n getScrollElement: () => scrollRef.current,\n estimateSize: () => rowHeight,\n overscan: 8,\n getItemKey: (index) => rows[index]?.id ?? index,\n // An expanded row and its detail row are one virtual item.\n measureElement: (element) => {\n let height = element.getBoundingClientRect().height;\n const next = element.nextElementSibling as HTMLElement | null;\n if (next?.dataset.slot === \"data-table-subrow\") height += next.getBoundingClientRect().height;\n return height;\n },\n });\n const virtualItems = isVirtual ? virtualizer.getVirtualItems() : null;\n const paddingTop = virtualItems?.length ? virtualItems[0]!.start : 0;\n const paddingBottom = virtualItems?.length\n ? virtualizer.getTotalSize() - virtualItems[virtualItems.length - 1]!.end\n : 0;\n\n /* Infinite scroll ----------------------------------------------------- */\n\n const onScroll = React.useCallback(\n (event: React.UIEvent<HTMLDivElement>) => {\n if (paginationMode !== \"infinite\" || !instance.hasNextPage || instance.isFetching) return;\n const el = event.currentTarget;\n if (el.scrollHeight - el.scrollTop - el.clientHeight < INFINITE_THRESHOLD_PX) instance.fetchNextPage();\n },\n [paginationMode, instance],\n );\n\n /* Keyboard navigation ------------------------------------------------- */\n\n const [focused, setFocused] = React.useState<CellCoord>({ r: -1, c: 0 });\n const pendingFocus = React.useRef<CellCoord | null>(null);\n\n const cellElement = (coord: CellCoord) =>\n tableRef.current?.querySelector<HTMLElement>(\n `[data-row-index=\"${coord.r}\"][data-col-index=\"${coord.c}\"]`,\n ) ?? null;\n\n const focusCell = React.useCallback(\n (coord: CellCoord) => {\n const el = cellElement(coord);\n if (el) {\n setFocused(coord);\n el.focus();\n return;\n }\n if (isVirtual && coord.r >= 0) {\n pendingFocus.current = coord;\n setFocused(coord);\n virtualizer.scrollToIndex(coord.r, { align: \"auto\" });\n }\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [isVirtual, virtualizer],\n );\n\n React.useEffect(() => {\n const coord = pendingFocus.current;\n if (!coord) return;\n const el = cellElement(coord);\n if (el) {\n pendingFocus.current = null;\n el.focus();\n }\n });\n\n const onKeyDown = (event: React.KeyboardEvent<HTMLTableElement>) => {\n const target = event.target as HTMLElement;\n // Portaled menus bubble through React but live outside the table element.\n if (!tableRef.current?.contains(target)) return;\n if (instance.editing.cell) return;\n const cell = target.closest<HTMLElement>(\"[data-row-index][data-col-index]\");\n if (!cell) return;\n const r = Number(cell.dataset.rowIndex);\n const c = Number(cell.dataset.colIndex);\n const lastRow = rows.length - 1;\n const lastCol = colCount - 1;\n const onCellItself = target === cell;\n\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n focusCell({ r: Math.min(lastRow, r + 1), c });\n return;\n case \"ArrowUp\":\n event.preventDefault();\n focusCell({ r: Math.max(-1, r - 1), c });\n return;\n case \"ArrowRight\":\n event.preventDefault();\n focusCell({ r, c: Math.min(lastCol, c + 1) });\n return;\n case \"ArrowLeft\":\n event.preventDefault();\n focusCell({ r, c: Math.max(0, c - 1) });\n return;\n case \"Home\":\n event.preventDefault();\n focusCell(event.ctrlKey ? { r: -1, c: 0 } : { r, c: 0 });\n return;\n case \"End\":\n event.preventDefault();\n focusCell(event.ctrlKey ? { r: lastRow, c: lastCol } : { r, c: lastCol });\n return;\n case \"PageDown\":\n event.preventDefault();\n focusCell({ r: Math.min(lastRow, r + 10), c });\n return;\n case \"PageUp\":\n event.preventDefault();\n focusCell({ r: Math.max(-1, r - 10), c });\n return;\n case \"Enter\": {\n if (!onCellItself) return;\n if (r === -1) {\n const column = leafColumns[c];\n if (column?.getCanSort()) {\n event.preventDefault();\n column.toggleSorting(undefined, event.shiftKey);\n }\n return;\n }\n const row = rows[r];\n const column = leafColumns[c];\n if (row && column && column.columnDef.meta?.editable) {\n event.preventDefault();\n instance.editing.start({ rowId: row.id, columnId: column.id });\n }\n return;\n }\n case \" \": {\n if (!onCellItself || r === -1) return;\n const row = rows[r];\n if (row?.getCanSelect()) {\n event.preventDefault();\n row.toggleSelected();\n }\n return;\n }\n default:\n return;\n }\n };\n\n const onFocus = (event: React.FocusEvent<HTMLTableElement>) => {\n const cell = (event.target as HTMLElement).closest<HTMLElement>(\"[data-row-index][data-col-index]\");\n if (!cell || !tableRef.current?.contains(cell)) return;\n const r = Number(cell.dataset.rowIndex);\n const c = Number(cell.dataset.colIndex);\n setFocused((f) => (f.r === r && f.c === c ? f : { r, c }));\n };\n\n const tabIndexFor = (r: number, c: number) => (focused.r === r && focused.c === c ? 0 : -1);\n\n /* Rendering ----------------------------------------------------------- */\n\n const cellPadding = density === \"compact\" ? \"px-3 py-1.5\" : \"px-3 py-2.5\";\n const headPadding = density === \"compact\" ? \"px-3 py-1.5\" : \"px-3 py-2\";\n const editingCell = instance.editing.cell;\n\n const renderRow = (row: DataTableRow<Row>, index: number, virtualIndex?: number) => {\n const cells = row.getVisibleCells();\n const endStart = cells.findIndex((cell) => cell.column.getIsPinned() === \"end\");\n const splitAt = endStart === -1 ? cells.length : endStart;\n const selected = row.getIsSelected();\n const active = activeRowId != null && row.id === activeRowId;\n const expanded = renderSubComponent && row.getIsExpanded();\n\n const renderCell = (cell: DataTableCell<Row, any>, c: number) => {\n const column = cell.column;\n const meta = column.columnDef.meta;\n const pin = pinAttrs(column, table);\n const isEditing = editingCell?.rowId === row.id && editingCell.columnId === column.id;\n const canEdit = Boolean(meta?.editable);\n return (\n <td\n key={cell.id}\n role=\"gridcell\"\n data-slot=\"data-table-cell\"\n data-row-index={index}\n data-col-index={c}\n data-pinned={pin.pinned}\n data-pinned-edge={pin.edge ? \"\" : undefined}\n data-editable={canEdit ? \"\" : undefined}\n data-editing={isEditing ? \"\" : undefined}\n aria-colindex={c + 1}\n tabIndex={tabIndexFor(index, c)}\n title={isEditing ? undefined : hoverTextFor(cell)}\n style={{ width: widthFor(column, column.getSize()), ...pin.style }}\n onDoubleClick={\n canEdit && !isEditing\n ? () => instance.editing.start({ rowId: row.id, columnId: column.id })\n : undefined\n }\n className={cn(\n \"relative align-middle outline-none\",\n isEditing ? \"px-1.5 py-1\" : cellPadding,\n alignClass[meta?.align ?? \"start\"],\n \"focus-visible:ring-[2px] focus-visible:ring-inset focus-visible:ring-[var(--ds-ring)]\",\n // Pinned cells cover what scrolls under them, so their background is\n // opaque card with the translucent hover / selected tint layered on.\n pin.pinned &&\n \"z-[1] bg-[var(--ds-card)] group-hover/row:bg-[linear-gradient(var(--ds-hover),var(--ds-hover))] group-data-[selected]/row:bg-[linear-gradient(var(--ds-primary-soft),var(--ds-primary-soft))] group-data-[active]/row:bg-[linear-gradient(var(--ds-primary-soft),var(--ds-primary-soft))]\",\n pin.pinned === \"start\" && pin.edge && \"shadow-[inset_-1px_0_0_var(--ds-border)]\",\n pin.pinned === \"end\" && pin.edge && \"shadow-[inset_1px_0_0_var(--ds-border)]\",\n !isEditing && \"truncate\",\n meta?.cellClassName,\n )}\n >\n {isEditing ? (\n <EditableCell\n cell={cell}\n instance={instance}\n onDone={() => {\n pendingFocus.current = { r: index, c };\n }}\n />\n ) : (\n <table.FlexRender cell={cell} />\n )}\n </td>\n );\n };\n\n return (\n <React.Fragment key={row.id}>\n <tr\n role=\"row\"\n data-slot=\"data-table-row\"\n data-index={virtualIndex}\n data-selected={selected ? \"\" : undefined}\n data-active={active ? \"\" : undefined}\n data-expanded={expanded ? \"\" : undefined}\n aria-rowindex={index + 2}\n aria-selected={table.options.enableRowSelection ? selected : undefined}\n ref={isVirtual ? virtualizer.measureElement : undefined}\n onClick={onRowClick ? (e) => onRowClick(row.original, e) : undefined}\n className={cn(\n \"group/row border-b border-[var(--ds-border-subtle)] transition-colors duration-[var(--ds-duration-fast)]\",\n \"hover:bg-[var(--ds-hover)] data-[selected]:bg-[var(--ds-primary-soft)]\",\n // Independent of selection: a detail pane highlights one row\n // without ticking its checkbox.\n \"data-[active]:bg-[var(--ds-primary-soft)]\",\n onRowClick && \"cursor-pointer\",\n rowClassName?.(row.original),\n )}\n >\n {cells.slice(0, splitAt).map((cell, c) => renderCell(cell, c))}\n <td aria-hidden data-slot=\"data-table-filler\" style={fillerStyle} className=\"p-0\" />\n {cells.slice(splitAt).map((cell, i) => renderCell(cell, splitAt + i))}\n </tr>\n {expanded ? (\n <tr role=\"row\" data-slot=\"data-table-subrow\" className=\"border-b border-[var(--ds-border-subtle)]\">\n <td\n role=\"gridcell\"\n colSpan={colCount + 1}\n className=\"bg-[var(--ds-muted)] px-4 py-3 text-[length:var(--ds-text-body-sm)]\"\n >\n {renderSubComponent(row)}\n </td>\n </tr>\n ) : null}\n </React.Fragment>\n );\n };\n\n const skeletonRows = Array.from({ length: Math.min(8, table.state.pagination.pageSize) }, (_, i) => (\n <tr key={`skeleton-${i}`} aria-hidden data-slot=\"data-table-skeleton-row\" className=\"border-b border-[var(--ds-border-subtle)]\">\n {leafColumns.map((column) => (\n <td key={column.id} className={cellPadding} style={{ width: widthFor(column, column.getSize()) }}>\n <Skeleton className=\"h-3.5 w-3/4\" />\n </td>\n ))}\n <td style={fillerStyle} className=\"p-0\" />\n </tr>\n ));\n\n return (\n <div data-slot=\"data-table\" className={cn(\"flex min-w-0 flex-col gap-3\", className)}>\n {errorMessage ? (\n <ErrorBanner message={errorMessage} onRetry={instance.error ? instance.refetch : undefined} />\n ) : null}\n {toolbar === false ? null : typeof toolbar === \"function\" ? (\n toolbar({ instance })\n ) : (\n <DataTableToolbar\n table={table}\n bulkActions={bulkActions}\n searchPlaceholder={searchPlaceholder}\n enableExport={enableExport}\n exportFileName={exportFileName}\n refresh={refreshProps}\n >\n {toolbarActions}\n </DataTableToolbar>\n )}\n <div\n ref={scrollRef}\n data-slot=\"data-table-container\"\n onScroll={onScroll}\n style={{ maxHeight: resolvedHeight }}\n className={cn(\n \"relative w-full overflow-auto rounded-[var(--ds-radius-md)] border border-[var(--ds-border)] bg-[var(--ds-card)]\",\n )}\n >\n <table\n ref={tableRef}\n role=\"grid\"\n data-slot=\"data-table-grid\"\n data-virtualized={isVirtual ? \"\" : undefined}\n aria-rowcount={rows.length + 1}\n aria-colcount={colCount}\n aria-busy={instance.isFetching || undefined}\n aria-multiselectable={table.options.enableRowSelection ? true : undefined}\n onKeyDown={onKeyDown}\n onFocus={onFocus}\n style={{ minWidth: table.getTotalSize() }}\n className={cn(\n \"w-full table-fixed border-collapse caption-bottom\",\n \"text-[length:var(--ds-text-body-sm)] leading-[var(--ds-text-body-sm--line-height)] text-[color:var(--ds-foreground)]\",\n )}\n >\n <thead data-slot=\"data-table-header\" className={cn(stickyHeader && \"sticky top-0 z-[2]\")}>\n {headerGroups.map((group) => {\n const endStart = group.headers.findIndex((h) => h.column.getIsPinned() === \"end\");\n const splitAt = endStart === -1 ? group.headers.length : endStart;\n const renderHead = (header: (typeof group.headers)[number], c: number) => {\n const column = header.column;\n const meta = column.columnDef.meta;\n const pin = pinAttrs(column, table);\n const sorted = column.getIsSorted();\n const def = column.columnDef;\n return (\n <th\n key={header.id}\n role=\"columnheader\"\n scope=\"col\"\n data-slot=\"data-table-head\"\n data-row-index={-1}\n data-col-index={c}\n data-pinned={pin.pinned}\n data-pinned-edge={pin.edge ? \"\" : undefined}\n aria-colindex={c + 1}\n aria-sort={\n sorted === \"asc\"\n ? \"ascending\"\n : sorted === \"desc\"\n ? \"descending\"\n : column.getCanSort()\n ? \"none\"\n : undefined\n }\n colSpan={header.colSpan}\n tabIndex={tabIndexFor(-1, c)}\n // Only where the column says so. A header is one short\n // phrase the consumer chose, so repeating it in a tooltip\n // on every `<th>` is noise, and it was wrong outright on\n // the select and expand columns, whose header is a node\n // with no sentence in it. `meta.label` is the column\n // saying what it is called; when it is set, `title` on the\n // `<th>` reaches the button inside it too, since a pointer\n // over a node with no title of its own takes its\n // ancestor's. Body cells keep their own hover text.\n title={meta?.label}\n style={{ width: widthFor(column, header.getSize()), ...pin.style }}\n className={cn(\n \"group/head relative h-10 align-middle font-medium outline-none\",\n headPadding,\n \"bg-[var(--ds-muted)] text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]\",\n \"border-b border-[var(--ds-border)]\",\n alignClass[meta?.align ?? \"start\"],\n \"focus-visible:ring-[2px] focus-visible:ring-inset focus-visible:ring-[var(--ds-ring)]\",\n pin.pinned && \"z-[3]\",\n pin.pinned === \"start\" && pin.edge && \"shadow-[inset_-1px_0_0_var(--ds-border)]\",\n pin.pinned === \"end\" && pin.edge && \"shadow-[inset_1px_0_0_var(--ds-border)]\",\n meta?.headerClassName,\n )}\n >\n {header.isPlaceholder ? null : typeof def.header === \"string\" ? (\n <DataTableColumnHeader column={column} title={def.header} />\n ) : (\n <table.FlexRender header={header} />\n )}\n {column.getCanResize() ? (\n <div\n data-slot=\"data-table-resizer\"\n data-resizing={column.getIsResizing() ? \"\" : undefined}\n onMouseDown={header.getResizeHandler()}\n onTouchStart={header.getResizeHandler()}\n onDoubleClick={() => column.resetSize()}\n className={cn(\n \"absolute right-0 top-0 h-full w-1 cursor-col-resize touch-none select-none\",\n \"opacity-0 transition-opacity duration-[var(--ds-duration-fast)] hover:opacity-100 data-[resizing]:opacity-100\",\n \"bg-[var(--ds-ring)]\",\n )}\n />\n ) : null}\n </th>\n );\n };\n return (\n <tr key={group.id} role=\"row\" aria-rowindex={1}>\n {group.headers.slice(0, splitAt).map((header, c) => renderHead(header, c))}\n <th\n aria-hidden\n data-slot=\"data-table-filler-head\"\n style={fillerStyle}\n className=\"border-b border-[var(--ds-border)] bg-[var(--ds-muted)] p-0\"\n />\n {group.headers.slice(splitAt).map((header, i) => renderHead(header, splitAt + i))}\n </tr>\n );\n })}\n </thead>\n <tbody data-slot=\"data-table-body\">\n {showSkeleton ? skeletonRows : null}\n {showEmpty ? (\n <tr role=\"row\" data-slot=\"data-table-empty\">\n <td role=\"gridcell\" colSpan={colCount + 1} className=\"p-0\">\n {emptyState ?? (\n <div className=\"flex flex-col items-center justify-center gap-2 px-4 py-12 text-center text-[color:var(--ds-muted-foreground)]\">\n <Inbox aria-hidden className=\"size-6 text-[color:var(--ds-text-tertiary)]\" />\n <p className=\"text-[length:var(--ds-text-body-sm)]\">{emptyMessage}</p>\n </div>\n )}\n </td>\n </tr>\n ) : null}\n {virtualItems ? (\n <>\n {paddingTop > 0 ? (\n <tr aria-hidden data-slot=\"data-table-spacer\" style={{ height: paddingTop }}>\n <td colSpan={colCount + 1} className=\"p-0\" />\n </tr>\n ) : null}\n {virtualItems.map((item) => {\n const row = rows[item.index];\n return row ? renderRow(row, item.index, item.index) : null;\n })}\n {paddingBottom > 0 ? (\n <tr aria-hidden data-slot=\"data-table-spacer\" style={{ height: paddingBottom }}>\n <td colSpan={colCount + 1} className=\"p-0\" />\n </tr>\n ) : null}\n </>\n ) : (\n rows.map((row, index) => renderRow(row, index))\n )}\n </tbody>\n </table>\n {paginationMode === \"infinite\" && instance.isFetching && rows.length > 0 ? (\n <div\n data-slot=\"data-table-loading-more\"\n className=\"flex items-center justify-center gap-2 border-t border-[var(--ds-border-subtle)] py-2 text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]\"\n >\n <Spinner size={14} /> Loading more\n </div>\n ) : null}\n </div>\n {paginationMode === \"pages\" ? (\n <DataTablePagination table={table} pageSizeOptions={pageSizeOptions} />\n ) : paginationMode === \"infinite\" ? (\n <div className=\"text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]\">\n {rows.length} of {instance.total}\n </div>\n ) : null}\n </div>\n );\n}\n\nexport { DataTable, DataTableView };\n"],"mappings":"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"}
1
+ {"version":3,"file":"data-table.js","names":[],"sources":["../../../src/components/data-table/data-table.tsx"],"sourcesContent":["\"use client\";\n\nimport type { RowData } from \"@tanstack/react-table\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { Inbox } from \"lucide-react\";\nimport * as React from \"react\";\nimport { ErrorBanner } from \"@/components/error-banner\";\nimport { Input } from \"@/components/input\";\nimport { Skeleton } from \"@/components/skeleton\";\nimport { Spinner } from \"@/components/spinner\";\nimport { cn } from \"@/lib/cn\";\nimport { DataTableColumnHeader, DataTablePagination, DataTableToolbar } from \"./parts\";\nimport type {\n DataTableCell,\n DataTableColumn,\n DataTableInstance,\n DataTableRow,\n DataTableTable,\n UseDataTableOptions,\n} from \"./types\";\nimport { useDataTable } from \"./use-data-table\";\n\n/**\n * DataTable — the composite from cortenaUI&Design/04-data-tables.md.\n *\n * `<DataTable columns={columns} dataSource={{ kind: \"client\", rows }} />`\n * renders a complete table: toolbar with search, faceted filters, view and\n * export menus; sortable, pinnable, hideable, resizable columns; sticky\n * header; pager; and, past 100 rows, virtualised rows. A server source\n * changes nothing in the markup and everything in the data flow.\n *\n * Markup is a real `<table>` with `role=\"grid\"` so screen readers get rows\n * and columns, and keyboard users get one tab stop with arrow-key movement\n * between cells (roving tabindex), Enter to edit an editable cell, Escape to\n * cancel, Space to toggle selection.\n *\n * Virtualised bodies keep the table element: rows outside the viewport are\n * replaced by two spacer rows sized from the virtualizer, which keeps column\n * widths, sticky headers and pinned columns working without absolute\n * positioning.\n */\n\nconst VIRTUALIZE_ABOVE = 100;\nconst DEFAULT_VIRTUAL_HEIGHT = 600;\nconst INFINITE_THRESHOLD_PX = 240;\n\n/** What a `ref` on `DataTable` hands back. */\nexport interface DataTableHandle<Row extends RowData> {\n /**\n * Refetch the page on screen through the same query the auto-refresh\n * interval drives — never a second fetch path. A table subscribed to a\n * notification stream calls this from the stream and leaves the interval as\n * the fallback for when the stream is unavailable.\n */\n refresh: () => void;\n /** Everything else the table knows, for a caller that needs more than a refresh. */\n instance: DataTableInstance<Row>;\n}\n\nexport interface DataTableViewProps<Row extends RowData> {\n className?: string;\n /** Imperative handle: `ref.current.refresh()`. */\n ref?: React.Ref<DataTableHandle<Row>>;\n /** Height of the scrolling body. Required for virtualisation to mean anything; defaults to 600px when virtualised. */\n maxHeight?: number | string;\n /** Force virtualisation on or off; undefined virtualises above 100 rows. */\n virtualize?: boolean;\n /** Estimated row height in px for the virtualizer. @default 40 */\n rowHeight?: number;\n /** @default true */\n stickyHeader?: boolean;\n /** @default \"default\" */\n density?: \"default\" | \"compact\";\n /** Detail panel under an expanded row; enables the expander column. */\n renderSubComponent?: (row: DataTableRow<Row>) => React.ReactNode;\n /** `false` hides the toolbar; a function replaces it. */\n toolbar?: boolean | ((ctx: { instance: DataTableInstance<Row> }) => React.ReactNode);\n /** Rendered in the toolbar while rows are selected. */\n bulkActions?: (ctx: { rows: Row[]; table: DataTableTable<Row> }) => React.ReactNode;\n /** Extra toolbar controls (right side). */\n toolbarActions?: React.ReactNode;\n searchPlaceholder?: string;\n emptyState?: React.ReactNode;\n /** @default \"No results\" */\n emptyMessage?: string;\n pageSizeOptions?: number[];\n /** @default true */\n enableExport?: boolean;\n /** @default \"export\" */\n exportFileName?: string;\n /** Show the loading skeleton regardless of the source's own state. */\n loading?: boolean;\n /** Error to show above the table, in addition to the server source's. */\n error?: string | Error | null;\n onRowClick?: (row: Row, event: React.MouseEvent<HTMLTableRowElement>) => void;\n /**\n * The row a detail pane is currently showing, matched against the row id\n * (`getRowId`, the row index when that is not set). It gets `data-active`\n * and the `--ds-primary-soft` tint, independent of checkbox selection — a\n * master/detail list highlights one row without selecting it, and a table\n * with both can show a selected set and one open row at once.\n */\n activeRowId?: string | null;\n /** Extra classes per row, e.g. to dim rows the app considers stale. */\n rowClassName?: (row: Row) => string | undefined;\n}\n\nexport type DataTableProps<Row extends RowData> = DataTableViewProps<Row> &\n ({ instance: DataTableInstance<Row> } | UseDataTableOptions<Row>);\n\nfunction DataTable<Row extends RowData>(props: DataTableProps<Row>) {\n if (\"instance\" in props) {\n const { instance, ...view } = props;\n return <DataTableView instance={instance} {...view} />;\n }\n return <DataTableOwned {...props} />;\n}\n\nfunction DataTableOwned<Row extends RowData>(props: DataTableViewProps<Row> & UseDataTableOptions<Row>) {\n const {\n className,\n maxHeight,\n virtualize,\n rowHeight,\n stickyHeader,\n density,\n renderSubComponent,\n toolbar,\n bulkActions,\n toolbarActions,\n searchPlaceholder,\n emptyState,\n emptyMessage,\n pageSizeOptions,\n enableExport,\n exportFileName,\n loading,\n error,\n onRowClick,\n activeRowId,\n rowClassName,\n ref,\n ...options\n } = props;\n const instance = useDataTable<Row>({\n ...options,\n enableExpanding: options.enableExpanding ?? Boolean(renderSubComponent),\n });\n return (\n <DataTableView\n instance={instance}\n className={className}\n maxHeight={maxHeight}\n virtualize={virtualize}\n rowHeight={rowHeight}\n stickyHeader={stickyHeader}\n density={density}\n renderSubComponent={renderSubComponent}\n toolbar={toolbar}\n bulkActions={bulkActions}\n toolbarActions={toolbarActions}\n searchPlaceholder={searchPlaceholder}\n emptyState={emptyState}\n emptyMessage={emptyMessage}\n pageSizeOptions={pageSizeOptions}\n enableExport={enableExport}\n exportFileName={exportFileName}\n loading={loading}\n error={error}\n onRowClick={onRowClick}\n activeRowId={activeRowId}\n rowClassName={rowClassName}\n ref={ref}\n />\n );\n}\n\n/* ------------------------------------------------------------------------ */\n/* Pinning */\n/* ------------------------------------------------------------------------ */\n\nfunction pinAttrs<Row extends RowData>(\n column: DataTableColumn<Row, any>,\n table: DataTableTable<Row>,\n): { style: React.CSSProperties; pinned: \"start\" | \"end\" | undefined; edge: boolean } {\n const pinned = column.getIsPinned();\n if (!pinned) return { style: {}, pinned: undefined, edge: false };\n if (pinned === \"start\") {\n const last = table.getStartVisibleLeafColumns().at(-1);\n return {\n style: { position: \"sticky\", left: column.getStart(\"start\") },\n pinned,\n edge: last?.id === column.id,\n };\n }\n const first = table.getEndVisibleLeafColumns()[0];\n return {\n style: { position: \"sticky\", right: column.getAfter(\"end\") },\n pinned,\n edge: first?.id === column.id,\n };\n}\n\nconst alignClass = { start: \"text-left\", center: \"text-center\", end: \"text-right\" } as const;\n\n/* ------------------------------------------------------------------------ */\n/* Column widths and hover text (DESIGN-77) */\n/* ------------------------------------------------------------------------ */\n\n/**\n * Full text for a cell's `title`, or undefined for no title.\n *\n * Cells truncate and never wrap, so the only way back to the full text is the\n * pointer. A column that renders its own value can be read straight off the\n * value; a column with a `cell` renderer draws a node the table cannot turn\n * into a sentence, and guessing produces titles like \"[object Object]\" on a\n * row of buttons. Such a column says what its hover text is with\n * `meta.hoverText`, or has none.\n */\nfunction hoverTextFor<Row extends RowData>(cell: DataTableCell<Row, any>): string | undefined {\n const def = cell.column.columnDef;\n const meta = def.meta;\n if (meta?.hoverText) return meta.hoverText(cell.row.original) || undefined;\n if (!meta?.plainCell) return undefined;\n const value = cell.getValue();\n // `|| undefined` on the way out too: an empty string is a `title=\"\"`, which\n // is a pointer that stops on an empty tooltip rather than no tooltip.\n return typeof value === \"string\" || typeof value === \"number\"\n ? String(value) || undefined\n : undefined;\n}\n\n/* ------------------------------------------------------------------------ */\n/* Editable cell */\n/* ------------------------------------------------------------------------ */\n\nfunction EditableCell<Row extends RowData>({\n cell,\n instance,\n onDone,\n}: {\n cell: DataTableCell<Row, any>;\n instance: DataTableInstance<Row>;\n onDone: () => void;\n}) {\n const editable = cell.column.columnDef.meta?.editable;\n const config = typeof editable === \"object\" ? editable : {};\n const initial = cell.getValue();\n const [draft, setDraft] = React.useState(initial == null ? \"\" : String(initial));\n const done = React.useRef(false);\n\n const finish = () => {\n done.current = true;\n instance.editing.stop();\n onDone();\n };\n const commit = () => {\n if (done.current) return;\n const value = config.parse ? config.parse(draft) : config.type === \"number\" ? Number(draft) : draft;\n void instance.editing.commit(cell.row, cell.column.id, value);\n finish();\n };\n const cancel = () => {\n if (done.current) return;\n finish();\n };\n\n return (\n <Input\n data-slot=\"data-table-editor\"\n autoFocus\n type={config.type === \"number\" ? \"number\" : \"text\"}\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n onFocus={(e) => e.target.select()}\n onBlur={commit}\n onKeyDown={(e) => {\n // The editor owns its keys; the grid must not move the focus cell.\n e.stopPropagation();\n if (e.key === \"Enter\") {\n e.preventDefault();\n commit();\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n cancel();\n }\n }}\n className=\"h-7 px-2 py-0 text-[length:var(--ds-text-body-sm)]\"\n />\n );\n}\n\n/* ------------------------------------------------------------------------ */\n/* View */\n/* ------------------------------------------------------------------------ */\n\ntype CellCoord = { r: number; c: number };\n\nfunction DataTableView<Row extends RowData>({\n instance,\n className,\n maxHeight,\n virtualize,\n rowHeight = 40,\n stickyHeader = true,\n density = \"default\",\n renderSubComponent,\n toolbar = true,\n bulkActions,\n toolbarActions,\n searchPlaceholder,\n emptyState,\n emptyMessage = \"No results\",\n pageSizeOptions,\n enableExport = true,\n exportFileName = \"export\",\n loading,\n error,\n onRowClick,\n activeRowId,\n rowClassName,\n ref,\n}: DataTableViewProps<Row> & { instance: DataTableInstance<Row> }) {\n const { table, paginationMode } = instance;\n const rows = table.getRowModel().rows;\n const leafColumns = table.getVisibleLeafColumns();\n // The one column drawn with no fixed width, so the space the sized columns\n // leave lands on it instead of on the filler cell (DESIGN-77). First visible\n // one wins; hide it and the table falls back to the old filler behaviour.\n // A pinned column is skipped: pinning positions it with a `left`/`right`\n // offset read off the widths of the columns beside it, and a column with no\n // width of its own would leave every pinned neighbour at the wrong offset.\n const fillColumnId = leafColumns.find(\n (column) => column.columnDef.meta?.fill && !column.getIsPinned(),\n )?.id;\n const widthFor = (column: DataTableColumn<Row, any>, size: number) =>\n column.id === fillColumnId ? undefined : size;\n // A width the fill column would otherwise have to share the remainder with.\n const fillerStyle = fillColumnId ? ({ width: 0 } as const) : undefined;\n const colCount = leafColumns.length;\n const headerGroups = table.getHeaderGroups();\n const isVirtual = virtualize ?? rows.length > VIRTUALIZE_ABOVE;\n const isLoading = loading || instance.isLoading;\n const showSkeleton = isLoading && rows.length === 0;\n const showEmpty = !isLoading && rows.length === 0;\n const resolvedHeight = maxHeight ?? (isVirtual ? DEFAULT_VIRTUAL_HEIGHT : undefined);\n const errorMessage = error instanceof Error ? error.message : (error ?? instance.error?.message ?? \"\");\n\n const scrollRef = React.useRef<HTMLDivElement>(null);\n const tableRef = React.useRef<HTMLTableElement>(null);\n\n const refresh = instance.refetch;\n React.useImperativeHandle(ref, () => ({ refresh, instance }), [refresh, instance]);\n\n // Mandatory on every server-backed table (P-39): a Refresh button beside the\n // search box and a stamp saying which moment these rows are. The switch only\n // appears where the table asked for `autoRefresh`.\n const refreshProps =\n instance.dataSource.kind === \"server\"\n ? {\n onRefresh: refresh,\n isFetching: instance.isFetching,\n lastUpdatedAt: instance.lastUpdatedAt,\n lastErrorAt: instance.lastErrorAt,\n autoRefresh: instance.autoRefresh,\n }\n : undefined;\n\n /* Virtualisation ------------------------------------------------------ */\n\n const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({\n count: rows.length,\n enabled: isVirtual,\n getScrollElement: () => scrollRef.current,\n estimateSize: () => rowHeight,\n overscan: 8,\n getItemKey: (index) => rows[index]?.id ?? index,\n // An expanded row and its detail row are one virtual item.\n measureElement: (element) => {\n let height = element.getBoundingClientRect().height;\n const next = element.nextElementSibling as HTMLElement | null;\n if (next?.dataset.slot === \"data-table-subrow\") height += next.getBoundingClientRect().height;\n return height;\n },\n });\n const virtualItems = isVirtual ? virtualizer.getVirtualItems() : null;\n const paddingTop = virtualItems?.length ? virtualItems[0]!.start : 0;\n const paddingBottom = virtualItems?.length\n ? virtualizer.getTotalSize() - virtualItems[virtualItems.length - 1]!.end\n : 0;\n\n /* Infinite scroll ----------------------------------------------------- */\n\n const onScroll = React.useCallback(\n (event: React.UIEvent<HTMLDivElement>) => {\n if (paginationMode !== \"infinite\" || !instance.hasNextPage || instance.isFetching) return;\n const el = event.currentTarget;\n if (el.scrollHeight - el.scrollTop - el.clientHeight < INFINITE_THRESHOLD_PX) instance.fetchNextPage();\n },\n [paginationMode, instance],\n );\n\n /* Keyboard navigation ------------------------------------------------- */\n\n const [focused, setFocused] = React.useState<CellCoord>({ r: -1, c: 0 });\n const pendingFocus = React.useRef<CellCoord | null>(null);\n\n const cellElement = (coord: CellCoord) =>\n tableRef.current?.querySelector<HTMLElement>(\n `[data-row-index=\"${coord.r}\"][data-col-index=\"${coord.c}\"]`,\n ) ?? null;\n\n const focusCell = React.useCallback(\n (coord: CellCoord) => {\n const el = cellElement(coord);\n if (el) {\n setFocused(coord);\n el.focus();\n return;\n }\n if (isVirtual && coord.r >= 0) {\n pendingFocus.current = coord;\n setFocused(coord);\n virtualizer.scrollToIndex(coord.r, { align: \"auto\" });\n }\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [isVirtual, virtualizer],\n );\n\n React.useEffect(() => {\n const coord = pendingFocus.current;\n if (!coord) return;\n const el = cellElement(coord);\n if (el) {\n pendingFocus.current = null;\n el.focus();\n }\n });\n\n const onKeyDown = (event: React.KeyboardEvent<HTMLTableElement>) => {\n const target = event.target as HTMLElement;\n // Portaled menus bubble through React but live outside the table element.\n if (!tableRef.current?.contains(target)) return;\n if (instance.editing.cell) return;\n const cell = target.closest<HTMLElement>(\"[data-row-index][data-col-index]\");\n if (!cell) return;\n const r = Number(cell.dataset.rowIndex);\n const c = Number(cell.dataset.colIndex);\n const lastRow = rows.length - 1;\n const lastCol = colCount - 1;\n const onCellItself = target === cell;\n\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n focusCell({ r: Math.min(lastRow, r + 1), c });\n return;\n case \"ArrowUp\":\n event.preventDefault();\n focusCell({ r: Math.max(-1, r - 1), c });\n return;\n case \"ArrowRight\":\n event.preventDefault();\n focusCell({ r, c: Math.min(lastCol, c + 1) });\n return;\n case \"ArrowLeft\":\n event.preventDefault();\n focusCell({ r, c: Math.max(0, c - 1) });\n return;\n case \"Home\":\n event.preventDefault();\n focusCell(event.ctrlKey ? { r: -1, c: 0 } : { r, c: 0 });\n return;\n case \"End\":\n event.preventDefault();\n focusCell(event.ctrlKey ? { r: lastRow, c: lastCol } : { r, c: lastCol });\n return;\n case \"PageDown\":\n event.preventDefault();\n focusCell({ r: Math.min(lastRow, r + 10), c });\n return;\n case \"PageUp\":\n event.preventDefault();\n focusCell({ r: Math.max(-1, r - 10), c });\n return;\n case \"Enter\": {\n if (!onCellItself) return;\n if (r === -1) {\n const column = leafColumns[c];\n if (column?.getCanSort()) {\n event.preventDefault();\n column.toggleSorting(undefined, event.shiftKey);\n }\n return;\n }\n const row = rows[r];\n const column = leafColumns[c];\n if (row && column && column.columnDef.meta?.editable) {\n event.preventDefault();\n instance.editing.start({ rowId: row.id, columnId: column.id });\n }\n return;\n }\n case \" \": {\n if (!onCellItself || r === -1) return;\n const row = rows[r];\n if (row?.getCanSelect()) {\n event.preventDefault();\n row.toggleSelected();\n }\n return;\n }\n default:\n return;\n }\n };\n\n const onFocus = (event: React.FocusEvent<HTMLTableElement>) => {\n const cell = (event.target as HTMLElement).closest<HTMLElement>(\"[data-row-index][data-col-index]\");\n if (!cell || !tableRef.current?.contains(cell)) return;\n const r = Number(cell.dataset.rowIndex);\n const c = Number(cell.dataset.colIndex);\n setFocused((f) => (f.r === r && f.c === c ? f : { r, c }));\n };\n\n const tabIndexFor = (r: number, c: number) => (focused.r === r && focused.c === c ? 0 : -1);\n\n /* Rendering ----------------------------------------------------------- */\n\n const cellPadding = density === \"compact\" ? \"px-3 py-1.5\" : \"px-3 py-2.5\";\n const headPadding = density === \"compact\" ? \"px-3 py-1.5\" : \"px-3 py-2\";\n const editingCell = instance.editing.cell;\n\n const renderRow = (row: DataTableRow<Row>, index: number, virtualIndex?: number) => {\n const cells = row.getVisibleCells();\n const endStart = cells.findIndex((cell) => cell.column.getIsPinned() === \"end\");\n const splitAt = endStart === -1 ? cells.length : endStart;\n const selected = row.getIsSelected();\n const active = activeRowId != null && row.id === activeRowId;\n const expanded = renderSubComponent && row.getIsExpanded();\n\n const renderCell = (cell: DataTableCell<Row, any>, c: number) => {\n const column = cell.column;\n const meta = column.columnDef.meta;\n const pin = pinAttrs(column, table);\n const isEditing = editingCell?.rowId === row.id && editingCell.columnId === column.id;\n const canEdit = Boolean(meta?.editable);\n return (\n <td\n key={cell.id}\n role=\"gridcell\"\n data-slot=\"data-table-cell\"\n data-row-index={index}\n data-col-index={c}\n data-pinned={pin.pinned}\n data-pinned-edge={pin.edge ? \"\" : undefined}\n data-editable={canEdit ? \"\" : undefined}\n data-editing={isEditing ? \"\" : undefined}\n aria-colindex={c + 1}\n tabIndex={tabIndexFor(index, c)}\n title={isEditing ? undefined : hoverTextFor(cell)}\n style={{ width: widthFor(column, column.getSize()), ...pin.style }}\n onDoubleClick={\n canEdit && !isEditing\n ? () => instance.editing.start({ rowId: row.id, columnId: column.id })\n : undefined\n }\n className={cn(\n \"relative align-middle outline-none\",\n isEditing ? \"px-1.5 py-1\" : cellPadding,\n alignClass[meta?.align ?? \"start\"],\n \"focus-visible:ring-[2px] focus-visible:ring-inset focus-visible:ring-[var(--ds-ring)]\",\n // Pinned cells cover what scrolls under them, so their background is\n // opaque card with the translucent hover / selected tint layered on.\n pin.pinned &&\n \"z-[1] bg-[var(--ds-background)] group-hover/row:bg-[linear-gradient(var(--ds-hover),var(--ds-hover))] group-data-[selected]/row:bg-[linear-gradient(var(--ds-primary-soft),var(--ds-primary-soft))] group-data-[active]/row:bg-[linear-gradient(var(--ds-primary-soft),var(--ds-primary-soft))]\",\n pin.pinned === \"start\" && pin.edge && \"shadow-[inset_-1px_0_0_var(--ds-border)]\",\n pin.pinned === \"end\" && pin.edge && \"shadow-[inset_1px_0_0_var(--ds-border)]\",\n !isEditing && \"truncate\",\n meta?.cellClassName,\n )}\n >\n {isEditing ? (\n <EditableCell\n cell={cell}\n instance={instance}\n onDone={() => {\n pendingFocus.current = { r: index, c };\n }}\n />\n ) : (\n <table.FlexRender cell={cell} />\n )}\n </td>\n );\n };\n\n return (\n <React.Fragment key={row.id}>\n <tr\n role=\"row\"\n data-slot=\"data-table-row\"\n data-index={virtualIndex}\n data-selected={selected ? \"\" : undefined}\n data-active={active ? \"\" : undefined}\n data-expanded={expanded ? \"\" : undefined}\n aria-rowindex={index + 2}\n aria-selected={table.options.enableRowSelection ? selected : undefined}\n ref={isVirtual ? virtualizer.measureElement : undefined}\n onClick={onRowClick ? (e) => onRowClick(row.original, e) : undefined}\n className={cn(\n \"group/row border-b border-[var(--ds-border-subtle)] transition-colors duration-[var(--ds-duration-fast)]\",\n \"hover:bg-[var(--ds-hover)] data-[selected]:bg-[var(--ds-primary-soft)]\",\n // Independent of selection: a detail pane highlights one row\n // without ticking its checkbox.\n \"data-[active]:bg-[var(--ds-primary-soft)]\",\n onRowClick && \"cursor-pointer\",\n rowClassName?.(row.original),\n )}\n >\n {cells.slice(0, splitAt).map((cell, c) => renderCell(cell, c))}\n <td aria-hidden data-slot=\"data-table-filler\" style={fillerStyle} className=\"p-0\" />\n {cells.slice(splitAt).map((cell, i) => renderCell(cell, splitAt + i))}\n </tr>\n {expanded ? (\n <tr role=\"row\" data-slot=\"data-table-subrow\" className=\"border-b border-[var(--ds-border-subtle)]\">\n <td\n role=\"gridcell\"\n colSpan={colCount + 1}\n className=\"bg-[var(--ds-muted)] px-4 py-3 text-[length:var(--ds-text-body-sm)]\"\n >\n {renderSubComponent(row)}\n </td>\n </tr>\n ) : null}\n </React.Fragment>\n );\n };\n\n const skeletonRows = Array.from({ length: Math.min(8, table.state.pagination.pageSize) }, (_, i) => (\n <tr key={`skeleton-${i}`} aria-hidden data-slot=\"data-table-skeleton-row\" className=\"border-b border-[var(--ds-border-subtle)]\">\n {leafColumns.map((column) => (\n <td key={column.id} className={cellPadding} style={{ width: widthFor(column, column.getSize()) }}>\n <Skeleton className=\"h-3.5 w-3/4\" />\n </td>\n ))}\n <td style={fillerStyle} className=\"p-0\" />\n </tr>\n ));\n\n return (\n <div data-slot=\"data-table\" className={cn(\"flex min-w-0 flex-col gap-3\", className)}>\n {errorMessage ? (\n <ErrorBanner message={errorMessage} onRetry={instance.error ? instance.refetch : undefined} />\n ) : null}\n {toolbar === false ? null : typeof toolbar === \"function\" ? (\n toolbar({ instance })\n ) : (\n <DataTableToolbar\n table={table}\n bulkActions={bulkActions}\n searchPlaceholder={searchPlaceholder}\n enableExport={enableExport}\n exportFileName={exportFileName}\n refresh={refreshProps}\n >\n {toolbarActions}\n </DataTableToolbar>\n )}\n <div\n ref={scrollRef}\n data-slot=\"data-table-container\"\n onScroll={onScroll}\n style={{ maxHeight: resolvedHeight }}\n /*\n * The page background, not the card tint (DESIGN-140).\n *\n * `--ds-card` made a table read as a grey slab, and a table placed\n * inside a `SectionCard` — which is itself the card tint — was\n * card-on-card: one flat block with nothing separating the rows from\n * the panel around them. The border is what separates a table from\n * what it sits on, and it is already here.\n *\n * Correct in both themes because the tokens invert together: light\n * gets white rows inside a grey panel, dark gets the canvas with the\n * border carrying the edge. The frozen-column cell below has to match\n * this, or a pinned column shows as a stripe of a different colour.\n */\n className={cn(\n \"relative w-full overflow-auto rounded-[var(--ds-radius-md)] border border-[var(--ds-border)] bg-[var(--ds-background)]\",\n )}\n >\n <table\n ref={tableRef}\n role=\"grid\"\n data-slot=\"data-table-grid\"\n data-virtualized={isVirtual ? \"\" : undefined}\n aria-rowcount={rows.length + 1}\n aria-colcount={colCount}\n aria-busy={instance.isFetching || undefined}\n aria-multiselectable={table.options.enableRowSelection ? true : undefined}\n onKeyDown={onKeyDown}\n onFocus={onFocus}\n style={{ minWidth: table.getTotalSize() }}\n className={cn(\n \"w-full table-fixed border-collapse caption-bottom\",\n \"text-[length:var(--ds-text-body-sm)] leading-[var(--ds-text-body-sm--line-height)] text-[color:var(--ds-foreground)]\",\n )}\n >\n <thead data-slot=\"data-table-header\" className={cn(stickyHeader && \"sticky top-0 z-[2]\")}>\n {headerGroups.map((group) => {\n const endStart = group.headers.findIndex((h) => h.column.getIsPinned() === \"end\");\n const splitAt = endStart === -1 ? group.headers.length : endStart;\n const renderHead = (header: (typeof group.headers)[number], c: number) => {\n const column = header.column;\n const meta = column.columnDef.meta;\n const pin = pinAttrs(column, table);\n const sorted = column.getIsSorted();\n const def = column.columnDef;\n return (\n <th\n key={header.id}\n role=\"columnheader\"\n scope=\"col\"\n data-slot=\"data-table-head\"\n data-row-index={-1}\n data-col-index={c}\n data-pinned={pin.pinned}\n data-pinned-edge={pin.edge ? \"\" : undefined}\n aria-colindex={c + 1}\n aria-sort={\n sorted === \"asc\"\n ? \"ascending\"\n : sorted === \"desc\"\n ? \"descending\"\n : column.getCanSort()\n ? \"none\"\n : undefined\n }\n colSpan={header.colSpan}\n tabIndex={tabIndexFor(-1, c)}\n // Only where the column says so. A header is one short\n // phrase the consumer chose, so repeating it in a tooltip\n // on every `<th>` is noise, and it was wrong outright on\n // the select and expand columns, whose header is a node\n // with no sentence in it. `meta.label` is the column\n // saying what it is called; when it is set, `title` on the\n // `<th>` reaches the button inside it too, since a pointer\n // over a node with no title of its own takes its\n // ancestor's. Body cells keep their own hover text.\n title={meta?.label}\n style={{ width: widthFor(column, header.getSize()), ...pin.style }}\n className={cn(\n \"group/head relative h-10 align-middle font-medium outline-none\",\n headPadding,\n \"bg-[var(--ds-muted)] text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]\",\n \"border-b border-[var(--ds-border)]\",\n alignClass[meta?.align ?? \"start\"],\n \"focus-visible:ring-[2px] focus-visible:ring-inset focus-visible:ring-[var(--ds-ring)]\",\n pin.pinned && \"z-[3]\",\n pin.pinned === \"start\" && pin.edge && \"shadow-[inset_-1px_0_0_var(--ds-border)]\",\n pin.pinned === \"end\" && pin.edge && \"shadow-[inset_1px_0_0_var(--ds-border)]\",\n meta?.headerClassName,\n )}\n >\n {header.isPlaceholder ? null : typeof def.header === \"string\" ? (\n <DataTableColumnHeader column={column} title={def.header} />\n ) : (\n <table.FlexRender header={header} />\n )}\n {column.getCanResize() ? (\n <div\n data-slot=\"data-table-resizer\"\n data-resizing={column.getIsResizing() ? \"\" : undefined}\n onMouseDown={header.getResizeHandler()}\n onTouchStart={header.getResizeHandler()}\n onDoubleClick={() => column.resetSize()}\n className={cn(\n \"absolute right-0 top-0 h-full w-1 cursor-col-resize touch-none select-none\",\n \"opacity-0 transition-opacity duration-[var(--ds-duration-fast)] hover:opacity-100 data-[resizing]:opacity-100\",\n \"bg-[var(--ds-ring)]\",\n )}\n />\n ) : null}\n </th>\n );\n };\n return (\n <tr key={group.id} role=\"row\" aria-rowindex={1}>\n {group.headers.slice(0, splitAt).map((header, c) => renderHead(header, c))}\n <th\n aria-hidden\n data-slot=\"data-table-filler-head\"\n style={fillerStyle}\n className=\"border-b border-[var(--ds-border)] bg-[var(--ds-muted)] p-0\"\n />\n {group.headers.slice(splitAt).map((header, i) => renderHead(header, splitAt + i))}\n </tr>\n );\n })}\n </thead>\n <tbody data-slot=\"data-table-body\">\n {showSkeleton ? skeletonRows : null}\n {showEmpty ? (\n <tr role=\"row\" data-slot=\"data-table-empty\">\n <td role=\"gridcell\" colSpan={colCount + 1} className=\"p-0\">\n {emptyState ?? (\n <div className=\"flex flex-col items-center justify-center gap-2 px-4 py-12 text-center text-[color:var(--ds-muted-foreground)]\">\n <Inbox aria-hidden className=\"size-6 text-[color:var(--ds-text-tertiary)]\" />\n <p className=\"text-[length:var(--ds-text-body-sm)]\">{emptyMessage}</p>\n </div>\n )}\n </td>\n </tr>\n ) : null}\n {virtualItems ? (\n <>\n {paddingTop > 0 ? (\n <tr aria-hidden data-slot=\"data-table-spacer\" style={{ height: paddingTop }}>\n <td colSpan={colCount + 1} className=\"p-0\" />\n </tr>\n ) : null}\n {virtualItems.map((item) => {\n const row = rows[item.index];\n return row ? renderRow(row, item.index, item.index) : null;\n })}\n {paddingBottom > 0 ? (\n <tr aria-hidden data-slot=\"data-table-spacer\" style={{ height: paddingBottom }}>\n <td colSpan={colCount + 1} className=\"p-0\" />\n </tr>\n ) : null}\n </>\n ) : (\n rows.map((row, index) => renderRow(row, index))\n )}\n </tbody>\n </table>\n {paginationMode === \"infinite\" && instance.isFetching && rows.length > 0 ? (\n <div\n data-slot=\"data-table-loading-more\"\n className=\"flex items-center justify-center gap-2 border-t border-[var(--ds-border-subtle)] py-2 text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]\"\n >\n <Spinner size={14} /> Loading more\n </div>\n ) : null}\n </div>\n {paginationMode === \"pages\" ? (\n <DataTablePagination table={table} pageSizeOptions={pageSizeOptions} />\n ) : paginationMode === \"infinite\" ? (\n <div className=\"text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]\">\n {rows.length} of {instance.total}\n </div>\n ) : null}\n </div>\n );\n}\n\nexport { DataTable, DataTableView };\n"],"mappings":"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"}
@@ -21,7 +21,7 @@ import { Toast } from "@base-ui/react/toast";
21
21
  * `toastManager` to `Toaster` when you need to fire toasts outside React.
22
22
  */
23
23
  declare const toastVariants: (props?: ({
24
- variant?: "default" | "success" | "destructive" | "warning" | null | undefined;
24
+ variant?: "default" | "success" | "warning" | "destructive" | null | undefined;
25
25
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
26
26
  /** The Base UI toast manager: `{ toasts, add, close, update, promise }`. */
27
27
  declare const useToast: typeof Toast.useToastManager;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cortena-ui",
3
- "version": "1.16.1",
4
- "description": "Cortena UI \u2014 the shared React component library for cortenaweb and every Cortena extension. shadcn over Base UI, styled entirely from cortena-design tokens.",
3
+ "version": "1.17.0",
4
+ "description": "Cortena UI — the shared React component library for cortenaweb and every Cortena extension. shadcn over Base UI, styled entirely from cortena-design tokens.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
7
7
  "type": "git",
@@ -1049,8 +1049,32 @@ function AppShell({
1049
1049
  </aside>
1050
1050
  ) : null}
1051
1051
 
1052
+ {/*
1053
+ * The content measure (DESIGN-140).
1054
+ *
1055
+ * This was `min-w-0 flex-1` and nothing else, so a page grew to
1056
+ * whatever the window was. On a wide monitor that is not generous, it
1057
+ * is unreadable: a DataTable's fill column took the slack, so the
1058
+ * first cell sat on the far left, every other column was pushed to
1059
+ * the far right, and one row had to be read corner to corner.
1060
+ *
1061
+ * `max-width` alone would have been the other half of the same bug —
1062
+ * the skill's own example is content pinned to the left edge of a
1063
+ * wide screen — so the margin is here with it and neither is written
1064
+ * without the other.
1065
+ *
1066
+ * The cap is on an inner element rather than on `main`, which keeps
1067
+ * `flex-1` doing its job: `main` still fills the row beside the
1068
+ * sidebar, so a page that wants the full width for a canvas or a map
1069
+ * can still reach it, and only this measure is constrained.
1070
+ */}
1052
1071
  <main data-slot="app-shell-main" className="min-w-0 flex-1">
1053
- {children}
1072
+ <div
1073
+ data-slot="app-shell-content"
1074
+ className="mx-auto w-full max-w-[var(--ds-layout-content)]"
1075
+ >
1076
+ {children}
1077
+ </div>
1054
1078
  </main>
1055
1079
  </div>
1056
1080
 
@@ -574,7 +574,7 @@ function DataTableView<Row extends RowData>({
574
574
  // Pinned cells cover what scrolls under them, so their background is
575
575
  // opaque card with the translucent hover / selected tint layered on.
576
576
  pin.pinned &&
577
- "z-[1] bg-[var(--ds-card)] group-hover/row:bg-[linear-gradient(var(--ds-hover),var(--ds-hover))] group-data-[selected]/row:bg-[linear-gradient(var(--ds-primary-soft),var(--ds-primary-soft))] group-data-[active]/row:bg-[linear-gradient(var(--ds-primary-soft),var(--ds-primary-soft))]",
577
+ "z-[1] bg-[var(--ds-background)] group-hover/row:bg-[linear-gradient(var(--ds-hover),var(--ds-hover))] group-data-[selected]/row:bg-[linear-gradient(var(--ds-primary-soft),var(--ds-primary-soft))] group-data-[active]/row:bg-[linear-gradient(var(--ds-primary-soft),var(--ds-primary-soft))]",
578
578
  pin.pinned === "start" && pin.edge && "shadow-[inset_-1px_0_0_var(--ds-border)]",
579
579
  pin.pinned === "end" && pin.edge && "shadow-[inset_1px_0_0_var(--ds-border)]",
580
580
  !isEditing && "truncate",
@@ -673,8 +673,22 @@ function DataTableView<Row extends RowData>({
673
673
  data-slot="data-table-container"
674
674
  onScroll={onScroll}
675
675
  style={{ maxHeight: resolvedHeight }}
676
+ /*
677
+ * The page background, not the card tint (DESIGN-140).
678
+ *
679
+ * `--ds-card` made a table read as a grey slab, and a table placed
680
+ * inside a `SectionCard` — which is itself the card tint — was
681
+ * card-on-card: one flat block with nothing separating the rows from
682
+ * the panel around them. The border is what separates a table from
683
+ * what it sits on, and it is already here.
684
+ *
685
+ * Correct in both themes because the tokens invert together: light
686
+ * gets white rows inside a grey panel, dark gets the canvas with the
687
+ * border carrying the edge. The frozen-column cell below has to match
688
+ * this, or a pinned column shows as a stripe of a different colour.
689
+ */
676
690
  className={cn(
677
- "relative w-full overflow-auto rounded-[var(--ds-radius-md)] border border-[var(--ds-border)] bg-[var(--ds-card)]",
691
+ "relative w-full overflow-auto rounded-[var(--ds-radius-md)] border border-[var(--ds-border)] bg-[var(--ds-background)]",
678
692
  )}
679
693
  >
680
694
  <table