cortena-ui 1.4.2 → 1.6.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.
- package/CHANGELOG.md +107 -0
- package/LICENSE +7 -0
- package/README.md +235 -3
- package/dist/a2ui/views.js +2 -2
- package/dist/agent-chat/a2ui-block.d.ts +60 -0
- package/dist/agent-chat/a2ui-block.js +69 -0
- package/dist/agent-chat/a2ui-block.js.map +1 -0
- package/dist/agent-chat/agui-client.d.ts +40 -0
- package/dist/agent-chat/agui-client.js +251 -0
- package/dist/agent-chat/agui-client.js.map +1 -0
- package/dist/agent-chat/bridge.d.ts +109 -0
- package/dist/agent-chat/bridge.js +353 -0
- package/dist/agent-chat/bridge.js.map +1 -0
- package/dist/agent-chat/session.d.ts +79 -0
- package/dist/agent-chat/session.js +391 -0
- package/dist/agent-chat/session.js.map +1 -0
- package/dist/agent-chat/step-label.d.ts +99 -0
- package/dist/agent-chat/step-label.js +116 -0
- package/dist/agent-chat/step-label.js.map +1 -0
- package/dist/agent-chat/store.d.ts +102 -0
- package/dist/agent-chat/store.js +876 -0
- package/dist/agent-chat/store.js.map +1 -0
- package/dist/agent-chat/types.d.ts +277 -0
- package/dist/agent-chat/types.js +17 -0
- package/dist/agent-chat/types.js.map +1 -0
- package/dist/agent-chat.d.ts +11 -0
- package/dist/agent-chat.js +11 -0
- package/dist/components/admin-permissions/admin-permissions.d.ts +66 -0
- package/dist/components/admin-permissions/admin-permissions.js +101 -0
- package/dist/components/admin-permissions/admin-permissions.js.map +1 -0
- package/dist/components/admin-permissions/context.d.ts +70 -0
- package/dist/components/admin-permissions/context.js +258 -0
- package/dist/components/admin-permissions/context.js.map +1 -0
- package/dist/components/admin-permissions/index.d.ts +10 -0
- package/dist/components/admin-permissions/licence.d.ts +15 -0
- package/dist/components/admin-permissions/licence.js +78 -0
- package/dist/components/admin-permissions/licence.js.map +1 -0
- package/dist/components/admin-permissions/matrix.d.ts +20 -0
- package/dist/components/admin-permissions/matrix.js +191 -0
- package/dist/components/admin-permissions/matrix.js.map +1 -0
- package/dist/components/admin-permissions/members.d.ts +18 -0
- package/dist/components/admin-permissions/members.js +185 -0
- package/dist/components/admin-permissions/members.js.map +1 -0
- package/dist/components/admin-permissions/role-assignment.d.ts +35 -0
- package/dist/components/admin-permissions/role-assignment.js +174 -0
- package/dist/components/admin-permissions/role-assignment.js.map +1 -0
- package/dist/components/admin-permissions/roles.d.ts +25 -0
- package/dist/components/admin-permissions/roles.js +168 -0
- package/dist/components/admin-permissions/roles.js.map +1 -0
- package/dist/components/admin-permissions/types.d.ts +152 -0
- package/dist/components/admin-permissions/types.js +63 -0
- package/dist/components/admin-permissions/types.js.map +1 -0
- package/dist/components/agent-chat-popup.d.ts +29 -0
- package/dist/components/agent-chat-popup.js +188 -0
- package/dist/components/agent-chat-popup.js.map +1 -0
- package/dist/components/agent-chat.d.ts +163 -0
- package/dist/components/agent-chat.js +673 -0
- package/dist/components/agent-chat.js.map +1 -0
- package/dist/components/app-shell.d.ts +126 -0
- package/dist/components/app-shell.js +297 -0
- package/dist/components/app-shell.js.map +1 -0
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button-link.js +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/checkbox.d.ts +1 -1
- package/dist/components/combobox.d.ts +1 -1
- package/dist/components/combobox.js +1 -1
- package/dist/components/consent-screen.d.ts +65 -0
- package/dist/components/consent-screen.js +123 -0
- package/dist/components/consent-screen.js.map +1 -0
- package/dist/components/data-table/data-table.d.ts +15 -1
- package/dist/components/data-table/data-table.js +18 -4
- package/dist/components/data-table/data-table.js.map +1 -1
- package/dist/components/data-table/index.d.ts +4 -4
- package/dist/components/data-table/parts.d.ts +27 -3
- package/dist/components/data-table/parts.js +175 -55
- package/dist/components/data-table/parts.js.map +1 -1
- package/dist/components/data-table/types.d.ts +61 -0
- package/dist/components/data-table/use-data-table.js +91 -6
- package/dist/components/data-table/use-data-table.js.map +1 -1
- package/dist/components/data-table/use-server-source.js +119 -28
- package/dist/components/data-table/use-server-source.js.map +1 -1
- package/dist/components/help-panel.d.ts +131 -0
- package/dist/components/help-panel.js +545 -0
- package/dist/components/help-panel.js.map +1 -0
- package/dist/components/login-screen.d.ts +127 -0
- package/dist/components/login-screen.js +339 -0
- package/dist/components/login-screen.js.map +1 -0
- package/dist/components/session-guard.d.ts +268 -0
- package/dist/components/session-guard.js +632 -0
- package/dist/components/session-guard.js.map +1 -0
- package/dist/components/toast.d.ts +1 -1
- package/dist/core.d.ts +5 -1
- package/dist/core.js +11 -7
- package/dist/data-table.d.ts +13 -4
- package/dist/data-table.js +10 -2
- package/dist/hooks/use-cortena-theme.js +49 -3
- package/dist/hooks/use-cortena-theme.js.map +1 -1
- package/dist/index.d.ts +17 -4
- package/dist/index.js +21 -8
- package/dist/markdown.d.ts +2 -1
- package/dist/markdown.js +2 -1
- package/package.json +18 -5
- package/src/agent-chat/a2ui-block.ts +118 -0
- package/src/agent-chat/agui-client.ts +405 -0
- package/src/agent-chat/bridge.ts +445 -0
- package/src/agent-chat/session.ts +549 -0
- package/src/agent-chat/step-label.ts +177 -0
- package/src/agent-chat/store.ts +1234 -0
- package/src/agent-chat/types.ts +308 -0
- package/src/components/admin-permissions/admin-permissions.tsx +130 -0
- package/src/components/admin-permissions/context.tsx +376 -0
- package/src/components/admin-permissions/index.tsx +32 -0
- package/src/components/admin-permissions/licence.tsx +84 -0
- package/src/components/admin-permissions/matrix.tsx +257 -0
- package/src/components/admin-permissions/members.tsx +204 -0
- package/src/components/admin-permissions/role-assignment.tsx +239 -0
- package/src/components/admin-permissions/roles.tsx +169 -0
- package/src/components/admin-permissions/types.ts +231 -0
- package/src/components/agent-chat-popup.tsx +289 -0
- package/src/components/agent-chat.tsx +1006 -0
- package/src/components/app-shell.tsx +502 -0
- package/src/components/consent-screen.tsx +239 -0
- package/src/components/data-table/data-table.tsx +36 -0
- package/src/components/data-table/index.tsx +6 -1
- package/src/components/data-table/parts.tsx +223 -47
- package/src/components/data-table/types.ts +68 -0
- package/src/components/data-table/use-data-table.ts +152 -4
- package/src/components/data-table/use-server-source.ts +150 -12
- package/src/components/help-panel.tsx +765 -0
- package/src/components/login-screen.tsx +479 -0
- package/src/components/session-guard.tsx +1071 -0
- package/src/entries/agent-chat.ts +137 -0
- package/src/entries/core.ts +8 -0
- package/src/entries/data-table.ts +41 -0
- package/src/entries/markdown.ts +25 -0
- package/src/hooks/use-cortena-theme.ts +63 -4
- package/src/index.ts +6 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-chat.js","names":[],"sources":["../../src/components/agent-chat.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * AgentChat — the chat surface an extension's own agent runs in.\n *\n * §17 of how-to-create-a-cortena-extension, audit rule P-25. Moved here from\n * `cortenaweb/components/chat/**` so there is one chat and not two: a cut-down\n * widget diverges on approvals, A2UI and MCP Apps within one release, and every\n * fix then has to be made twice. cortenaweb adopts this component as a\n * follow-up; until then the two are kept in step by hand.\n *\n * Everything comes from the `AgentChatClient` prop — nothing here knows about\n * HTTP, AG-UI or a store — which is what lets `test/agent-chat.test.tsx` drive\n * the whole surface from a fake that replays scripted event sequences.\n *\n * Not in this package, on purpose: the MCP App frame. Rendering an extension's\n * own screen means a sandboxed iframe, a `srcdoc` CSP, a JSON-RPC postMessage\n * bridge and a `tools/invoke` proxy holding a credential — a host concern, not\n * a component one. `renderToolCall` is the seam: return a node for a tool call\n * and it replaces the card, return nothing and the default card is drawn.\n */\n\nimport {\n Check,\n ChevronDown,\n ChevronRight,\n MessageSquarePlus,\n Send,\n ShieldCheck,\n ShieldX,\n Sparkles,\n Square,\n X,\n} from \"lucide-react\";\nimport * as React from \"react\";\nimport { A2UIRenderer } from \"@/a2ui/renderer\";\nimport { A2UIErrorCard } from \"@/a2ui/views\";\nimport type { A2UIAction, A2UIMessage } from \"@/a2ui/message\";\nimport { Badge } from \"@/components/badge\";\nimport { Button } from \"@/components/button\";\nimport { ErrorBanner } from \"@/components/error-banner\";\nimport { Markdown } from \"@/components/markdown\";\nimport { Spinner } from \"@/components/spinner\";\nimport { Textarea } from \"@/components/textarea\";\nimport { isHiddenMessage, STORED_TEXT_LIMIT } from \"@/agent-chat/session\";\nimport { isGenericLabel, truncate } from \"@/agent-chat/step-label\";\nimport { type AgentToolEntry, useAgentChat } from \"@/agent-chat/store\";\nimport type {\n AgentApprovalRequest,\n AgentChatClient,\n AgentChatMessage,\n AgentChatStep,\n AgentSession,\n} from \"@/agent-chat/types\";\nimport { sessionTitle } from \"@/agent-chat/types\";\nimport type { A2UIChatBlock } from \"@/agent-chat/a2ui-block\";\nimport { cn } from \"@/lib/cn\";\n\n/* ── stick to bottom ─────────────────────────────────────────────────────── */\n\n/**\n * Should the list jump back to the bottom after its content grew?\n *\n * Judged against the height BEFORE the growth. Measuring afterwards asks \"is\n * the newest content already visible\", which is false by construction for any\n * append taller than the threshold — a paragraph, a code block, an image — so\n * the re-pin skipped exactly when it was needed.\n */\nexport function shouldStickToBottom(params: {\n prevScrollHeight: number;\n scrollTop: number;\n clientHeight: number;\n threshold?: number;\n}): boolean {\n const { prevScrollHeight, scrollTop, clientHeight, threshold = 150 } = params;\n if (prevScrollHeight <= 0) {\n return true;\n }\n return prevScrollHeight - scrollTop - clientHeight < threshold;\n}\n\n/* ── thinking ────────────────────────────────────────────────────────────── */\n\nexport interface AgentThinkingProps {\n text: string;\n /** Open by default while the turn is live, collapsed once it is finished. */\n defaultOpen?: boolean;\n streaming?: boolean;\n}\n\nfunction AgentThinking({ text, defaultOpen, streaming }: AgentThinkingProps) {\n const [open, setOpen] = React.useState(defaultOpen ?? false);\n if (!text) {\n return null;\n }\n return (\n <div data-slot=\"agent-chat-thinking\" className=\"mt-1\">\n <button\n type=\"button\"\n aria-expanded={open}\n onClick={() => setOpen((v) => !v)}\n className={cn(\n \"inline-flex items-center gap-1 rounded-[var(--ds-radius-sm)] px-1 py-0.5\",\n \"text-[length:var(--ds-text-caption)] text-[color:var(--ds-muted-foreground)]\",\n \"transition-colors duration-[var(--ds-duration-fast)] hover:text-[color:var(--ds-foreground)]\",\n )}\n >\n {open ? <ChevronDown className=\"size-3\" aria-hidden /> : <ChevronRight className=\"size-3\" aria-hidden />}\n {streaming ? \"Thinking…\" : \"Chain of thought\"}\n </button>\n {open ? (\n <div\n data-slot=\"agent-chat-thinking-body\"\n className={cn(\n \"mt-1 max-h-64 overflow-auto rounded-[var(--ds-radius-sm)] border border-[var(--ds-border-subtle)]\",\n \"bg-[var(--ds-muted)] px-3 py-2 whitespace-pre-wrap\",\n \"text-[length:var(--ds-text-caption)] text-[color:var(--ds-muted-foreground)]\",\n )}\n >\n {text}\n </div>\n ) : null}\n </div>\n );\n}\n\n/* ── progress steps ──────────────────────────────────────────────────────── */\n\n/**\n * How long a step runs before it starts saying how long it has been running.\n *\n * Under this, a timer is noise: every call takes a second or two and a number\n * ticking beside each one is a fidget spinner. Over it, the user has begun to\n * wonder whether anything is happening at all, and the number is the answer.\n */\nconst ELAPSED_AFTER_MS = 8_000;\n\n/**\n * `Date.now()`, re-read once a second while `active`.\n *\n * The interval only exists while something is running, so a finished chat sits\n * idle rather than re-rendering the whole transcript once a second for ever.\n */\nfunction useTickingNow(active: boolean): number {\n const [now, setNow] = React.useState(() => Date.now());\n React.useEffect(() => {\n if (!active) {\n return;\n }\n setNow(Date.now());\n const id = setInterval(() => setNow(Date.now()), 1000);\n return () => clearInterval(id);\n }, [active]);\n return now;\n}\n\nexport interface AgentStepsProps {\n steps: readonly AgentChatStep[];\n}\n\n/**\n * What the agent is doing, one plain sentence per tool call (SKILLS-9).\n *\n * This is the surface a person watches while they wait, and it is deliberately\n * not the tool strip: the strip is the record — names, arguments, output, the\n * MCP App seam — and reading it requires knowing what `broker_invoke` is. The\n * strip stays at the foot for anyone who wants the detail.\n *\n * NOT a live region of its own. The strip is drawn inside the transcript, and\n * the transcript is already `role=\"log\" aria-live=\"polite\"` — two nested live\n * regions is one announcement per change from the inner one and another from\n * the outer, which is how a screen reader ends up saying every step twice.\n * `aria-label` stays: it names the group for anyone who navigates to it.\n */\nfunction AgentSteps({ steps }: AgentStepsProps) {\n const running = steps.some((step) => step.status === \"running\");\n const now = useTickingNow(running);\n if (steps.length === 0) {\n return null;\n }\n return (\n <div\n data-slot=\"agent-chat-steps\"\n aria-label=\"Progress\"\n className=\"mt-1 flex flex-col gap-0.5\"\n >\n {steps.map((step) => {\n const elapsed = step.status === \"running\" ? now - step.startedAt : 0;\n const seconds = elapsed >= ELAPSED_AFTER_MS ? Math.floor(elapsed / 1000) : 0;\n // Held out of the announcement while the line is still the fallback.\n // The arguments arrive one event after the start, so every call passes\n // through \"Working: broker_invoke\" for a few hundred milliseconds, and\n // a live region reads it out — then reads the real sentence over the\n // top of it. The row is still drawn; it joins the announcement the\n // moment it has something to say, or when it ends, whichever is first.\n const held = step.status === \"running\" && isGenericLabel(step.toolName, step.label);\n return (\n <p\n key={step.id}\n data-slot=\"agent-chat-step\"\n data-status={step.status}\n // The tool name is the model's, not ours, and an attribute is a\n // poor place for 9 kB of it.\n data-tool={truncate(step.toolName, STORED_TEXT_LIMIT)}\n {...(held ? { \"aria-hidden\": true } : {})}\n className={cn(\n \"flex items-start gap-2 text-[length:var(--ds-text-caption-lg)]\",\n step.status === \"error\"\n ? \"text-[color:var(--ds-destructive)]\"\n : \"text-[color:var(--ds-muted-foreground)]\",\n )}\n >\n <span\n aria-hidden\n className=\"mt-0.5 inline-flex size-3.5 shrink-0 items-center justify-center\"\n >\n {step.status === \"running\" ? (\n <Spinner size={12} aria-hidden />\n ) : step.status === \"error\" ? (\n <X className=\"size-3\" />\n ) : step.status === \"stopped\" ? (\n <Square className=\"size-2.5\" />\n ) : step.status === \"paused\" ? (\n // A plain dot, drawn in the muted colour: neither a spinner\n // (nothing is happening) nor a tick (nothing has happened).\n <span className=\"size-1.5 rounded-full bg-[color:var(--ds-muted-foreground)]\" />\n ) : (\n <Check className=\"size-3 text-[color:var(--ds-success)]\" />\n )}\n </span>\n <span className=\"min-w-0\">\n {/* Keyed on the words. A step is re-worded in place when its\n arguments land, and a text node mutated under a live region\n is a change some screen readers do not announce at all; a new\n key makes it a node replacement, which they all do. */}\n <span key={step.label} data-slot=\"agent-chat-step-label\">\n {step.label}\n </span>\n {/* The seconds change once a second, and a live region reads the\n whole line again each time they do. The count is for the eye:\n a screen reader hearing \"Fetching tasks task list\" once, and\n again when it lands, is told everything it needs. */}\n {seconds > 0 ? (\n <span aria-hidden data-slot=\"agent-chat-step-elapsed\">{` · ${seconds}s`}</span>\n ) : null}\n {step.status === \"stopped\" ? (\n <span data-slot=\"agent-chat-step-note\">{\" — Stopped\"}</span>\n ) : null}\n {step.status === \"paused\" ? (\n <span data-slot=\"agent-chat-step-note\">{\" — Waiting for your approval\"}</span>\n ) : null}\n {step.status === \"error\" && step.errorMessage ? (\n <span data-slot=\"agent-chat-step-error\">{` — ${step.errorMessage}`}</span>\n ) : null}\n </span>\n </p>\n );\n })}\n </div>\n );\n}\n\n/* ── A2UI blocks ─────────────────────────────────────────────────────────── */\n\nexport interface AgentA2UIBlocksProps {\n blocks: readonly A2UIChatBlock[] | undefined;\n /** Set on the streaming bubble: actions are disabled while the turn is live. */\n streaming?: boolean;\n onAction?: (action: A2UIAction) => void;\n}\n\n/**\n * Every block of one message, in arrival order.\n *\n * A block carrying a transport `error` — an unclosed fence, a body that did not\n * parse — gets an error card rather than being dropped. A block that is merely\n * malformed inside is the renderer's error to draw, and it draws the parts that\n * were fine.\n */\nfunction AgentA2UIBlocks({ blocks, streaming, onAction }: AgentA2UIBlocksProps) {\n if (!blocks || blocks.length === 0) {\n return null;\n }\n return (\n <div className=\"mt-2 flex flex-col gap-3\" data-slot=\"agent-chat-a2ui\">\n {blocks.map((block, index) => (\n <div key={`${block.surfaceId}:${index}`}>\n {block.error ? (\n <A2UIErrorCard title=\"The agent's UI did not arrive complete\" detail={block.error} />\n ) : null}\n {block.messages.length > 0 ? (\n <A2UIRenderer\n // The payload stays `unknown[]`: it is the agent's, and only the\n // renderer validates it. Every node goes through the catalogue's\n // schema before it is drawn.\n message={block.messages as A2UIMessage[]}\n // A surface still streaming must not fire actions at the agent\n // from a half-built form, so it is read-only until final.\n {...(streaming || !onAction ? {} : { onAction })}\n />\n ) : null}\n </div>\n ))}\n </div>\n );\n}\n\n/* ── bubbles ─────────────────────────────────────────────────────────────── */\n\nfunction AssistantAvatar() {\n return (\n <span\n data-slot=\"agent-chat-avatar\"\n aria-hidden\n className={cn(\n \"grid size-7 shrink-0 place-items-center rounded-[var(--ds-radius-sm)]\",\n \"border border-[var(--ds-ring)] bg-[var(--ds-primary-soft)] text-[color:var(--ds-primary)]\",\n )}\n >\n <Sparkles className=\"size-3.5\" aria-hidden />\n </span>\n );\n}\n\nexport interface AgentMessageBubbleProps {\n message: AgentChatMessage;\n agentName: string;\n /**\n * True while a run is live. Every A2UI surface in the transcript goes\n * read-only, not just the one being drawn: an earlier surface is still a\n * form wired to `sendAction`, and pressing it mid-run starts a second\n * concurrent turn on the same session.\n */\n streaming?: boolean;\n onAction?: (action: A2UIAction) => void;\n}\n\nfunction AgentMessageBubble({ message, agentName, streaming, onAction }: AgentMessageBubbleProps) {\n if (isHiddenMessage(message)) {\n return null;\n }\n if (message.isError) {\n return (\n <div data-slot=\"agent-chat-message\" data-role=\"error\">\n <ErrorBanner message={message.text} />\n </div>\n );\n }\n if (message.role === \"user\") {\n return (\n <div data-slot=\"agent-chat-message\" data-role=\"user\" className=\"flex w-full justify-end\">\n <div\n className={cn(\n \"max-w-[85%] rounded-[var(--ds-radius-lg)] border border-[var(--ds-border-subtle)]\",\n \"bg-[var(--ds-popover)] px-3.5 py-2 text-[color:var(--ds-foreground)]\",\n )}\n >\n <Markdown compact>{message.text}</Markdown>\n </div>\n </div>\n );\n }\n return (\n <div data-slot=\"agent-chat-message\" data-role=\"assistant\" className=\"flex w-full gap-3\">\n <AssistantAvatar />\n <div className=\"min-w-0 flex-1\">\n <span\n className={cn(\n \"text-[length:var(--ds-text-caption)] font-semibold\",\n \"text-[color:var(--ds-foreground)]\",\n )}\n >\n {agentName}\n </span>\n <AgentThinking text={message.thinkingText ?? \"\"} />\n {message.text ? (\n <div className=\"mt-1\">\n <Markdown compact>{message.text}</Markdown>\n </div>\n ) : null}\n <AgentA2UIBlocks\n blocks={message.a2ui}\n {...(streaming ? { streaming } : {})}\n {...(onAction ? { onAction } : {})}\n />\n </div>\n </div>\n );\n}\n\nexport interface AgentStreamingBubbleProps {\n agentName: string;\n text: string;\n thinkingText: string;\n a2ui: readonly A2UIChatBlock[];\n /** The turn's tool calls in plain words, drawn under the agent's name. */\n steps?: readonly AgentChatStep[];\n}\n\n/**\n * The assistant bubble while a turn is live.\n *\n * The filter chain is deliberately not applied: streaming text is\n * agent-authored prose in progress, and the filters are about finished\n * messages. Markdown is not processed either — partially arrived source\n * (`**bo`) looks worse than its raw form — so the streaming view is plain text\n * under `whitespace-pre-wrap` and `Markdown` takes over on `final`.\n */\nfunction AgentStreamingBubble({\n agentName,\n text,\n thinkingText,\n a2ui,\n steps,\n}: AgentStreamingBubbleProps) {\n return (\n <div data-slot=\"agent-chat-streaming\" className=\"flex w-full gap-3\">\n <AssistantAvatar />\n <div className=\"min-w-0 flex-1\">\n <span\n className={cn(\n \"text-[length:var(--ds-text-caption)] font-semibold\",\n \"text-[color:var(--ds-foreground)]\",\n )}\n >\n {agentName}\n </span>\n <AgentSteps steps={steps ?? []} />\n {thinkingText ? <AgentThinking text={thinkingText} defaultOpen streaming /> : null}\n {text ? (\n <div\n data-slot=\"agent-chat-streaming-text\"\n className=\"mt-1 break-words whitespace-pre-wrap text-[color:var(--ds-foreground)]\"\n >\n {text}\n </div>\n ) : null}\n <AgentA2UIBlocks blocks={a2ui} streaming />\n </div>\n </div>\n );\n}\n\n/* ── tool cards ──────────────────────────────────────────────────────────── */\n\n/** A `ui://` resource in a tool result is an MCP App: the extension's own screen. */\nexport function readMcpAppUri(output: unknown): string | null {\n const text =\n typeof output === \"string\" ? output : output === undefined ? \"\" : JSON.stringify(output);\n return text.match(/\"uri\"\\s*:\\s*\"(ui:\\/\\/[^\"]+)\"/)?.[1] ?? null;\n}\n\nfunction summarise(input: unknown): string {\n if (input === undefined || input === null) {\n return \"\";\n }\n if (typeof input === \"string\") {\n return input;\n }\n const record = input as Record<string, unknown>;\n for (const key of [\"command\", \"cmd\", \"query\", \"path\", \"url\", \"name\"]) {\n const value = record[key];\n if (typeof value === \"string\" && value.trim()) {\n return value;\n }\n }\n const json = JSON.stringify(input);\n return json.length > 120 ? `${json.slice(0, 117)}…` : json;\n}\n\nexport interface AgentToolCardProps {\n entry: AgentToolEntry;\n}\n\n/**\n * The default card. It shows the tool's name, its input and its output — the\n * input being the part that was missing for the whole of the AG-UI path,\n * because the bridge had no `TOOL_CALL_ARGS` case.\n */\nfunction AgentToolCard({ entry }: AgentToolCardProps) {\n const [open, setOpen] = React.useState(false);\n const summary = summarise(entry.input);\n return (\n <div\n data-slot=\"agent-tool-card\"\n data-tool={entry.name}\n data-status={entry.status}\n className={cn(\n \"overflow-hidden rounded-[var(--ds-radius-sm)] border border-[var(--ds-border-subtle)]\",\n \"bg-[var(--ds-muted)]\",\n )}\n >\n <button\n type=\"button\"\n aria-expanded={open}\n onClick={() => setOpen((v) => !v)}\n className={cn(\n \"flex w-full items-center gap-2.5 px-3 py-2 text-left\",\n \"transition-colors duration-[var(--ds-duration-fast)] hover:bg-[var(--ds-hover)]\",\n )}\n >\n <span className=\"inline-flex size-3.5 shrink-0 items-center justify-center\">\n {entry.status === \"running\" ? (\n <Spinner size={14} />\n ) : (\n <span\n aria-hidden\n className={cn(\n \"size-2 rounded-[var(--ds-radius-full)]\",\n entry.status === \"error\" ? \"bg-[var(--ds-destructive)]\" : \"bg-[var(--ds-success)]\",\n )}\n />\n )}\n </span>\n <span className=\"font-mono text-[length:var(--ds-text-caption)] text-[color:var(--ds-primary)]\">\n {entry.name}\n </span>\n {summary ? (\n <span\n data-slot=\"agent-tool-card-input\"\n className=\"min-w-0 truncate text-[length:var(--ds-text-caption)] text-[color:var(--ds-muted-foreground)]\"\n >\n {summary}\n </span>\n ) : null}\n <span className=\"ml-auto shrink-0 text-[color:var(--ds-text-tertiary)]\">\n {open ? <ChevronDown className=\"size-3\" /> : <ChevronRight className=\"size-3\" />}\n </span>\n </button>\n {open ? (\n <div className=\"border-t border-[var(--ds-border-subtle)] bg-[var(--ds-background)] px-3 py-2\">\n {entry.input !== undefined ? (\n <pre\n data-slot=\"agent-tool-card-args\"\n className=\"max-h-40 overflow-auto font-mono text-[length:var(--ds-text-caption-sm)] whitespace-pre-wrap text-[color:var(--ds-muted-foreground)]\"\n >\n {typeof entry.input === \"string\" ? entry.input : JSON.stringify(entry.input, null, 2)}\n </pre>\n ) : null}\n {entry.output !== undefined && entry.output !== null ? (\n <pre className=\"mt-2 max-h-40 overflow-auto font-mono text-[length:var(--ds-text-caption-sm)] whitespace-pre-wrap text-[color:var(--ds-muted-foreground)]\">\n {typeof entry.output === \"string\"\n ? entry.output\n : JSON.stringify(entry.output, null, 2)}\n </pre>\n ) : null}\n </div>\n ) : null}\n </div>\n );\n}\n\nexport interface AgentApprovalCardProps {\n request: AgentApprovalRequest;\n onResolve: (id: string, decision: \"allow-once\" | \"allow-always\" | \"deny\") => void;\n}\n\n/**\n * `exec` as a render-and-wait card: the run is paused on this decision.\n *\n * The three answers are the three cortenacore accepts. \"Always allow\" is not a\n * nicety — without it the same command is asked about on every turn, and a user\n * who has to answer the same question repeatedly stops reading the question.\n */\nfunction AgentApprovalCard({ request, onResolve }: AgentApprovalCardProps) {\n const command =\n typeof request.args.command === \"string\" ? request.args.command : request.toolName;\n return (\n <div\n data-slot=\"agent-approval-card\"\n role=\"group\"\n aria-label=\"Command approval required\"\n className={cn(\n \"flex flex-col gap-3 rounded-[var(--ds-radius-md)] border border-[var(--ds-border)]\",\n \"bg-[var(--ds-card)] p-3\",\n )}\n >\n <div className=\"flex items-center gap-2\">\n <ShieldCheck className=\"size-4 text-[color:var(--ds-primary)]\" aria-hidden />\n <span className=\"text-[length:var(--ds-text-body-sm)] font-medium text-[color:var(--ds-foreground)]\">\n Approval required\n </span>\n </div>\n <pre className=\"max-h-32 overflow-auto rounded-[var(--ds-radius-sm)] bg-[var(--ds-muted)] p-2 font-mono text-[length:var(--ds-text-caption)] whitespace-pre-wrap text-[color:var(--ds-foreground)]\">\n {command}\n </pre>\n {/* A decision the server refused. The card came back rather than\n vanishing, so the answer can be given again. */}\n {request.error ? (\n <p\n data-slot=\"agent-approval-error\"\n role=\"alert\"\n className=\"text-[length:var(--ds-text-caption)] text-[color:var(--ds-destructive)]\"\n >\n {request.error}\n </p>\n ) : null}\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button size=\"sm\" onClick={() => onResolve(request.id, \"allow-once\")}>\n <ShieldCheck aria-hidden />\n Approve\n </Button>\n <Button size=\"sm\" variant=\"outline\" onClick={() => onResolve(request.id, \"allow-always\")}>\n Always allow\n </Button>\n <Button size=\"sm\" variant=\"destructive\" onClick={() => onResolve(request.id, \"deny\")}>\n <ShieldX aria-hidden />\n Deny\n </Button>\n </div>\n </div>\n );\n}\n\n/* ── the surface ─────────────────────────────────────────────────────────── */\n\nexport interface AgentToolRenderContext {\n entry: AgentToolEntry;\n /** The `ui://` resource this result carries, when it carries one. */\n mcpAppUri: string | null;\n}\n\nexport interface AgentChatProps extends Omit<React.ComponentProps<\"div\">, \"children\" | \"onError\"> {\n /** The transport. `createAguiAgentChatClient` is the AG-UI implementation. */\n client: AgentChatClient;\n /** The extension name, shown on each assistant turn. */\n agentName: string;\n /**\n * Names the `sessionStorage` slot this window's session key lives in. Scope\n * it to the extension id; two chats with different scopes never share a key.\n */\n storageScope: string;\n /** Do not read or write `sessionStorage`; for a guide or a test. */\n ephemeral?: boolean;\n thinking?: \"low\" | \"medium\" | \"high\";\n /** Rendered between the tool strip and the composer — an attachment picker. */\n attachmentsSlot?: React.ReactNode;\n /**\n * Host wiring for a tool call. Return a node to replace the default card —\n * this is the seam an MCP App frame mounts through, since the sandboxed\n * iframe, its CSP and the `tools/invoke` proxy are the host's to own.\n */\n renderToolCall?: (context: AgentToolRenderContext) => React.ReactNode | undefined;\n /** Header content, e.g. the pop-up's own window controls. */\n headerSlot?: React.ReactNode;\n}\n\nfunction AgentChat({\n client,\n agentName,\n storageScope,\n ephemeral,\n thinking,\n attachmentsSlot,\n renderToolCall,\n headerSlot,\n className,\n ...props\n}: AgentChatProps) {\n const chat = useAgentChat({\n client,\n storageScope,\n ...(ephemeral === undefined ? {} : { ephemeral }),\n ...(thinking === undefined ? {} : { thinking }),\n });\n const [drawerOpen, setDrawerOpen] = React.useState(false);\n const [value, setValue] = React.useState(\"\");\n const scrollRef = React.useRef<HTMLDivElement>(null);\n const contentRef = React.useRef<HTMLDivElement>(null);\n const prevHeightRef = React.useRef(0);\n\n /**\n * Re-pin to the bottom whenever the content grows or the viewport shrinks.\n * A message keeps growing after it mounts — markdown settles, the tool strip\n * appears between the list and the composer — and every one of those leaves\n * the newest content under the composer until the reader scrolls by hand.\n *\n * Jumping rather than smooth-scrolling: this fires many times a second during\n * streaming, and queued smooth animations fight each other and lose.\n */\n React.useEffect(() => {\n const scrollEl = scrollRef.current;\n const contentEl = contentRef.current;\n if (!scrollEl || !contentEl || typeof ResizeObserver === \"undefined\") {\n return;\n }\n const observer = new ResizeObserver(() => {\n const { scrollTop, scrollHeight, clientHeight } = scrollEl;\n const stick = shouldStickToBottom({\n prevScrollHeight: prevHeightRef.current,\n scrollTop,\n clientHeight,\n });\n prevHeightRef.current = scrollHeight;\n if (stick) {\n scrollEl.scrollTop = scrollEl.scrollHeight;\n }\n });\n observer.observe(contentEl);\n observer.observe(scrollEl);\n return () => observer.disconnect();\n }, []);\n\n const submit = React.useCallback(() => {\n const trimmed = value.trim();\n // Enter reaches here while a run is live, where the button is a Stop\n // control and cannot be pressed. One run at a time, by both routes.\n if (!trimmed || chat.streaming) {\n return;\n }\n chat.send(trimmed);\n setValue(\"\");\n }, [chat, value]);\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (event.key === \"Escape\" && chat.streaming) {\n // Escape stops the run. It does not close the pop-up while a run is in\n // flight: the first Escape is nearly always \"stop\", not \"go away\".\n event.preventDefault();\n event.stopPropagation();\n chat.abort();\n return;\n }\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n submit();\n }\n },\n [chat, submit],\n );\n\n const toolEntries = chat.toolOrder\n .map((id) => chat.tools[id])\n .filter((entry): entry is AgentToolEntry => entry !== undefined);\n // The strip outlives the run rather than vanishing with it. The steps of a\n // finished turn are the record of what the agent did on the user's behalf,\n // and an MCP App drawn by one of them is a surface the user is meant to work\n // in — one that disappeared the instant the agent stopped talking would be no\n // use to anybody. The next run clears it.\n const showTools = toolEntries.length > 0 || chat.approvals.length > 0;\n const visible = chat.messages.filter((message) => !isHiddenMessage(message));\n\n return (\n <div\n data-slot=\"agent-chat\"\n className={cn(\"flex h-full min-h-0 w-full flex-col bg-[var(--ds-background)]\", className)}\n {...props}\n >\n <div\n data-slot=\"agent-chat-header\"\n className={cn(\n \"flex shrink-0 items-center gap-2 border-b border-[var(--ds-border-subtle)] px-3 py-2\",\n )}\n >\n <Button\n size=\"sm\"\n variant=\"ghost\"\n data-slot=\"agent-chat-sessions-trigger\"\n aria-expanded={drawerOpen}\n onClick={() => {\n setDrawerOpen((open) => !open);\n chat.refreshSessions();\n }}\n >\n {chat.sessions.length === 1 ? \"1 session\" : `${chat.sessions.length} sessions`}\n </Button>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n data-slot=\"agent-chat-new-session\"\n aria-label=\"New session\"\n onClick={() => {\n chat.newSession();\n setDrawerOpen(false);\n }}\n >\n <MessageSquarePlus aria-hidden />\n New\n </Button>\n <div className=\"ml-auto flex items-center gap-1\">{headerSlot}</div>\n </div>\n\n {drawerOpen ? (\n <AgentSessionDrawer\n sessions={chat.sessions}\n current={chat.sessionKey}\n loading={chat.loadingSessions}\n onResume={(key) => {\n chat.resumeSession(key);\n setDrawerOpen(false);\n }}\n />\n ) : null}\n\n <div\n ref={scrollRef}\n data-slot=\"agent-chat-scroll\"\n className=\"min-h-0 flex-1 overflow-x-hidden overflow-y-auto\"\n >\n <div\n ref={contentRef}\n role=\"log\"\n aria-live=\"polite\"\n aria-label={`${agentName} chat`}\n className=\"flex min-w-0 flex-col gap-5 px-4 py-4\"\n >\n {chat.error ? (\n <ErrorBanner message={chat.error} />\n ) : null}\n\n {chat.hasMoreHistory ? (\n <div className=\"flex justify-center\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n data-slot=\"agent-chat-load-older\"\n disabled={chat.loadingHistory}\n onClick={chat.loadOlder}\n >\n {chat.loadingHistory ? \"Loading…\" : \"Load older messages\"}\n </Button>\n </div>\n ) : null}\n\n {visible.length === 0 && !chat.streaming ? (\n <p\n data-slot=\"agent-chat-empty\"\n className=\"text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]\"\n >\n Ask {agentName} anything about this screen.\n </p>\n ) : null}\n\n {visible.map((message) => (\n <AgentMessageBubble\n key={message.id}\n message={message}\n agentName={agentName}\n streaming={chat.streaming}\n onAction={chat.sendAction}\n />\n ))}\n\n {chat.streaming ? (\n <AgentStreamingBubble\n agentName={agentName}\n text={chat.streamText}\n thinkingText={chat.streamThinkingText}\n a2ui={chat.streamA2ui}\n steps={chat.steps}\n />\n ) : chat.steps.length > 0 ? (\n // The turn is over but its steps are the record of what was done to\n // answer it, and they stay until the next run replaces them — the\n // same rule the tool strip below follows.\n <AgentSteps steps={chat.steps} />\n ) : null}\n </div>\n </div>\n\n {showTools ? (\n <div\n data-slot=\"agent-chat-tools\"\n className=\"flex shrink-0 flex-col gap-1.5 border-t border-[var(--ds-border-subtle)] px-3 py-2\"\n >\n {toolEntries.length > 0 ? (\n <Badge variant=\"neutral\" className=\"w-fit\">\n {toolEntries.length === 1 ? \"1 step\" : `${toolEntries.length} steps`}\n </Badge>\n ) : null}\n {toolEntries.map((entry) => {\n const custom = renderToolCall?.({\n entry,\n mcpAppUri: readMcpAppUri(entry.output),\n });\n return (\n <React.Fragment key={entry.id}>\n {custom ?? <AgentToolCard entry={entry} />}\n </React.Fragment>\n );\n })}\n {chat.approvals.map((request) => (\n <AgentApprovalCard\n key={request.id}\n request={request}\n onResolve={chat.resolveApproval}\n />\n ))}\n </div>\n ) : null}\n\n {attachmentsSlot ? (\n <div data-slot=\"agent-chat-attachments\" className=\"shrink-0 px-3\">\n {attachmentsSlot}\n </div>\n ) : null}\n\n <div\n data-slot=\"agent-chat-composer\"\n className=\"flex shrink-0 items-end gap-2 border-t border-[var(--ds-border-subtle)] p-3\"\n >\n <Textarea\n data-slot=\"agent-chat-input\"\n aria-label={`Message ${agentName}`}\n placeholder={`Message ${agentName}…`}\n rows={1}\n className=\"max-h-40 min-h-9 resize-none\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n onKeyDown={onKeyDown}\n />\n {chat.streaming ? (\n <Button\n size=\"icon\"\n variant=\"outline\"\n data-slot=\"agent-chat-stop\"\n aria-label=\"Stop the run\"\n onClick={chat.abort}\n >\n <Square aria-hidden />\n </Button>\n ) : (\n <Button\n size=\"icon\"\n data-slot=\"agent-chat-send\"\n aria-label=\"Send\"\n disabled={!value.trim()}\n onClick={submit}\n >\n <Send aria-hidden />\n </Button>\n )}\n </div>\n </div>\n );\n}\n\n/* ── the session drawer ──────────────────────────────────────────────────── */\n\nexport interface AgentSessionDrawerProps {\n sessions: readonly AgentSession[];\n current: string | null;\n loading?: boolean;\n onResume: (key: string) => void;\n}\n\n/**\n * Every session of this agent, resumable from any window.\n *\n * The list is not this window's history: a session started in another window is\n * here and can be resumed, which is the difference between \"sessions are per\n * window\" (which key a window starts on) and \"sessions are private to a window\"\n * (which they are not).\n */\nfunction AgentSessionDrawer({ sessions, current, loading, onResume }: AgentSessionDrawerProps) {\n return (\n <div\n data-slot=\"agent-chat-sessions\"\n className={cn(\n \"max-h-56 shrink-0 overflow-y-auto border-b border-[var(--ds-border-subtle)]\",\n \"bg-[var(--ds-card)] px-2 py-1.5\",\n )}\n >\n {loading && sessions.length === 0 ? (\n <p className=\"px-2 py-1 text-[length:var(--ds-text-caption)] text-[color:var(--ds-muted-foreground)]\">\n Loading sessions…\n </p>\n ) : null}\n {!loading && sessions.length === 0 ? (\n <p className=\"px-2 py-1 text-[length:var(--ds-text-caption)] text-[color:var(--ds-muted-foreground)]\">\n No earlier sessions.\n </p>\n ) : null}\n {sessions.map((session) => (\n <button\n key={session.key}\n type=\"button\"\n data-slot=\"agent-chat-session\"\n data-current={session.key === current ? \"\" : undefined}\n onClick={() => onResume(session.key)}\n className={cn(\n \"flex w-full items-center gap-2 rounded-[var(--ds-radius-sm)] px-2 py-1.5 text-left\",\n \"text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-foreground)]\",\n \"transition-colors duration-[var(--ds-duration-fast)] hover:bg-[var(--ds-hover)]\",\n session.key === current && \"bg-[var(--ds-primary-soft)]\",\n )}\n >\n <span className=\"min-w-0 truncate\">{sessionTitle(session)}</span>\n </button>\n ))}\n </div>\n );\n}\n\nexport {\n AgentA2UIBlocks,\n AgentApprovalCard,\n AgentChat,\n AgentMessageBubble,\n AgentSessionDrawer,\n AgentSteps,\n AgentStreamingBubble,\n AgentThinking,\n AgentToolCard,\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
//#region src/components/app-shell.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* AppShell — the chrome every Cortena extension wears, so that moving between
|
|
6
|
+
* two of them is moving between two screens of one product rather than between
|
|
7
|
+
* two applications (how-to-create-a-cortena-extension §9, audit rule P-10).
|
|
8
|
+
*
|
|
9
|
+
* Four things are fixed, and they are fixed because an extension that places
|
|
10
|
+
* them itself places them somewhere slightly different:
|
|
11
|
+
*
|
|
12
|
+
* top left the extension's name and its registered mark, 20px inside a
|
|
13
|
+
* 32px square. The mark comes from `cortena-design/marks` by
|
|
14
|
+
* `extension.id` — the same file the Apps tile, the MCP app
|
|
15
|
+
* card, the agent pop-up pill and the favicon draw (§9.1), not
|
|
16
|
+
* an SVG in this app's web/public.
|
|
17
|
+
* top right the avatar menu. It is the ONLY settings entry point:
|
|
18
|
+
* Profile, Settings, Log out. Not a gear in a sidebar as well
|
|
19
|
+
* (§9.2).
|
|
20
|
+
* bottom right one fixed cluster, theme toggle then help button, left to
|
|
21
|
+
* right, 24px from both viewport edges, 40px each, 12px apart
|
|
22
|
+
* (§9.3). The agent pop-up pill stacks above it.
|
|
23
|
+
* everywhere the theme is `useCortenaTheme`, so the choice persists and a
|
|
24
|
+
* host `postMessage` still wins.
|
|
25
|
+
*
|
|
26
|
+
* The help panel itself is EXTBP-24 and does not exist yet, so `helpPanel` is a
|
|
27
|
+
* slot: pass a node or a render function and the shell owns the button, the
|
|
28
|
+
* open state and the corner. When HelpPanel ships, `help.source` is what it
|
|
29
|
+
* reads — the extension's functional document (§12) — and the default panel
|
|
30
|
+
* replaces the slot without a call-site change.
|
|
31
|
+
*/
|
|
32
|
+
export interface BrandMarkProps extends Omit<React.ComponentProps<"span">, "children"> {
|
|
33
|
+
/** Registered mark id — what the extension puts in `x-cortena.icon`. */
|
|
34
|
+
id: string;
|
|
35
|
+
/** The extension name. Labels the mark, and supplies the fallback initial. */
|
|
36
|
+
name: string;
|
|
37
|
+
/** Rendered size of the glyph in pixels. 20 in the shell, 24 in the Apps tile, 16 on the pill. */
|
|
38
|
+
size?: number;
|
|
39
|
+
/** Set when the mark is decorative because the name is already beside it. */
|
|
40
|
+
decorative?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The registered mark for `id`, or the extension's initial when nothing is
|
|
44
|
+
* registered. The fallback matters: an unregistered id used to render an empty
|
|
45
|
+
* box, which reads as a broken image rather than as "this extension has not
|
|
46
|
+
* registered a mark yet".
|
|
47
|
+
*/
|
|
48
|
+
declare function BrandMark({ className, id, name, size, decorative, ...props }: BrandMarkProps): React.JSX.Element;
|
|
49
|
+
export interface BottomRightClusterProps extends React.ComponentProps<"div"> {
|
|
50
|
+
/** The agent pop-up pill (§17). Stacks above the row, never beside it. */
|
|
51
|
+
agentSlot?: React.ReactNode;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The one fixed container in the bottom-right corner. It exists so that no
|
|
55
|
+
* extension positions a floating control itself — that is how a third one
|
|
56
|
+
* appears and covers one of the first two.
|
|
57
|
+
*/
|
|
58
|
+
declare function BottomRightCluster({ className, agentSlot, children, ...props }: BottomRightClusterProps): React.JSX.Element;
|
|
59
|
+
export interface ThemeToggleProps extends Omit<React.ComponentProps<"button">, "onClick"> {
|
|
60
|
+
/** localStorage key, forwarded to `useCortenaTheme`. */
|
|
61
|
+
storageKey?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The theme selector. It lives here rather than in the avatar menu because a
|
|
65
|
+
* user changing theme is usually reacting to the room they are in, and that is
|
|
66
|
+
* a one-tap decision, not a settings visit.
|
|
67
|
+
*/
|
|
68
|
+
declare function ThemeToggle({ className, storageKey, ...props }: ThemeToggleProps): React.JSX.Element;
|
|
69
|
+
export interface HelpButtonProps extends React.ComponentProps<"button"> {
|
|
70
|
+
/** Whether the panel it controls is open. */
|
|
71
|
+
open?: boolean;
|
|
72
|
+
}
|
|
73
|
+
/** The help affordance. Right of the theme toggle, always — order is fixed (§9.3). */
|
|
74
|
+
declare function HelpButton({ className, open, ...props }: HelpButtonProps): React.JSX.Element;
|
|
75
|
+
export interface AppShellExtension {
|
|
76
|
+
/** Registered mark id, from `x-cortena.icon` in the OpenAPI document. */
|
|
77
|
+
id: string;
|
|
78
|
+
/** Display name. Also the first half of the document title, "<name> · Cortena". */
|
|
79
|
+
name: string;
|
|
80
|
+
}
|
|
81
|
+
export interface AppShellUser {
|
|
82
|
+
name: string;
|
|
83
|
+
avatarUrl?: string;
|
|
84
|
+
}
|
|
85
|
+
export interface AppShellHelp {
|
|
86
|
+
/**
|
|
87
|
+
* The extension's functional document — the thing the help panel answers
|
|
88
|
+
* from (§12). Giving that document a runtime consumer is the point: a stale
|
|
89
|
+
* doc becomes a visibly wrong help panel rather than a file nobody opens.
|
|
90
|
+
*/
|
|
91
|
+
source: string;
|
|
92
|
+
}
|
|
93
|
+
export interface HelpPanelSlotProps {
|
|
94
|
+
open: boolean;
|
|
95
|
+
source?: string;
|
|
96
|
+
onClose: () => void;
|
|
97
|
+
}
|
|
98
|
+
export interface AppShellProps extends Omit<React.ComponentProps<"div">, "title"> {
|
|
99
|
+
extension: AppShellExtension;
|
|
100
|
+
user: AppShellUser;
|
|
101
|
+
/** Opens the extension's own settings route. The avatar menu is the only way in. */
|
|
102
|
+
onSettings?: () => void;
|
|
103
|
+
onProfile?: () => void;
|
|
104
|
+
onLogout?: () => void;
|
|
105
|
+
help?: AppShellHelp;
|
|
106
|
+
/**
|
|
107
|
+
* The help panel, until HelpPanel ships (EXTBP-24). A render function gets
|
|
108
|
+
* the open state and owns its own markup; a node is mounted in a right-hand
|
|
109
|
+
* region while open.
|
|
110
|
+
*/
|
|
111
|
+
helpPanel?: React.ReactNode | ((props: HelpPanelSlotProps) => React.ReactNode);
|
|
112
|
+
/** The agent pop-up pill. Stacks above the bottom-right cluster. */
|
|
113
|
+
agentSlot?: React.ReactNode;
|
|
114
|
+
/** localStorage key for the theme choice, forwarded to `useCortenaTheme`. */
|
|
115
|
+
themeStorageKey?: string;
|
|
116
|
+
}
|
|
117
|
+
declare function AppShell({ className, extension, user, onSettings, onProfile, onLogout, help, helpPanel, agentSlot, themeStorageKey, children, ...props }: AppShellProps): React.JSX.Element;
|
|
118
|
+
/**
|
|
119
|
+
* `<title>` for an extension: the extension first, because that is what
|
|
120
|
+
* distinguishes one tab from another, and the product second so a row of tabs
|
|
121
|
+
* reads as one product (§9.5, audit rule P-38).
|
|
122
|
+
*/
|
|
123
|
+
declare function documentTitle(extensionName: string, page?: string): string;
|
|
124
|
+
//#endregion
|
|
125
|
+
export { AppShell, BottomRightCluster, BrandMark, HelpButton, ThemeToggle, documentTitle };
|
|
126
|
+
//# sourceMappingURL=app-shell.d.ts.map
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
import { cn } from "../lib/cn.js";
|
|
4
|
+
import { Avatar, AvatarFallback, AvatarImage } from "./avatar.js";
|
|
5
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "./dropdown-menu.js";
|
|
6
|
+
import { useCortenaTheme } from "../hooks/use-cortena-theme.js";
|
|
7
|
+
import { CircleQuestionMark, LogOut, Monitor, Moon, Settings, Sun, User } from "lucide-react";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { getMark } from "cortena-design/marks";
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
//#region src/components/app-shell.tsx
|
|
12
|
+
/**
|
|
13
|
+
* The presentation attributes a mark's root may carry into the DOM.
|
|
14
|
+
*
|
|
15
|
+
* The same list cortena-design's `validateMarkup` enforces at build time,
|
|
16
|
+
* repeated here because this component spreads values that came from a
|
|
17
|
+
* package the consumer resolves — an older `cortena-design`, a patched one, a
|
|
18
|
+
* hand-edited `dist/marks.js` — straight onto an element.
|
|
19
|
+
*/
|
|
20
|
+
const MARK_ROOT_ATTRIBUTES = [
|
|
21
|
+
"fill",
|
|
22
|
+
"stroke",
|
|
23
|
+
"stroke-width",
|
|
24
|
+
"stroke-linecap",
|
|
25
|
+
"stroke-linejoin",
|
|
26
|
+
"transform",
|
|
27
|
+
"opacity"
|
|
28
|
+
];
|
|
29
|
+
/** `root`, with anything outside the allow-list dropped. */
|
|
30
|
+
function safeRootAttributes(root) {
|
|
31
|
+
const safe = {};
|
|
32
|
+
for (const name of MARK_ROOT_ATTRIBUTES) {
|
|
33
|
+
const value = root?.[name];
|
|
34
|
+
if (typeof value === "string") safe[name] = value;
|
|
35
|
+
}
|
|
36
|
+
return safe;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The registered mark for `id`, or the extension's initial when nothing is
|
|
40
|
+
* registered. The fallback matters: an unregistered id used to render an empty
|
|
41
|
+
* box, which reads as a broken image rather than as "this extension has not
|
|
42
|
+
* registered a mark yet".
|
|
43
|
+
*/
|
|
44
|
+
function BrandMark({ className, id, name, size = 20, decorative, ...props }) {
|
|
45
|
+
const mark = getMark(id);
|
|
46
|
+
const label = decorative ? void 0 : name;
|
|
47
|
+
return /* @__PURE__ */ jsx("span", {
|
|
48
|
+
"data-slot": "brand-mark",
|
|
49
|
+
"data-mark": mark ? mark.id : "fallback",
|
|
50
|
+
className: cn("inline-flex shrink-0 items-center justify-center", className),
|
|
51
|
+
style: {
|
|
52
|
+
width: size,
|
|
53
|
+
height: size
|
|
54
|
+
},
|
|
55
|
+
...props,
|
|
56
|
+
children: mark ? /* @__PURE__ */ jsx("svg", {
|
|
57
|
+
viewBox: mark.viewBox,
|
|
58
|
+
width: size,
|
|
59
|
+
height: size,
|
|
60
|
+
role: decorative ? void 0 : "img",
|
|
61
|
+
"aria-label": label,
|
|
62
|
+
"aria-hidden": decorative ? true : void 0,
|
|
63
|
+
focusable: "false",
|
|
64
|
+
dangerouslySetInnerHTML: { __html: mark.body },
|
|
65
|
+
...safeRootAttributes(mark.root)
|
|
66
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
67
|
+
"aria-label": label,
|
|
68
|
+
"aria-hidden": decorative ? true : void 0,
|
|
69
|
+
role: decorative ? void 0 : "img",
|
|
70
|
+
className: cn("font-display font-bold uppercase leading-none", "text-[length:var(--ds-text-caption-lg)]"),
|
|
71
|
+
children: name.trim().charAt(0) || "?"
|
|
72
|
+
})
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The one fixed container in the bottom-right corner. It exists so that no
|
|
77
|
+
* extension positions a floating control itself — that is how a third one
|
|
78
|
+
* appears and covers one of the first two.
|
|
79
|
+
*/
|
|
80
|
+
function BottomRightCluster({ className, agentSlot, children, ...props }) {
|
|
81
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
82
|
+
"data-slot": "bottom-right-cluster",
|
|
83
|
+
className: cn("fixed right-6 bottom-6 z-40 flex flex-col items-end gap-3", className),
|
|
84
|
+
...props,
|
|
85
|
+
children: [agentSlot ? /* @__PURE__ */ jsx("div", {
|
|
86
|
+
"data-slot": "bottom-right-cluster-agent",
|
|
87
|
+
className: "flex items-center justify-end",
|
|
88
|
+
children: agentSlot
|
|
89
|
+
}) : null, /* @__PURE__ */ jsx("div", {
|
|
90
|
+
"data-slot": "bottom-right-cluster-row",
|
|
91
|
+
className: "flex items-center gap-3",
|
|
92
|
+
children
|
|
93
|
+
})]
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/** The cluster's distance from both viewport edges (§9.3). `right-6` / `bottom-6`. */
|
|
97
|
+
const CLUSTER_INSET = 24;
|
|
98
|
+
/** What the help panel leaves between itself and the cluster. */
|
|
99
|
+
const HELP_PANEL_GAP = 12;
|
|
100
|
+
const clusterButtonClassName = [
|
|
101
|
+
"inline-flex size-10 shrink-0 items-center justify-center rounded-[var(--ds-radius-full)]",
|
|
102
|
+
"border border-[var(--ds-border)] bg-[var(--ds-card)] text-[color:var(--ds-foreground)]",
|
|
103
|
+
"shadow-[var(--ds-shadow-md)] outline-none",
|
|
104
|
+
"transition-colors duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]",
|
|
105
|
+
"hover:bg-[var(--ds-hover)]",
|
|
106
|
+
"focus-visible:ring-2 focus-visible:ring-[var(--ds-ring)] focus-visible:ring-offset-2",
|
|
107
|
+
"focus-visible:ring-offset-[var(--ds-background)]"
|
|
108
|
+
].join(" ");
|
|
109
|
+
/** light → dark → system → light. Three states, one control, no menu to open. */
|
|
110
|
+
const THEME_ORDER = [
|
|
111
|
+
"light",
|
|
112
|
+
"dark",
|
|
113
|
+
"system"
|
|
114
|
+
];
|
|
115
|
+
const THEME_LABEL = {
|
|
116
|
+
light: "Light",
|
|
117
|
+
dark: "Dark",
|
|
118
|
+
system: "System"
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* The theme selector. It lives here rather than in the avatar menu because a
|
|
122
|
+
* user changing theme is usually reacting to the room they are in, and that is
|
|
123
|
+
* a one-tap decision, not a settings visit.
|
|
124
|
+
*/
|
|
125
|
+
function ThemeToggle({ className, storageKey, ...props }) {
|
|
126
|
+
const { theme, resolvedTheme, setTheme } = useCortenaTheme(storageKey === void 0 ? {} : { storageKey });
|
|
127
|
+
const next = THEME_ORDER[(THEME_ORDER.indexOf(theme) + 1) % THEME_ORDER.length] ?? "light";
|
|
128
|
+
const Icon = theme === "system" ? Monitor : theme === "dark" ? Moon : Sun;
|
|
129
|
+
return /* @__PURE__ */ jsx("button", {
|
|
130
|
+
type: "button",
|
|
131
|
+
"data-slot": "theme-toggle",
|
|
132
|
+
"data-theme-choice": theme,
|
|
133
|
+
"data-theme-resolved": resolvedTheme,
|
|
134
|
+
"aria-label": `Theme: ${THEME_LABEL[theme]}. Switch to ${THEME_LABEL[next].toLowerCase()}.`,
|
|
135
|
+
className: cn(clusterButtonClassName, className),
|
|
136
|
+
onClick: () => setTheme(next),
|
|
137
|
+
...props,
|
|
138
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
139
|
+
className: "size-5",
|
|
140
|
+
"aria-hidden": true
|
|
141
|
+
})
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/** The id the shell gives its help region, and the button's `aria-controls`. */
|
|
145
|
+
const HELP_PANEL_ID = "cortena-help-panel";
|
|
146
|
+
/** The help affordance. Right of the theme toggle, always — order is fixed (§9.3). */
|
|
147
|
+
function HelpButton({ className, open, ...props }) {
|
|
148
|
+
return /* @__PURE__ */ jsx("button", {
|
|
149
|
+
type: "button",
|
|
150
|
+
"data-slot": "help-button",
|
|
151
|
+
"aria-label": "Help",
|
|
152
|
+
"aria-expanded": open,
|
|
153
|
+
"aria-controls": HELP_PANEL_ID,
|
|
154
|
+
"data-open": open ? "" : void 0,
|
|
155
|
+
className: cn(clusterButtonClassName, open && "bg-[var(--ds-hover)]", className),
|
|
156
|
+
...props,
|
|
157
|
+
children: /* @__PURE__ */ jsx(CircleQuestionMark, {
|
|
158
|
+
className: "size-5",
|
|
159
|
+
"aria-hidden": true
|
|
160
|
+
})
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function AppShell({ className, extension, user, onSettings, onProfile, onLogout, help, helpPanel, agentSlot, themeStorageKey, children, ...props }) {
|
|
164
|
+
const [helpOpen, setHelpOpen] = React.useState(false);
|
|
165
|
+
const helpButtonRef = React.useRef(null);
|
|
166
|
+
const closeHelp = React.useCallback(() => {
|
|
167
|
+
setHelpOpen(false);
|
|
168
|
+
helpButtonRef.current?.focus();
|
|
169
|
+
}, []);
|
|
170
|
+
/**
|
|
171
|
+
* The help panel stops 12px above the bottom-right cluster, so the button
|
|
172
|
+
* that opened it stays visible and clickable. The gap was hard-coded at
|
|
173
|
+
* 76px — one 40px control plus the two margins — and the cluster is not
|
|
174
|
+
* always that tall: the agent pop-up pill stacks ABOVE the row, which makes
|
|
175
|
+
* it 92px, and the panel then covered the pill it was sitting on.
|
|
176
|
+
*/
|
|
177
|
+
const clusterRef = React.useRef(null);
|
|
178
|
+
const [clusterHeight, setClusterHeight] = React.useState(40);
|
|
179
|
+
React.useEffect(() => {
|
|
180
|
+
const el = clusterRef.current;
|
|
181
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
182
|
+
const measure = () => setClusterHeight(el.getBoundingClientRect().height);
|
|
183
|
+
measure();
|
|
184
|
+
const observer = new ResizeObserver(measure);
|
|
185
|
+
observer.observe(el);
|
|
186
|
+
return () => observer.disconnect();
|
|
187
|
+
}, [agentSlot, helpOpen]);
|
|
188
|
+
const hasHelp = help !== void 0 || helpPanel !== void 0;
|
|
189
|
+
const panel = typeof helpPanel === "function" ? helpPanel({
|
|
190
|
+
open: helpOpen,
|
|
191
|
+
source: help?.source,
|
|
192
|
+
onClose: closeHelp
|
|
193
|
+
}) : helpOpen ? helpPanel : null;
|
|
194
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
195
|
+
"data-slot": "app-shell",
|
|
196
|
+
className: cn("flex min-h-screen w-full flex-col bg-[var(--ds-background)] text-[color:var(--ds-foreground)]", className),
|
|
197
|
+
...props,
|
|
198
|
+
children: [
|
|
199
|
+
/* @__PURE__ */ jsxs("header", {
|
|
200
|
+
"data-slot": "app-shell-header",
|
|
201
|
+
className: cn("sticky top-0 z-30 flex h-16 w-full shrink-0 items-center justify-between gap-3", "border-b border-[var(--ds-border-subtle)] bg-[var(--ds-background)] px-4 sm:px-6"),
|
|
202
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
203
|
+
"data-slot": "app-shell-brand",
|
|
204
|
+
className: "flex min-w-0 items-center gap-2.5",
|
|
205
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
206
|
+
"data-slot": "app-shell-brand-square",
|
|
207
|
+
className: cn("flex size-8 shrink-0 items-center justify-center rounded-[var(--ds-radius-md)]", "bg-[var(--ds-primary-soft)] text-[color:var(--ds-primary)]"),
|
|
208
|
+
children: /* @__PURE__ */ jsx(BrandMark, {
|
|
209
|
+
id: extension.id,
|
|
210
|
+
name: extension.name,
|
|
211
|
+
size: 20,
|
|
212
|
+
decorative: true
|
|
213
|
+
})
|
|
214
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
215
|
+
"data-slot": "app-shell-brand-name",
|
|
216
|
+
className: cn("truncate font-display text-[length:var(--ds-text-body-md)] font-bold", "leading-[var(--ds-text-body-md--line-height)] text-[color:var(--ds-foreground)]"),
|
|
217
|
+
children: extension.name
|
|
218
|
+
})]
|
|
219
|
+
}), /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
220
|
+
"data-slot": "app-shell-avatar-trigger",
|
|
221
|
+
"aria-label": `Account: ${user.name}`,
|
|
222
|
+
className: cn("flex shrink-0 items-center rounded-[var(--ds-radius-full)] outline-none", "focus-visible:ring-2 focus-visible:ring-[var(--ds-ring)] focus-visible:ring-offset-2", "focus-visible:ring-offset-[var(--ds-background)]"),
|
|
223
|
+
children: /* @__PURE__ */ jsxs(Avatar, { children: [user.avatarUrl ? /* @__PURE__ */ jsx(AvatarImage, {
|
|
224
|
+
src: user.avatarUrl,
|
|
225
|
+
alt: ""
|
|
226
|
+
}) : null, /* @__PURE__ */ jsx(AvatarFallback, { children: initials(user.name) })] })
|
|
227
|
+
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
228
|
+
align: "end",
|
|
229
|
+
sideOffset: 8,
|
|
230
|
+
className: "min-w-48",
|
|
231
|
+
children: [
|
|
232
|
+
/* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
233
|
+
"data-item": "profile",
|
|
234
|
+
disabled: !onProfile,
|
|
235
|
+
onClick: onProfile,
|
|
236
|
+
children: [/* @__PURE__ */ jsx(User, {}), " Profile"]
|
|
237
|
+
}),
|
|
238
|
+
/* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
239
|
+
"data-item": "settings",
|
|
240
|
+
disabled: !onSettings,
|
|
241
|
+
onClick: onSettings,
|
|
242
|
+
children: [/* @__PURE__ */ jsx(Settings, {}), " Settings"]
|
|
243
|
+
}),
|
|
244
|
+
/* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
245
|
+
/* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
246
|
+
"data-item": "logout",
|
|
247
|
+
variant: "destructive",
|
|
248
|
+
disabled: !onLogout,
|
|
249
|
+
onClick: onLogout,
|
|
250
|
+
children: [/* @__PURE__ */ jsx(LogOut, {}), " Log out"]
|
|
251
|
+
})
|
|
252
|
+
]
|
|
253
|
+
})] })]
|
|
254
|
+
}),
|
|
255
|
+
/* @__PURE__ */ jsx("main", {
|
|
256
|
+
"data-slot": "app-shell-main",
|
|
257
|
+
className: "min-w-0 flex-1",
|
|
258
|
+
children
|
|
259
|
+
}),
|
|
260
|
+
panel ? /* @__PURE__ */ jsx("aside", {
|
|
261
|
+
id: HELP_PANEL_ID,
|
|
262
|
+
"data-slot": "app-shell-help-panel",
|
|
263
|
+
"aria-label": `${extension.name} help`,
|
|
264
|
+
style: { bottom: CLUSTER_INSET + clusterHeight + HELP_PANEL_GAP },
|
|
265
|
+
className: cn("fixed top-6 right-6 z-40 flex w-[min(24rem,calc(100vw-3rem))] flex-col", "overflow-hidden rounded-[var(--ds-radius-lg)] border border-[var(--ds-border)]", "bg-[var(--ds-card)] shadow-[var(--ds-shadow-lg)]"),
|
|
266
|
+
children: panel
|
|
267
|
+
}) : null,
|
|
268
|
+
/* @__PURE__ */ jsxs(BottomRightCluster, {
|
|
269
|
+
ref: clusterRef,
|
|
270
|
+
agentSlot,
|
|
271
|
+
children: [/* @__PURE__ */ jsx(ThemeToggle, { storageKey: themeStorageKey }), hasHelp ? /* @__PURE__ */ jsx(HelpButton, {
|
|
272
|
+
ref: helpButtonRef,
|
|
273
|
+
open: helpOpen,
|
|
274
|
+
onClick: () => setHelpOpen((open) => !open)
|
|
275
|
+
}) : null]
|
|
276
|
+
})
|
|
277
|
+
]
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/** One or two letters. Two words give two initials; anything else gives one. */
|
|
281
|
+
function initials(name) {
|
|
282
|
+
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
283
|
+
if (words.length === 0) return "?";
|
|
284
|
+
return ((words[0]?.charAt(0) ?? "") + (words.length > 1 ? words.at(-1)?.charAt(0) ?? "" : "")).toUpperCase();
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* `<title>` for an extension: the extension first, because that is what
|
|
288
|
+
* distinguishes one tab from another, and the product second so a row of tabs
|
|
289
|
+
* reads as one product (§9.5, audit rule P-38).
|
|
290
|
+
*/
|
|
291
|
+
function documentTitle(extensionName, page) {
|
|
292
|
+
return page ? `${page} · ${extensionName} · Cortena` : `${extensionName} · Cortena`;
|
|
293
|
+
}
|
|
294
|
+
//#endregion
|
|
295
|
+
export { AppShell, BottomRightCluster, BrandMark, HELP_PANEL_ID, HelpButton, ThemeToggle, documentTitle };
|
|
296
|
+
|
|
297
|
+
//# sourceMappingURL=app-shell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-shell.js","names":[],"sources":["../../src/components/app-shell.tsx"],"sourcesContent":["\"use client\";\n\nimport { getMark } from \"cortena-design/marks\";\nimport { CircleQuestionMark, LogOut, Monitor, Moon, Settings, Sun, User } 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.\n *\n * The same list cortena-design's `validateMarkup` enforces at build time,\n * 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 */\nconst MARK_ROOT_ATTRIBUTES: readonly string[] = [\n \"fill\",\n \"stroke\",\n \"stroke-width\",\n \"stroke-linecap\",\n \"stroke-linejoin\",\n \"transform\",\n \"opacity\",\n];\n\n/** `root`, with anything outside the allow-list dropped. */\nfunction safeRootAttributes(root: Record<string, string> | undefined): Record<string, string> {\n const safe: Record<string, string> = {};\n for (const name of MARK_ROOT_ATTRIBUTES) {\n const value = root?.[name];\n if (typeof value === \"string\") {\n safe[name] = 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 */\nfunction BottomRightCluster({ className, agentSlot, children, ...props }: BottomRightClusterProps) {\n return (\n <div\n data-slot=\"bottom-right-cluster\"\n className={cn(\"fixed right-6 bottom-6 z-40 flex flex-col items-end gap-3\", className)}\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. */\nconst HELP_PANEL_GAP = 12;\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\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.\n aria-controls={HELP_PANEL_ID}\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/* ── 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 AppShellProps extends Omit<React.ComponentProps<\"div\">, \"title\"> {\n extension: AppShellExtension;\n user: AppShellUser;\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\nfunction AppShell({\n className,\n extension,\n user,\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 const clusterRef = React.useRef<HTMLDivElement>(null);\n const [clusterHeight, setClusterHeight] = React.useState(40);\n React.useEffect(() => {\n const el = clusterRef.current;\n if (!el || typeof ResizeObserver === \"undefined\") {\n return;\n }\n const measure = () => setClusterHeight(el.getBoundingClientRect().height);\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(el);\n return () => observer.disconnect();\n }, [agentSlot, helpOpen]);\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 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 {...props}\n >\n <header\n data-slot=\"app-shell-header\"\n className={cn(\n \"sticky top-0 z-30 flex h-16 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 >\n <div data-slot=\"app-shell-brand\" className=\"flex min-w-0 items-center gap-2.5\">\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 <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 <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 <main data-slot=\"app-shell-main\" className=\"min-w-0 flex-1\">\n {children}\n </main>\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, the cluster the\n // panel must clear, and 12px between the two.\n style={{ bottom: CLUSTER_INSET + clusterHeight + HELP_PANEL_GAP }}\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 top-6 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={clusterRef} agentSlot={agentSlot}>\n <ThemeToggle storageKey={themeStorageKey} />\n {hasHelp ? (\n <HelpButton\n ref={helpButtonRef}\n open={helpOpen}\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" | "
|
|
6
|
+
variant?: "default" | "success" | "warn" | "destructive" | "danger" | "info" | "warning" | "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;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
"use client";
|
|
3
3
|
import { cn } from "../lib/cn.js";
|
|
4
|
-
import { renderWith } from "../lib/render.js";
|
|
5
4
|
import { buttonVariants } from "./button.js";
|
|
5
|
+
import { renderWith } from "../lib/render.js";
|
|
6
6
|
//#region src/components/button-link.tsx
|
|
7
7
|
function ButtonLink({ className, variant, size, render, disabled, children, ...props }) {
|
|
8
8
|
const classes = cn(buttonVariants({
|