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.
Files changed (138) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/LICENSE +7 -0
  3. package/README.md +235 -3
  4. package/dist/a2ui/views.js +2 -2
  5. package/dist/agent-chat/a2ui-block.d.ts +60 -0
  6. package/dist/agent-chat/a2ui-block.js +69 -0
  7. package/dist/agent-chat/a2ui-block.js.map +1 -0
  8. package/dist/agent-chat/agui-client.d.ts +40 -0
  9. package/dist/agent-chat/agui-client.js +251 -0
  10. package/dist/agent-chat/agui-client.js.map +1 -0
  11. package/dist/agent-chat/bridge.d.ts +109 -0
  12. package/dist/agent-chat/bridge.js +353 -0
  13. package/dist/agent-chat/bridge.js.map +1 -0
  14. package/dist/agent-chat/session.d.ts +79 -0
  15. package/dist/agent-chat/session.js +391 -0
  16. package/dist/agent-chat/session.js.map +1 -0
  17. package/dist/agent-chat/step-label.d.ts +99 -0
  18. package/dist/agent-chat/step-label.js +116 -0
  19. package/dist/agent-chat/step-label.js.map +1 -0
  20. package/dist/agent-chat/store.d.ts +102 -0
  21. package/dist/agent-chat/store.js +876 -0
  22. package/dist/agent-chat/store.js.map +1 -0
  23. package/dist/agent-chat/types.d.ts +277 -0
  24. package/dist/agent-chat/types.js +17 -0
  25. package/dist/agent-chat/types.js.map +1 -0
  26. package/dist/agent-chat.d.ts +11 -0
  27. package/dist/agent-chat.js +11 -0
  28. package/dist/components/admin-permissions/admin-permissions.d.ts +66 -0
  29. package/dist/components/admin-permissions/admin-permissions.js +101 -0
  30. package/dist/components/admin-permissions/admin-permissions.js.map +1 -0
  31. package/dist/components/admin-permissions/context.d.ts +70 -0
  32. package/dist/components/admin-permissions/context.js +258 -0
  33. package/dist/components/admin-permissions/context.js.map +1 -0
  34. package/dist/components/admin-permissions/index.d.ts +10 -0
  35. package/dist/components/admin-permissions/licence.d.ts +15 -0
  36. package/dist/components/admin-permissions/licence.js +78 -0
  37. package/dist/components/admin-permissions/licence.js.map +1 -0
  38. package/dist/components/admin-permissions/matrix.d.ts +20 -0
  39. package/dist/components/admin-permissions/matrix.js +191 -0
  40. package/dist/components/admin-permissions/matrix.js.map +1 -0
  41. package/dist/components/admin-permissions/members.d.ts +18 -0
  42. package/dist/components/admin-permissions/members.js +185 -0
  43. package/dist/components/admin-permissions/members.js.map +1 -0
  44. package/dist/components/admin-permissions/role-assignment.d.ts +35 -0
  45. package/dist/components/admin-permissions/role-assignment.js +174 -0
  46. package/dist/components/admin-permissions/role-assignment.js.map +1 -0
  47. package/dist/components/admin-permissions/roles.d.ts +25 -0
  48. package/dist/components/admin-permissions/roles.js +168 -0
  49. package/dist/components/admin-permissions/roles.js.map +1 -0
  50. package/dist/components/admin-permissions/types.d.ts +152 -0
  51. package/dist/components/admin-permissions/types.js +63 -0
  52. package/dist/components/admin-permissions/types.js.map +1 -0
  53. package/dist/components/agent-chat-popup.d.ts +29 -0
  54. package/dist/components/agent-chat-popup.js +188 -0
  55. package/dist/components/agent-chat-popup.js.map +1 -0
  56. package/dist/components/agent-chat.d.ts +163 -0
  57. package/dist/components/agent-chat.js +673 -0
  58. package/dist/components/agent-chat.js.map +1 -0
  59. package/dist/components/app-shell.d.ts +126 -0
  60. package/dist/components/app-shell.js +297 -0
  61. package/dist/components/app-shell.js.map +1 -0
  62. package/dist/components/badge.d.ts +1 -1
  63. package/dist/components/button-link.js +1 -1
  64. package/dist/components/button.d.ts +1 -1
  65. package/dist/components/checkbox.d.ts +1 -1
  66. package/dist/components/combobox.d.ts +1 -1
  67. package/dist/components/combobox.js +1 -1
  68. package/dist/components/consent-screen.d.ts +65 -0
  69. package/dist/components/consent-screen.js +123 -0
  70. package/dist/components/consent-screen.js.map +1 -0
  71. package/dist/components/data-table/data-table.d.ts +15 -1
  72. package/dist/components/data-table/data-table.js +18 -4
  73. package/dist/components/data-table/data-table.js.map +1 -1
  74. package/dist/components/data-table/index.d.ts +4 -4
  75. package/dist/components/data-table/parts.d.ts +27 -3
  76. package/dist/components/data-table/parts.js +175 -55
  77. package/dist/components/data-table/parts.js.map +1 -1
  78. package/dist/components/data-table/types.d.ts +61 -0
  79. package/dist/components/data-table/use-data-table.js +91 -6
  80. package/dist/components/data-table/use-data-table.js.map +1 -1
  81. package/dist/components/data-table/use-server-source.js +119 -28
  82. package/dist/components/data-table/use-server-source.js.map +1 -1
  83. package/dist/components/help-panel.d.ts +131 -0
  84. package/dist/components/help-panel.js +545 -0
  85. package/dist/components/help-panel.js.map +1 -0
  86. package/dist/components/login-screen.d.ts +127 -0
  87. package/dist/components/login-screen.js +339 -0
  88. package/dist/components/login-screen.js.map +1 -0
  89. package/dist/components/session-guard.d.ts +268 -0
  90. package/dist/components/session-guard.js +632 -0
  91. package/dist/components/session-guard.js.map +1 -0
  92. package/dist/components/toast.d.ts +1 -1
  93. package/dist/core.d.ts +5 -1
  94. package/dist/core.js +11 -7
  95. package/dist/data-table.d.ts +13 -4
  96. package/dist/data-table.js +10 -2
  97. package/dist/hooks/use-cortena-theme.js +49 -3
  98. package/dist/hooks/use-cortena-theme.js.map +1 -1
  99. package/dist/index.d.ts +17 -4
  100. package/dist/index.js +21 -8
  101. package/dist/markdown.d.ts +2 -1
  102. package/dist/markdown.js +2 -1
  103. package/package.json +18 -5
  104. package/src/agent-chat/a2ui-block.ts +118 -0
  105. package/src/agent-chat/agui-client.ts +405 -0
  106. package/src/agent-chat/bridge.ts +445 -0
  107. package/src/agent-chat/session.ts +549 -0
  108. package/src/agent-chat/step-label.ts +177 -0
  109. package/src/agent-chat/store.ts +1234 -0
  110. package/src/agent-chat/types.ts +308 -0
  111. package/src/components/admin-permissions/admin-permissions.tsx +130 -0
  112. package/src/components/admin-permissions/context.tsx +376 -0
  113. package/src/components/admin-permissions/index.tsx +32 -0
  114. package/src/components/admin-permissions/licence.tsx +84 -0
  115. package/src/components/admin-permissions/matrix.tsx +257 -0
  116. package/src/components/admin-permissions/members.tsx +204 -0
  117. package/src/components/admin-permissions/role-assignment.tsx +239 -0
  118. package/src/components/admin-permissions/roles.tsx +169 -0
  119. package/src/components/admin-permissions/types.ts +231 -0
  120. package/src/components/agent-chat-popup.tsx +289 -0
  121. package/src/components/agent-chat.tsx +1006 -0
  122. package/src/components/app-shell.tsx +502 -0
  123. package/src/components/consent-screen.tsx +239 -0
  124. package/src/components/data-table/data-table.tsx +36 -0
  125. package/src/components/data-table/index.tsx +6 -1
  126. package/src/components/data-table/parts.tsx +223 -47
  127. package/src/components/data-table/types.ts +68 -0
  128. package/src/components/data-table/use-data-table.ts +152 -4
  129. package/src/components/data-table/use-server-source.ts +150 -12
  130. package/src/components/help-panel.tsx +765 -0
  131. package/src/components/login-screen.tsx +479 -0
  132. package/src/components/session-guard.tsx +1071 -0
  133. package/src/entries/agent-chat.ts +137 -0
  134. package/src/entries/core.ts +8 -0
  135. package/src/entries/data-table.ts +41 -0
  136. package/src/entries/markdown.ts +25 -0
  137. package/src/hooks/use-cortena-theme.ts +63 -4
  138. package/src/index.ts +6 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"help-panel.js","names":[],"sources":["../../src/components/help-panel.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * HelpPanel — the extension's own help, answered from its own functional\n * document.\n *\n * §12 of how-to-create-a-cortena-extension, audit rule P-13. Every extension\n * with a UI ships one: anchored bottom-right, collapsed by default, expanding\n * into a right-hand vertical panel. It fills `AppShell`'s help slot, so the\n * shell keeps the button, the open state and the corner geometry (the panel\n * stops 12px above the cluster so the button that opened it stays clickable).\n *\n * ```tsx\n * <AppShell\n * extension={{ id: \"tasks\", name: \"Tasks\" }}\n * user={user}\n * help={{ source: \"/docs/functional.md\" }}\n * helpPanel={helpPanelSlot({\n * // lazy: the document is fetched the first time help is opened\n * source: () => fetch(\"/docs/functional.md\").then((r) => r.text()),\n * documentUrl: \"/docs/functional.md\",\n * context: location.pathname,\n * })}\n * >\n * <HelpHotkeys />\n * …\n * </AppShell>\n * ```\n *\n * It is NOT the agent pop-up (§17) and the two must not be merged. Help\n * answers *about* the extension from a document; the agent *acts* in the\n * extension through its tools. Same corner, different affordances.\n *\n * Giving the functional document a runtime consumer is the whole point: a\n * stale document becomes a visibly wrong help panel rather than a file nobody\n * opens. So the panel renders the document itself — outline from its headings,\n * search over its text, `#heading` deep links, and \"open in new tab\" for the\n * whole thing — rather than a hand-written copy of it that can drift.\n *\n * `context` is the current route. On open the panel jumps to the section that\n * route is about, matched against the front matter's `routes:` map first and\n * against the headings second, so help opens on the part of the document the\n * user is looking at rather than at the top of it.\n *\n * Lives on the `cortena-ui/markdown` entry: it renders markdown, so it carries\n * react-markdown, and no extension that only draws a Button may pay for that.\n */\n\nimport { ExternalLink, Search, X } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Markdown, markdownSanitizeSchema } from \"@/components/markdown\";\nimport { Spinner } from \"@/components/spinner\";\nimport { cn } from \"@/lib/cn\";\n\n/* ── the document ────────────────────────────────────────────────────────── */\n\n/**\n * The markdown, or a loader for it. A function is called the first time the\n * panel is opened, which is what keeps a long document out of the initial\n * payload; keep its identity stable (module scope, or `useCallback`) or it is\n * called again on every parent render.\n */\nexport type HelpSource = string | (() => Promise<string>);\n\nexport interface HelpSection {\n /** Slug of the heading — the `#anchor` the outline links to. `\"\"` for the preamble. */\n id: string;\n /** 1–6 for a heading; 0 for the text before the first heading. */\n depth: number;\n /** Heading text with inline markdown removed. */\n title: string;\n /** The heading and everything under it, verbatim, so it can be re-rendered alone. */\n markdown: string;\n}\n\nexport interface HelpRoute {\n /** A route the extension serves, e.g. `/board`. */\n route: string;\n /** The section it is about: a heading, or that heading's slug. */\n target: string;\n}\n\nexport interface HelpDocument {\n /** `title:` from the front matter, else the first `#` heading. */\n title?: string;\n /** Every scalar key in the front matter. */\n frontMatter: Record<string, string>;\n /** The `route:` / `routes:` block, longest route first. */\n routes: HelpRoute[];\n /** The document without its front matter. */\n body: string;\n sections: HelpSection[];\n}\n\n/** Lowercase, punctuation to hyphens. The same slug the rendered heading gets. */\nfunction slugify(text: string): string {\n const slug = text\n .toLowerCase()\n .normalize(\"NFKD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return slug || \"section\";\n}\n\n/** Slugs in document order, with `-1`, `-2` for repeats, as GitHub numbers them. */\nfunction createSlugger(): (text: string) => string {\n const seen = new Map<string, number>();\n return (text) => {\n const base = slugify(text);\n const n = seen.get(base) ?? 0;\n seen.set(base, n + 1);\n return n === 0 ? base : `${base}-${n}`;\n };\n}\n\n/**\n * Heading text with the inline markdown taken off, so the outline entry and\n * the slug match what the rendered heading actually says. Without this a\n * `## [Reports](/reports)` heading slugs to `reports-reports` in the outline\n * and to `reports` in the document, and the deep link lands nowhere.\n */\nfunction plainText(md: string): string {\n return md\n .replace(/`([^`]*)`/g, \"$1\")\n .replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, \"$1\")\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, \"$1\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/[*_~]+/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nconst FRONT_MATTER = /^---[ \\t]*\\r?\\n([\\s\\S]*?)\\r?\\n---[ \\t]*(?:\\r?\\n|$)/;\nconst HEADING = /^(#{1,6})[ \\t]+(.+?)[ \\t]*#*[ \\t]*$/;\nconst FENCE = /^[ \\t]*(```|~~~)/;\n\nfunction unquote(value: string): string {\n const trimmed = value.trim();\n const quoted = /^(['\"])([\\s\\S]*)\\1$/.exec(trimmed);\n return quoted ? (quoted[2] ?? \"\") : trimmed;\n}\n\n/**\n * Parse the document once: front matter, then sections split at ATX headings.\n *\n * Exported because the route map and the outline are worth asserting on\n * without a browser, and because an extension that wants its own navigation\n * over the same document should read it the same way rather than write a\n * second parser.\n */\nexport function parseHelpDocument(source: string): HelpDocument {\n const matter = FRONT_MATTER.exec(source);\n const body = matter ? source.slice(matter[0].length) : source;\n\n const frontMatter: Record<string, string> = {};\n const routes: HelpRoute[] = [];\n if (matter) {\n let inRoutes = false;\n for (const line of (matter[1] ?? \"\").split(/\\r?\\n/)) {\n if (!line.trim() || line.trim().startsWith(\"#\")) continue;\n const indented = /^[ \\t]+/.test(line);\n const pair = /^[ \\t]*([^:]+):[ \\t]*(.*)$/.exec(line);\n if (!pair) continue;\n const key = unquote(pair[1] ?? \"\");\n const value = unquote(pair[2] ?? \"\");\n if (inRoutes && indented) {\n // ` /board: Working the board` — a route, and the section it is about.\n if (key && value) routes.push({ route: key, target: value });\n continue;\n }\n inRoutes = !indented && (key === \"route\" || key === \"routes\") && value === \"\";\n if (!inRoutes && !indented && key) frontMatter[key] = value;\n }\n }\n // Longest route first, so `/reports/exports` wins over `/reports`.\n routes.sort((a, b) => b.route.length - a.route.length);\n\n const slug = createSlugger();\n const sections: HelpSection[] = [];\n let current: HelpSection | undefined;\n let fence: string | undefined;\n for (const line of body.split(/\\r?\\n/)) {\n const fenced = FENCE.exec(line);\n if (fenced) {\n const mark = fenced[1] ?? \"\";\n fence = fence === undefined ? mark : fence === mark ? undefined : fence;\n }\n const heading = fence === undefined ? HEADING.exec(line) : null;\n if (heading) {\n const title = plainText(heading[2] ?? \"\");\n current = { id: slug(title), depth: (heading[1] ?? \"\").length, title, markdown: \"\" };\n sections.push(current);\n } else if (!current) {\n // Everything before the first heading is one unnamed section, so a\n // search that matches the opening paragraph still shows it.\n current = { id: \"\", depth: 0, title: \"\", markdown: \"\" };\n sections.push(current);\n }\n current.markdown += `${line}\\n`;\n }\n const preamble = sections[0];\n if (preamble && preamble.depth === 0 && !preamble.markdown.trim()) sections.shift();\n\n const title = frontMatter.title ?? sections.find((s) => s.depth === 1)?.title;\n return { title, frontMatter, routes, body, sections };\n}\n\nfunction normalizeRoute(route: string): string {\n const path = (route.split(/[?#]/)[0] ?? \"\").trim().toLowerCase();\n return path.length > 1 ? path.replace(/\\/+$/, \"\") : path;\n}\n\nfunction findSection(doc: HelpDocument, target: string): HelpSection | undefined {\n const wanted = target.trim().toLowerCase();\n return doc.sections.find(\n (s) =>\n s.id !== \"\" &&\n (s.id === wanted || s.id === slugify(wanted) || s.title.toLowerCase() === wanted),\n );\n}\n\n/**\n * The section a route is about: the front matter's map first (exact, then the\n * longest matching prefix, so `/tasks/123` finds `/tasks`), then a heading\n * whose slug is the route or its last segment.\n */\nexport function resolveHelpTarget(doc: HelpDocument, context?: string): string | undefined {\n if (!context) return undefined;\n const path = normalizeRoute(context);\n if (!path) return undefined;\n\n for (const { route, target } of doc.routes) {\n const candidate = normalizeRoute(route);\n const prefix = candidate === \"/\" ? \"/\" : `${candidate}/`;\n if (candidate === path || path.startsWith(prefix)) {\n const section = findSection(doc, target);\n if (section) return section.id;\n }\n }\n const segments = path.split(\"/\").filter(Boolean);\n for (const candidate of [path, segments.at(-1)]) {\n if (!candidate) continue;\n const section = findSection(doc, candidate);\n if (section) return section.id;\n }\n return undefined;\n}\n\n/* ── heading anchors ─────────────────────────────────────────────────────── */\n\ninterface HastNode {\n type?: string;\n tagName?: string;\n value?: string;\n properties?: Record<string, unknown>;\n children?: HastNode[];\n}\n\nfunction nodeText(node: HastNode): string {\n if (node.type === \"text\") return node.value ?? \"\";\n return (node.children ?? []).map(nodeText).join(\"\");\n}\n\n/**\n * Give every heading the slug the outline links to. It runs AFTER sanitize —\n * the ids are ours, not the document's, so they are added to trusted nodes\n * rather than smuggled through the sanitizer.\n */\nfunction rehypeHelpAnchors() {\n return (tree: HastNode) => {\n const slug = createSlugger();\n const walk = (node: HastNode) => {\n const tag = node.tagName ?? \"\";\n if (/^h[1-6]$/.test(tag)) {\n node.properties = { ...node.properties, id: slug(plainText(nodeText(node))) };\n }\n for (const child of node.children ?? []) walk(child);\n };\n walk(tree);\n };\n}\n\n/* ── keyboard ────────────────────────────────────────────────────────────── */\n\nfunction isTypingTarget(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) return false;\n if (target.isContentEditable) return true;\n const tag = target.tagName;\n if (tag === \"TEXTAREA\" || tag === \"SELECT\") return true;\n if (tag !== \"INPUT\") return target.closest('[role=\"textbox\"], [role=\"searchbox\"]') !== null;\n const type = (target as HTMLInputElement).type;\n return type !== \"checkbox\" && type !== \"radio\" && type !== \"button\" && type !== \"submit\";\n}\n\n/**\n * The shell owns the open state and exposes only its button, so with no\n * `onOpen` the shortcut presses exactly the control a pointer would. Already\n * open (the button says so) means `?` is a no-op rather than a close.\n */\nfunction pressHelpButton(): void {\n const button = document.querySelector<HTMLElement>('[data-slot=\"help-button\"]');\n if (!button || button.getAttribute(\"aria-expanded\") === \"true\") return;\n button.click();\n}\n\nexport interface HelpHotkeysOptions {\n /** Whether the panel is open. Escape only closes when it is. */\n open?: boolean;\n /** Defaults to pressing the shell's help button. */\n onOpen?: () => void;\n onClose?: () => void;\n /** Turn the shortcuts off without unmounting. */\n disabled?: boolean;\n}\n\n/**\n * Escape closes, `?` opens — the second only when focus is not in an input,\n * because `?` is a character before it is a shortcut and stealing it from\n * someone typing a question into a filter box is worse than having no\n * shortcut at all.\n *\n * `HelpPanel` calls this itself for Escape. Mount `<HelpHotkeys />` in the\n * shell's children for `?`, which has to be listening while the panel is\n * closed and therefore unmounted.\n */\nexport function useHelpHotkeys({ open, onOpen, onClose, disabled }: HelpHotkeysOptions = {}): void {\n const state = React.useRef({ open, onOpen, onClose });\n state.current = { open, onOpen, onClose };\n React.useEffect(() => {\n if (disabled) return;\n const onKeyDown = (event: KeyboardEvent) => {\n const { open: isOpen, onOpen: openIt, onClose: closeIt } = state.current;\n if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return;\n if (event.key === \"Escape\") {\n if (isOpen && closeIt) closeIt();\n return;\n }\n if (event.key !== \"?\" || isOpen || isTypingTarget(event.target)) return;\n event.preventDefault();\n (openIt ?? pressHelpButton)();\n };\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, [disabled]);\n}\n\nexport interface HelpHotkeysProps extends HelpHotkeysOptions {}\n\n/** `useHelpHotkeys` as a component, for a shell whose children are markup. */\nexport function HelpHotkeys(props: HelpHotkeysProps): null {\n useHelpHotkeys(props);\n return null;\n}\n\n/**\n * `documentUrl` when it is safe to hang off an `href`, `undefined` otherwise.\n *\n * A relative URL is resolved against the current page first, so \"/docs/x.md\"\n * — the ordinary case — passes on any http(s) origin and is not special-cased\n * into a hole. `javascript:`, `data:`, `blob:` and `vbscript:` all fall out of\n * the same allow-list rather than each needing to be thought of.\n */\nexport function httpDocumentUrl(documentUrl: string | undefined): string | undefined {\n if (!documentUrl) return undefined;\n try {\n const base = typeof location === \"undefined\" ? undefined : location.href;\n const { protocol } = base === undefined ? new URL(documentUrl) : new URL(documentUrl, base);\n return protocol === \"http:\" || protocol === \"https:\" ? documentUrl : undefined;\n } catch {\n // Not a URL. No anchor at all beats an anchor that goes nowhere.\n return undefined;\n }\n}\n\n/* ── the panel ───────────────────────────────────────────────────────────── */\n\nexport interface HelpPanelProps extends Omit<React.ComponentProps<\"div\">, \"title\" | \"onSelect\"> {\n /** The functional document, as markdown. A function is loaded on first open. */\n source: HelpSource;\n /** From the shell's help slot. `false` renders nothing and loads nothing. */\n open?: boolean;\n /** Closes the panel: the header button and Escape. */\n onClose?: () => void;\n /** The current route. Opens the panel on the section that route is about. */\n context?: string;\n /** Header title. Defaults to the front matter's `title:`, then the first `#`. */\n title?: string;\n /**\n * Where \"open in new tab\" goes — the document's own URL, so the tab can be\n * shared. Without one the panel opens the text it already has.\n *\n * **http(s) only.** The value reaches an `href` the user clicks, and the\n * panel's source is the extension's functional document, which an extension\n * may well be fetching from somewhere configurable. A `javascript:` URL in\n * that slot is script execution one click away, and `data:` opens a document\n * of the author's choosing on a tab the user believes is the help page.\n */\n documentUrl?: string;\n /** Deepest heading level in the outline. 3 by default; 6 shows every heading. */\n outlineDepth?: number;\n /** Start with the search box filled. For the guide and for tests. */\n defaultQuery?: string;\n}\n\nconst headerButton = [\n \"inline-flex size-8 shrink-0 items-center justify-center rounded-[var(--ds-radius-md)]\",\n \"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 \"[&_svg]:size-4\",\n].join(\" \");\n\n/**\n * Documents already loaded, by the loader that produced them.\n *\n * `helpPanelSlot` returns `null` while closed, so closing the panel UNMOUNTS\n * it and takes its state with it. Every reopen therefore re-ran the loader:\n * on a `fetch` that is a network round trip per open, and on a slow one the\n * panel shows a skeleton for a document the user read a minute ago. The cache\n * is keyed by the loader itself and held weakly, so a caller that memoises its\n * loader — the documented shape — gets the document back instantly, and one\n * that builds a new closure each render is simply not cached rather than\n * leaking.\n */\nconst loadedDocuments = /* @__PURE__ */ new WeakMap<object, string>();\n\nfunction HelpPanel({\n source,\n open = true,\n onClose,\n context,\n title,\n documentUrl,\n outlineDepth = 3,\n defaultQuery = \"\",\n className,\n ...props\n}: HelpPanelProps) {\n const [text, setText] = React.useState<string | undefined>(\n typeof source === \"string\" ? source : loadedDocuments.get(source),\n );\n const [failed, setFailed] = React.useState(false);\n const [query, setQuery] = React.useState(defaultQuery);\n const [activeId, setActiveId] = React.useState<string>();\n const bodyRef = React.useRef<HTMLDivElement>(null);\n const panelRef = React.useRef<HTMLDivElement>(null);\n\n useHelpHotkeys({ open, onClose });\n\n /**\n * Focus moves into the panel when it opens.\n *\n * The shell's help button stays where it is, so without this, focus is\n * still on the button: Escape works (the handler is on the document) but\n * Tab walks the page BEHIND the panel, and a screen reader announces\n * nothing at all — the panel is simply not where the user is. The search\n * box is the target, because searching is what the panel is for.\n */\n React.useEffect(() => {\n if (!open) return;\n // After paint, so the element exists and Base UI's own focus work — the\n // pop-up, a dialog the panel is rendered beside — has settled.\n const frame = requestAnimationFrame(() => {\n const root = panelRef.current;\n if (!root || root.contains(document.activeElement)) return;\n // `preventScroll`: focusing scrolls an element into view by default, and\n // the panel has just opened on the section the current route is about.\n // Scrolling it back to the top would undo exactly that.\n root\n .querySelector<HTMLElement>('[data-slot=\"help-panel-search-input\"]')\n ?.focus({ preventScroll: true });\n });\n return () => cancelAnimationFrame(frame);\n }, [open]);\n\n React.useEffect(() => {\n if (!open) return;\n if (typeof source === \"string\") {\n setText(source);\n setFailed(false);\n return;\n }\n const cached = loadedDocuments.get(source);\n if (cached !== undefined) {\n setText(cached);\n setFailed(false);\n return;\n }\n let cancelled = false;\n setFailed(false);\n source().then(\n (loaded) => {\n loadedDocuments.set(source, loaded);\n if (!cancelled) setText(loaded);\n },\n () => {\n if (!cancelled) setFailed(true);\n },\n );\n return () => {\n cancelled = true;\n };\n }, [open, source]);\n\n const doc = React.useMemo(() => parseHelpDocument(text ?? \"\"), [text]);\n const needle = query.trim().toLowerCase();\n const matches = React.useMemo(\n () =>\n needle\n ? doc.sections.filter((section) => section.markdown.toLowerCase().includes(needle))\n : doc.sections,\n [doc, needle],\n );\n const markdown = needle ? matches.map((section) => section.markdown).join(\"\\n\") : doc.body;\n const outline = matches.filter((s) => s.depth >= 1 && s.depth <= outlineDepth);\n\n // Where the last programmatic scroll left the panel, so a re-align after the\n // document reflows can tell \"nobody has scrolled since\" from \"the user has\".\n const scrolledTo = React.useRef<number | undefined>(undefined);\n const scrollTo = React.useCallback((id: string, onlyIfUntouched = false) => {\n const scroller = bodyRef.current;\n const heading = scroller?.querySelector<HTMLElement>(`#${CSS.escape(id)}`);\n if (!scroller || !heading) return false;\n if (onlyIfUntouched && scrolledTo.current !== scroller.scrollTop) return false;\n // Not `scrollIntoView`: the panel is a scroller inside a fixed aside, and\n // scrollIntoView would scroll the page behind it as well.\n scroller.scrollTop += heading.getBoundingClientRect().top - scroller.getBoundingClientRect().top;\n scrolledTo.current = scroller.scrollTop;\n setActiveId(id);\n return true;\n }, []);\n\n // Open on the section the current route is about, once the document is here.\n const target = React.useMemo(() => resolveHelpTarget(doc, context), [doc, context]);\n React.useEffect(() => {\n if (!open || !target || needle) return;\n let cancelled = false;\n scrollTo(target);\n // Twice more, and only while the user has not scrolled since: the markdown\n // mounts in the same commit, and the display face lands after that. A\n // heading that grows 4px once the font arrives leaves the jump short of\n // the section it was supposed to open on.\n const again = () => {\n if (!cancelled) scrollTo(target, scrolledTo.current !== undefined);\n };\n const frame = requestAnimationFrame(again);\n void document.fonts?.ready.then(again);\n return () => {\n cancelled = true;\n cancelAnimationFrame(frame);\n };\n }, [open, target, needle, scrollTo]);\n\n // \"Open in new tab\" without a URL still has the text, so it opens that. A\n // `documentUrl` this panel refused counts as not having one.\n const safeDocumentUrl = httpDocumentUrl(documentUrl);\n const [fallbackUrl, setFallbackUrl] = React.useState<string>();\n React.useEffect(() => {\n if (safeDocumentUrl || text === undefined) return;\n const url = URL.createObjectURL(new Blob([text], { type: \"text/markdown;charset=utf-8\" }));\n setFallbackUrl(url);\n return () => URL.revokeObjectURL(url);\n }, [safeDocumentUrl, text]);\n // The blob: fallback is ours — made here from text already in the panel —\n // so it is not put through the same check.\n const href = safeDocumentUrl ?? fallbackUrl;\n\n if (!open) return null;\n\n const heading = title ?? doc.title ?? \"Help\";\n const loading = text === undefined && !failed;\n\n return (\n <div\n ref={panelRef}\n data-slot=\"help-panel\"\n data-loading={loading || undefined}\n className={cn(\n \"flex h-full min-h-0 w-full flex-col overflow-hidden\",\n \"bg-[var(--ds-card)] text-[color:var(--ds-foreground)]\",\n className,\n )}\n {...props}\n >\n <header\n data-slot=\"help-panel-header\"\n className=\"flex shrink-0 items-center gap-2 border-b border-[var(--ds-border-subtle)] px-4 py-3\"\n >\n <h2\n data-slot=\"help-panel-title\"\n className={cn(\n \"min-w-0 flex-1 truncate font-display font-bold\",\n \"text-[length:var(--ds-text-body-sm)] leading-[var(--ds-text-body-sm--line-height)]\",\n \"text-[color:var(--ds-foreground)]\",\n )}\n >\n {heading}\n </h2>\n {href ? (\n <a\n data-slot=\"help-panel-open-full\"\n href={href}\n target=\"_blank\"\n rel=\"noreferrer\"\n aria-label=\"Open the full document in a new tab\"\n className={headerButton}\n >\n <ExternalLink aria-hidden />\n </a>\n ) : null}\n {onClose ? (\n <button\n type=\"button\"\n data-slot=\"help-panel-close\"\n aria-label=\"Close help\"\n onClick={onClose}\n className={headerButton}\n >\n <X aria-hidden />\n </button>\n ) : null}\n </header>\n\n <div\n data-slot=\"help-panel-search\"\n className=\"shrink-0 border-b border-[var(--ds-border-subtle)] px-4 py-3\"\n >\n <div className=\"relative\">\n <Search\n aria-hidden\n className=\"pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-[color:var(--ds-text-tertiary)]\"\n />\n <input\n type=\"search\"\n data-slot=\"help-panel-search-input\"\n aria-label={`Search ${heading}`}\n placeholder=\"Search this document\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n className={cn(\n \"h-9 w-full rounded-[var(--ds-radius-md)] border border-[var(--ds-border)]\",\n \"bg-[var(--ds-muted)] py-2 pr-3 pl-8\",\n \"text-[length:var(--ds-text-caption-lg)] leading-[var(--ds-text-caption-lg--line-height)]\",\n \"text-[color:var(--ds-foreground)] placeholder:text-[color:var(--ds-text-tertiary)]\",\n \"transition-[border-color,box-shadow] duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]\",\n \"outline-none hover:border-[var(--ds-border-strong)]\",\n \"focus-visible:border-[var(--ds-ring)] focus-visible:ring-[3px] focus-visible:ring-[var(--ds-ring)]/40\",\n \"[&::-webkit-search-cancel-button]:hidden\",\n )}\n />\n </div>\n </div>\n\n {outline.length > 0 ? (\n <nav\n data-slot=\"help-panel-outline\"\n aria-label={`${heading} sections`}\n className=\"max-h-40 shrink-0 overflow-y-auto border-b border-[var(--ds-border-subtle)] px-2 py-2\"\n >\n <ol className=\"flex flex-col\">\n {outline.map((section) => (\n <li key={section.id}>\n <a\n data-slot=\"help-panel-outline-item\"\n data-depth={section.depth}\n data-active={section.id === activeId || undefined}\n href={`#${section.id}`}\n onClick={(event) => {\n // The anchor is a real deep link — copyable, and it works\n // in a new tab — but inside an SPA the panel scrolls\n // itself rather than handing the route a hash.\n event.preventDefault();\n scrollTo(section.id);\n }}\n className={cn(\n \"block truncate rounded-[var(--ds-radius-sm)] px-2 py-1 no-underline\",\n \"text-[length:var(--ds-text-caption)] leading-[var(--ds-text-caption--line-height)]\",\n \"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 \"data-[active]:bg-[var(--ds-primary-soft)] data-[active]:text-[color:var(--ds-primary-soft-foreground)]\",\n section.depth === 2 && \"pl-5\",\n section.depth >= 3 && \"pl-8\",\n )}\n >\n {section.title}\n </a>\n </li>\n ))}\n </ol>\n </nav>\n ) : null}\n\n <div\n ref={bodyRef}\n data-slot=\"help-panel-body\"\n className=\"min-h-0 flex-1 overflow-y-auto px-4 py-3\"\n >\n {loading ? (\n <div data-slot=\"help-panel-loading\" className=\"flex items-center gap-2 py-6\">\n <Spinner />\n <span className=\"text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]\">\n Loading help…\n </span>\n </div>\n ) : failed ? (\n <p\n data-slot=\"help-panel-error\"\n className=\"py-6 text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-destructive)]\"\n >\n The help document could not be loaded.\n </p>\n ) : needle && matches.length === 0 ? (\n <p\n data-slot=\"help-panel-empty\"\n className=\"py-6 text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]\"\n >\n No section matches “{query.trim()}”.\n </p>\n ) : (\n <Markdown\n compact\n sanitizeSchema={markdownSanitizeSchema}\n rehypePlugins={[rehypeHelpAnchors]}\n >\n {markdown}\n </Markdown>\n )}\n </div>\n </div>\n );\n}\n\n/* ── the shell slot ──────────────────────────────────────────────────────── */\n\nexport interface HelpPanelSlotConfig extends Omit<HelpPanelProps, \"open\" | \"onClose\"> {}\n\n/** What `AppShell` hands its `helpPanel` render function. */\ninterface ShellHelpSlot {\n open: boolean;\n source?: string;\n onClose: () => void;\n}\n\n/**\n * `AppShell`'s `helpPanel` slot, filled.\n *\n * The shell renders whatever the function returns, open or closed, so this\n * returns `null` while closed — a panel that rendered an empty box would draw\n * the shell's bordered aside over the corner with nothing in it. `help.source`\n * from the shell is used when no `source` is passed here.\n */\nexport function helpPanelSlot(\n config: HelpPanelSlotConfig,\n): (slot: ShellHelpSlot) => React.ReactNode {\n return ({ open, source, onClose }) =>\n open ? (\n <HelpPanel {...config} source={config.source ?? source ?? \"\"} open onClose={onClose} />\n ) : null;\n}\n\nexport { HelpPanel };\n"],"mappings":"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"}
@@ -0,0 +1,127 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ //#region src/components/login-screen.d.ts
4
+ /**
5
+ * LoginScreen — the one sign-in composition for Cortena
6
+ * (how-to-create-a-cortena-extension §7.4, audit rule P-08, decision DESIGN-D1).
7
+ *
8
+ * Every surface that asks a user to sign in — cortenaweb, cortena-auth's hosted
9
+ * page, every extension, and the MCP consent page through `ConsentScreen` —
10
+ * renders this. The failure it removes is a fleet of near-identical login pages
11
+ * that differ in wording, spacing and which methods they offer, so a user
12
+ * cannot tell whether the page in front of them is Cortena's.
13
+ *
14
+ * It is props-only and fetches nothing. The org's `auth-info` call, the
15
+ * Firebase REST sign-in and the popup all stay in the app: this package cannot
16
+ * take a dependency on firebase, and a component that fetches cannot be
17
+ * rendered in the guide, in a test, or in a story. The caller passes
18
+ * `methods` and handles `onPassword` / `onProvider`.
19
+ *
20
+ * Two rules it encodes, because both were shipped wrong:
21
+ *
22
+ * Offer what the org advertises, and nothing else. An extension once shipped
23
+ * with email/password only; every Google-registered user in the org was
24
+ * locked out and the screen said nothing about why. `methods` is the gate,
25
+ * and it defaults to `["password"]` rather than to an empty screen.
26
+ *
27
+ * A session that ends lands here, not on a broken screen (§9.4, P-37). Pass
28
+ * `signedOut` for "You were signed out" and `returnTo` for where the user
29
+ * was, so signing in again is a return rather than a restart.
30
+ */
31
+ export interface AuthScreenBrand {
32
+ /** Registered mark id — what the extension puts in `x-cortena.icon`. */
33
+ id?: string;
34
+ /** Display name, beside the mark. */
35
+ name: string;
36
+ /**
37
+ * An arbitrary image, for a brand with no registered mark (an MCP client).
38
+ * **https only** — see `httpsLogoUrl`. Anything else falls back to the mark.
39
+ */
40
+ logoUrl?: string;
41
+ }
42
+ /**
43
+ * The logo URL to render, or `undefined`.
44
+ *
45
+ * A logo on a sign-in or consent screen is the one image on the page that
46
+ * comes from outside Cortena: an MCP client registers it, and the screen it
47
+ * appears on is where a user decides whether to hand that client their
48
+ * credentials. So the scheme is checked rather than trusted.
49
+ *
50
+ * Only `https:` passes. `data:` and `blob:` let a registration inline
51
+ * arbitrary bytes — an SVG among them, which the browser will render with its
52
+ * own scripts if it is ever loaded as a document — and `http:` is a mixed
53
+ * -content image that any network can replace with the attacker's logo of
54
+ * choice. A relative URL resolves against the current page and so passes only
55
+ * on an https origin, which is where sign-in belongs.
56
+ *
57
+ * `javascript:` never reaches an `<img src>` as code, but it has no business
58
+ * being one either; it falls out of the same rule rather than needing its own.
59
+ */
60
+ export declare function httpsLogoUrl(logoUrl: string | undefined): string | undefined;
61
+ export interface AuthScreenProps extends Omit<React.ComponentProps<"div">, "title"> {
62
+ brand: AuthScreenBrand;
63
+ /** The `<h1>`. */
64
+ title: React.ReactNode;
65
+ /** One line under the title. */
66
+ subtitle?: React.ReactNode;
67
+ /** Rendered above the title, for the signed-out notice and the error. */
68
+ notices?: React.ReactNode;
69
+ /** Rendered under the card, for `returnTo` and small print. */
70
+ footer?: React.ReactNode;
71
+ }
72
+ /**
73
+ * The chrome both screens wear: one centred card on the canvas.
74
+ *
75
+ * Centring is explicit (`place-items-center`, and `mx-auto` on the column)
76
+ * because a max-width alone leaves the card against the left edge on a wide
77
+ * monitor — which shipped once already. The column is `w-full` under a
78
+ * `max-w-sm`, so it fits a 360px viewport with the page padding intact.
79
+ */
80
+ declare function AuthScreen({ className, brand, title, subtitle, notices, footer, children, ...props }: AuthScreenProps): React.JSX.Element;
81
+ /**
82
+ * The identity providers' own marks. These are the one justified colour waiver
83
+ * in the whole design system (§7.4): a provider's brand mark is prescribed by
84
+ * that provider and may not be recoloured, so it cannot come from a token. The
85
+ * waiver lives here so no consumer of cortena-ui ever needs one — which is the
86
+ * point of moving these buttons into the package.
87
+ */
88
+ declare function GoogleMark(): React.JSX.Element;
89
+ declare function MicrosoftMark(): React.JSX.Element;
90
+ /** The sign-in methods an org can advertise through `/api/orgs/:orgId/auth-info`. */
91
+ export type LoginMethod = "password" | "google" | "microsoft";
92
+ export interface LoginExtension {
93
+ /** Registered mark id, from `x-cortena.icon` in the OpenAPI document (§9.1). */
94
+ id: string;
95
+ /** Display name. "Sign in to <name>". */
96
+ name: string;
97
+ }
98
+ export interface PasswordCredentials {
99
+ email: string;
100
+ password: string;
101
+ }
102
+ export interface LoginScreenProps extends Omit<React.ComponentProps<"div">, "onSubmit"> {
103
+ /** The extension (or product) being signed into. Supplies the mark and the name. */
104
+ extension: LoginExtension;
105
+ /**
106
+ * What the org advertises. An empty or omitted list means password — never an
107
+ * empty screen, which is what a failed `auth-info` call would otherwise draw.
108
+ */
109
+ methods?: readonly LoginMethod[];
110
+ onPassword?: (credentials: PasswordCredentials) => void;
111
+ onProvider?: (provider: Exclude<LoginMethod, "password">) => void;
112
+ /** Shown above the card. A failed sign-in, in the user's words, not the API's. */
113
+ error?: string;
114
+ /** Disables every control and marks the submit button busy. */
115
+ loading?: boolean;
116
+ /**
117
+ * Where the user was when the session ended (§9.4). Shown, so the promise is
118
+ * visible before signing in; honouring it after sign-in is the caller's job.
119
+ */
120
+ returnTo?: string;
121
+ /** The signed-out state a failed refresh or an idle timeout lands on (P-37). */
122
+ signedOut?: boolean;
123
+ }
124
+ declare function LoginScreen({ className, extension, methods, onPassword, onProvider, error, loading, returnTo, signedOut, ...props }: LoginScreenProps): React.JSX.Element;
125
+ //#endregion
126
+ export { AuthScreen, GoogleMark, LoginScreen, MicrosoftMark };
127
+ //# sourceMappingURL=login-screen.d.ts.map
@@ -0,0 +1,339 @@
1
+ "use client";
2
+ "use client";
3
+ import { cn } from "../lib/cn.js";
4
+ import { Alert, AlertDescription, AlertIcon, AlertTitle } from "./alert.js";
5
+ import { BrandMark } from "./app-shell.js";
6
+ import { Button } from "./button.js";
7
+ import { Input } from "./input.js";
8
+ import { Label } from "./label.js";
9
+ import { Spinner } from "./spinner.js";
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
+ import * as React from "react";
12
+ //#region src/components/login-screen.tsx
13
+ /**
14
+ * The logo URL to render, or `undefined`.
15
+ *
16
+ * A logo on a sign-in or consent screen is the one image on the page that
17
+ * comes from outside Cortena: an MCP client registers it, and the screen it
18
+ * appears on is where a user decides whether to hand that client their
19
+ * credentials. So the scheme is checked rather than trusted.
20
+ *
21
+ * Only `https:` passes. `data:` and `blob:` let a registration inline
22
+ * arbitrary bytes — an SVG among them, which the browser will render with its
23
+ * own scripts if it is ever loaded as a document — and `http:` is a mixed
24
+ * -content image that any network can replace with the attacker's logo of
25
+ * choice. A relative URL resolves against the current page and so passes only
26
+ * on an https origin, which is where sign-in belongs.
27
+ *
28
+ * `javascript:` never reaches an `<img src>` as code, but it has no business
29
+ * being one either; it falls out of the same rule rather than needing its own.
30
+ */
31
+ function httpsLogoUrl(logoUrl) {
32
+ if (!logoUrl) return;
33
+ try {
34
+ const base = typeof location === "undefined" ? void 0 : location.href;
35
+ return (base === void 0 ? new URL(logoUrl) : new URL(logoUrl, base)).protocol === "https:" ? logoUrl : void 0;
36
+ } catch {
37
+ return;
38
+ }
39
+ }
40
+ /**
41
+ * The chrome both screens wear: one centred card on the canvas.
42
+ *
43
+ * Centring is explicit (`place-items-center`, and `mx-auto` on the column)
44
+ * because a max-width alone leaves the card against the left edge on a wide
45
+ * monitor — which shipped once already. The column is `w-full` under a
46
+ * `max-w-sm`, so it fits a 360px viewport with the page padding intact.
47
+ */
48
+ function AuthScreen({ className, brand, title, subtitle, notices, footer, children, ...props }) {
49
+ const logoUrl = httpsLogoUrl(brand.logoUrl);
50
+ return /* @__PURE__ */ jsx("div", {
51
+ "data-slot": "auth-screen",
52
+ className: cn("flex min-h-screen w-full flex-col items-center justify-center gap-6 px-4 py-10", "bg-[var(--ds-background)] text-[color:var(--ds-foreground)]", className),
53
+ ...props,
54
+ children: /* @__PURE__ */ jsxs("div", {
55
+ "data-slot": "auth-screen-column",
56
+ className: "mx-auto flex w-full max-w-sm flex-col items-stretch gap-4",
57
+ children: [
58
+ /* @__PURE__ */ jsxs("div", {
59
+ "data-slot": "auth-screen-brand",
60
+ className: "flex items-center justify-center gap-2.5",
61
+ children: [/* @__PURE__ */ jsx("span", {
62
+ "data-slot": "auth-screen-brand-square",
63
+ className: cn("flex size-8 shrink-0 items-center justify-center overflow-hidden", "rounded-[var(--ds-radius-md)] bg-[var(--ds-primary-soft)]", "text-[color:var(--ds-primary)]"),
64
+ children: logoUrl ? /* @__PURE__ */ jsx("img", {
65
+ src: logoUrl,
66
+ alt: "",
67
+ referrerPolicy: "no-referrer",
68
+ className: "size-5 object-contain"
69
+ }) : /* @__PURE__ */ jsx(BrandMark, {
70
+ id: brand.id ?? "",
71
+ name: brand.name,
72
+ size: 20,
73
+ decorative: true
74
+ })
75
+ }), /* @__PURE__ */ jsx("span", {
76
+ "data-slot": "auth-screen-brand-name",
77
+ className: cn("truncate font-display text-[length:var(--ds-text-body-md)] font-bold", "leading-[var(--ds-text-body-md--line-height)]"),
78
+ children: brand.name
79
+ })]
80
+ }),
81
+ notices ? /* @__PURE__ */ jsx("div", {
82
+ "data-slot": "auth-screen-notices",
83
+ className: "flex flex-col gap-3",
84
+ children: notices
85
+ }) : null,
86
+ /* @__PURE__ */ jsxs("div", {
87
+ "data-slot": "auth-screen-card",
88
+ className: cn("flex flex-col gap-5 rounded-[var(--ds-radius-lg)] border border-[var(--ds-border-subtle)]", "bg-[var(--ds-card)] p-6 shadow-[var(--ds-shadow-md)]"),
89
+ children: [/* @__PURE__ */ jsxs("div", {
90
+ "data-slot": "auth-screen-heading",
91
+ className: "flex flex-col gap-1.5",
92
+ children: [/* @__PURE__ */ jsx("h1", {
93
+ "data-slot": "auth-screen-title",
94
+ className: cn("font-display text-[length:var(--ds-text-h3)] font-bold", "leading-[var(--ds-text-h3--line-height)] text-[color:var(--ds-foreground)]"),
95
+ children: title
96
+ }), subtitle ? /* @__PURE__ */ jsx("p", {
97
+ "data-slot": "auth-screen-subtitle",
98
+ className: cn("text-[length:var(--ds-text-caption-lg)]", "leading-[var(--ds-text-caption-lg--line-height)]", "text-[color:var(--ds-muted-foreground)]"),
99
+ children: subtitle
100
+ }) : null]
101
+ }), children]
102
+ }),
103
+ footer ? /* @__PURE__ */ jsx("div", {
104
+ "data-slot": "auth-screen-footer",
105
+ className: cn("text-center text-[length:var(--ds-text-caption)]", "leading-[var(--ds-text-caption--line-height)]", "text-[color:var(--ds-muted-foreground)]"),
106
+ children: footer
107
+ }) : null
108
+ ]
109
+ })
110
+ });
111
+ }
112
+ /**
113
+ * The identity providers' own marks. These are the one justified colour waiver
114
+ * in the whole design system (§7.4): a provider's brand mark is prescribed by
115
+ * that provider and may not be recoloured, so it cannot come from a token. The
116
+ * waiver lives here so no consumer of cortena-ui ever needs one — which is the
117
+ * point of moving these buttons into the package.
118
+ */
119
+ function GoogleMark() {
120
+ return /* @__PURE__ */ jsxs("svg", {
121
+ viewBox: "0 0 18 18",
122
+ "aria-hidden": true,
123
+ focusable: "false",
124
+ className: "size-4",
125
+ children: [
126
+ /* @__PURE__ */ jsx("path", {
127
+ fill: "#4285F4",
128
+ d: "M17.64 9.2c0-.64-.06-1.25-.16-1.84H9v3.48h4.84a4.14 4.14 0 0 1-1.8 2.72v2.26h2.92c1.7-1.57 2.68-3.88 2.68-6.62Z"
129
+ }),
130
+ /* @__PURE__ */ jsx("path", {
131
+ fill: "#34A853",
132
+ d: "M9 18c2.43 0 4.47-.8 5.96-2.18l-2.92-2.26c-.8.54-1.84.86-3.04.86-2.34 0-4.32-1.58-5.03-3.7H.96v2.33A9 9 0 0 0 9 18Z"
133
+ }),
134
+ /* @__PURE__ */ jsx("path", {
135
+ fill: "#FBBC05",
136
+ d: "M3.97 10.72a5.41 5.41 0 0 1 0-3.44V4.95H.96a9 9 0 0 0 0 8.1l3.01-2.33Z"
137
+ }),
138
+ /* @__PURE__ */ jsx("path", {
139
+ fill: "#EA4335",
140
+ d: "M9 3.58c1.32 0 2.5.45 3.44 1.35l2.58-2.59C13.46.89 11.43 0 9 0A9 9 0 0 0 .96 4.95l3.01 2.33C4.68 5.16 6.66 3.58 9 3.58Z"
141
+ })
142
+ ]
143
+ });
144
+ }
145
+ function MicrosoftMark() {
146
+ return /* @__PURE__ */ jsxs("svg", {
147
+ viewBox: "0 0 18 18",
148
+ "aria-hidden": true,
149
+ focusable: "false",
150
+ className: "size-4",
151
+ children: [
152
+ /* @__PURE__ */ jsx("path", {
153
+ fill: "#F25022",
154
+ d: "M0 0h8.5v8.5H0z"
155
+ }),
156
+ /* @__PURE__ */ jsx("path", {
157
+ fill: "#7FBA00",
158
+ d: "M9.5 0H18v8.5H9.5z"
159
+ }),
160
+ /* @__PURE__ */ jsx("path", {
161
+ fill: "#00A4EF",
162
+ d: "M0 9.5h8.5V18H0z"
163
+ }),
164
+ /* @__PURE__ */ jsx("path", {
165
+ fill: "#FFB900",
166
+ d: "M9.5 9.5H18V18H9.5z"
167
+ })
168
+ ]
169
+ });
170
+ }
171
+ const PROVIDERS = {
172
+ google: {
173
+ label: "Continue with Google",
174
+ Mark: GoogleMark
175
+ },
176
+ microsoft: {
177
+ label: "Continue with Microsoft",
178
+ Mark: MicrosoftMark
179
+ }
180
+ };
181
+ /** The order providers appear in, so two orgs never show them in different orders. */
182
+ const PROVIDER_ORDER = ["google", "microsoft"];
183
+ function LoginScreen({ className, extension, methods, onPassword, onProvider, error, loading, returnTo, signedOut, ...props }) {
184
+ const fieldId = React.useId();
185
+ const [email, setEmail] = React.useState("");
186
+ const [password, setPassword] = React.useState("");
187
+ const active = methods && methods.length > 0 ? methods : ["password"];
188
+ const showPassword = active.includes("password");
189
+ const providers = PROVIDER_ORDER.filter((p) => active.includes(p));
190
+ const submit = (event) => {
191
+ event.preventDefault();
192
+ if (loading) return;
193
+ onPassword?.({
194
+ email,
195
+ password
196
+ });
197
+ };
198
+ return /* @__PURE__ */ jsxs(AuthScreen, {
199
+ "data-slot": "login-screen",
200
+ "data-loading": loading ? "" : void 0,
201
+ className,
202
+ brand: {
203
+ id: extension.id,
204
+ name: extension.name
205
+ },
206
+ title: `Sign in to ${extension.name}`,
207
+ subtitle: signedOut ? void 0 : "Use your Cortena account.",
208
+ notices: /* @__PURE__ */ jsxs(Fragment, { children: [signedOut ? /* @__PURE__ */ jsxs(Alert, {
209
+ variant: "warn",
210
+ "data-slot": "login-screen-signed-out",
211
+ children: [/* @__PURE__ */ jsx(AlertIcon, { children: /* @__PURE__ */ jsx(SignedOutGlyph, {}) }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(AlertTitle, { children: "You were signed out" }), /* @__PURE__ */ jsx(AlertDescription, { children: "Sign in again to pick up where you left off." })] })]
212
+ }) : null, error ? /* @__PURE__ */ jsx(Alert, {
213
+ variant: "destructive",
214
+ "data-slot": "login-screen-error",
215
+ children: /* @__PURE__ */ jsx(AlertDescription, {
216
+ className: "text-[color:var(--ds-foreground)]",
217
+ children: error
218
+ })
219
+ }) : null] }),
220
+ footer: returnTo ? /* @__PURE__ */ jsxs("span", {
221
+ "data-slot": "login-screen-return-to",
222
+ children: ["You will return to ", /* @__PURE__ */ jsx("span", {
223
+ className: "font-mono",
224
+ children: returnTo
225
+ })]
226
+ }) : null,
227
+ ...props,
228
+ children: [
229
+ showPassword ? /* @__PURE__ */ jsxs("form", {
230
+ "data-slot": "login-screen-form",
231
+ className: "flex flex-col gap-4",
232
+ onSubmit: submit,
233
+ noValidate: true,
234
+ children: [
235
+ /* @__PURE__ */ jsxs("div", {
236
+ className: "flex flex-col gap-1.5",
237
+ children: [/* @__PURE__ */ jsx(Label, {
238
+ htmlFor: `${fieldId}-email`,
239
+ children: "Email"
240
+ }), /* @__PURE__ */ jsx(Input, {
241
+ id: `${fieldId}-email`,
242
+ name: "email",
243
+ type: "email",
244
+ autoComplete: "username",
245
+ autoFocus: true,
246
+ required: true,
247
+ disabled: loading,
248
+ "aria-invalid": error ? true : void 0,
249
+ value: email,
250
+ onChange: (event) => setEmail(event.target.value)
251
+ })]
252
+ }),
253
+ /* @__PURE__ */ jsxs("div", {
254
+ className: "flex flex-col gap-1.5",
255
+ children: [/* @__PURE__ */ jsx(Label, {
256
+ htmlFor: `${fieldId}-password`,
257
+ children: "Password"
258
+ }), /* @__PURE__ */ jsx(Input, {
259
+ id: `${fieldId}-password`,
260
+ name: "password",
261
+ type: "password",
262
+ autoComplete: "current-password",
263
+ required: true,
264
+ disabled: loading,
265
+ "aria-invalid": error ? true : void 0,
266
+ value: password,
267
+ onChange: (event) => setPassword(event.target.value)
268
+ })]
269
+ }),
270
+ /* @__PURE__ */ jsxs(Button, {
271
+ type: "submit",
272
+ size: "lg",
273
+ disabled: loading,
274
+ "data-slot": "login-screen-submit",
275
+ children: [loading ? /* @__PURE__ */ jsx(Spinner, { tone: "inverse" }) : null, loading ? "Signing in…" : "Sign in"]
276
+ })
277
+ ]
278
+ }) : null,
279
+ showPassword && providers.length > 0 ? /* @__PURE__ */ jsxs("div", {
280
+ "data-slot": "login-screen-divider",
281
+ className: "flex items-center gap-3",
282
+ "aria-hidden": true,
283
+ children: [
284
+ /* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-[var(--ds-border)]" }),
285
+ /* @__PURE__ */ jsx("span", {
286
+ className: cn("text-[length:var(--ds-text-caption)]", "text-[color:var(--ds-muted-foreground)]"),
287
+ children: "or"
288
+ }),
289
+ /* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-[var(--ds-border)]" })
290
+ ]
291
+ }) : null,
292
+ providers.length > 0 ? /* @__PURE__ */ jsx("div", {
293
+ "data-slot": "login-screen-providers",
294
+ className: "flex flex-col gap-2.5",
295
+ children: providers.map((provider) => {
296
+ const { label, Mark } = PROVIDERS[provider];
297
+ return /* @__PURE__ */ jsxs(Button, {
298
+ type: "button",
299
+ variant: "secondary",
300
+ size: "lg",
301
+ disabled: loading,
302
+ "data-provider": provider,
303
+ onClick: () => onProvider?.(provider),
304
+ children: [/* @__PURE__ */ jsx(Mark, {}), label]
305
+ }, provider);
306
+ })
307
+ }) : null
308
+ ]
309
+ });
310
+ }
311
+ /** The signed-out notice's glyph. Inline rather than a lucide import: one icon. */
312
+ function SignedOutGlyph() {
313
+ return /* @__PURE__ */ jsxs("svg", {
314
+ viewBox: "0 0 24 24",
315
+ fill: "none",
316
+ stroke: "currentColor",
317
+ strokeWidth: "2",
318
+ "aria-hidden": true,
319
+ children: [
320
+ /* @__PURE__ */ jsx("circle", {
321
+ cx: "12",
322
+ cy: "12",
323
+ r: "9"
324
+ }),
325
+ /* @__PURE__ */ jsx("path", {
326
+ d: "M12 8v5",
327
+ strokeLinecap: "round"
328
+ }),
329
+ /* @__PURE__ */ jsx("path", {
330
+ d: "M12 16h.01",
331
+ strokeLinecap: "round"
332
+ })
333
+ ]
334
+ });
335
+ }
336
+ //#endregion
337
+ export { AuthScreen, GoogleMark, LoginScreen, MicrosoftMark, httpsLogoUrl };
338
+
339
+ //# sourceMappingURL=login-screen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login-screen.js","names":[],"sources":["../../src/components/login-screen.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { Alert, AlertDescription, AlertIcon, AlertTitle } from \"@/components/alert\";\nimport { BrandMark } from \"@/components/app-shell\";\nimport { Button } from \"@/components/button\";\nimport { Input } from \"@/components/input\";\nimport { Label } from \"@/components/label\";\nimport { Spinner } from \"@/components/spinner\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * LoginScreen — the one sign-in composition for Cortena\n * (how-to-create-a-cortena-extension §7.4, audit rule P-08, decision DESIGN-D1).\n *\n * Every surface that asks a user to sign in — cortenaweb, cortena-auth's hosted\n * page, every extension, and the MCP consent page through `ConsentScreen` —\n * renders this. The failure it removes is a fleet of near-identical login pages\n * that differ in wording, spacing and which methods they offer, so a user\n * cannot tell whether the page in front of them is Cortena's.\n *\n * It is props-only and fetches nothing. The org's `auth-info` call, the\n * Firebase REST sign-in and the popup all stay in the app: this package cannot\n * take a dependency on firebase, and a component that fetches cannot be\n * rendered in the guide, in a test, or in a story. The caller passes\n * `methods` and handles `onPassword` / `onProvider`.\n *\n * Two rules it encodes, because both were shipped wrong:\n *\n * Offer what the org advertises, and nothing else. An extension once shipped\n * with email/password only; every Google-registered user in the org was\n * locked out and the screen said nothing about why. `methods` is the gate,\n * and it defaults to `[\"password\"]` rather than to an empty screen.\n *\n * A session that ends lands here, not on a broken screen (§9.4, P-37). Pass\n * `signedOut` for \"You were signed out\" and `returnTo` for where the user\n * was, so signing in again is a return rather than a restart.\n */\n\n/* ── the frame ───────────────────────────────────────────────────────────── */\n\nexport interface AuthScreenBrand {\n /** Registered mark id — what the extension puts in `x-cortena.icon`. */\n id?: string;\n /** Display name, beside the mark. */\n name: string;\n /**\n * An arbitrary image, for a brand with no registered mark (an MCP client).\n * **https only** — see `httpsLogoUrl`. Anything else falls back to the mark.\n */\n logoUrl?: string;\n}\n\n/**\n * The logo URL to render, or `undefined`.\n *\n * A logo on a sign-in or consent screen is the one image on the page that\n * comes from outside Cortena: an MCP client registers it, and the screen it\n * appears on is where a user decides whether to hand that client their\n * credentials. So the scheme is checked rather than trusted.\n *\n * Only `https:` passes. `data:` and `blob:` let a registration inline\n * arbitrary bytes — an SVG among them, which the browser will render with its\n * own scripts if it is ever loaded as a document — and `http:` is a mixed\n * -content image that any network can replace with the attacker's logo of\n * choice. A relative URL resolves against the current page and so passes only\n * on an https origin, which is where sign-in belongs.\n *\n * `javascript:` never reaches an `<img src>` as code, but it has no business\n * being one either; it falls out of the same rule rather than needing its own.\n */\nexport function httpsLogoUrl(logoUrl: string | undefined): string | undefined {\n if (!logoUrl) {\n return undefined;\n }\n try {\n // Relative URLs are resolved, not rejected: the check is about the scheme\n // the browser will actually fetch, and `new URL` is what decides that.\n const base = typeof location === \"undefined\" ? undefined : location.href;\n const url = base === undefined ? new URL(logoUrl) : new URL(logoUrl, base);\n return url.protocol === \"https:\" ? logoUrl : undefined;\n } catch {\n // Not a URL at all. A broken image is a worse answer than the mark.\n return undefined;\n }\n}\n\nexport interface AuthScreenProps extends Omit<React.ComponentProps<\"div\">, \"title\"> {\n brand: AuthScreenBrand;\n /** The `<h1>`. */\n title: React.ReactNode;\n /** One line under the title. */\n subtitle?: React.ReactNode;\n /** Rendered above the title, for the signed-out notice and the error. */\n notices?: React.ReactNode;\n /** Rendered under the card, for `returnTo` and small print. */\n footer?: React.ReactNode;\n}\n\n/**\n * The chrome both screens wear: one centred card on the canvas.\n *\n * Centring is explicit (`place-items-center`, and `mx-auto` on the column)\n * because a max-width alone leaves the card against the left edge on a wide\n * monitor — which shipped once already. The column is `w-full` under a\n * `max-w-sm`, so it fits a 360px viewport with the page padding intact.\n */\nfunction AuthScreen({\n className,\n brand,\n title,\n subtitle,\n notices,\n footer,\n children,\n ...props\n}: AuthScreenProps) {\n const logoUrl = httpsLogoUrl(brand.logoUrl);\n return (\n <div\n data-slot=\"auth-screen\"\n className={cn(\n \"flex min-h-screen w-full flex-col items-center justify-center gap-6 px-4 py-10\",\n \"bg-[var(--ds-background)] text-[color:var(--ds-foreground)]\",\n className,\n )}\n {...props}\n >\n <div\n data-slot=\"auth-screen-column\"\n className=\"mx-auto flex w-full max-w-sm flex-col items-stretch gap-4\"\n >\n <div data-slot=\"auth-screen-brand\" className=\"flex items-center justify-center gap-2.5\">\n <span\n data-slot=\"auth-screen-brand-square\"\n className={cn(\n \"flex size-8 shrink-0 items-center justify-center overflow-hidden\",\n \"rounded-[var(--ds-radius-md)] bg-[var(--ds-primary-soft)]\",\n \"text-[color:var(--ds-primary)]\",\n )}\n >\n {logoUrl ? (\n <img\n src={logoUrl}\n alt=\"\"\n // The URL is the client's, so the request must not tell it\n // which Cortena screen the user is standing on.\n referrerPolicy=\"no-referrer\"\n className=\"size-5 object-contain\"\n />\n ) : (\n <BrandMark id={brand.id ?? \"\"} name={brand.name} size={20} decorative />\n )}\n </span>\n <span\n data-slot=\"auth-screen-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)]\",\n )}\n >\n {brand.name}\n </span>\n </div>\n\n {notices ? (\n <div data-slot=\"auth-screen-notices\" className=\"flex flex-col gap-3\">\n {notices}\n </div>\n ) : null}\n\n <div\n data-slot=\"auth-screen-card\"\n className={cn(\n \"flex flex-col gap-5 rounded-[var(--ds-radius-lg)] border border-[var(--ds-border-subtle)]\",\n \"bg-[var(--ds-card)] p-6 shadow-[var(--ds-shadow-md)]\",\n )}\n >\n <div data-slot=\"auth-screen-heading\" className=\"flex flex-col gap-1.5\">\n <h1\n data-slot=\"auth-screen-title\"\n className={cn(\n \"font-display text-[length:var(--ds-text-h3)] font-bold\",\n \"leading-[var(--ds-text-h3--line-height)] text-[color:var(--ds-foreground)]\",\n )}\n >\n {title}\n </h1>\n {subtitle ? (\n <p\n data-slot=\"auth-screen-subtitle\"\n className={cn(\n \"text-[length:var(--ds-text-caption-lg)]\",\n \"leading-[var(--ds-text-caption-lg--line-height)]\",\n \"text-[color:var(--ds-muted-foreground)]\",\n )}\n >\n {subtitle}\n </p>\n ) : null}\n </div>\n {children}\n </div>\n\n {footer ? (\n <div\n data-slot=\"auth-screen-footer\"\n className={cn(\n \"text-center text-[length:var(--ds-text-caption)]\",\n \"leading-[var(--ds-text-caption--line-height)]\",\n \"text-[color:var(--ds-muted-foreground)]\",\n )}\n >\n {footer}\n </div>\n ) : null}\n </div>\n </div>\n );\n}\n\n/* ── provider marks ──────────────────────────────────────────────────────── */\n\n/**\n * The identity providers' own marks. These are the one justified colour waiver\n * in the whole design system (§7.4): a provider's brand mark is prescribed by\n * that provider and may not be recoloured, so it cannot come from a token. The\n * waiver lives here so no consumer of cortena-ui ever needs one — which is the\n * point of moving these buttons into the package.\n */\nfunction GoogleMark() {\n return (\n <svg viewBox=\"0 0 18 18\" aria-hidden focusable=\"false\" className=\"size-4\">\n {/* cortena-design-lint-disable-next-line -- Google brand blue; a provider mark may not be recoloured (§7.4) */}\n <path fill=\"#4285F4\" d=\"M17.64 9.2c0-.64-.06-1.25-.16-1.84H9v3.48h4.84a4.14 4.14 0 0 1-1.8 2.72v2.26h2.92c1.7-1.57 2.68-3.88 2.68-6.62Z\" />\n {/* cortena-design-lint-disable-next-line -- Google brand green; a provider mark may not be recoloured (§7.4) */}\n <path fill=\"#34A853\" d=\"M9 18c2.43 0 4.47-.8 5.96-2.18l-2.92-2.26c-.8.54-1.84.86-3.04.86-2.34 0-4.32-1.58-5.03-3.7H.96v2.33A9 9 0 0 0 9 18Z\" />\n {/* cortena-design-lint-disable-next-line -- Google brand yellow; a provider mark may not be recoloured (§7.4) */}\n <path fill=\"#FBBC05\" d=\"M3.97 10.72a5.41 5.41 0 0 1 0-3.44V4.95H.96a9 9 0 0 0 0 8.1l3.01-2.33Z\" />\n {/* cortena-design-lint-disable-next-line -- Google brand red; a provider mark may not be recoloured (§7.4) */}\n <path fill=\"#EA4335\" d=\"M9 3.58c1.32 0 2.5.45 3.44 1.35l2.58-2.59C13.46.89 11.43 0 9 0A9 9 0 0 0 .96 4.95l3.01 2.33C4.68 5.16 6.66 3.58 9 3.58Z\" />\n </svg>\n );\n}\n\nfunction MicrosoftMark() {\n return (\n <svg viewBox=\"0 0 18 18\" aria-hidden focusable=\"false\" className=\"size-4\">\n {/* cortena-design-lint-disable-next-line -- Microsoft brand orange; a provider mark may not be recoloured (§7.4) */}\n <path fill=\"#F25022\" d=\"M0 0h8.5v8.5H0z\" />\n {/* cortena-design-lint-disable-next-line -- Microsoft brand green; a provider mark may not be recoloured (§7.4) */}\n <path fill=\"#7FBA00\" d=\"M9.5 0H18v8.5H9.5z\" />\n {/* cortena-design-lint-disable-next-line -- Microsoft brand blue; a provider mark may not be recoloured (§7.4) */}\n <path fill=\"#00A4EF\" d=\"M0 9.5h8.5V18H0z\" />\n {/* cortena-design-lint-disable-next-line -- Microsoft brand yellow; a provider mark may not be recoloured (§7.4) */}\n <path fill=\"#FFB900\" d=\"M9.5 9.5H18V18H9.5z\" />\n </svg>\n );\n}\n\n/* ── the login screen ────────────────────────────────────────────────────── */\n\n/** The sign-in methods an org can advertise through `/api/orgs/:orgId/auth-info`. */\nexport type LoginMethod = \"password\" | \"google\" | \"microsoft\";\n\nexport interface LoginExtension {\n /** Registered mark id, from `x-cortena.icon` in the OpenAPI document (§9.1). */\n id: string;\n /** Display name. \"Sign in to <name>\". */\n name: string;\n}\n\nexport interface PasswordCredentials {\n email: string;\n password: string;\n}\n\nconst PROVIDERS: Record<\n Exclude<LoginMethod, \"password\">,\n { label: string; Mark: () => React.ReactElement }\n> = {\n google: { label: \"Continue with Google\", Mark: GoogleMark },\n microsoft: { label: \"Continue with Microsoft\", Mark: MicrosoftMark },\n};\n\n/** The order providers appear in, so two orgs never show them in different orders. */\nconst PROVIDER_ORDER = [\"google\", \"microsoft\"] as const;\n\nexport interface LoginScreenProps extends Omit<React.ComponentProps<\"div\">, \"onSubmit\"> {\n /** The extension (or product) being signed into. Supplies the mark and the name. */\n extension: LoginExtension;\n /**\n * What the org advertises. An empty or omitted list means password — never an\n * empty screen, which is what a failed `auth-info` call would otherwise draw.\n */\n methods?: readonly LoginMethod[];\n onPassword?: (credentials: PasswordCredentials) => void;\n onProvider?: (provider: Exclude<LoginMethod, \"password\">) => void;\n /** Shown above the card. A failed sign-in, in the user's words, not the API's. */\n error?: string;\n /** Disables every control and marks the submit button busy. */\n loading?: boolean;\n /**\n * Where the user was when the session ended (§9.4). Shown, so the promise is\n * visible before signing in; honouring it after sign-in is the caller's job.\n */\n returnTo?: string;\n /** The signed-out state a failed refresh or an idle timeout lands on (P-37). */\n signedOut?: boolean;\n}\n\nfunction LoginScreen({\n className,\n extension,\n methods,\n onPassword,\n onProvider,\n error,\n loading,\n returnTo,\n signedOut,\n ...props\n}: LoginScreenProps) {\n const fieldId = React.useId();\n const [email, setEmail] = React.useState(\"\");\n const [password, setPassword] = React.useState(\"\");\n\n // An org that advertises nothing still has password: an empty methods list is\n // a missing or failed auth-info call, and a screen with no way in is worse\n // than one that offers the method every org has.\n const active = methods && methods.length > 0 ? methods : ([\"password\"] as const);\n const showPassword = active.includes(\"password\");\n const providers = PROVIDER_ORDER.filter((p) => active.includes(p));\n\n const submit = (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n if (loading) {\n return;\n }\n onPassword?.({ email, password });\n };\n\n return (\n <AuthScreen\n data-slot=\"login-screen\"\n data-loading={loading ? \"\" : undefined}\n className={className}\n brand={{ id: extension.id, name: extension.name }}\n title={`Sign in to ${extension.name}`}\n subtitle={signedOut ? undefined : \"Use your Cortena account.\"}\n notices={\n <>\n {signedOut ? (\n <Alert variant=\"warn\" data-slot=\"login-screen-signed-out\">\n <AlertIcon>\n <SignedOutGlyph />\n </AlertIcon>\n <div>\n <AlertTitle>You were signed out</AlertTitle>\n <AlertDescription>\n Sign in again to pick up where you left off.\n </AlertDescription>\n </div>\n </Alert>\n ) : null}\n {error ? (\n <Alert variant=\"destructive\" data-slot=\"login-screen-error\">\n <AlertDescription className=\"text-[color:var(--ds-foreground)]\">\n {error}\n </AlertDescription>\n </Alert>\n ) : null}\n </>\n }\n footer={\n returnTo ? (\n <span data-slot=\"login-screen-return-to\">\n You will return to <span className=\"font-mono\">{returnTo}</span>\n </span>\n ) : null\n }\n {...props}\n >\n {showPassword ? (\n <form\n data-slot=\"login-screen-form\"\n className=\"flex flex-col gap-4\"\n onSubmit={submit}\n noValidate\n >\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor={`${fieldId}-email`}>Email</Label>\n <Input\n id={`${fieldId}-email`}\n name=\"email\"\n type=\"email\"\n autoComplete=\"username\"\n autoFocus\n required\n disabled={loading}\n aria-invalid={error ? true : undefined}\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n />\n </div>\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor={`${fieldId}-password`}>Password</Label>\n <Input\n id={`${fieldId}-password`}\n name=\"password\"\n type=\"password\"\n autoComplete=\"current-password\"\n required\n disabled={loading}\n aria-invalid={error ? true : undefined}\n value={password}\n onChange={(event) => setPassword(event.target.value)}\n />\n </div>\n {/* A real submit button, so Enter in either field signs in. */}\n <Button type=\"submit\" size=\"lg\" disabled={loading} data-slot=\"login-screen-submit\">\n {loading ? <Spinner tone=\"inverse\" /> : null}\n {loading ? \"Signing in…\" : \"Sign in\"}\n </Button>\n </form>\n ) : null}\n\n {showPassword && providers.length > 0 ? (\n <div data-slot=\"login-screen-divider\" className=\"flex items-center gap-3\" aria-hidden>\n <span className=\"h-px flex-1 bg-[var(--ds-border)]\" />\n <span\n className={cn(\n \"text-[length:var(--ds-text-caption)]\",\n \"text-[color:var(--ds-muted-foreground)]\",\n )}\n >\n or\n </span>\n <span className=\"h-px flex-1 bg-[var(--ds-border)]\" />\n </div>\n ) : null}\n\n {providers.length > 0 ? (\n <div data-slot=\"login-screen-providers\" className=\"flex flex-col gap-2.5\">\n {providers.map((provider) => {\n const { label, Mark } = PROVIDERS[provider];\n return (\n <Button\n key={provider}\n type=\"button\"\n variant=\"secondary\"\n size=\"lg\"\n disabled={loading}\n data-provider={provider}\n onClick={() => onProvider?.(provider)}\n >\n <Mark />\n {label}\n </Button>\n );\n })}\n </div>\n ) : null}\n </AuthScreen>\n );\n}\n\n/** The signed-out notice's glyph. Inline rather than a lucide import: one icon. */\nfunction SignedOutGlyph() {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" aria-hidden>\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M12 8v5\" strokeLinecap=\"round\" />\n <path d=\"M12 16h.01\" strokeLinecap=\"round\" />\n </svg>\n );\n}\n\nexport { AuthScreen, GoogleMark, LoginScreen, MicrosoftMark };\n"],"mappings":"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"}